Subscriptions
Create subscribe links, and read, cancel, resume and test-renew wallet subscriptions from your server.
A subscription charges a fixed amount from a payer's Jami wallet every period, after the payer approves it on Jami with their wallet PIN. Your token can propose a subscription (a subscribe link) and manage existing ones, but it can never start one or charge a wallet. The guide is Subscriptions.
With the SDK (0.6.0 or later): jami.subscriptions.createCheckout,
.list, .get, .cancel, .resume and .simulateRenewal.
Every endpoint takes an API token. Money is integer santim:
25000 is 250.00 ETB. Errors return { "error": "…", "code": "…" }.
Every endpoint is scoped to your organization's current environment. A jamidev_test_
token only sees sandbox subscriptions and a jamidev_live_ token only sees live ones, so a
test key can never read or cancel a live subscription; a subscription from the other
environment returns 404.
The subscription object
{
"id": "sub_Qm3xT8vLp2NaR7kd",
"status": "active",
"productId": "6710c2e8b1a2f0012ab34c01",
"productTitle": "Gebeta Club membership",
"customerId": "6710c2e8b1a2f0012ab34c33",
"customerEmail": "abebe@example.com",
"customerExternalId": "user_42",
"amount": 25000,
"currency": "ETB",
"interval": "month",
"intervalCount": 1,
"currentPeriodStart": "2026-10-23T09:30:00.000Z",
"currentPeriodEnd": "2026-11-23T09:30:00.000Z",
"nextChargeAt": "2026-11-23T09:30:00.000Z",
"cancelAtPeriodEnd": false,
"cancelRequestedBy": null,
"canceledAt": null,
"endedAt": null,
"cancellationReason": null,
"failedAttempts": 0,
"lastFailure": null,
"metadata": { "plan": "monthly" },
"livemode": true,
"createdAt": "2026-10-23T09:30:00.000Z"
}Prop
Type
The charge object
A charge is one billing period. Each paid charge creates an order with billingReason and
subscriptionId set.
{
"id": "6719f0a2c4e1b70012cd5e10",
"sequence": 2,
"billingReason": "subscription_cycle",
"status": "succeeded",
"amount": 25000,
"currency": "ETB",
"periodStart": "2026-11-23T09:30:00.000Z",
"periodEnd": "2026-12-23T09:30:00.000Z",
"attempts": 1,
"failure": null,
"orderId": "6719f0a2c4e1b70012cd5e11",
"paidAt": "2026-11-23T09:30:04.000Z",
"createdAt": "2026-11-23T09:30:02.000Z"
}Prop
Type
Create a subscribe link
POST /api/jamidev/subscriptions/checkoutCreates a hosted page where the payer approves the subscription. Nothing is charged until
they do. The product must be a published recurring product in your organization (see
Products).
curl https://jami.bio/api/jamidev/subscriptions/checkout \
-H "Authorization: Bearer $JAMI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"productId": "p_x7Kf3mQz9aBc",
"customer": { "email": "abebe@example.com", "externalId": "user_42" },
"successUrl": "https://gebeta.club/welcome?sub={SUBSCRIPTION_ID}",
"cancelUrl": "https://gebeta.club/pricing",
"metadata": { "plan": "monthly" }
}'Prop
Type
Returns 201:
{
"id": "sc_4hT9qLmX2vRb8NcKp0WzYe3s",
"url": "https://jami.bio/jamidev/subscribe/sc_4hT9qLmX2vRb8NcKp0WzYe3s",
"expiresAt": "2026-10-24T09:30:00.000Z",
"livemode": true
}The link lasts 24 hours and can be approved once. It records the plan's amount, interval and title when you create it: the approval page shows exactly that and approval charges exactly that, even if you change the product's price afterwards. If the first charge fails, nothing is created and the payer can try again on the same link until it expires.
The approval page is served with frame-ancestors 'none' and can't be embedded in an
iframe. Redirect the payer to url or open it in a new tab.
| Status | code | When |
|---|---|---|
400 | invalid_request | Missing productId, the product isn't recurring, a bad URL, customer or metadata |
404 | not_found | The product isn't in your organization |
409 | product_unavailable | The product isn't published, or has no valid price |
429 | Too many links. A bucket per organization: bursts of 100, refilling at 10 per second. |
List subscriptions
GET /api/jamidev/subscriptions?status=active&customerExternalId=user_42&page=1&limit=20Newest first.
Prop
Type
{ "items": [{ "id": "sub_Qm3xT8vLp2NaR7kd", "status": "active", … }], "total": 1, "page": 1, "limit": 20 }An unknown status returns 400.
Get a subscription
GET /api/jamidev/subscriptions/{id}{id} is the sub_… id or the internal id. Returns the subscription and its newest 24
charges, newest first:
{ "subscription": { "id": "sub_Qm3xT8vLp2NaR7kd", … }, "charges": [{ "sequence": 2, … }, { "sequence": 1, … }] }404 if it isn't in your organization.
Cancel a subscription
POST /api/jamidev/subscriptions/{id}/cancelcurl -X POST https://jami.bio/api/jamidev/subscriptions/sub_Qm3xT8vLp2NaR7kd/cancel \
-H "Authorization: Bearer $JAMI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "atPeriodEnd": true }'Prop
Type
Returns { subscription }. A past_due subscription always ends now. Cancelling one that
has already ended returns it unchanged. There are no refunds. Sends subscription.updated
(scheduled) or subscription.canceled (now). An atPeriodEnd that isn't a boolean
returns 400.
Resume a subscription
POST /api/jamidev/subscriptions/{id}/resumeUndoes a cancel your organization scheduled. Returns { subscription } and sends
subscription.updated with change: 'resumed'. If no cancel is scheduled, it returns the
subscription unchanged.
| Status | code | When |
|---|---|---|
409 | not_allowed | The payer scheduled the cancel (only they can resume it), or the subscription has already ended |
Simulate a renewal (sandbox)
POST /api/jamidev/subscriptions/{id}/simulate-renewalRuns the next renewal now, in a sandbox organization, and makes it succeed or fail. It sends the same webhooks a real renewal would.
curl -X POST https://jami.bio/api/jamidev/subscriptions/sub_Qm3xT8vLp2NaR7kd/simulate-renewal \
-H "Authorization: Bearer $JAMI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "outcome": "failure" }'Prop
Type
Returns { result, subscription }. result is one of:
result | Meaning |
|---|---|
renewed | Paid. The subscription moved forward one period. |
retry_scheduled | Failed; the subscription is past_due and will be retried. |
canceled | It ended: the last retry failed, or a cancel was scheduled. |
skipped | Nothing to do, for example another renewal was running at the same moment. |
deferred | Jami hit an error on its side and will try again later. |
409 not_allowed for production subscriptions or ones that have ended; 400 for an unknown
outcome.
Errors
| Status | code | When |
|---|---|---|
400 | invalid_request | Validation failed. The message names the field. |
401 | Bad token | |
404 | not_found | Not found in your organization, or in its other environment |
409 | product_unavailable | The plan isn't published or has no valid price |
409 | not_allowed | Resuming a payer's cancel or an ended subscription; simulating outside sandbox |
429 | Too many subscribe links |
Next
- The full flow, dunning and emails: Subscriptions.
- Event payloads: Webhooks.
