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
idstringUnique identifier for the subscription.app_idstringThe app the subscription belongs to.customer_idstringThe customer who pays.price_idstringThe price the customer pays.statusstringpendinguntil the first period is paid, thenactive.canceledonce it ends.starts_attimestampWhen the first billing period starts, in Unix seconds.activated_attimestampnullableWhen the first period was paid.canceled_attimestampnullableWhen the subscription was canceled.cancel_attimestampnullableWhen the subscription will end, if a cancel is scheduled for the end of the paid period. Null otherwise. The subscription staysactiveuntil then.metadataobjectYour own keys and values.{}when there are none. A subscription started by a checkout session gets the session's. See Metadata.created_attimestampWhen the subscription was created, in Unix seconds.manage_tokenstringThe token in the customer portal link. Anyone with the link can see and cancel the subscription, so keep it private.
{
"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_idstringrequiredThe customer to bill.price_idstringrequiredThe price to bill them.metadataobjectYour 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.
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"}'{
"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
statusstringReturns only subscriptions with this status:pending,active, orcanceled.emailstringReturns only subscriptions whose customer's email contains this text. Case doesn't matter.
Extra attributes
customerobjectWho pays.Show child attributesHide child attributes
namestringnullableThe customer's name.emailstringThe customer's email address.
priceobjectWhat they pay.Show child attributesHide child attributes
namestringThe price's name.amountintegerThe amount for each period, in the currency's smallest unit.currencystringXAForXOF.intervalstringday,week,month, oryear.interval_countintegerHow many intervals make up one period.
next_payment_attimestampnullableWhen 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.
curl 'https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions?status=active' \
-H "Authorization: Bearer sk_test_..."[
{
"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
subscriptionobjectThe subscription object.subscription_page_urlstringThe customer portal link, where the customer sees and cancels the subscription.customerobjectThe customer object, plustelegram_connected.priceobjectThe price object.entriesarrayEvery billing period in force, oldest first. An upgrade's period waiting for payment, or one that lapsed, isn't listed. The waiting one shows inpending_change, and its invoice is still in the invoice list.Show child attributesHide child attributes
idstringThe billing period's ID.starts_attimestampWhen the period starts.ends_attimestampWhen the period ends.grace_period_ends_attimestampThe date to pay by.paid_attimestampnullableWhen the period was paid.pay_urlstringThe pay link for the period.invoiceobjectnullableThe period's invoice object.invoice_urlstringnullableA link to the invoice page. It expires after 7 days.
paymentsarrayEvery payment on the subscription, newest first.pending_changeobjectnullableThe 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 incancel_atinstead.Show child attributesHide child attributes
idstringThe plan change.typestringupgradeordowngrade.priceobjectThe new price:id,name,amount,currency,interval, andinterval_count.requested_bystringcustomerwhen they chose it in the customer portal, ormerchantwhen they confirmed a change you sent them to.created_attimestampWhen the change was requested.amount_dueintegernullableWhat the customer pays to switch now. Null for a downgrade.creditintegernullableThe unused time of the current period taken off the new price. Null for a downgrade.expires_attimestampnullableIf the upgrade isn't paid by then, it lapses and the current plan carries on. Null for a downgrade.pay_urlstringnullableThe pay link for the upgrade. Null for a downgrade.effective_attimestampnullableWhen the downgrade starts. Null for an upgrade.
plan_offerobjectnullableThe plan change waiting for the customer to confirm, or null. It is null once it is past itsexpires_at.plan_changesarrayEvery plan change on the subscription, newest first.Show child attributesHide child attributes
idstringThe plan change.directionstringupgradeordowngrade.statusstringawaiting_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), orcanceled(the subscription ended first).from_priceobjectThe price before, in the same shape as above.to_priceobjectThe new price.requested_bystringcustomerormerchant.amountintegernullableWhat the upgrade charges. Null for a downgrade.creditintegernullableThe unused time taken off the upgrade. Null for a downgrade.effective_attimestampnullableWhen it took effect, or for a downgrade, when it will. Null for an upgrade that hasn't taken effect.created_attimestampWhen it was requested.resolved_attimestampnullableWhen it left the pending state.
If the app has no subscription with that ID, returns 404 with code not_found.
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions/01J9ZQ6M1N2P3Q4R5S6T7V8W9X \
-H "Authorization: Bearer sk_test_..."{
"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
metadataobjectrequiredThe 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.
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"}}'{
"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_endbooleanOptional,falseby default. Withtrue, the subscription staysactiveuntil the current paid period ends, and then ends by itself. A value that isn't a boolean returns400with codeinvalid_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.
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}'{
"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.
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions/01J9ZQ6M1N2P3Q4R5S6T7V8W9X/resume \
-X POST \
-H "Authorization: Bearer sk_test_..."{
"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
priceobjectThe price:id,name,amount,currency,interval, andinterval_count.directionstringupgradeordowngrade.amount_dueintegerFor an upgrade, what the customer pays now. It can be0. Always0for a downgrade.creditintegerFor an upgrade, the unused time of the current period taken off the new price. Always0for a downgrade.new_billing_datetimestampnullableFor an upgrade, when the new period would end if paid now. Null for a downgrade.effective_attimestampnullableFor 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.
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/subscriptions/01J9ZQ6M1N2P3Q4R5S6T7V8W9X/plan-options \
-H "Authorization: Bearer sk_test_..."{
"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
activeand carriescancel_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 becomescanceled. - 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, andsubscription.updatedcarries itscancel_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
409with codepayment_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 confirm page
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.