Skip to content
ABUNA
DocsSubscriptions

API reference

Subscriptions

Start, list, read, update, cancel, keep, and change the plan of a customer's subscription to a price.

A subscription bills a customer for a price, one billing period at a time. It starts pending, becomes active when the customer pays the first period, and ends canceled.

The subscription object

Attributes

  • idstring

    Unique identifier for the subscription.
  • app_idstring

    The app the subscription belongs to.
  • customer_idstring

    The customer who pays.
  • price_idstring

    The price the customer pays.
  • statusstring

    pending until the first period is paid, then active. canceled once it ends.
  • starts_attimestamp

    When the first billing period starts, in Unix seconds.
  • activated_attimestampnullable

    When the first period was paid.
  • canceled_attimestampnullable

    When the subscription was canceled.
  • cancel_attimestampnullable

    When the subscription will end, if a cancel is scheduled for the end of the paid period. Null otherwise. The subscription stays active until then.
  • metadataobject

    Your own keys and values. {} when there are none. A subscription started by a checkout session gets the session's. See Metadata.
  • created_attimestamp

    When the subscription was created, in Unix seconds.
  • manage_tokenstring

    The token in the customer portal link. Anyone with the link can see and cancel the subscription, so keep it private.
The subscription object
{
  "id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
  "app_id": "01J9ZQ4Y7R3T6V8W2X5B1C0DEF",
  "customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
  "price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
  "status": "active",
  "starts_at": 1727600000,
  "activated_at": 1727600100,
  "canceled_at": null,
  "cancel_at": null,
  "metadata": {
    "user_id": "42"
  },
  "created_at": 1727600000,
  "manage_token": "8c1f4a7d2e9b3c6f0a5d8e1b4c7f2a9d3e6b"
}

Create a subscription

POST/v1/apps/{appID}/subscriptions

Starts a pending subscription and its first billing period, which starts now. Abuna issues the period's invoice and sends the customer a notice with a link to pay it. To have the customer sign up and pay on a hosted checkout instead, use Start a subscription.

Parameters

  • customer_idstringrequired

    The customer to bill.
  • price_idstringrequired

    The price to bill them.
  • metadataobject

    Your own keys and values. See Metadata for the limits.

Returns an object with the subscription and its first entry, with status 201. If the app has no customer or price with that ID, returns 404 with code not_found. If the price is archived, returns 410 with code price_archived. If a Live app can't take payments yet, returns 422 with code not_accepting_payments. If the customer would pass your plan's Live subscriber limit, returns 403 with code plan_limit.

POST /v1/apps/{appID}/subscriptions
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K", "price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ"}'
Response
{
  "subscription": {
    "id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
    "app_id": "01J9ZQ4Y7R3T6V8W2X5B1C0DEF",
    "customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
    "price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
    "status": "pending",
    "starts_at": 1727600000,
    "activated_at": null,
    "canceled_at": null,
    "cancel_at": null,
    "metadata": {
      "user_id": "42"
    },
    "created_at": 1727600000,
    "manage_token": "8c1f4a7d2e9b3c6f0a5d8e1b4c7f2a9d3e6b"
  },
  "entry": {
    "id": "01J9ZQ7E1F2G3H4J5K6M7N8P9Q",
    "subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
    "price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
    "pay_token": "3a7e1c9f5b2d8a4e6c0f7b3d9a1e5c8f2b6d",
    "starts_at": 1727600000,
    "ends_at": 1730192000,
    "period_index": 0,
    "grace_period_ends_at": 1727686400,
    "paid_at": null,
    "paused_at": null,
    "in_force": true,
    "created_at": 1727600000
  }
}

List subscriptions

GET/v1/apps/{appID}/subscriptions

Lists the app's subscriptions, newest first. Each item has the subscription's fields, including cancel_at and metadata, without app_id and manage_token, plus three more.

