Skip to main content

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.

CallWhat it doesRestricted key needs
GET /v1/subscription_products/{product}/termsReads a product's terms before the sign-uppayment_intents.write
POST /v1/payment_intents with recurringCharges the first payment and starts the subscriptionpayment_intents.write
GET /v1/subscriptionsLists subscriptions, newest first. Filters: buyer_id, email, status. Pages with limit (1 to 100, default 10) and starting_after while has_more is truesubscriptions.read
GET /v1/subscriptions/{id}Reads one subscriptionsubscriptions.read
POST /v1/subscriptions/{id}/cancelCancels now, or at the end of the paid periodsubscriptions.write
DELETE /v1/subscriptions/{id}/scheduled_cancelWithdraws a cancel scheduled for the end of the periodsubscriptions.write
POST /v1/subscriptions/{id}/pausePauses renewalssubscriptions.write
POST /v1/subscriptions/{id}/resumeResumes a paused subscriptionsubscriptions.write
POST /v1/subscriptions/{id}/skipSkips the next renewalsubscriptions.write
POST /v1/subscriptions/{id}/next_bill_dateMoves the next charge to a datesubscriptions.write
POST /v1/subscriptions/{id}/intervalChanges how often it renewssubscriptions.write
POST /v1/subscriptions/{id}/priceSets this subscription's renewal pricesubscriptions.write
POST /v1/subscriptions/{id}/discountSets a discount on future renewalssubscriptions.write
DELETE /v1/subscriptions/{id}/discountRemoves the discountsubscriptions.write
POST /v1/subscriptions/{id}/portal_linksCreates a one-time link to the customer's subscription pagesubscriptions.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-Key is required. Up to 255 characters. A retry with the same key and body changes nothing twice and answers 200 with X-Idempotency-Replay: true, showing the subscription as it is now. The same key on a different change answers 409 idempotency_key_reused. A change that was already in effect (changed: false) records nothing, so its key can be used again.
  • ?dry_run=true runs 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 as actor.label on the event. Never personal data.

Cancel​

FieldRequiredMeaning
reasonYestoo_expensive, not_using, switching_provider, technical_issues, other, chargeback or duplicate. Recorded on the subscription.
at_period_endNotrue 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.
noteNoUp to 2,000 characters, kept encrypted. Never card details.
payment_intent_idNoWith 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_change on 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: true and a note saying how the customer asked. It then applies at once. Without them, it answers 409 schedule_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_notice warning.
  • 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 403 price_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 400 subscription_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.

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: an https address on one of your own domains, or 422 return_url_not_allowed. A test key may also send a localhost address.
  • Idempotency-Key is optional here. A repeat with the same key and request answers a new url for the same link, and the earlier url stops working. The same key once that link was opened or expired answers 422 idempotency_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​

FieldMeaning
objectsubscription_change
actionThe change made.
dry_runtrue on a dry run.
changedfalse when the change was already in effect.
subscriptionThe subscription as it is now.
warningsA list of { code, message }, for example renewal_in_flight or already_scheduled. More codes may be added.

The subscription object​

FieldTypeMeaning
objectstringsubscription
idstringvsub_live_… or vsub_test_….
livemodebooleanfalse for a test subscription.
statusstringSee Statuses. More values may be added.
buyer_idstring | nullYour own customer id, as sent at sign-up.
productstringYour product reference.
priceobjectamount (the renewal price before any discount, in minor units), currency, and source: product, or custom once a price was set for this subscription alone.
intervalobjectunit (day, week or month) and count.
billing_cycleintegerPaid periods so far. The sign-up is 1.
next_bill_atinteger | nullThe next charge attempt. null while paused, on hold or ended.
cancel_at_period_endbooleanA cancel is scheduled for the end of the paid period.
cancel_atinteger | nullWhen that scheduled cancel takes effect.
pauseobject | nullSet while paused: paused_at, and resumes_at (null until a person resumes it).
discountobject | nullA discount on future renewals: id, percent_off or amount_off, duration (once, repeating, forever) and renewals_remaining.
upcoming_renewalobject | nullThe next charge: at, amount (after any discount; 0 is a free renewal), base_amount, discount_amount, free. null when nothing is due.
pending_schedule_changeobject | nullA date or interval change that bills sooner or more often, waiting for its notice to the customer. null when there is none.
payment_methodobject | nullThe card renewals charge: id, brand, last4.
cancel_reasonstring | nullWhy it ended or was cancelled.
cancelled_atinteger | nullWhen it was cancelled.
created_atintegerWhen it was created.
metadataobjectYour own metadata from the sign-up payment.

