Subscriptions reference
The lookup page for Subscriptions. For the steps in order, see the Integration guide. The full request and response schemas are in the API reference.
Calls
Every call takes a secret key. Times on a subscription are Unix seconds.
| Call | What it does | Restricted key needs |
|---|---|---|
GET /v1/subscription_products/{product}/terms | Reads a product's terms before the sign-up | payment_intents.write |
POST /v1/payment_intents with recurring | Charges the first payment and starts the subscription | payment_intents.write |
GET /v1/subscriptions | Lists subscriptions, newest first. Filters: buyer_id, email, status. Pages with limit (1 to 100, default 10) and starting_after while has_more is true | subscriptions.read |
GET /v1/subscriptions/{id} | Reads one subscription | subscriptions.read |
POST /v1/subscriptions/{id}/cancel | Cancels now, or at the end of the paid period | subscriptions.write |
DELETE /v1/subscriptions/{id}/scheduled_cancel | Withdraws a cancel scheduled for the end of the period | subscriptions.write |
POST /v1/subscriptions/{id}/pause | Pauses renewals | subscriptions.write |
POST /v1/subscriptions/{id}/resume | Resumes a paused subscription | subscriptions.write |
POST /v1/subscriptions/{id}/skip | Skips the next renewal | subscriptions.write |
POST /v1/subscriptions/{id}/next_bill_date | Moves the next charge to a date | subscriptions.write |
POST /v1/subscriptions/{id}/interval | Changes how often it renews | subscriptions.write |
POST /v1/subscriptions/{id}/price | Sets this subscription's renewal price | subscriptions.write |
POST /v1/subscriptions/{id}/discount | Sets a discount on future renewals | subscriptions.write |
DELETE /v1/subscriptions/{id}/discount | Removes the discount | subscriptions.write |
POST /v1/subscriptions/{id}/portal_links | Creates a one-time link to the customer's subscription page | subscriptions.write |
A key with subscriptions.write can also read. When Subscriptions is not switched on for your account, every /v1/subscriptions call answers 503 not_switched_on.
Rules for every change
Idempotency-Keyis required. Up to 255 characters. A retry with the same key and body changes nothing twice and answers200withX-Idempotency-Replay: true, showing the subscription as it is now. The same key on a different change answers409idempotency_key_reused. A change that was already in effect (changed: false) records nothing, so its key can be used again.?dry_run=trueruns the change and rolls it back. The answer shows what it would do, or the error it would return. Nothing is kept, no event is sent, and no key is needed.X-Vonpay-Actor-Label(optional) names the system making the change: 1 to 64 lowercase letters, digits and_ . : -. It comes back asactor.labelon the event. Never personal data.
Cancel
| Field | Required | Meaning |
|---|---|---|
reason | Yes | too_expensive, not_using, switching_provider, technical_issues, other, chargeback or duplicate. Recorded on the subscription. |
at_period_end | No | true cancels when the paid period ends: nothing more is charged, and the customer keeps what they paid for. Needs an active subscription. Not allowed with chargeback. |
note | No | Up to 2,000 characters, kept encrypted. Never card details. |
payment_intent_id | No | With chargeback only: the disputed payment, one of this subscription's. |
A cancel now always goes through, even while a renewal is being charged: the answer then carries the renewal_in_flight warning. A cancel never refunds.
Pause and resume
resumes_on (optional) is a date, YYYY-MM-DD, from tomorrow to 365 days ahead, in your billing time zone. The subscription resumes by itself that day. On a paused subscription, it sets, moves or (with null) clears that date. A bad date answers 400 subscription_date_out_of_range with min_date and max_date.
A resume whose next bill date is already past moves it to tomorrow. There is no catch-up charge.
Skip
Moves the next bill date one billing interval later, so the next renewal is not charged. It applies at once. Allowed on active. The new date can be at most two billing intervals plus 90 days from today; a skip past that limit answers 400 subscription_date_out_of_range with reason: skip_limit_reached.
Next bill date
date (YYYY-MM-DD, in your billing time zone) moves the next charge. Allowed on active and past_due. The date runs from tomorrow to one billing interval plus 90 days from today.
- A later date applies at once.
- A sooner date waits for the notice we email to the customer (
pending_schedule_changeon the subscription). It applies once that email has gone out at least 7 days before the new date. Nothing is charged from it until then. If the email cannot go out at least 7 days before that date, the change is dropped. - A sooner date less than 8 days away needs
customer_requested: trueand anotesaying how the customer asked. It then applies at once. Without them, it answers409schedule_notice_required. - Sending the date it already has withdraws a pending date change.
Interval
unit (day, week or month) and count (1 to 365) change how often it renews. The next bill date does not move; the renewal after it uses the new interval. Allowed on active and paused. Billing less often applies at once. Billing more often waits for the notice we email to the customer, and applies from the first charge at least 7 days after that email. Sending the interval it already has withdraws a pending interval change.
Only one schedule change waits at a time: with a date or interval change pending, the other answers 409 schedule_change_pending.
Price
amount (minor units, in the subscription's currency) sets this subscription's renewal price, and price.source becomes custom. It applies to the next renewal. Allowed on active, paused and past_due.
- Tell the customer before a raised price is charged. The answer carries a
price_increase_noticewarning. - A restricted key may raise what the customer is charged only up to its own per-charge maximum. A full secret key may raise it without limit. Otherwise it answers
403price_increase_not_permitted.
Discount
POST takes percent_off (below 100, up to two decimals) or amount_off with currency (the subscription's own), and a duration: once (the next renewal), repeating (with duration_in_renewals, 1 to 120) or forever. DELETE removes it. Allowed on active, paused and past_due.
- One discount at a time. A new one replaces the current one, and the answer warns
discount_replaced. - 100% off is refused with
400subscription_discount_invalid. To stop charging, cancel or pause. - A period already being charged keeps the discount it started with, and the answer carries
discount_applies_next_cycle. - A restricted key: a smaller discount, or removing one, raises what the customer is charged, and is checked like a price raise.
Manage subscription link
Answers 201 with url and expires_at. The link opens the customer's page for this subscription only, once, within five minutes. Only active, past_due, paused and held subscriptions get a link; for an ended one it answers 404 subscription_not_found. See Let the customer manage it.
return_url(optional) is where the page's Back button goes: anhttpsaddress on one of your own domains, or422return_url_not_allowed. A test key may also send alocalhostaddress.Idempotency-Keyis optional here. A repeat with the same key and request answers a newurlfor the same link, and the earlierurlstops working. The same key once that link was opened or expired answers422idempotency_replay_incompatible.
While a renewal is being charged
A skip, date, interval, price or discount change waits for a renewal charge that was already sent: it answers 409 subscription_renewal_in_progress until the charge's answer arrives. Retry with the same key after about a minute. A skip or date change while a cancel is scheduled answers 409 subscription_cancel_scheduled: withdraw the scheduled cancel first.
The change answer
| Field | Meaning |
|---|---|
object | subscription_change |
action | The change made. |
dry_run | true on a dry run. |
changed | false when the change was already in effect. |
subscription | The subscription as it is now. |
warnings | A list of { code, message }, for example renewal_in_flight or already_scheduled. More codes may be added. |
The subscription object
| Field | Type | Meaning |
|---|---|---|
object | string | subscription |
id | string | vsub_live_… or vsub_test_…. |
livemode | boolean | false for a test subscription. |
status | string | See Statuses. More values may be added. |
buyer_id | string | null | Your own customer id, as sent at sign-up. |
product | string | Your product reference. |
price | object | amount (the renewal price before any discount, in minor units), currency, and source: product, or custom once a price was set for this subscription alone. |
interval | object | unit (day, week or month) and count. |
billing_cycle | integer | Paid periods so far. The sign-up is 1. |
next_bill_at | integer | null | The next charge attempt. null while paused, on hold or ended. |
cancel_at_period_end | boolean | A cancel is scheduled for the end of the paid period. |
cancel_at | integer | null | When that scheduled cancel takes effect. |
pause | object | null | Set while paused: paused_at, and resumes_at (null until a person resumes it). |
discount | object | null | A discount on future renewals: id, percent_off or amount_off, duration (once, repeating, forever) and renewals_remaining. |
upcoming_renewal | object | null | The next charge: at, amount (after any discount; 0 is a free renewal), base_amount, discount_amount, free. null when nothing is due. |
pending_schedule_change | object | null | A date or interval change that bills sooner or more often, waiting for its notice to the customer. null when there is none. |
payment_method | object | null | The card renewals charge: id, brand, last4. |
cancel_reason | string | null | Why it ended or was cancelled. |
cancelled_at | integer | null | When it was cancelled. |
created_at | integer | When it was created. |
metadata | object | Your own metadata from the sign-up payment. |
Events
| Event | Sent when |
|---|---|
subscription.created | A sign-up payment created a subscription. payment_intent_id is that payment. |
subscription.renewed | A renewal charge succeeded. Its charge.succeeded and payment_intent.succeeded carry the same subscription_id and billing_cycle. |
subscription.payment_failed | A renewal charge was declined, once per declined attempt. The last one, when no retries are left, has status: "failed" and a cancel_reason (hard_decline or retries_exhausted). No subscription.cancelled follows it. |
subscription.cancelled | The subscription was cancelled: by the customer, by your staff, after a chargeback, or after the card's permission for repeat charges was withdrawn. |
subscription.updated | Anything else changed: paused, resumed, put on hold, released, price, next date or interval changed, a discount set or removed, a cancel at the end of the period scheduled or withdrawn, or restarted after a cancel. A restart is updated, never a second created. |
You have to select them. Each is delivered only to an endpoint that names it in its events. "*" does not include them, and they are never added to an existing endpoint for you.
All five carry the same data, with every key present and null when it does not apply:
{
"id": "vp_evt_test_4q8n2m7r5t",
"type": "subscription.payment_failed",
"created": 1796396400,
"livemode": false,
"data": {
"subscription_id": "vsub_test_x7Q2mN9pR4tL8vWc",
"status": "past_due",
"billing_cycle": 2,
"next_bill_at": 1796655600,
"amount": 999,
"renewal_amount": 999,
"currency": "USD",
"payment_intent_id": "vpi_test_7Hq2Lm9Rt4Xw8NcP",
"action": "soft",
"decline_message": "The card was declined for insufficient funds.",
"decline_code": "insufficient_funds",
"cancel_reason": null,
"changed_fields": null,
"actor": { "type": "system", "api_key_id": null, "label": null },
"cancel_at_period_end": false,
"discount": null,
"metadata": { "order_id": "ord_42" }
}
}
data field | Type | Meaning |
|---|---|---|
subscription_id | string | The subscription (vsub_…). |
status | string | The status after this change. More values may be added. |
billing_cycle | integer | null | The cycle of the payment this event is about (the sign-up is 1). null when the event is about no payment. |
next_bill_at | integer | null | Unix seconds of the next charge attempt (a retry date while past_due). null while paused or on hold, and once ended. |
amount | integer | null | The amount of the payment this event is about, in minor units. |
renewal_amount | integer | The current renewal price, in minor units. |
currency | string | ISO 4217, for both amounts. |
payment_intent_id | string | null | The payment this event is about: the sign-up on created, the renewal attempt on renewed and payment_failed, the new payment on a restart. |
action, decline_message, decline_code | string | null | On payment_failed only: the same values as on charge.failed. |
cancel_reason | string | null | On cancelled, and on the final payment_failed. More values may be added. |
changed_fields | string[] | null | On updated only: any of status, renewal_amount, next_bill_at, interval, payment_method (on a restart with a new card), cancel_at_period_end, discount. More values may be added. |
actor | object | null | Who made the change. type is api_key (your API), staff (your dashboard), customer (their subscription page), system (the renewal job or a schedule) or webhook (a connected store). api_key_id and label are set for api_key only. |
cancel_at_period_end | boolean | null | A cancel is scheduled for the end of the paid period. |
discount | object | null | The discount on future renewals, as on the subscription. |
metadata | object | null | Your own metadata from the sign-up payment, so you can match every event to your order or customer. Each renewal payment's metadata carries the same copy. |
Each event describes the subscription as that change left it. Events arrive in no guaranteed order, also relative to the charge.* events for the same payment. Order them by the envelope's created, and deduplicate on id. Verify every event as described in Webhook signature verification.
Error codes
| Where | Codes |
|---|---|
Sign-up (POST /v1/payment_intents with recurring) | Subscription sign-up refusals |
| Terms read | subscription_product_not_found, product_not_ready, recurring_not_enabled, recurring_terms_mismatch |
/v1/subscriptions calls | Subscription management refusals |
SDKs and tools
| Call | Node (@vonpay/checkout-node 4.4.0+) | Python (vonpay-checkout 4.4.0+) |
|---|---|---|
| Read terms | subscriptionProducts.terms(product) | subscription_products.terms(product) |
| List | subscriptions.list({ buyerId, email, status, limit, startingAfter }) | subscriptions.list(buyer_id=…, email=…, status=…, limit=…, starting_after=…) |
| Read | subscriptions.retrieve(id) | subscriptions.retrieve(id) |
| Cancel | subscriptions.cancel(id, { reason, atPeriodEnd?, note?, paymentIntentId? }, options) | subscriptions.cancel(id, reason=…, at_period_end=…, …) |
| Withdraw a scheduled cancel | subscriptions.unscheduleCancel(id, options) | subscriptions.unschedule_cancel(id, …) |
| Pause | subscriptions.pause(id, { resumesOn? }, options) | subscriptions.pause(id, resumes_on=…, …) |
| Resume | subscriptions.resume(id, options) | subscriptions.resume(id, …) |
| Skip | subscriptions.skip(id, options) | subscriptions.skip(id, …) |
| Next bill date | subscriptions.setNextBillDate(id, { date, customerRequested?, note? }, options) | subscriptions.set_next_bill_date(id, date, customer_requested=…, note=…, …) |
| Interval | subscriptions.setInterval(id, { unit, count }, options) | subscriptions.set_interval(id, unit, count, …) |
| Price | subscriptions.setPrice(id, { amount }, options) | subscriptions.set_price(id, amount, …) |
| Discount | subscriptions.setDiscount(id, { duration, percentOff?, amountOff?, currency?, durationInRenewals? }, options) | subscriptions.set_discount(id, duration, percent_off=…, amount_off=…, currency=…, duration_in_renewals=…, …) |
| Remove discount | subscriptions.removeDiscount(id, options) | subscriptions.remove_discount(id, …) |
| Manage subscription link | subscriptions.createPortalLink(id, { returnUrl? }, { idempotencyKey? }), returns { url, expiresAt } | subscriptions.create_portal_link(id, return_url=…, idempotency_key=…) |
options is { idempotencyKey, dryRun?, actorLabel? } in Node, and the keyword arguments idempotency_key, dry_run and actor_label in Python. The SDK refuses a change without a key before it sends anything, unless it is a dry run. For the link, the SDK makes a key when you pass none.
MCP server (@vonpay/checkout-mcp 6.3.0+): one tool for each call above, for example vonpay_checkout_list_subscriptions and vonpay_checkout_cancel_subscription. Every change needs an idempotency key unless it is a dry run, and on a live key a person must approve it. See MCP server.
CLI (@vonpay/checkout-cli 0.17.0+): vonpay checkout trigger <event> --url <your local endpoint> sends a sample of each of the five events. See CLI.