Query parameters

  • statusstring

    Returns only subscriptions with this status: pending, active, or canceled.
  • emailstring

    Returns only subscriptions whose customer's email contains this text. Case doesn't matter.

Extra attributes

  • customerobject

    Who pays.
    Show child attributesHide child attributes
    • namestringnullable

      The customer's name.
    • emailstring

      The customer's email address.
  • priceobject

    What they pay.
    Show child attributesHide child attributes
    • namestring

      The price's name.
    • amountinteger

      The amount for each period, in the currency's smallest unit.
    • currencystring

      XAF or XOF.
    • intervalstring

      day, week, month, or year.
    • interval_countinteger

      How many intervals make up one period.
  • next_payment_attimestampnullable

    When the next payment is due. While the latest period is unpaid, it is that period's start, even if that is in the past. Once it is paid, it is the next period's start. Null once canceled, and null while a cancel is scheduled.

Returns an array. It isn't paginated. An unknown status returns 422 with code invalid.

GET /v1/apps/{appID}/subscriptions
curl 'https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions?status=active' \
  -H "Authorization: Bearer sk_test_..."
Response
[
  {
    "id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
    "customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
    "price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
    "status": "active",
    "starts_at": 1727600000,
    "activated_at": 1727600100,
    "canceled_at": null,
    "cancel_at": null,
    "metadata": {
      "user_id": "42"
    },
    "created_at": 1727600000,
    "customer": {
      "name": "Jane Doe",
      "email": "jane@example.com"
    },
    "price": {
      "name": "Monthly",
      "amount": 5000,
      "currency": "XAF",
      "interval": "month",
      "interval_count": 1
    },
    "next_payment_at": 1730192000
  }
]

Retrieve a subscription

GET/v1/apps/{appID}/subscriptions/{id}

Retrieves a subscription with its customer, price, billing periods, and payments.

Response attributes

  • subscriptionobject

    The subscription object.
  • subscription_page_urlstring

    The customer portal link, where the customer sees and cancels the subscription.
  • customerobject

    The customer object, plus telegram_connected.
  • priceobject

  • entriesarray

    Every billing period in force, oldest first. An upgrade's period waiting for payment, or one that lapsed, isn't listed. The waiting one shows in pending_change, and its invoice is still in the invoice list.
    Show child attributesHide child attributes
    • idstring

      The billing period's ID.
    • starts_attimestamp

      When the period starts.
    • ends_attimestamp

      When the period ends.
    • grace_period_ends_attimestamp

      The date to pay by.
    • paid_attimestampnullable

      When the period was paid.
    • pay_urlstring

      The pay link for the period.
    • invoiceobjectnullable

      The period's invoice object.
    • invoice_urlstringnullable

      A link to the invoice page. It expires after 7 days.
  • paymentsarray

    Every payment on the subscription, newest first.
  • pending_changeobjectnullable

    The plan change waiting to happen: an upgrade waiting for payment, or a downgrade scheduled for the end of the paid period. Null when there is none. A scheduled cancel shows in cancel_at instead.
    Show child attributesHide child attributes
    • idstring

      The plan change.
    • typestring

      upgrade or downgrade.
    • priceobject

      The new price: id, name, amount, currency, interval, and interval_count.
    • requested_bystring

      customer when they chose it in the customer portal, or merchant when they confirmed a change you sent them to.
    • created_attimestamp

      When the change was requested.
    • amount_dueintegernullable

      What the customer pays to switch now. Null for a downgrade.
    • creditintegernullable

      The unused time of the current period taken off the new price. Null for a downgrade.
    • expires_attimestampnullable

      If the upgrade isn't paid by then, it lapses and the current plan carries on. Null for a downgrade.
    • pay_urlstringnullable

      The pay link for the upgrade. Null for a downgrade.
    • effective_attimestampnullable

      When the downgrade starts. Null for an upgrade.
  • plan_offerobjectnullable

    The plan change waiting for the customer to confirm, or null. It is null once it is past its expires_at.
  • plan_changesarray

    Every plan change on the subscription, newest first.
    Show child attributesHide child attributes
    • idstring

      The plan change.
    • directionstring

      upgrade or downgrade.
    • statusstring

      awaiting_payment (an upgrade not paid yet), scheduled (a downgrade waiting for the period end), applied, lapsed (an upgrade not paid in time), undone (a downgrade taken back), replaced (another change took its place), or canceled (the subscription ended first).
    • from_priceobject

      The price before, in the same shape as above.
    • to_priceobject

      The new price.
    • requested_bystring

      customer or merchant.
    • amountintegernullable

      What the upgrade charges. Null for a downgrade.
    • creditintegernullable

      The unused time taken off the upgrade. Null for a downgrade.
    • effective_attimestampnullable

      When it took effect, or for a downgrade, when it will. Null for an upgrade that hasn't taken effect.
    • created_attimestamp

      When it was requested.
    • resolved_attimestampnullable

      When it left the pending state.

