Skip to content
ABUNA
DocsSessions

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:

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

  • idstring

    Unique identifier for the session.
  • typestring

    What the session is for: checkout, payment, plan_change, cancel, or portal. The endpoint that created the session sets it.
  • statusstring

    open until it ends. completed once the customer finishes, as each type defines it, or expired once it runs out or you expire it. A session that ends never changes again.
  • urlstring

    The hosted page to send the customer to. It belongs to this customer, so don't share it.
  • success_urlstringnullable

    Where 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 for portal. See Success URL placeholders.
  • cancel_urlstringnullable

    Where 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_urlstringnullable

    For portal, where the portal's "Back to" link goes. Null for other types.
  • metadataobject

    Your own keys and values, as you sent them. {} when you sent none.
  • customer_idstringnullable

    The customer you passed, or the one the customer's details created. For the other types, the subscription's customer. Null until known.
  • price_idstringnullable

    For checkout, the price the session sells. For plan_change, the new price. Null for the other types.
  • subscription_idstringnullable

    For checkout, 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_idstringnullable

    For payment, the invoice the customer pays. Null for other types.
  • plan_offer_idstringnullable

    For plan_change, the plan offer the session created. Null for other types.
  • at_period_endbooleannullable

    For cancel, 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's customer_cancel. Null for other types.
  • expires_attimestamp

    When the session runs out, in Unix seconds. 24 hours after creation for checkout, payment, and cancel. 7 days, or the end of the current period if sooner, for plan_change. 1 hour for portal, which must be opened by then.
  • completed_attimestampnullable

    When the session completed.
  • created_attimestamp

    When the session was created, in Unix seconds.
The session object
{
  "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_idstringrequired

    The price to sell. It must not be archived.
  • customer_idstring

    An existing customer to bill. The checkout shows their details locked. Don't send it with customer.
  • customerobject

    Details 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 with customer_id.
    Show child parametersHide child parameters
    • emailstring

      Their email address. It must contain @.
    • namestring

      Their name.
    • phone_numberstring

      Their phone number with its country code, like +237671234567.
  • success_urlstring

    Where 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_urlstring

    Where to send the customer if they leave before paying. Defaults to your app's Cancel URL.
  • metadataobject

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

  • 422 with code invalid when a field fails. Each errors item names the field: no price_id (required), a price_id or customer_id the app doesn't have (not_found), customer sent with customer_id (invalid), a bad email (invalid) or phone number (phone_invalid), a URL that breaks the URL rules (url_invalid), or metadata past its limits (invalid).
  • 410 with code price_archived when the price is archived.
  • 422 with code not_accepting_payments when a Live app can't take payments yet.
  • 422 with code idempotency_key_reused or 409 with code idempotency_in_progress. See Idempotency.
POST /v1/apps/{appID}/subscriptions/start
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"}
  }'
Response
{
  "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_urlstring

    Where 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_urlstring

    Where to send the customer if they leave without paying. Defaults to your app's Cancel URL.
  • metadataobject

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

  • 404 with code not_found when the app has no invoice with that ID.
  • 409 with code already_paid when the invoice is already paid, or 409 with code invoice_not_open when it is void, including the invoice of a canceled subscription.
  • 422 with code invalid when a field fails: a URL that breaks the URL rules (url_invalid), or metadata past its limits (invalid).
  • 422 with code not_accepting_payments when a Live app can't take payments yet.
  • 422 with code idempotency_key_reused or 409 with code idempotency_in_progress. See Idempotency.
POST /v1/apps/{appID}/invoices/{id}/pay
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"}
  }'
Response
{
  "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_idstringrequired

    The new price. It must be one of the subscription's plan options.
  • success_urlstring

    Where 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_urlstring

    The "Back to" link on the confirm and pay steps. Defaults to your app's Cancel URL.
  • metadataobject

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

  • 404 with code not_found when the app has no subscription with that ID.
  • 422 with code price_not_eligible when the price isn't one of the plan options.
  • 409 with code plan_change_unavailable when the subscription isn't active.
  • 422 with code invalid when a field fails: no price_id (required), a URL that breaks the URL rules (url_invalid), or metadata past its limits (invalid).
  • 422 with code idempotency_key_reused or 409 with code idempotency_in_progress. See Idempotency.
POST /v1/apps/{appID}/subscriptions/{id}/change-plan
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"}
  }'
