Authentication
Include your API key in the Authorization header as a Bearer token.
Find your API keys in Admin → Settings → Checkout API Keys. Use test keys for development and live keys for production.
Authorization: Bearer sk_live_your_api_key
/sessions
Create Checkout Session
Create a new checkout session and redirect your customer to the returned URL. After payment, they'll be redirected to your success_url.
Parameters
line_items
required
array
Products to purchase. Each item: name, amount (cents), currency, quantity
success_url
required
string
Redirect URL after payment. We append ?session_id={id}
cancel_url
required
string
Redirect URL if customer cancels
customer_email
string
Email for invoice delivery
metadata
object
Key-value pairs returned in webhooks
curl -X POST https://jopay.me/api/checkout/v1/sessions \
-H "Authorization: Bearer sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"line_items": [{
"name": "Premium Coffee Beans",
"amount": 2500,
"currency": "eur",
"quantity": 1
}],
"customer_email": "customer@example.com",
"success_url": "https://yoursite.com/success",
"cancel_url": "https://yoursite.com/cancel",
"metadata": {"order_id": "12345"}
}'
const response = await fetch('https://jopay.me/api/checkout/v1/sessions', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_your_api_key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
line_items: [{
name: 'Premium Coffee Beans',
amount: 2500,
currency: 'eur',
quantity: 1
}],
customer_email: 'customer@example.com',
success_url: 'https://yoursite.com/success',
cancel_url: 'https://yoursite.com/cancel',
metadata: { order_id: '12345' }
})
});
const session = await response.json();
res.redirect(session.checkout_url);
import requests
response = requests.post(
'https://jopay.me/api/checkout/v1/sessions',
headers={
'Authorization': 'Bearer sk_live_your_api_key',
'Content-Type': 'application/json'
},
json={
'line_items': [{
'name': 'Premium Coffee Beans',
'amount': 2500,
'currency': 'eur',
'quantity': 1
}],
'customer_email': 'customer@example.com',
'success_url': 'https://yoursite.com/success',
'cancel_url': 'https://yoursite.com/cancel'
}
)
session = response.json()
return redirect(session['checkout_url'])
Response
{
"id": "cs_a1b2c3d4e5f6...",
"checkout_url": "https://jopay.me/checkout/cs_a1b2c3d4e5f6",
"status": "pending",
"amount_cents": 2500,
"currency": "eur",
"expires_at": "2025-01-10T13:00:00Z"
}
Webhooks
Configure your webhook endpoint in Admin → Settings → Checkout Webhook. We send events when payment status changes.
Events
checkout.session.completed
Payment successful. Fulfill the order.
checkout.session.expired
Session expired without payment.
checkout.payment.failed
Payment attempt failed.
Webhook Payload
{
"event": "checkout.session.completed",
"data": {
"id": "cs_a1b2c3d4e5f6...",
"status": "completed",
"amount_cents": 2500,
"currency": "eur",
"customer_email": "customer@example.com",
"metadata": {"order_id": "12345"},
"completed_at": "2025-01-10T12:05:00Z"
},
"created_at": "2025-01-10T12:05:01Z"
}
Verifying Signatures
Verify the X-JoPay-Signature header to ensure webhooks are authentic.
const crypto = require('crypto');
// Header format: t=timestamp,v1=signature
function verifySignature(payload, signatureHeader, secret) {
const [timestamp, hash] = signatureHeader
.split(',')
.map(p => p.split('=')[1]);
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex');
return hash === expected;
}
app.post('/webhook', (req, res) => {
const signature = req.headers['x-jopay-signature'];
if (!verifySignature(JSON.stringify(req.body), signature, SECRET)) {
return res.status(401).send('Invalid signature');
}
// Handle the event
});
Authentication
Include your API key in the Authorization header as a Bearer token.
Find your API keys in Admin → Settings → Checkout API Keys. Use test keys for development and live keys for production.
Authorization: Bearer sk_live_your_api_key
/sessions
Create Subscription Session
Create a checkout session with mode: "subscription". Customer enters payment details on our hosted page and is automatically billed each period.
Parameters
mode
required
string
Must be "subscription" for recurring billing
plan
required
object
Subscription plan: name, amount (cents), interval (day/week/month/year)
success_url
required
string
Redirect URL after successful subscription
cancel_url
required
string
Redirect URL if customer cancels
customer_email
string
Pre-fill customer email on checkout
trial_period_days
integer
Free trial days before first charge (e.g., 7 or 14)
metadata
object
Key-value pairs returned in webhooks (e.g., user_id)
curl -X POST https://jopay.me/api/checkout/v1/sessions \
-H "Authorization: Bearer sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"mode": "subscription",
"plan": {
"name": "Pro Monthly",
"amount": 1499,
"interval": "month"
},
"trial_period_days": 7,
"customer_email": "customer@example.com",
"success_url": "https://yoursite.com/welcome",
"cancel_url": "https://yoursite.com/pricing",
"metadata": {"user_id": "usr_123"}
}'
const response = await fetch('https://jopay.me/api/checkout/v1/sessions', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_your_api_key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
mode: 'subscription',
plan: {
name: 'Pro Monthly',
amount: 1499,
interval: 'month'
},
trial_period_days: 7,
customer_email: 'customer@example.com',
success_url: 'https://yoursite.com/welcome',
cancel_url: 'https://yoursite.com/pricing',
metadata: { user_id: 'usr_123' }
})
});
const session = await response.json();
res.redirect(session.checkout_url);
import requests
response = requests.post(
'https://jopay.me/api/checkout/v1/sessions',
headers={
'Authorization': 'Bearer sk_live_your_api_key',
'Content-Type': 'application/json'
},
json={
'mode': 'subscription',
'plan': {
'name': 'Pro Monthly',
'amount': 1499,
'interval': 'month'
},
'trial_period_days': 7,
'customer_email': 'customer@example.com',
'success_url': 'https://yoursite.com/welcome',
'cancel_url': 'https://yoursite.com/pricing',
'metadata': {'user_id': 'usr_123'}
}
)
session = response.json()
return redirect(session['checkout_url'])
Response
{
"id": "cs_a1b2c3d4e5f6...",
"checkout_url": "https://jopay.me/checkout/cs_a1b2c3d4e5f6",
"status": "pending",
"mode": "subscription",
"expires_at": "2025-01-10T13:00:00Z"
}
Subscription Lifecycle
Understanding the subscription statuses and what actions to take:
trialing
Trial Period
Customer is in their free trial. Grant full access. They won't be charged until trial ends.
active
Active
Subscription is active and paid. Customer has full access. This is the normal state.
past_due
Past Due
Payment failed. We're retrying. Show a warning and ask customer to update payment method.
canceled
Canceled
Subscription canceled but still active until period end. Customer retains access until current_period_end.
expired
Expired
Subscription ended. Revoke access completely. Customer must resubscribe to continue.
/subscriptions
List Subscriptions
Retrieve all subscriptions for your place. Filter by status or customer email.
Query Parameters
status
string
Filter by status: active, trialing, past_due, canceled, expired
customer_email
string
Filter by customer email address
curl "https://jopay.me/api/checkout/v1/subscriptions?status=active" \ -H "Authorization: Bearer sk_live_your_api_key"
const response = await fetch(
'https://jopay.me/api/checkout/v1/subscriptions?status=active',
{
headers: {
'Authorization': 'Bearer sk_live_your_api_key'
}
}
);
const { subscriptions } = await response.json();
import requests
response = requests.get(
'https://jopay.me/api/checkout/v1/subscriptions',
headers={'Authorization': 'Bearer sk_live_your_api_key'},
params={'status': 'active'}
)
subscriptions = response.json()['subscriptions']
Response
{
"subscriptions": [
{
"id": "sub_abc123",
"status": "active",
"customer_email": "customer@example.com",
"plan_name": "Pro Monthly",
"amount_cents": 1499,
"currency": "eur",
"current_period_end": "2025-02-10T00:00:00Z"
}
],
"total": 1
}
/subscriptions/:id/cancel
Cancel Subscription
Cancel a subscription. By default, access continues until the end of the current billing period.
Parameters
immediate
boolean
Cancel immediately instead of at period end. Default: false
# Cancel at period end (recommended)
curl -X POST "https://jopay.me/api/checkout/v1/subscriptions/sub_abc123/cancel" \
-H "Authorization: Bearer sk_live_your_api_key"
# Cancel immediately
curl -X POST "https://jopay.me/api/checkout/v1/subscriptions/sub_abc123/cancel" \
-H "Authorization: Bearer sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"immediate": true}'
// Cancel at period end (recommended)
const response = await fetch(
'https://jopay.me/api/checkout/v1/subscriptions/sub_abc123/cancel',
{
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_your_api_key'
}
}
);
// Cancel immediately
const response = await fetch(
'https://jopay.me/api/checkout/v1/subscriptions/sub_abc123/cancel',
{
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_your_api_key',
'Content-Type': 'application/json'
},
body: JSON.stringify({ immediate: true })
}
);
# Cancel at period end (recommended)
response = requests.post(
'https://jopay.me/api/checkout/v1/subscriptions/sub_abc123/cancel',
headers={'Authorization': 'Bearer sk_live_your_api_key'}
)
# Cancel immediately
response = requests.post(
'https://jopay.me/api/checkout/v1/subscriptions/sub_abc123/cancel',
headers={
'Authorization': 'Bearer sk_live_your_api_key',
'Content-Type': 'application/json'
},
json={'immediate': True}
)
Response
{
"id": "sub_abc123",
"status": "canceled",
"cancel_at_period_end": true,
"current_period_end": "2025-02-10T00:00:00Z"
}
/subscriptions/:id/portal
Customer Portal
Get a URL to redirect customers to manage their subscription. They can update payment method, view invoices, and cancel.
Parameters
return_url
required
string
URL to redirect customer back after they're done
curl "https://jopay.me/api/checkout/v1/subscriptions/sub_abc123/portal?return_url=https://yourapp.com/account" \ -H "Authorization: Bearer sk_live_your_api_key"
const params = new URLSearchParams({
return_url: 'https://yourapp.com/account'
});
const response = await fetch(
`https://jopay.me/api/checkout/v1/subscriptions/sub_abc123/portal?${params}`,
{
headers: {
'Authorization': 'Bearer sk_live_your_api_key'
}
}
);
const { portal_url } = await response.json();
// Redirect customer to portal_url
response = requests.get(
'https://jopay.me/api/checkout/v1/subscriptions/sub_abc123/portal',
headers={'Authorization': 'Bearer sk_live_your_api_key'},
params={'return_url': 'https://yourapp.com/account'}
)
portal_url = response.json()['portal_url']
# Redirect customer to portal_url
Response
{
"portal_url": "https://billing.stripe.com/session/..."
}
Webhooks
Configure your webhook endpoint in Admin → Settings → Checkout Webhook. We send events when subscription status changes.
Events
subscription.created
New subscription started. Grant access to your service.
subscription.renewed
Recurring payment successful. Subscription continues.
subscription.trial_ending
Trial ends in 3 days. Send reminder email.
subscription.payment_failed
Payment failed. Prompt to update payment method.
subscription.cancelled
Will end at period end. Access continues until then.
subscription.expired
Subscription ended. Revoke access immediately.
Webhook Payload
{
"event": "subscription.renewed",
"data": {
"id": "sub_abc123",
"stripe_subscription_id": "sub_1234567890",
"status": "active",
"plan_name": "Pro Monthly",
"plan_interval": "month",
"amount_cents": 1499,
"currency": "eur",
"customer_email": "customer@example.com",
"current_period_start": "2025-02-10T00:00:00Z",
"current_period_end": "2025-03-10T00:00:00Z",
"metadata": {"user_id": "usr_123"}
},
"created_at": "2025-02-10T00:01:00Z"
}
Verifying Signatures
Verify the X-JoPay-Signature header to ensure webhooks are authentic.
const crypto = require('crypto');
// Header format: t=timestamp,v1=signature
function verifySignature(payload, signatureHeader, secret) {
const [timestamp, hash] = signatureHeader
.split(',')
.map(p => p.split('=')[1]);
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex');
return hash === expected;
}
app.post('/webhook', (req, res) => {
const signature = req.headers['x-jopay-signature'];
if (!verifySignature(JSON.stringify(req.body), signature, SECRET)) {
return res.status(401).send('Invalid signature');
}
// Handle the event
});
import hmac
import hashlib
# Header format: t=timestamp,v1=signature
def verify_signature(payload, signature_header, secret):
parts = dict(p.split('=') for p in signature_header.split(','))
timestamp = parts['t']
signature = parts['v1']
expected = hmac.new(
secret.encode(),
f'{timestamp}.{payload}'.encode(),
hashlib.sha256
).hexdigest()
return signature == expected
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-JoPay-Signature')
if not verify_signature(request.data.decode(), signature, SECRET):
return 'Invalid signature', 401
# Handle the event
return 'OK', 200
# Header format: t=timestamp,v1=signature
def verify_signature(payload, signature_header, secret)
parts = signature_header.split(',').to_h { |p| p.split('=') }
timestamp = parts['t']
signature = parts['v1']
expected = OpenSSL::HMAC.hexdigest('sha256', secret, "#{timestamp}.#{payload}")
signature == expected
end
post '/webhook' do
signature = request.env['HTTP_X_JOPAY_SIGNATURE']
unless verify_signature(request.body.read, signature, SECRET)
halt 401, 'Invalid signature'
end
# Handle the event
status 200
end
Example Webhook Handler
app.post('/webhooks/jopay', async (req, res) => {
const signature = req.headers['x-jopay-signature'];
if (!verifySignature(JSON.stringify(req.body), signature, SECRET)) {
return res.status(401).send('Invalid signature');
}
const { event, data } = req.body;
const userId = data.metadata?.user_id;
switch (event) {
case 'subscription.created':
case 'subscription.renewed':
await db.users.update(userId, {
subscriptionStatus: 'active',
subscriptionId: data.id,
currentPeriodEnd: data.current_period_end
});
break;
case 'subscription.payment_failed':
await sendEmail(data.customer_email, 'payment-failed');
break;
case 'subscription.cancelled':
await db.users.update(userId, {
subscriptionStatus: 'canceling',
accessUntil: data.current_period_end
});
break;
case 'subscription.expired':
await db.users.update(userId, {
subscriptionStatus: 'expired',
subscriptionId: null
});
break;
}
res.status(200).send('OK');
});
@app.route('/webhooks/jopay', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-JoPay-Signature')
if not verify_signature(request.data.decode(), signature, SECRET):
return 'Invalid signature', 401
payload = request.json
event = payload['event']
data = payload['data']
user_id = data.get('metadata', {}).get('user_id')
if event in ['subscription.created', 'subscription.renewed']:
db.users.update(user_id, {
'subscription_status': 'active',
'subscription_id': data['id'],
'current_period_end': data['current_period_end']
})
elif event == 'subscription.payment_failed':
send_email(data['customer_email'], 'payment-failed')
elif event == 'subscription.cancelled':
db.users.update(user_id, {
'subscription_status': 'canceling',
'access_until': data['current_period_end']
})
elif event == 'subscription.expired':
db.users.update(user_id, {
'subscription_status': 'expired',
'subscription_id': None
})
return 'OK', 200
post '/webhooks/jopay' do
signature = request.env['HTTP_X_JOPAY_SIGNATURE']
payload = request.body.read
unless verify_signature(payload, signature, SECRET)
halt 401, 'Invalid signature'
end
data = JSON.parse(payload)
event = data['event']
sub = data['data']
user_id = sub.dig('metadata', 'user_id')
case event
when 'subscription.created', 'subscription.renewed'
User.find(user_id).update(
subscription_status: 'active',
subscription_id: sub['id'],
current_period_end: sub['current_period_end']
)
when 'subscription.payment_failed'
UserMailer.payment_failed(sub['customer_email']).deliver_later
when 'subscription.cancelled'
User.find(user_id).update(
subscription_status: 'canceling',
access_until: sub['current_period_end']
)
when 'subscription.expired'
User.find(user_id).update(
subscription_status: 'expired',
subscription_id: nil
)
end
status 200
end
Frequently Asked Questions
Does JoPay automatically charge customers each month?
Yes! Once a subscription is created, JoPay handles all recurring billing automatically. Customers are charged at each billing interval (monthly, yearly, etc.) without any action from you. You just listen for webhooks.
What happens if a payment fails?
JoPay automatically retries failed payments several times over a few days. You'll receive a subscription.payment_failed webhook with the failure reason. The subscription moves to "past_due" status. If all retries fail, it expires.
How can customers update their payment method?
Use the /subscriptions/:id/portal endpoint to get a secure URL. Redirect customers there and they can update their card, view invoices, or cancel their subscription.
When is the trial customer first charged?
If you set trial_period_days: 7, the customer enters their card during checkout but isn't charged until day 8. If they cancel during the trial, they're never charged.
What's the difference between canceled and expired?
Canceled: Subscription will end at period end, but customer still has access until then. Expired: Subscription has fully ended, revoke access immediately.