If the app has no subscription with that ID, returns 404 with code not_found.

GET /v1/apps/{appID}/subscriptions/{id}
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions/01J9ZQ6M1N2P3Q4R5S6T7V8W9X \
  -H "Authorization: Bearer sk_test_..."
Response
{
  "subscription": {
    "id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
    "app_id": "01J9ZQ4Y7R3T6V8W2X5B1C0DEF",
    "customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
    "price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
    "status": "active",
    "starts_at": 1727600000,
    "activated_at": 1727600100,
    "canceled_at": null,
    "cancel_at": null,
    "metadata": {
      "user_id": "42"
    },
    "created_at": 1727600000,
    "manage_token": "8c1f4a7d2e9b3c6f0a5d8e1b4c7f2a9d3e6b"
  },
  "subscription_page_url": "https://app.abuna.app/portal/8c1f4a7d2e9b3c6f0a5d8e1b4c7f2a9d3e6b",
  "customer": {
    "id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
    "app_id": "01J9ZQ4Y7R3T6V8W2X5B1C0DEF",
    "name": "Jane Doe",
    "phone_number": "+237671234567",
    "email": "jane@example.com",
    "created_at": 1727600000,
    "telegram_connected": false
  },
  "price": {
    "id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
    "product_id": "01J9ZQ5A2B3C4D5E6F7G8H9JKM",
    "name": "Monthly",
    "currency": "XAF",
    "amount": 5000,
    "interval": "month",
    "interval_count": 1,
    "created_at": 1727600000,
    "archived_at": null
  },
  "entries": [
    {
      "id": "01J9ZQ7E1F2G3H4J5K6M7N8P9Q",
      "starts_at": 1727600000,
      "ends_at": 1730192000,
      "grace_period_ends_at": 1727686400,
      "paid_at": 1727600100,
      "pay_url": "https://app.abuna.app/pay/3a7e1c9f5b2d8a4e6c0f7b3d9a1e5c8f2b6d",
      "invoice": {
        "id": "01J9ZQ7V1W2X3Y4Z5A6B7C8D9E",
        "app_id": "01J9ZQ4Y7R3T6V8W2X5B1C0DEF",
        "entry_id": "01J9ZQ7E1F2G3H4J5K6M7N8P9Q",
        "subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
        "number": "01J9ZQ7N1P2Q3R4S5T6V7W8X9Y",
        "status": "paid",
        "kind": "period",
        "amount": 5000,
        "credit": 0,
        "currency": "XAF",
        "item_label": "Monthly",
        "buyer_name": "Jane Doe",
        "buyer_email": "jane@example.com",
        "seller_name": "My store",
        "period_start": 1727600000,
        "period_end": 1730192000,
        "due_at": 1727686400,
        "created_at": 1727600000,
        "paid_at": 1727600100,
        "voided_at": null
      },
      "invoice_url": "https://app.abuna.app/invoice/01J9ZQ7E1F2G3H4J5K6M7N8P9Q?exp=1728204800&sig=b4e1c8f2a7d3960e5b1f8c4a2d7e3b9f6a0c5e8d1b4f7a2c9e6d3b0f8a5c1e7d"
    }
  ],
  "payments": [
    {
      "id": "01J9ZQ7P1Q2R3S4T5V6W7X8Y9Z",
      "app_id": "01J9ZQ4Y7R3T6V8W2X5B1C0DEF",
      "entry_id": "01J9ZQ7E1F2G3H4J5K6M7N8P9Q",
      "driver": "sandbox",
      "provider": "test",
      "provider_reference": "test_9f3b7d1a5c2e8f4b6d0a",
      "amount": 5000,
      "currency": "XAF",
      "status": "succeeded",
      "message": null,
      "created_at": 1727600090,
      "fee": 100,
      "session_id": null,
      "method_label": "Successful payment (test)"
    }
  ],
  "pending_change": {
    "id": "01J9ZQAC1D2E3F4G5H6J7K8M9N",
    "type": "downgrade",
    "price": {
      "id": "01J9ZQ5B1C2D3E4F5G6H7J8KMN",
      "name": "Basic monthly",
      "amount": 2000,
      "currency": "XAF",
      "interval": "month",
      "interval_count": 1
    },
    "requested_by": "customer",
    "created_at": 1728464000,
    "amount_due": null,
    "credit": null,
    "expires_at": null,
    "pay_url": null,
    "effective_at": 1730192000
  },
  "plan_offer": null,
  "plan_changes": [
    {
      "id": "01J9ZQAC1D2E3F4G5H6J7K8M9N",
      "direction": "downgrade",
      "status": "scheduled",
      "from_price": {
        "id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
        "name": "Monthly",
        "amount": 5000,
        "currency": "XAF",
        "interval": "month",
        "interval_count": 1
      },
      "to_price": {
        "id": "01J9ZQ5B1C2D3E4F5G6H7J8KMN",
        "name": "Basic monthly",
        "amount": 2000,
        "currency": "XAF",
        "interval": "month",
        "interval_count": 1
      },
      "requested_by": "customer",
      "amount": null,
      "credit": null,
      "effective_at": 1730192000,
      "created_at": 1728464000,
      "resolved_at": null
    }
  ]
}