Response
{
  "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_endboolean

    true ends the subscription at the end of the paid period, and false ends it right away. Defaults to your app's customer_cancel, read when the session is created.
  • success_urlstring

    Where 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_urlstring

    The "Back to" link on the confirm page. Defaults to your app's Cancel URL.
  • metadataobject

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

  • 404 with code not_found when the app has no subscription with that ID.
  • 409 with code subscription_canceled when the subscription has ended, 409 with code subscription_not_active when it isn't active, or 409 with code cancel_already_scheduled when a cancel is already scheduled.
  • 422 with code invalid when a field fails: a URL that breaks the URL rules (url_invalid), or metadata past its limits (invalid).
  • 422 with code idempotency_key_reused or 409 with code idempotency_in_progress. See Idempotency.
POST /v1/apps/{appID}/subscriptions/{id}/request-cancel
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"}
  }'
Response
{
  "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_urlstringrequired

    Where the portal's "Back to" link goes.
  • metadataobject

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

  • 404 with code not_found when the app has no subscription with that ID.
  • 422 with code invalid when a field fails: no return_url (required), a return_url that breaks the URL rules (url_invalid), or metadata past its limits (invalid).
  • 422 with code idempotency_key_reused or 409 with code idempotency_in_progress. See Idempotency.
POST /v1/apps/{appID}/subscriptions/{id}/portal
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"}}'
Response
{
  "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

  • typestring

    Only sessions of this type: checkout, payment, plan_change, cancel, or portal.
  • statusstring

    open, completed, or expired.
  • subscription_idstring

    Only sessions for this subscription.
  • limitinteger

    How many sessions to return, from 1 to 100. Defaults to 25.
  • cursorstring

    The next_cursor from the previous page. Send the same filters with it.

Response attributes

  • itemsarray

    The page's sessions. Empty when nothing matches.
  • next_cursorstringnullable

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

GET /v1/apps/{appID}/sessions
curl "https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/sessions?status=completed&limit=25" \
  -H "Authorization: Bearer sk_test_..."
Response
{
  "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.

GET /v1/apps/{appID}/sessions/{id}
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/sessions/01J9ZQ9S1T2V3W4X5Y6Z7A8B9C \
  -H "Authorization: Bearer sk_test_..."
Response
{
  "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.

POST /v1/apps/{appID}/sessions/{id}/expire
curl -X POST https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/sessions/01J9ZQ9S1T2V3W4X5Y6Z7A8B9C/expire \
  -H "Authorization: Bearer sk_test_..."
Response
{
  "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_idstring

    The session.
  • typestring

    Always payment.
  • invoice_idstring

    The invoice the customer paid.
  • subscription_idstring

    Its subscription.
  • customer_idstring

    Its customer.
  • metadataobject

    The metadata you passed when you created the session.
  • completed_attimestamp

    When the session completed.
session.completed payload
{
  "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 0 applies, 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_idstring

    The session.
  • typestring

    Always plan_change.
  • subscription_idstring

    The subscription.
  • customer_idstring

    Its customer.
  • metadataobject

    The metadata you passed when you created the session.
  • completed_attimestamp

    When the session completed.
  • plan_offer_idstring

    The plan offer the session created.
  • previous_price_idstring

    The price before the change.
  • price_idstring

    The new price.
  • directionstring

    upgrade or downgrade.
  • effective_attimestamp

    When the new price starts: when the upgrade applied, or the end of the paid period for a downgrade.
session.completed payload
{
  "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 active and 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_idstring

    The session.
  • typestring

    Always cancel.
  • subscription_idstring

    The subscription.
  • customer_idstring

    Its customer.
  • metadataobject

    The metadata you passed when you created the session.
  • completed_attimestamp

    When the customer confirmed.
  • cancel_attimestampnullable

    When the subscription ends. Null when it ended right away.
  • immediateboolean

    true when the subscription ended right away.
session.completed payload
{
  "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_idstring

    The session.
  • typestring

    Always portal.
  • subscription_idstring

    The subscription.
  • customer_idstring

    Its customer.
  • metadataobject

    The metadata you passed when you created the session.
  • completed_attimestamp

    When the customer first opened the portal.
session.completed payload
{
  "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}: payment only.

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 422 with code idempotency_key_reused.
  • While the first request is still running, it returns 409 with code idempotency_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