Events​

EventSent when
subscription.createdA sign-up payment created a subscription. payment_intent_id is that payment.
subscription.renewedA renewal charge succeeded. Its charge.succeeded and payment_intent.succeeded carry the same subscription_id and billing_cycle.
subscription.payment_failedA 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.cancelledThe subscription was cancelled: by the customer, by your staff, after a chargeback, or after the card's permission for repeat charges was withdrawn.
subscription.updatedAnything 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 fieldTypeMeaning
subscription_idstringThe subscription (vsub_…).
statusstringThe status after this change. More values may be added.
billing_cycleinteger | nullThe cycle of the payment this event is about (the sign-up is 1). null when the event is about no payment.
next_bill_atinteger | nullUnix seconds of the next charge attempt (a retry date while past_due). null while paused or on hold, and once ended.
amountinteger | nullThe amount of the payment this event is about, in minor units.
renewal_amountintegerThe current renewal price, in minor units.
currencystringISO 4217, for both amounts.
payment_intent_idstring | nullThe 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_codestring | nullOn payment_failed only: the same values as on charge.failed.
cancel_reasonstring | nullOn cancelled, and on the final payment_failed. More values may be added.
changed_fieldsstring[] | nullOn 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.
actorobject | nullWho 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_endboolean | nullA cancel is scheduled for the end of the paid period.
discountobject | nullThe discount on future renewals, as on the subscription.
metadataobject | nullYour 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​

WhereCodes
Sign-up (POST /v1/payment_intents with recurring)Subscription sign-up refusals
Terms readsubscription_product_not_found, product_not_ready, recurring_not_enabled, recurring_terms_mismatch
/v1/subscriptions callsSubscription management refusals

SDKs and tools​

CallNode (@vonpay/checkout-node 4.4.0+)Python (vonpay-checkout 4.4.0+)
Read termssubscriptionProducts.terms(product)subscription_products.terms(product)
Listsubscriptions.list({ buyerId, email, status, limit, startingAfter })subscriptions.list(buyer_id=…, email=…, status=…, limit=…, starting_after=…)
Readsubscriptions.retrieve(id)subscriptions.retrieve(id)
Cancelsubscriptions.cancel(id, { reason, atPeriodEnd?, note?, paymentIntentId? }, options)subscriptions.cancel(id, reason=…, at_period_end=…, …)
Withdraw a scheduled cancelsubscriptions.unscheduleCancel(id, options)subscriptions.unschedule_cancel(id, …)
Pausesubscriptions.pause(id, { resumesOn? }, options)subscriptions.pause(id, resumes_on=…, …)
Resumesubscriptions.resume(id, options)subscriptions.resume(id, …)
Skipsubscriptions.skip(id, options)subscriptions.skip(id, …)
Next bill datesubscriptions.setNextBillDate(id, { date, customerRequested?, note? }, options)subscriptions.set_next_bill_date(id, date, customer_requested=…, note=…, …)
Intervalsubscriptions.setInterval(id, { unit, count }, options)subscriptions.set_interval(id, unit, count, …)
Pricesubscriptions.setPrice(id, { amount }, options)subscriptions.set_price(id, amount, …)
Discountsubscriptions.setDiscount(id, { duration, percentOff?, amountOff?, currency?, durationInRenewals? }, options)subscriptions.set_discount(id, duration, percent_off=…, amount_off=…, currency=…, duration_in_renewals=…, …)
Remove discountsubscriptions.removeDiscount(id, options)subscriptions.remove_discount(id, …)
Manage subscription linksubscriptions.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.