Update a subscription

PATCH/v1/apps/{appID}/subscriptions/{id}

Sets the subscription's metadata. The map you send replaces the whole map: keys you leave out are removed. Send {} to clear it.

Parameters

  • metadataobjectrequired

    The new metadata. See Metadata for the limits.

Returns the subscription. Metadata past its limits returns 422 with code invalid and an errors item for metadata. If the app has no subscription with that ID, returns 404 with code not_found.

PATCH /v1/apps/{appID}/subscriptions/{id}
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions/01J9ZQ6M1N2P3Q4R5S6T7V8W9X \
  -X PATCH \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{"metadata": {"user_id": "42"}}'
Response
{
  "id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
  "app_id": "01J9ZQ4Y7R3T6V8W2X5B1C0DEF",
  "customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
  "price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
  "status": "active",
  "starts_at": 1727600000,
  "activated_at": 1727600100,
  "canceled_at": null,
  "cancel_at": null,
  "metadata": {
    "user_id": "42"
  },
  "created_at": 1727600000,
  "manage_token": "8c1f4a7d2e9b3c6f0a5d8e1b4c7f2a9d3e6b"
}

Cancel a subscription

POST/v1/apps/{appID}/subscriptions/{id}/cancel

Ends the subscription, now or at the end of the paid period. Send at_period_end to choose. An empty body, or false, cancels now, as it always has. To have the customer confirm the cancel themselves, use Request a cancel.

Body parameters

  • at_period_endboolean

    Optional, false by default. With true, the subscription stays active until the current paid period ends, and then ends by itself. A value that isn't a boolean returns 400 with code invalid_json.

