Overview
Manage recurring subscription lifecycle actions across supported providers.
Subscriptions are recurring product purchases. What you can change depends on the payment provider, the subscription's current state, and whether the action belongs to the seller or customer. Read capabilities immediately before showing or using pause, resume, cancellation, payment-method, plan, or renewal-date controls.
Stripe, PayPal, and Razorpay bill a subscription's renewals themselves. With any
other payment method, such as crypto, Cash App, or a custom payment method, the
store bills the subscription in house: its provider is internal, and SellApp
collects each renewal itself by emailing the customer a payment request.
Lifecycle writes accept an Idempotency-Key header of up to 128 characters or
JSON idempotency_key; the header takes precedence. Save one key per intended
change and reuse it only with identical input. Replaying a key returns the action
the first request recorded instead of starting a second one. Reusing a key with
different input, or from a different actor, returns 422 under
errors.subscription and changes nothing. Dismissals are the
exception: the key is accepted but never replayed, so retrying a dismissal that
already succeeded returns 422 because the action is no longer pending. Read
endpoints ignore the header. A successful response can mean the provider is
still processing the action, not that the change has finished.
The lifecycle endpoints use the SellApp subscription record ID in
productSubscription. The compatibility cancel route can also resolve
supported order IDs. Operation schemas define the current provider-neutral
request and response fields.
Endpoints
- Read subscriptions
- Cancel a subscription
- Cancel immediately with a refund
- View capabilities
- Cancel at period end
- Cancel immediately
- Keep a subscription
- Pause a subscription
- Resume a subscription
- Update payment method
- Preview a plan change
- Confirm a plan change
- Preview a renewal date change
- Confirm a renewal date change
- List subscription actions
- Retrieve a subscription action
- Dismiss a subscription action
Capability responses explain which actions are available for the current actor and provider. Some actions may be customer_only, seller_only, require a redirect, or be unsupported by the payment provider. customer_only describes who owns the action, not who may call the API. A store token can request a plan change or a payment method update on the customer's behalf. Auto-renewal is the exception: set_auto_renew is only available from the customer portal and has no API endpoint.
Buying a plan
You sell a plan by creating an invoice or an order for its variant. Both follow the same rules:
- A cart can carry one subscription plan, and nothing else: a plan mixed with other products or with a second plan returns
422onproduct_variants.{variant_id}. - The plan's
quantitymust be1. Any other quantity returns422onquantitywithEntered quantity must be equal to 1 for subscribable products. - A plan that offers a trial is priced at the trial: nothing for a free trial, the trial price for a paid one. A customer who already had a trial of that product is priced at the plan price instead. Send the IP address the buyer is shopping from as
customer_ip. Without it, onlycustomer_emailis checked, so a buyer could take another trial with a new email address. - When two requests race for the same trial, one can be refused with
422oninvoice, withYour checkout is already being processed. Please try again in a moment.orYou have already used the trial for this product. Refresh the checkout to see the plan price.Create the invoice or order again, and it is priced for the customer as they now stand.
A customer, matched by customer_email, can hold one subscription per plan, whichever payment method bills it. While they hold the plan in an active, trialing, past-due, or unpaid subscription, creating an invoice or order for it again returns 422 on invoice. An active or trialing subscription, or a past-due one the store bills in house that isn't on hold, answers You already have an active subscription to this plan. You can manage it from your customer portal. A past-due or unpaid Stripe, PayPal, or Razorpay subscription answers with the renewal still owed instead, where {store} is the store's name:
- Stripe:
You already have this subscription. Its renewal is unpaid, so pay it from the payment email you received, or contact {store} for help. - Razorpay:
You already have this subscription. Its renewal is unpaid, so update your payment method from the payment email you received, or contact {store} for help. - PayPal, which collects a missed payment no other way:
You already have this subscription. Its renewal is unpaid, so contact {store} for help.
A subscription scheduled to end still counts while the customer can keep it. The message then points them to the customer portal instead: You already have this subscription. You can keep it from your customer portal. A subscription that can no longer be kept doesn't count. That includes a PayPal subscription cancelled at period end and a Razorpay trial cancelled at period end. The plan is the variant the subscription bills on now, so a customer who changed plans can buy their old plan again.
For a subscription the store bills in house that is on hold for an unpaid renewal, the message tells the customer to pay and resume it instead: You already have this subscription. You can pay and resume it from your customer portal.
Checkout doesn't repeat this check. If a pending invoice or order created earlier is paid for a plan the customer already holds, the payment is still recorded, SellApp cancels the second subscription, and subscription.signup_needs_attention tells you how that went.
A coupon discounts only the lines of its kind, set by the coupon's applies_to. A subscriptions coupon discounts the plan's first subscription_payments payments when the payment_method can carry the discount: Stripe, or a method the store bills in house. On PayPal or Razorpay, or on a weekly or daily Stripe plan discounted for more than one payment, the plan sells at its full price and the invoice or order is created without the coupon. These coupons return 422 on coupon instead:
- A
subscriptionscoupon without a plan to discount:Coupon code MEMBER20 only applies to subscriptions. - A
one_timecoupon on a plan:Coupon code SPRING10 does not apply to subscriptions. - A
subscriptionscoupon on a plan that starts with a trial:Coupon code MEMBER20 does not apply to a subscription that starts with a trial. - A fixed amount that covers the plan's whole price:
Coupon code MEMBER20 cannot make a subscription free.
Trials
A subscription plan can start with a free or paid trial, which you set as pricing.trial through the variant pricing endpoint. A subscription sold with a trial is TRIALING until the trial ends, and trial_ends_at records when that is. The plan price is billed from then on.
When the trial ends, the customer makes the first full payment, at the plan price:
- On Stripe, PayPal, and Razorpay, the provider charges the payment method the customer authorised when they started the trial. SellApp emails the customer before the trial ends to say when they'll be charged.
- On a subscription the store bills in house, SellApp creates the renewal order when the trial ends and emails the customer a payment request for it.
Once that payment is made, the subscription is ACTIVE and subscription.renewed is sent, as for any renewal. If it isn't paid, the subscription goes past due like any other unpaid renewal; see Failed payments.
While a subscription is trialing, some lifecycle actions work differently:
- Pausing is refused on every provider. On a subscription the store bills in house, resuming and changing the renewal date are refused too. The capabilities response reports these actions as
unsupported, and their endpoints return422. - A plan change charges nothing during the trial. Where the provider can defer it, the new plan starts at the next renewal. Stripe switches the plan straight away, without proration.
- On Razorpay, cancelling a trial at period end cancels it with Razorpay straight away, so it can't be kept afterwards. The customer keeps access until the trial ends.
Each customer gets one trial per product, across all of its plans. SellApp recognises a returning customer by email address, and by the IP address a trial was taken from in the last 30 days. An unpaid order that sold a trial counts while it can still be paid. A customer who already had a trial is not refused: checkout prices the plan at its full price instead.
Changing a plan's trial doesn't change the trial on orders that already sold one.
Discounts
A coupon or promotion whose applies_to is subscriptions discounts the first subscription_payments payments of a plan. The signup is the first of them, and each renewal after it bills the discounted price until those payments are used up. A subscription the store bills in house then bills the plan price it was sold at, even if you have changed the plan's price since.
Whether the discount is carried depends on who bills the subscription:
| Provider | Discount |
|---|---|
Billed in house (internal) | Carried for 1 to 36 payments on any plan. |
| Stripe | Carried by a single-use Stripe coupon SellApp adds to the subscription on the store's Stripe account. SellApp deletes that coupon once the checkout it was made for completes or expires; a subscription that already has the discount keeps it. More than one discounted payment needs a monthly or yearly plan, because Stripe counts a repeating discount in months; a weekly or daily plan discounted for more than one payment sells at its full price. |
| PayPal | Not carried. The plan sells at its full price. |
| Razorpay | Not carried. The plan sells at its full price. |
On your storefront, a customer whose wallet balance covers the plan's full price can pay with the wallet instead, even beside a Stripe, PayPal, or Razorpay pick. That bills the subscription in house, so it is provider: internal and takes the discount. The API's wallet routes don't sell subscriptions.
A plan that starts with a trial takes no subscription discount. When the payment method can't carry the discount, checkout sells the plan at its full price without an error, so check the order's totals rather than assuming the discount applied.
- Any plan change ends the discount, whichever way it changes. The plan change preview says so in
preview_payload.discount_warningwhile discounted payments remain. On Stripe, SellApp removes its discount before it asks Stripe for the change, so the discount ends even if the change then fails or waits on a payment. - On Stripe, a paused or unpaid billing period still uses up a discounted payment, the same way Stripe counts its own coupons. A discount you added to the subscription in Stripe yourself is left alone.
- Renewal and plan-change orders don't carry the coupon: their
coupon_idis null. A coupon'slimitand a promotion'smax_redemptionscount each subscription once, at signup.
Failed payments
When a renewal payment fails, the subscription's status says where collecting it stands:
PAST_DUE: the payment failed and is still being collected. The customer keeps access meanwhile.UNPAID: the provider has stopped retrying. The customer loses access, such as license keys and community roles, until the payment is made.
Each provider gets there its own way:
| Provider | PAST_DUE | UNPAID |
|---|---|---|
| Stripe | Stripe reports past_due, or paused because a trial ended without a payment method | Stripe reports unpaid |
| PayPal | A failed payment PayPal is still retrying | PayPal suspended the subscription because payments failed |
| Razorpay | Razorpay reports pending | Razorpay reports halted |
Billed in house (internal) | The renewal is still unpaid when its grace period, the days the customer has to pay before it counts as late, ends | Never |
A PayPal subscription you paused with the pause action is suspended at PayPal too. That stays a pause and doesn't change status. PayPal and Razorpay never end an unpaid subscription on their own. Stripe ends one after its last retry only if the Stripe account is set to cancel subscriptions whose payments fail. A subscription the store bills in house is cancelled or put on hold after its last payment reminder, depending on the store's payment retry policy. On hold, it stays PAST_DUE with paused_at set and no access, until the customer pays the renewal from the store's customer portal. Webhook payloads don't carry paused_at: an on-hold subscription shows up there as the paused_for_nonpayment reason, and you read paused_at from the API.
What you can do with it:
- A Stripe, PayPal, or Razorpay subscription that is
PAST_DUEorUNPAIDcan only be cancelled immediately, or, on Razorpay, have its payment method updated too. Its capabilities report every other action asunsupported, with thecustomer_reasonThis subscription has a payment that is still owed, so it can only be cancelled for now.Their endpoints return422with theseller_reasonundererrors.subscription. - Cancelling it can refund the last payment in full, but not pro-rated, meaning a refund of only the unused part of the current period, because that period was never paid for.
refund_last_payment: truewithpro_rated_refund: truereturns422undererrors.subscriptionwithA pro-rated refund isn't available while this subscription has a payment that is still owed. Choose a full refund or no refund instead.Sendpro_rated_refund: falsefor a full refund, or leaverefund_last_paymentoff. - A PayPal subscription that is
UNPAIDisn't collected on its own: PayPal doesn't retry it again, and the customer can't pay the missed payment from a payment page. To give the customer access again, reactivate the subscription in your PayPal account. PayPal then bills the unpaid amount with the next payment. - A Razorpay subscription that is
PAST_DUEorUNPAIDcan also update its payment method; capabilities report itcustomer_onlyinstead ofunsupported. Razorpay then charges the unpaid invoice itself on aPAST_DUEsubscription. On anUNPAIDone, the subscription runs again on the new card, but the invoices it raised while halted are not charged automatically, so charge them from the Razorpay dashboard. - A past-due subscription the store bills in house can only be cancelled immediately too. One on hold comes back only when the customer pays the unpaid renewal from the customer portal, and the API can't take that payment. Capabilities report
resumeascustomer_onlyto a store token and asunsupportedon the customer portal API. Both resume endpoints return422for it; the customer portal API's message isPay the unpaid renewal from your customer portal to resume this subscription.
These refusals last until the payment is made or the subscription is cancelled, so don't retry them unchanged. Read capabilities again after payment_recovered.
A subscription_access entitlement follows the same rule: it is active while the subscription is past due, and suspended while it is unpaid, paused, or on hold.
SellApp tells you through subscription.renewal_needs_attention: past_due when a subscription falls past due, unpaid when a Stripe, PayPal, or Razorpay subscription goes unpaid, whether straight away or after past_due, payment_recovered when the payment is made, and, on Razorpay, resumed_without_payment when a halted one runs again on a new card without its unpaid invoices being paid. A subscription that ends because a payment went unpaid sends subscription.cancelled with reason: "nonpayment". Each is sent once per unpaid renewal, when SellApp sees the status change. The exception is unpaid: a subscription that comes back to PAST_DUE while the payment is still owed, such as an unpaid PayPal subscription you reactivate whose payment then fails again, sends unpaid again if it goes unpaid again. A subscription can still reach PAST_DUE or UNPAID without one, for example after a PayPal failure SellApp only hears about days later. So read the subscription's status when you need to know whether the customer has access.
Deliveries can repeat or arrive out of order. Deduplicate on the envelope's top-level id. Automatic retries resend the same id, but a delivery you replay by hand gets a new one, and so does each webhook channel, so also compare data.status and data.updated_at with what you have already processed, so a late past_due doesn't undo a payment_recovered.
Subscription webhooks
Seven webhook events describe a subscription's lifecycle. subscription.created, subscription.renewed, and subscription.cancelled carry the subscription record itself, including trial_ends_at, and subscription.cancelled adds why it ended. The other four events carry a lifecycle action, the reason a renewal needs attention, or the reason a signup needs the seller's decision. All of them use the standard SellApp webhook envelope (id, created_at, event, version, data, store) and are signed with the store's webhook secret when one is configured. Deduplicate on the top-level id: data.id is the subscription or action ID, which repeats across events. The webhooks guide covers delivery and signature verification, and the Webhook Channels API lets you subscribe a destination to them.
Note
Webhook payloads format every timestamp in data as Y-m-d H:i:s in UTC (for example
2026-08-14 09:12:44), while the API returns ISO-8601 for the same field names. Parse them
separately. This is existing behaviour and applies to every subscription.* event. The
envelope's own created_at is ISO-8601.
subscription.cancelled
Sent when a subscription is cancelled. data is the subscription.created payload plus reason:
nonpayment: the subscription ended because a renewal went unpaid. That happens when a subscription the store bills in house runs out of reminders and its payment retry policy cancels it, or when Stripe cancels a subscription after its last retry.null: every other cancel, including one you or the customer make while a payment is owed.
{
"id": "9d0f88a1-07c5-472a-9a75-8a45d7122396",
"created_at": "2026-09-22T00:07:32.000000Z",
"event": "subscription.cancelled",
"version": "1",
"data": {
"id": 123,
"provider": "stripe",
"provider_subscription_id": "sub_1QwertyExample",
"provider_customer_id": "cus_Example123",
"provider_customer_email": "maya.chen@example.com",
"status": "CANCELED",
"current_period_start": "2026-09-01 00:00:00",
"current_period_end": "2026-10-01 00:00:00",
"trial_ends_at": null,
"cancel_at_period_end": false,
"metadata": {},
"created_at": "2026-03-01 00:00:00",
"updated_at": "2026-09-22 00:07:31",
"store_id": 1,
"product_variant_id": 99,
"original_invoice_id": 456,
"reason": "nonpayment"
},
"store": 1
}subscription.action
data is the subscription action object the lifecycle endpoints return, with the same keys and webhook timestamp formatting. SellApp sends this event when a customer-initiated action reaches confirmed, or when any action other than a plan change reaches failed or expired, regardless of who started it. Confirmed actions started by a seller, a store token, or SellApp itself are not sent. The event is only sent for the transition out of pending, so a resolved action is never sent twice.
{
"id": "a052ab90-581d-49b6-b8ab-f62cde3dca76",
"created_at": "2026-08-14T09:12:45.000000Z",
"event": "subscription.action",
"version": "1",
"data": {
"id": 4821,
"product_subscription_id": 123,
"subscription_id": "sub_1QwertyExample",
"action": "pause",
"status": "confirmed",
"provider": "stripe",
"proration_order_id": null,
"error": null,
"customer_message": null,
"confirmed_at": "2026-08-14 09:12:44",
"failed_at": null,
"created_at": "2026-08-14 09:12:41",
"updated_at": "2026-08-14 09:12:44"
},
"store": 1
}subscription.plan_change_needs_attention
The plan-change counterpart of subscription.action, with the same action payload. A change_plan action that reaches failed or expired is sent here instead of on subscription.action, so a destination can handle plan-change failures separately. Subscribe to both events to receive every failure in one place.
{
"id": "ae02a8dc-fc77-40c6-9acf-5c773440b806",
"created_at": "2026-08-14T11:02:08.000000Z",
"event": "subscription.plan_change_needs_attention",
"version": "1",
"data": {
"id": 4832,
"product_subscription_id": 124,
"subscription_id": null,
"action": "change_plan",
"status": "failed",
"provider": "internal",
"proration_order_id": 88231,
"error": "The proration payment for the new plan was never completed.",
"customer_message": null,
"confirmed_at": null,
"failed_at": "2026-08-14 11:02:07",
"created_at": "2026-08-13 16:40:15",
"updated_at": "2026-08-14 11:02:07"
},
"store": 1
}subscription.renewal_needs_attention
Sent when a renewal needs the seller's attention: its payment failed, a failed payment was paid after all, or the renewal stopped in a way only the seller can settle. past_due, payment_recovered, and payment_refunded are sent whichever provider bills the subscription, and unpaid for Stripe, PayPal, and Razorpay subscriptions, so provider-billed integrations should subscribe to this event too. resumed_without_payment is Razorpay-only. The other reasons only apply to subscriptions the store bills in house. data is the subscription.created payload (the sixteen keys in the example below) plus four keys that explain the reason, so an integration that already parses subscription.created can parse this event as well.
{
"id": "5a6287d0-54cc-47fc-9c73-5417ccb0ba4d",
"created_at": "2026-09-01T01:04:13.000000Z",
"event": "subscription.renewal_needs_attention",
"version": "1",
"data": {
"id": 123,
"provider": "stripe",
"provider_subscription_id": "sub_1QwertyExample",
"provider_customer_id": "cus_Example123",
"provider_customer_email": "maya.chen@example.com",
"status": "PAST_DUE",
"current_period_start": "2026-09-01 00:00:00",
"current_period_end": "2026-10-01 00:00:00",
"trial_ends_at": null,
"cancel_at_period_end": false,
"metadata": {},
"created_at": "2026-03-01 00:00:00",
"updated_at": "2026-09-01 01:04:12",
"store_id": 1,
"product_variant_id": 99,
"original_invoice_id": 456,
"reason": "past_due",
"reason_label": "A subscription renewal is past due",
"summary": "The renewal for a subscription is still unpaid, so the subscription is now past due.",
"renewal_order_id": null
},
"store": 1
}reason: why the renewal needs attention. Branch on this field rather than onreason_labelorsummary.past_due: the renewal is still unpaid and the subscription is nowPAST_DUE. The payment is still being collected, so there is nothing to do yet.unpaid: the subscription wentUNPAIDbecause the provider stopped retrying, either straight away or afterpast_due. The customer's access is paused until they pay. It's sent once per unpaid bill, and again if the subscription comes back toPAST_DUEwith the bill still owed and then goes unpaid again. Only Stripe, PayPal, and Razorpay subscriptions reach it.payment_recovered: a renewal that was past due or unpaid was paid, and the subscription is active again.resumed_without_payment: a halted Razorpay subscription ran again after the customer updated their card, but the invoices it raised while halted are not charged automatically. Charge them from the Razorpay dashboard. Only Razorpay subscriptions reach it.payment_refunded: an order for the subscription was refunded, and the subscription itself was left running. Cancel it if the refund was meant to end it.paused_for_nonpayment: the subscription was put on hold because its renewal went unpaid. The customer can pay and resume it from the customer portal.payment_after_closure: a renewal payment arrived after that renewal had been given up, so nothing was renewed or delivered.completion_failed: a renewal was paid, but finishing it didn't complete, so the delivery may not have gone out.period_mismatch: a second renewal payment arrived for a period that was already paid, so the renewal date was left alone.plan_change_after_closure: the payment for a plan change arrived after that change had expired, so the plan was left as it was.renewal_not_collectable: the renewal couldn't be billed because the plan or the order it bills from is no longer usable, so the customer was never asked to pay.
reason_label: the short seller-facing label for that reason, such asA renewal could not be billed.summary: a one-sentence, seller-facing explanation of what happened.renewal_order_id: the order the renewal was billed on, ornullwhen there was none.paused_for_nonpayment,renewal_not_collectable,unpaid, andresumed_without_paymentnever have one, and neither dopast_dueandpayment_recoveredon a Stripe, PayPal, or Razorpay subscription, because the provider bills the renewal.
subscription.signup_needs_attention
Sent when a signup leaves the seller something to decide. That happens when a customer signs up again for a plan they already hold, whichever provider bills either subscription, or when an order sold a trial but doesn't say how long the trial is or what it costs, so no subscription could be started. The payment is captured and the order still completes, so the seller decides what happens next, such as refunding the order. data names the order and the plan it bought, not a subscription.
{
"id": "c5f9d626-5fac-488b-9a74-5895a7b1f4aa",
"created_at": "2026-08-15T10:41:27.000000Z",
"event": "subscription.signup_needs_attention",
"version": "1",
"data": {
"order_id": 88240,
"product_variant_id": 99,
"reason": "duplicate",
"reason_label": "Signup paid for a plan the customer already has",
"summary": "A customer paid for a plan they already subscribe to, so the second subscription was cancelled. Decide whether to refund the order.",
"charge": null
},
"store": 1
}order_id: the order of the signup that needs attention.product_variant_id: the subscription plan the order bought.reason: what happened. Branch on this field rather than onreason_labelorsummary.duplicate: the customer paid for a plan they already subscribe to, so the second subscription was cancelled. Their existing subscription is not affected. Decide whether to refund the order.duplicate_free: the customer signed up again, at no charge, for a plan they already subscribe to, so the second subscription was cancelled. Nothing was charged, so there is nothing to refund.duplicate_still_live: the customer signed up again for a plan they already subscribe to, but the second subscription could not be cancelled at the payment provider. SellApp keeps retrying. If it stays active there, cancel it at the provider and decide whether to refund the order.duplicate_charged: a duplicate subscription that was cancelled in SellApp was charged again at the payment provider, and no order was booked for that charge.order_idis the duplicate's original order, andchargenames the provider charge to refund. Refund it at the provider and make sure the subscription is cancelled there.missing_trial_terms: the order sold a trial but doesn't record how long the trial is or what it costs, so no subscription was started. Start the subscription by hand, or refund the order. This only happens on subscriptions the store bills in house.
reason_label: the short seller-facing label for that reason.summary: a short, seller-facing explanation of what happened and what to do.charge: the provider charge to refund whenreasonisduplicate_charged, andnullfor every other reason. It hasid, the charge's own identifier at the provider, plusamount_centsandcurrency. Those two arenullwhen the provider didn't report the amount.
Field names across the subscription events
The two payload families use different names for the same identifiers. subscription.action and subscription.plan_change_needs_attention follow the API's action object. subscription.created, subscription.renewed, subscription.cancelled, and subscription.renewal_needs_attention follow the subscription webhook shape. An integration that parses both needs to map them accordingly.
| Value | Action payloads | Subscription payloads |
|---|---|---|
| The SellApp subscription ID | product_subscription_id | id |
| The provider subscription ID | subscription_id | provider_subscription_id |
| The provider customer ID | — | provider_customer_id |
| The customer email | — | provider_customer_email |
| The originating order ID | — | original_invoice_id |
The subscription API returns those last three as provider_customer_id (and the deprecated customer_id), customer_email, and invoice_id. Only the customer ID is a provider value, so it is null for a subscription the store bills in house, like provider_subscription_id. The other two are SellApp values: the email comes from checkout, and the order ID is resolved from the subscription's first invoice line, which every in-house subscription has.
See Handle subscriptions.