JamiDev
REST API

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

POST /api/jamidev/subscriptions/checkout

Creates 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.

StatuscodeWhen
400invalid_requestMissing productId, the product isn't recurring, a bad URL, customer or metadata
404not_foundThe product isn't in your organization
409product_unavailableThe product isn't published, or has no valid price
429Too 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=20

Newest 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}/cancel
curl -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}/resume

Undoes a cancel your organization scheduled. Returns { subscription } and sends subscription.updated with change: 'resumed'. If no cancel is scheduled, it returns the subscription unchanged.

StatuscodeWhen
409not_allowedThe 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-renewal

Runs 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:

resultMeaning
renewedPaid. The subscription moved forward one period.
retry_scheduledFailed; the subscription is past_due and will be retried.
canceledIt ended: the last retry failed, or a cancel was scheduled.
skippedNothing to do, for example another renewal was running at the same moment.
deferredJami 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

StatuscodeWhen
400invalid_requestValidation failed. The message names the field.
401Bad token
404not_foundNot found in your organization, or in its other environment
409product_unavailableThe plan isn't published or has no valid price
409not_allowedResuming a payer's cancel or an ended subscription; simulating outside sandbox
429Too many subscribe links

Next

On this page