Now. The subscription is canceled at once, with canceled_at set and cancel_at null. Abuna voids its open invoices, sends a subscription.canceled event with reason merchant, and tells the customer. Canceling now on a subscription with a scheduled cancel ends it now and clears the schedule. It also voids an upgrade waiting for payment and withdraws a pending plan change offer.

At the end of the paid period. The response has status active and cancel_at set to the end of the paid period. Abuna sends a subscription.updated event and tells the customer when it ends and how to keep it. Until then it issues no renewal and sends no renewal reminders, and next_payment_at in the list is null. When the date arrives, the subscription becomes canceled and Abuna sends subscription.canceled. Sending at_period_end again while a cancel is scheduled returns 200 and changes nothing: no new event and no new email. A scheduled cancel replaces a scheduled downgrade or an upgrade waiting for payment. See One change at a time.

If the current period isn't paid yet, or has already ended, at_period_end: true cancels now instead. Check status in the response to see which happened.

Returns the subscription. If the app has no subscription with that ID, or it is already canceled, returns 404 with code not_found. If at_period_end would replace an upgrade whose payment is in flight, returns 409 with code payment_in_progress.

POST /v1/apps/{appID}/subscriptions/{id}/cancel
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions/01J9ZQ6M1N2P3Q4R5S6T7V8W9X/cancel \
  -X POST \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{"at_period_end": true}'
Response
{
  "id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
  "app_id": "01J9ZQ4Y7R3T6V8W2X5B1C0DEF",
  "customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
  "price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
  "status": "active",
  "starts_at": 1727600000,
  "activated_at": 1727600100,
  "canceled_at": null,
  "cancel_at": 1730192000,
  "metadata": {
    "user_id": "42"
  },
  "created_at": 1727600000,
  "manage_token": "8c1f4a7d2e9b3c6f0a5d8e1b4c7f2a9d3e6b"
}

Keep a subscription

POST/v1/apps/{appID}/subscriptions/{id}/resume

Undoes a scheduled cancel. Billing carries on as before: cancel_at is null again and the next renewal is issued on time. Abuna sends a subscription.updated event and tells the customer the subscription continues.

It also undoes a scheduled downgrade, so the next period stays at the current price. Abuna sends subscription.updated with a downgrade_undone change, and no email.

If nothing is scheduled on an active subscription, returns 200 and changes nothing. If the subscription is already canceled, returns 409 with code subscription_canceled. If the app has no subscription with that ID, returns 404 with code not_found.

POST /v1/apps/{appID}/subscriptions/{id}/resume
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions/01J9ZQ6M1N2P3Q4R5S6T7V8W9X/resume \
  -X POST \
  -H "Authorization: Bearer sk_test_..."
Response
{
  "id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
  "app_id": "01J9ZQ4Y7R3T6V8W2X5B1C0DEF",
  "customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
  "price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
  "status": "active",
  "starts_at": 1727600000,
  "activated_at": 1727600100,
  "canceled_at": null,
  "cancel_at": null,
  "metadata": {
    "user_id": "42"
  },
  "created_at": 1727600000,
  "manage_token": "8c1f4a7d2e9b3c6f0a5d8e1b4c7f2a9d3e6b"
}

List plan options

GET/v1/apps/{appID}/subscriptions/{id}/plan-options

Lists the prices the subscription can move to, each quoted as of now. Amounts change as the period runs down, so quote again right before you show them. See Changing plan for how each number is worked out.

Each option

  • priceobject

    The price: id, name, amount, currency, interval, and interval_count.
  • directionstring

    upgrade or downgrade.
  • amount_dueinteger

    For an upgrade, what the customer pays now. It can be 0. Always 0 for a downgrade.
  • creditinteger

    For an upgrade, the unused time of the current period taken off the new price. Always 0 for a downgrade.
  • new_billing_datetimestampnullable

    For an upgrade, when the new period would end if paid now. Null for a downgrade.
  • effective_attimestampnullable

    For a downgrade, when the new price starts: the end of the paid period. Null for an upgrade.

