This guide explains how to migrate existing Xendit legacy Subscriptions integrations POST /recurring/plans with api-version: 2022-04-10) to the new Subscriptions version api-version: 2026-01-01), which consolidates the subscription workflow with the Payment Sessions product.
Scoped to the recurring plan lifecycle
/recurring/plans) and its Payment Sessionsession_type: SUBSCRIPTION) entry point. If you're integrating a one-off Payment Link or Payment Request, see Migrate from Payment Links/Invoice to Payment Session instead.
Why you should migrate
Both Payment Link and Components, from one integration. Legacy Subscriptions only supports the Payment Link (hosted redirect) flow for collecting a payment method. The new version supports both Payment Link and Components (embedded SDK), so you're not locked into a redirect if you want to keep the customer on your own checkout page.
Full channel and region coverage. Legacy Subscriptions is limited to legacy payment methods and specific regions, mirroring the same constraint that Legacy Payment Link had before Payment Sessions. The new version inherits the full channel/region catalog from Payment Sessions.
One lifecycle model instead of two. Legacy Subscriptions tracks its own lifecycle on the
payment_methodobject separately from the UI/session experience — meaning you reconcile two different mental models for the same transaction. The new version applies the same "one object, one lifecycle" principle used in Payment Sessions and Payment v3/Payment Tokens v3 across both the payment interface and the underlying payment object.A cleaner initial-payment pattern. Legacy Subscriptions only exposed
immediate_action_typeto trigger a payment at plan creation — an enum-driven flag bolted onto the plan. The new version recommends thePAY_AND_SAVEsession flow (create the payment and generate the payment token as the initial payment, and use the payment token on subscription for the upcoming schedule)
Overall changes
Before touching any code, a few things your team should know going in:
There are two migration paths, not one.
If you need Xendit to collect and tokenize the payment token (new customer), you now go through Payment Sessions
POST /sessionswithsession_type: SUBSCRIPTION) rather thanPOST /recurring/plansdirectly.If you already hold a valid
payment_token_idfor the customer (returning customer, token created elsewhere), you continue callingPOST /recurring/plans— just with the newpayment_tokensparameter instead of the legacypayment_methodsparameter. Route your engineering work accordingly; treating this as a single flat migration will cause the wrong team to pick up the wrong ticket.You can reuse the same identifier from Payment Methods to Payment Tokens — just swap the prefix from
pm-topt-. Please find more details on this page
The
api-versionheader is mandatory and versioned. Legacy Subscriptions runs on2022-04-10; the new version requires2026-01-01.recurring_actionis removed, not renamed. The new version streamlines the flow to default exclusively to PAYMENT behavior. Any code path that sets or readsrecurring_actionfor a non-payment purpose needs to be removed, not remapped.Three legacy notification fields collapse into one.
notification_config.recurring_created,notification_config.recurring_created, andnotification_config.recurring_failed(three separate keys in the legacy payload) become a singlenotification_channels. If your legacy config sends different channels per event today, confirm whether the new version supports that granularity before cutting over, or plan to standardize on one channel set for all notification events.Webhook payloads and event names both changed. The new version runs on Xendit's principle over Payment API v3 and Payment Tokens v3 webhook infrastructure rather than the legacy
payment_method.*events. See Webhook event mapping below.
Area | Legacy Subscription | New Subscription |
|---|---|---|
Payment Link and Components support | Payment Link only | Both Payment Link and Components |
Payment channel / region availability | Limited to legacy channels and specific regions | All channels and regions Xendit supports |
Lifecycle model | Separate lifecycles on | One integration principle across the payment interface and payment object |
Initial payment on plan creation |
| Recommended: |
Detailed changes
How to migrate
Update your integration flow
Bump your API version header. Change
api-versionfrom2022-04-10to2026-01-01to access the new features.Collecting a new payment method (hosted UI): instead of calling
POST /recurring/plansdirectly, callPOST /sessionswithsession_type: "SUBSCRIPTION". The response returns apayment_link_url(modePAYMENT_LINK) orcomponents_sdk_key(modeCOMPONENTS) depending on your integration mode, in place of the legacy response'sactions.url.Reusing an existing payment token: continue calling
POST /recurring/plans, but replace the legacypayment_methodsparameter with the new, unifiedpayment_tokensparameter.
Use Case A — Creating a new payment token (customer hasn't linked a payment token yet)
Call POST /sessions directly with session_type: "SUBSCRIPTION" .
This replaces calling POST /recurring/plans up front. Xendit collects and tokenizes the payment token via the hosted page (or Components), then creates the plan on your behalf once linking succeeds.
Use Case B — Reusing an existing payment token (customer already linked)
If you already hold a valid payment_token_id for the customer, skip Payment Sessions entirely and call POST /recurring/plans directly, replacing the legacy payment_methods parameter with payment_tokens.
Learn more on the differences on our subscription integration page
// Create new payment token
// POST https://api.xendit.co/sessions
{
"reference_id": "subscription_12345",
"session_type": "SUBSCRIPTION",
"mode": "PAYMENT_LINK",
"amount": 500,
"currency": "PHP",
"country": "PH",
"customer": {
"reference_id": "cust_Lorem_Ipsum",
"type": "INDIVIDUAL",
"email": "test@yourdomain.com",
"mobile_number": "+6212345678",
"individual_detail": {
"given_names": "Lorem",
"surname": "Ipsum"
}
},
"locale": "en",
"description": "Monthly subscription plan",
"subscription": {
"schedule": {
"interval": "MONTH",
"interval_count": 1,
"total_recurrence": 12,
"anchor_date": "2026-09-01",
"retry_interval": "DAY",
"retry_interval_count": 1,
"total_retry": 3,
"failed_attempt_notifications": [1, 2, 3]
},
"failed_cycle_action": "RESUME"
},
"success_return_url": "https://yourcompany.com/success",
"cancel_return_url": "https://yourcompany.com/cancel"
}// Reuse existing payment token
// POST https://api.xendit.co/recurring/plans
// header: api-version: 2026-01-01
{
"reference_id": "my-plan-01",
"customer_id": "cust-b98d6f63-d240-44ec-9bd5-aa42954c4f48",
"currency": "IDR",
"amount": 150000,
"schedule": {
"interval": "MONTH",
"interval_count": 1,
"total_recurrence": 12,
"anchor_date": "2026-09-01T00:00:00Z",
"retry_interval": "DAY",
"retry_interval_count": 1,
"total_retry": 3,
"failed_attempt_notifications": [1, 2, 3]
},
"payment_tokens": [
{ "payment_token_id": "pt-f8429206-f3ea-49f0-abb4-eaa89064056e", "rank": 1 }
],
"immediate_payment": true,
"failed_cycle_action": "RESUME",
"notification_channels": ["EMAIL"],
"description": "Monthly premium subscription"
}Field mapping on subscription object — what’s change
Legacy Subscription | New Subscription | Notes |
|---|---|---|
|
| Renamed — only present when charging a previously saved token. Prefix changed from |
| removed — replaced by | Changes on the flow for action are moved from subscription endpoint to session endpoint |
|
| Consolidated into a single array to configure notification channels for the plan. |
| — | We recommend you to use |
Handle new error responses
Ensure your server's error-handling logic is updated to catch and process the new error structures returned by the Payment Sessions API (upon session creation) and the upgraded Subscriptions API.
Adjust your webhook setup
The new version utilizes Xendit's upgraded Payment API v3 webhook infrastructure. We highly recommend updating your webhook URL endpoints to cleanly capture and process updates across your subscription lifecycles.
Legacy Subscription | New Subscription | Notes |
|---|---|---|
Plan Callback Event | ||
|
| Different payload structure (i.e. no |
Cycle Callback Event | ||
|
| Different payload structure |
|
| |
|
| |
Payment Callback Event - follow the Payments API v3 principle | ||
| Triggered when payment is succeeded, in concurrent with the | |
| Triggered when payment is failed, in concurrent with the | |
Rule of thumb: treat
recurring.plan.*andrecurring.cycle.*as your subscription lifecycle source of truth, and treatpayment.*/payment_token.*as your transaction/instrument source of truth — the same separation-of-concerns pattern used across the rest of Xendit's v3 migration (Payment Sessions, Payment API v2→v3).