API reference
Sessions
Send a customer to a hosted page to start a subscription, pay an invoice, change plan, cancel, or open the customer portal. Then list, read, and expire those sessions.
A session is a hosted page you create for one customer. You send the customer to its url, and Abuna tells your server how it ended with a session.completed or session.expired event. The Sessions guide walks through the whole flow.
Each customer action has its own endpoint, and each one creates a session of one type:
- Start a subscription:
checkout, a hosted checkout that sells one price. - Pay an invoice:
payment, a pay page for one open invoice. - Change plan:
plan_change, where the customer confirms a move to another price. - Request a cancel:
cancel, where the customer confirms a cancel. - Open the customer portal:
portal, the customer portal with a link back to your app.
All five return the same session object. You list, retrieve, and expire sessions of every type with the same endpoints.
These endpoints take a secret key only. A publishable key or a dashboard sign-in gets 403 with code forbidden. A body field an endpoint doesn't take returns 400 with code invalid_json.
The session object
Attributes
idstringUnique identifier for the session.typestringWhat the session is for:checkout,payment,plan_change,cancel, orportal. The endpoint that created the session sets it.statusstringopenuntil it ends.completedonce the customer finishes, as each type defines it, orexpiredonce it runs out or you expire it. A session that ends never changes again.urlstringThe hosted page to send the customer to. It belongs to this customer, so don't share it.success_urlstringnullableWhere the customer goes after finishing, as you sent it, or your app's Success URL if you sent none. Placeholders like{SESSION_ID}appear as written. Null when neither is set, and always null forportal. See Success URL placeholders.cancel_urlstringnullableWhere the customer goes if they leave before paying, or the session expires. Your app's Cancel URL if you sent none. Null when neither is set, and always null for portal.return_urlstringnullableForportal, where the portal's "Back to" link goes. Null for other types.metadataobjectYour own keys and values, as you sent them.{}when you sent none.customer_idstringnullableThe customer you passed, or the one the customer's details created. For the other types, the subscription's customer. Null until known.price_idstringnullableForcheckout, the price the session sells. Forplan_change, the new price. Null for the other types.subscription_idstringnullableForcheckout, the subscription the session started, set once the customer submits their details. For the other types, the subscription the session acts on. Null until known.invoice_idstringnullableForpayment, the invoice the customer pays. Null for other types.plan_offer_idstringnullableForplan_change, the plan offer the session created. Null for other types.at_period_endbooleannullableForcancel, whether the cancel waits for the end of the paid period (true) or ends the subscription right away (false). Set when the session is created, from your request or the app'scustomer_cancel. Null for other types.expires_attimestampWhen the session runs out, in Unix seconds. 24 hours after creation forcheckout,payment, andcancel. 7 days, or the end of the current period if sooner, forplan_change. 1 hour forportal, which must be opened by then.completed_attimestampnullableWhen the session completed.created_attimestampWhen the session was created, in Unix seconds.
{
"id": "01J9ZQ9S1T2V3W4X5Y6Z7A8B9C",
"type": "checkout",
"status": "open",
"url": "https://app.abuna.app/checkout/7f2c9e4a1b8d3f6e0c5a9d2b7e4f1a8c3d6b",
"success_url": "https://example.com/welcome",
"cancel_url": "https://example.com/pricing",
"return_url": null,
"metadata": {
"user_id": "42"
},
"customer_id": null,
"price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
"subscription_id": null,
"invoice_id": null,
"plan_offer_id": null,
"at_period_end": null,
"expires_at": 1727686400,
"completed_at": null,
"created_at": 1727600000
}Start a subscription
POST/v1/apps/{appID}/subscriptions/start
Creates a checkout session: a hosted checkout that sells one price to a new subscriber. Send the customer to its url. To start a subscription without a checkout, use Create a subscription.
Body parameters
price_idstringrequiredThe price to sell. It must not be archived.customer_idstringAn existing customer to bill. The checkout shows their details locked. Don't send it withcustomer.customerobjectDetails to prefill for a new customer. The checkout shows each one you send locked, and the customer fills in the rest. Don't send it withcustomer_id.Show child parametersHide child parameters
emailstringTheir email address. It must contain@.namestringTheir name.phone_numberstringTheir phone number with its country code, like+237671234567.
success_urlstringWhere to send the customer once the session completes. Abuna fills in its placeholders, or adds the IDs as query parameters if it has none. Defaults to your app's Success URL.cancel_urlstringWhere to send the customer if they leave before paying. Defaults to your app's Cancel URL.metadataobjectYour own keys and values, sent back in the session's events. The subscription the session starts gets them too. Up to 20 keys. Each key is 1 to 40 characters, and each value is a string of up to 500 characters.
Returns the session with status 201. It can also return:
422with codeinvalidwhen a field fails. Eacherrorsitem names the field: noprice_id(required), aprice_idorcustomer_idthe app doesn't have (not_found),customersent withcustomer_id(invalid), a bad email (invalid) or phone number (phone_invalid), a URL that breaks the URL rules (url_invalid), ormetadatapast its limits (invalid).410with codeprice_archivedwhen the price is archived.422with codenot_accepting_paymentswhen a Live app can't take payments yet.422with codeidempotency_key_reusedor409with codeidempotency_in_progress. See Idempotency.
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions/start \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2b9e-4d3a-4c8e-9a7f-2e5b8d1c0a43" \
-d '{
"price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
"customer": {"email": "ana@example.com"},
"success_url": "https://example.com/welcome",
"cancel_url": "https://example.com/pricing",
"metadata": {"user_id": "42"}
}'{
"id": "01J9ZQ9S1T2V3W4X5Y6Z7A8B9C",
"type": "checkout",
"status": "open",
"url": "https://app.abuna.app/checkout/7f2c9e4a1b8d3f6e0c5a9d2b7e4f1a8c3d6b",
"success_url": "https://example.com/welcome",
"cancel_url": "https://example.com/pricing",
"return_url": null,
"metadata": {
"user_id": "42"
},
"customer_id": null,
"price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
"subscription_id": null,
"invoice_id": null,
"plan_offer_id": null,
"at_period_end": null,
"expires_at": 1727686400,
"completed_at": null,
"created_at": 1727600000
}Pay an invoice
POST/v1/apps/{appID}/invoices/{id}/pay
Creates a payment session: a pay page for one open invoice. Nothing is charged until the customer pays on the page. To charge the customer's saved phone number from your server instead, use Pay a billing period.
The id in the path is the invoice's. Find open invoices with List invoices and status=open, or keep the invoice_id from the invoice.created event.
Body parameters
success_urlstringWhere to send the customer once the session completes. Abuna fills in its placeholders, or adds the IDs as query parameters if it has none. Defaults to your app's Success URL.cancel_urlstringWhere to send the customer if they leave without paying. Defaults to your app's Cancel URL.metadataobjectYour own keys and values, sent back in the session's events. Up to 20 keys. Each key is 1 to 40 characters, and each value is a string of up to 500 characters.
The body is optional. Returns the session with status 201. It can also return:
404with codenot_foundwhen the app has no invoice with that ID.409with codealready_paidwhen the invoice is already paid, or409with codeinvoice_not_openwhen it is void, including the invoice of a canceled subscription.422with codeinvalidwhen a field fails: a URL that breaks the URL rules (url_invalid), ormetadatapast its limits (invalid).422with codenot_accepting_paymentswhen a Live app can't take payments yet.422with codeidempotency_key_reusedor409with codeidempotency_in_progress. See Idempotency.
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/invoices/01J9ZQ7V1W2X3Y4Z5A6B7C8D9E/pay \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"success_url": "https://example.com/billing/paid",
"cancel_url": "https://example.com/billing",
"metadata": {"user_id": "42"}
}'{
"id": "01J9ZQ9V1W2X3Y4Z5A6B7C8D9E",
"type": "payment",
"status": "open",
"url": "https://app.abuna.app/pay/3a8d1f6c9e2b5a7d0f4c8e1b6a9d3f7c2e5b",
"success_url": "https://example.com/billing/paid",
"cancel_url": "https://example.com/billing",
"return_url": null,
"metadata": {
"user_id": "42"
},
"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
"price_id": null,
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"invoice_id": "01J9ZQ7V1W2X3Y4Z5A6B7C8D9E",
"plan_offer_id": null,
"at_period_end": null,
"expires_at": 1727686400,
"completed_at": null,
"created_at": 1727600000
}Change plan
POST/v1/apps/{appID}/subscriptions/{id}/change-plan
Creates a plan_change session: a page where the customer confirms a move to another price, and pays for an upgrade. Nothing changes until they confirm. Abuna doesn't email or message the customer, so send them to the url yourself. Changing plan explains upgrades, downgrades, and what each costs.
Body parameters
price_idstringrequiredThe new price. It must be one of the subscription's plan options.success_urlstringWhere to send the customer once the session completes. Abuna fills in its placeholders, or adds the IDs as query parameters if it has none. Defaults to your app's Success URL.cancel_urlstringThe "Back to" link on the confirm and pay steps. Defaults to your app's Cancel URL.metadataobjectYour own keys and values, sent back in the session's events. Up to 20 keys. Each key is 1 to 40 characters, and each value is a string of up to 500 characters.
Returns the session with status 201. It can also return:
404with codenot_foundwhen the app has no subscription with that ID.422with codeprice_not_eligiblewhen the price isn't one of the plan options.409with codeplan_change_unavailablewhen the subscription isn'tactive.422with codeinvalidwhen a field fails: noprice_id(required), a URL that breaks the URL rules (url_invalid), ormetadatapast its limits (invalid).422with codeidempotency_key_reusedor409with codeidempotency_in_progress. See Idempotency.
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions/01J9ZQ6M1N2P3Q4R5S6T7V8W9X/change-plan \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"price_id": "01J9ZQ5P1Q2R3S4T5V6W7X8YZA",
"success_url": "https://example.com/plan/changed",
"cancel_url": "https://example.com/plan",
"metadata": {"user_id": "42"}
}'{
"id": "01J9ZQ9W1X2Y3Z4A5B6C7D8E9F",
"type": "plan_change",
"status": "open",
"url": "https://app.abuna.app/plan-change/5d2b8e1f4a7c0d3e6b9f2a5c8e1d4b7a0f3c",
"success_url": "https://example.com/plan/changed",
"cancel_url": "https://example.com/plan",
"return_url": null,
"metadata": {
"user_id": "42"
},
"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
"price_id": "01J9ZQ5P1Q2R3S4T5V6W7X8YZA",
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"invoice_id": null,
"plan_offer_id": "01J9ZQAF1G2H3J4K5M6N7P8Q9R",
"at_period_end": null,
"expires_at": 1728204800,
"completed_at": null,
"created_at": 1727600000
}Request a cancel
POST/v1/apps/{appID}/subscriptions/{id}/request-cancel
Creates a cancel session: a page where the customer confirms a cancel. Nothing changes until they confirm. To cancel without asking the customer, use Cancel a subscription.
Body parameters
at_period_endbooleantrueends the subscription at the end of the paid period, andfalseends it right away. Defaults to your app'scustomer_cancel, read when the session is created.success_urlstringWhere to send the customer once the session completes. Abuna fills in its placeholders, or adds the IDs as query parameters if it has none. Defaults to your app's Success URL.cancel_urlstringThe "Back to" link on the confirm page. Defaults to your app's Cancel URL.metadataobjectYour own keys and values, sent back in the session's events. Up to 20 keys. Each key is 1 to 40 characters, and each value is a string of up to 500 characters.
The body is optional. Returns the session with status 201. It can also return:
404with codenot_foundwhen the app has no subscription with that ID.409with codesubscription_canceledwhen the subscription has ended,409with codesubscription_not_activewhen it isn'tactive, or409with codecancel_already_scheduledwhen a cancel is already scheduled.422with codeinvalidwhen a field fails: a URL that breaks the URL rules (url_invalid), ormetadatapast its limits (invalid).422with codeidempotency_key_reusedor409with codeidempotency_in_progress. See Idempotency.
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions/01J9ZQ6M1N2P3Q4R5S6T7V8W9X/request-cancel \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"at_period_end": true,
"success_url": "https://example.com/account/canceled",
"cancel_url": "https://example.com/account",
"metadata": {"user_id": "42"}
}'{
"id": "01J9ZQ9X1Y2Z3A4B5C6D7E8F9G",
"type": "cancel",
"status": "open",
"url": "https://app.abuna.app/cancel/9e4b7a1d3f6c0e8b2d5a9f1c4e7b0d3a6f2c",
"success_url": "https://example.com/account/canceled",
"cancel_url": "https://example.com/account",
"return_url": null,
"metadata": {
"user_id": "42"
},
"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
"price_id": null,
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"invoice_id": null,
"plan_offer_id": null,
"at_period_end": true,
"expires_at": 1727686400,
"completed_at": null,
"created_at": 1727600000
}Open the customer portal
POST/v1/apps/{appID}/subscriptions/{id}/portal
Creates a portal session: the customer portal for one subscription, with a "Back to" link to your app. The subscription can have any status. Create a new one each time the customer asks to manage billing.
Body parameters
return_urlstringrequiredWhere the portal's "Back to" link goes.metadataobjectYour own keys and values, sent back in the session's events. Up to 20 keys. Each key is 1 to 40 characters, and each value is a string of up to 500 characters.
Returns the session with status 201. It can also return:
404with codenot_foundwhen the app has no subscription with that ID.422with codeinvalidwhen a field fails: noreturn_url(required), areturn_urlthat breaks the URL rules (url_invalid), ormetadatapast its limits (invalid).422with codeidempotency_key_reusedor409with codeidempotency_in_progress. See Idempotency.
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions/01J9ZQ6M1N2P3Q4R5S6T7V8W9X/portal \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{"return_url": "https://example.com/account", "metadata": {"user_id": "42"}}'{
"id": "01J9ZQ9Y1Z2A3B4C5D6E7F8G9H",
"type": "portal",
"status": "open",
"url": "https://app.abuna.app/portal/2c7f0a3d6b9e1c4f8a2d5b7e0c3f6a9d1b4e",
"success_url": null,
"cancel_url": null,
"return_url": "https://example.com/account",
"metadata": {
"user_id": "42"
},
"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
"price_id": null,
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"invoice_id": null,
"plan_offer_id": null,
"at_period_end": null,
"expires_at": 1727603600,
"completed_at": null,
"created_at": 1727600000
}List sessions
GET/v1/apps/{appID}/sessions
Lists the app's sessions, newest first, one page at a time. All parameters are optional, and a blank one is ignored.
Parameters
typestringOnly sessions of this type:checkout,payment,plan_change,cancel, orportal.statusstringopen,completed, orexpired.subscription_idstringOnly sessions for this subscription.limitintegerHow many sessions to return, from 1 to 100. Defaults to 25.cursorstringThenext_cursorfrom the previous page. Send the same filters with it.
Response attributes
itemsarrayThe page's sessions. Empty when nothing matches.next_cursorstringnullablePass it as cursor to get the next page. It is null on the last page. Treat it as opaque.
Returns 200. A value that isn't allowed returns 422 with code invalid and the parameter's name in errors.
curl "https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/sessions?status=completed&limit=25" \
-H "Authorization: Bearer sk_test_..."{
"items": [
{
"id": "01J9ZQ9S1T2V3W4X5Y6Z7A8B9C",
"type": "checkout",
"status": "completed",
"url": "https://app.abuna.app/checkout/7f2c9e4a1b8d3f6e0c5a9d2b7e4f1a8c3d6b",
"success_url": "https://example.com/welcome",
"cancel_url": "https://example.com/pricing",
"return_url": null,
"metadata": {
"user_id": "42"
},
"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
"price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"invoice_id": null,
"plan_offer_id": null,
"at_period_end": null,
"expires_at": 1727686400,
"completed_at": 1727600600,
"created_at": 1727600000
}
],
"next_cursor": null
}Retrieve a session
GET/v1/apps/{appID}/sessions/{id}
Retrieves a session.
Returns the session. If the app has no session with that ID, returns 404 with code not_found.
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/sessions/01J9ZQ9S1T2V3W4X5Y6Z7A8B9C \
-H "Authorization: Bearer sk_test_..."{
"id": "01J9ZQ9S1T2V3W4X5Y6Z7A8B9C",
"type": "checkout",
"status": "completed",
"url": "https://app.abuna.app/checkout/7f2c9e4a1b8d3f6e0c5a9d2b7e4f1a8c3d6b",
"success_url": "https://example.com/welcome",
"cancel_url": "https://example.com/pricing",
"return_url": null,
"metadata": {
"user_id": "42"
},
"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
"price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"invoice_id": null,
"plan_offer_id": null,
"at_period_end": null,
"expires_at": 1727686400,
"completed_at": 1727600600,
"created_at": 1727600000
}Expire a session
POST/v1/apps/{appID}/sessions/{id}/expire
Ends an open session now, before its expires_at. Its page tells the customer the session has expired, and Abuna sends session.expired. A subscription a checkout session already started stays pending, as described in the Sessions guide. Expiring a plan_change session also withdraws its plan offer, and Abuna sends plan_offer.canceled.
Returns the session. If it is already completed or expired, returns 409 with code session_not_open. If the app has no session with that ID, returns 404 with code not_found.
curl -X POST https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/sessions/01J9ZQ9S1T2V3W4X5Y6Z7A8B9C/expire \
-H "Authorization: Bearer sk_test_..."{
"id": "01J9ZQ9S1T2V3W4X5Y6Z7A8B9C",
"type": "checkout",
"status": "expired",
"url": "https://app.abuna.app/checkout/7f2c9e4a1b8d3f6e0c5a9d2b7e4f1a8c3d6b",
"success_url": "https://example.com/welcome",
"cancel_url": "https://example.com/pricing",
"return_url": null,
"metadata": {
"user_id": "42"
},
"customer_id": null,
"price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
"subscription_id": null,
"invoice_id": null,
"plan_offer_id": null,
"at_period_end": null,
"expires_at": 1727686400,
"completed_at": null,
"created_at": 1727600000
}Session types
Each type has its own page, its own rule for completing, and its own session.completed payload. All of them send session.expired with the payload in the events reference.
checkout
Created by Start a subscription. The url is a hosted checkout for price_id. The session completes when the first payment succeeds and the subscription becomes active. It expires 24 hours after creation. Its session.completed payload has session_id, type, subscription_id, customer_id, price_id, metadata, and completed_at.
payment
Created by Pay an invoice. The url is a pay page for invoice_id. The session completes when that invoice is paid through it. It expires 24 hours after creation, or sooner once the invoice can't be paid through it: paid another way, voided, or its subscription ended. An invoice paid another way expires the session, so use invoice.paid to know an invoice is settled. The pay links Abuna sends in emails and on Telegram keep working on their own.
session.completed payload
session_idstringThe session.typestringAlwayspayment.invoice_idstringThe invoice the customer paid.subscription_idstringIts subscription.customer_idstringIts customer.metadataobjectThe metadata you passed when you created the session.completed_attimestampWhen the session completed.
{
"session_id": "01J9ZQ9V1W2X3Y4Z5A6B7C8D9E",
"type": "payment",
"invoice_id": "01J9ZQ7V1W2X3Y4Z5A6B7C8D9E",
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
"metadata": {
"user_id": "42"
},
"completed_at": 1727600600
}plan_change
Created by Change plan. The url is the confirm page for moving subscription_id to price_id. Creating the session creates a plan offer behind it and sends plan_offer.created, but Abuna doesn't email or message the customer. It replaces any pending offer or plan_change session, and the older session gets session.expired.
The session completes when the change is done:
- A downgrade: when the customer confirms. It is scheduled for the end of the paid period.
- An upgrade: when its payment succeeds. An upgrade that costs
0applies, and completes, at confirm.
It expires 7 days after creation, or at the end of the current period if that comes first. It also expires when its offer is replaced, canceled, or expires, or when an upgrade it started goes unpaid and lapses. A confirmed upgrade waiting for payment stays open until it is paid or lapses.
plan_offer.*, subscription.updated, and invoice.* events still fire as for any plan change. Use session.completed to know the customer finished the flow you started, and subscription.updated to know the price changed. A downgrade changes the price at the end of the period, after the session completes.
session.completed payload
session_idstringThe session.typestringAlwaysplan_change.subscription_idstringThe subscription.customer_idstringIts customer.metadataobjectThe metadata you passed when you created the session.completed_attimestampWhen the session completed.plan_offer_idstringThe plan offer the session created.previous_price_idstringThe price before the change.price_idstringThe new price.directionstringupgradeordowngrade.effective_attimestampWhen the new price starts: when the upgrade applied, or the end of the paid period for a downgrade.
{
"session_id": "01J9ZQ9W1X2Y3Z4A5B6C7D8E9F",
"type": "plan_change",
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
"metadata": {
"user_id": "42"
},
"completed_at": 1727600900,
"plan_offer_id": "01J9ZQAF1G2H3J4K5M6N7P8Q9R",
"previous_price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
"price_id": "01J9ZQ5P1Q2R3S4T5V6W7X8YZA",
"direction": "upgrade",
"effective_at": 1727600900
}cancel
Created by Request a cancel. The url is a confirm page for canceling subscription_id. It shows what happens and the end date, with a Confirm button and a "Back to" link to cancel_url. The subscription must be active and not already scheduled to cancel.
at_period_end is set when you create the session: the value you sent, or the app's customer_cancel setting. The cancel follows the usual cancel rules:
- At the end of the period, the subscription stays
activeand the customer keeps access until the paid period ends. - If the current period isn't paid, or is already over, the subscription ends right away instead. The page tells the customer before they confirm.
- A cancel that takes effect right away voids the subscription's open invoices.
The session completes when the customer confirms, whether the cancel is scheduled or immediate. It expires 24 hours after creation if they don't. subscription.updated (scheduled) or subscription.canceled (immediate) still fire as for any cancel. For a scheduled cancel, subscription.canceled fires later, at cancel_at. Rely on those events to change access.
session.completed payload
session_idstringThe session.typestringAlwayscancel.subscription_idstringThe subscription.customer_idstringIts customer.metadataobjectThe metadata you passed when you created the session.completed_attimestampWhen the customer confirmed.cancel_attimestampnullableWhen the subscription ends. Null when it ended right away.immediatebooleantruewhen the subscription ended right away.
{
"session_id": "01J9ZQ9X1Y2Z3A4B5C6D7E8F9G",
"type": "cancel",
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
"metadata": {
"user_id": "42"
},
"completed_at": 1727600300,
"cancel_at": 1729000000,
"immediate": false
}portal
Created by Open the customer portal. The url is the customer portal for subscription_id, with a "Back to" link to return_url at the top. The subscription can have any status. The link stays as the customer changes plan, pays, connects Telegram, or updates their contact details. On the pay page, the back and done links go to return_url, and "Manage subscription" returns to the portal.
The url must be opened within 1 hour of creation, or it shows an invalid link. Once opened, it keeps working like the customer portal. Create a new session each time the customer asks to manage billing.
The session completes when the customer first opens it. That doesn't mean they changed anything: what they do in the portal sends its own events, like subscription.updated, subscription.canceled, invoice.paid, and customer.updated. A session nobody opens expires after 1 hour. The subscription's subscription_page_url keeps working as before, with no link back to your app.
session.completed payload
session_idstringThe session.typestringAlwaysportal.subscription_idstringThe subscription.customer_idstringIts customer.metadataobjectThe metadata you passed when you created the session.completed_attimestampWhen the customer first opened the portal.
{
"session_id": "01J9ZQ9Y1Z2A3B4C5D6E7F8G9H",
"type": "portal",
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
"metadata": {
"user_id": "42"
},
"completed_at": 1727600020
}Redirect URLs
success_url and cancel_url default to your app's Success URL and Cancel URL. Abuna copies them onto the session when you create it, so changing the setting later doesn't change existing sessions. A portal session takes return_url instead, and has neither.
Every URL must be an absolute https URL with a host. In Test mode, http also works for localhost, 127.0.0.1, and [::1]. This includes the Test app's default Success URL and Cancel URL, for every session type. Any other URL returns 422 with code invalid and an errors item with code url_invalid.
Success URL placeholders
Cancel sessions and plan changes with no payment due send the customer straight to success_url. Checkout, payment, and paid upgrades show a "Back to" button instead. Both use success_url with these placeholders filled in:
{SESSION_ID}: every type.{SUBSCRIPTION_ID}: every type.{INVOICE_ID}:paymentonly.
If the URL has no placeholder, Abuna adds the same IDs as query parameters: session_id, subscription_id, and for payment, invoice_id. It keeps your own query parameters and the # fragment. cancel_url gets nothing added. A portal session has no success_url; its "Back to" link goes to return_url.
Idempotency
Each endpoint that creates a session takes an Idempotency-Key header, so you can retry a request without creating a second session. Send a new random value, up to 255 characters, for each new session, and the same value on each retry. Without the header, every request creates a session.
Abuna keeps a key for 24 hours, per app. When a request reuses a key:
- The same request gets the first response again, with its status and body, plus the header
Idempotent-Replayed: true. - A different request, to another path or with another body, returns
422with codeidempotency_key_reused. - While the first request is still running, it returns
409with codeidempotency_in_progress. Retry after a moment. - If the first request got a
5xx, Abuna forgets the key, so the retry runs again.
A key longer than 255 characters returns 400 with code invalid.
Next steps
- SessionsSend a customer from your app to subscribe, pay an invoice, change plan, cancel, or manage billing, bring them back, and know for sure when they're done.
- SubscriptionsStart, list, read, update, cancel, keep, and change the plan of a customer's subscription to a price.
- EventsThe record of everything that happened in an app, and every event type.
- ErrorsStatus codes, error codes, and field errors, and how to handle each.