Returns { "data": [...] }. The list is empty when no other price is eligible or the subscription isn't active. If the app has no subscription with that ID, returns 404 with code not_found.

GET /v1/apps/{appID}/subscriptions/{id}/plan-options
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions/01J9ZQ6M1N2P3Q4R5S6T7V8W9X/plan-options \
  -H "Authorization: Bearer sk_test_..."
Response
{
  "data": [
    {
      "price": {
        "id": "01J9ZQ5P1Q2R3S4T5V6W7X8YZA",
        "name": "Pro monthly",
        "amount": 10000,
        "currency": "XAF",
        "interval": "month",
        "interval_count": 1
      },
      "direction": "upgrade",
      "amount_due": 6666,
      "credit": 3334,
      "new_billing_date": 1731142400,
      "effective_at": null
    },
    {
      "price": {
        "id": "01J9ZQ5B1C2D3E4F5G6H7J8KMN",
        "name": "Basic monthly",
        "amount": 2000,
        "currency": "XAF",
        "interval": "month",
        "interval_count": 1
      },
      "direction": "downgrade",
      "amount_due": 0,
      "credit": 0,
      "new_billing_date": null,
      "effective_at": 1730192000
    }
  ]
}

Metadata

metadata holds your own keys and values on a subscription, like your user ID. Set it when you create the subscription, pass it to a checkout session, or update it later. Every read returns it.

  • Up to 20 keys.
  • Each key is 1 to 40 characters.
  • Each value is a string of up to 500 characters. Numbers, booleans, objects, and null aren't allowed.

The subscription, invoice, and plan_offer events carry this subscription's metadata. Session events carry the session's own metadata instead. Pass metadata on every session you create. See Event types.

Canceling a subscription

A cancel can take effect now or at the end of the paid period. You choose when you cancel in the dashboard or the API. For customers who cancel in the customer portal, the app's When customers cancel setting decides, which is customer_cancel. New apps start with at_period_end. A Request a cancel session uses that setting too, unless you send at_period_end.

  • Now. The subscription ends at once and any unpaid invoice is voided.
  • End of the paid period. The subscription stays active and carries cancel_at. The customer keeps access they paid for. Abuna issues no renewal and sends no renewal reminders. The dashboard and the customer portal show "Ends on" with the date. At that moment the subscription becomes canceled.
  • Unpaid period. If the current period isn't paid yet, an end-of-period cancel ends the subscription now and voids the unpaid invoice. The dashboard and the customer portal say so before the person confirms.
  • Undo. Until the end date, you can keep the subscription in the dashboard or with the resume endpoint, and the customer can keep it in the portal. Billing carries on as before.

The customer gets one email when a cancel is scheduled, with the end date and how to keep the subscription, one when it ends, and one short confirmation if it is undone. Your webhook endpoint gets subscription.updated when a cancel is scheduled or undone, and subscription.canceled when the subscription ends. See Customer notifications.

Test mode works the same way. Abuna records the emails for a Test customer instead of sending them, and Test and Live apps keep their own When customers cancel setting.

Changing plan

An active subscription can move to another price. The customer does it in the customer portal, or confirms a change you send them to with Change plan. You can't change the price directly, because an upgrade needs the customer to pay.

Which prices

Any other price of the same product in the same currency, unless it is archived. Never a price of another product or in another currency. If no price is eligible, the portal shows no option to change plan. A subscription that is still pending can't change plan.

Upgrade or downgrade

Abuna compares cost per day: the price's amount divided by the length in days of one of its periods starting now. A price that costs more per day is an upgrade. One that costs the same or less is a downgrade. This works across intervals, so moving from monthly to yearly follows the same rule.

Upgrades

If the current period is paid, the customer pays the difference now and keeps the old plan until that payment succeeds. At that moment the new plan starts with a fresh full period at the new price, and the billing date moves to the payment date.

The amount is the new price minus the unused value of the current period. The unused value is the period's value times the whole days left, divided by the days in the period. The period's value is what its invoice charged plus any credit on it, so a second upgrade in the same period loses nothing. The result is rounded down and never goes below 0. With 20 of 30 days left on a FCFA 5,000 period, moving to a FCFA 10,000 price costs 10000 − 3334 = 6666, and credit is 3334.

  • Nothing to pay. When the amount is 0, the upgrade takes effect at once. Its invoice is created already paid.
  • Not paid in time. The upgrade lapses at its expires_at: your app's days to pay from the request, or the end of the current period if that comes first. Its invoice is voided and the old plan carries on. There is no penalty. If the upgrade replaced a scheduled cancel, the cancel is scheduled again for its original date, and subscription.updated carries its cancel_at.
  • Unpaid current period. If the current period isn't paid yet, Abuna voids its invoice and starts a full period at the new price now. The new plan is in force at once and the customer pays it through its pay link, with the usual days to pay. There is no credit. While a payment on the old invoice is in flight, the change returns 409 with code payment_in_progress.

An upgrade's invoice has kind plan_change and the unused time in credit. Its amount is what the customer pays.

An upgrade with something to pay needs your app to take payments. Until it can, the change returns 422 with code not_accepting_payments. An upgrade with nothing to pay, and a downgrade, work either way.

Downgrades

A downgrade costs nothing now and refunds nothing. The customer keeps the current plan until the paid period ends, and the next period is issued at the new price. Until then, the customer can undo it in the customer portal, and you can with Keep a subscription. A scheduled downgrade shows in pending_change and in scheduled_price_change on subscription.updated.

A downgrade can be scheduled while the current period is still unpaid. It still starts at that period's end, and if the period stays unpaid, the subscription ends for non-payment as usual. While a downgrade is scheduled, the renewal reminder quotes the new price.

One change at a time

A subscription holds at most one of these: a scheduled cancel, a scheduled downgrade, or an upgrade waiting for payment. Starting one replaces the other. A replaced upgrade has its invoice voided, and the replaced change gets status replaced. While that upgrade's payment is in flight, it can't be replaced: the request returns 409 with code payment_in_progress.

Asking again for the change already waiting, to the same price, changes nothing and returns it as it stands.

A change you send the customer with Change plan waits for them to confirm it. A subscription has at most one change waiting to be confirmed. A new one, or a plan change the customer makes in the portal, replaces it. Confirming it replaces a scheduled cancel, a scheduled downgrade, or an upgrade waiting for payment, and the confirm page says so first. Canceling the subscription now voids an upgrade waiting for payment and withdraws the change waiting to be confirmed.

In the customer portal

The customer portal lists each eligible price with what it costs now: the amount to pay and the new billing date for an upgrade, or the date it starts for a downgrade. An upgrade sends the customer to its pay link, unless there is nothing to pay. While an upgrade waits for payment, the portal shows it with its pay link. Keeping the subscription in the portal also undoes a scheduled downgrade. To send a customer to the portal with a link back to your app, use Open the customer portal.

The url of a Change plan session opens a page with the current plan, the new one, and what the change costs, worked out when the page loads. The customer confirms there, and the change follows the same rules as in the portal. The page stops working when the session expires (7 days, or the end of the current period if that comes first), when a newer change replaces it, or once it is used.

Abuna doesn't email the customer when you create a Change plan session, so send them to its url yourself. The customer gets an email when an upgrade takes effect and when a downgrade is scheduled. Nothing is sent when an upgrade starts waiting for payment, when it lapses, or when a downgrade is undone. See Customer notifications. In Test mode, Abuna records these emails instead of sending them.

Next steps