This guide explains how to migrate existing Xendit legacy Payment Link integrations /v2/invoices) to the Payment Sessions API /sessions) using mode: PAYMENT_LINK.
Scoped to merchants using the Xendit-hosted checkout flow. If you plan to build your own checkout UI instead of using the Xendit-hosted page, see the Xendit Components integration guide
mode: COMPONENTS) instead — this doc focuses on the drop-in hosted-page replacement.
Terminology
Legacy Payment Link: It’s referring to the legacy payment link (previously called Invoice)
Payment Link (New): It’s referring to the new Payment Sessions API with
mode: PAYMENT LINK. It might be interchangeable terms with the Payment Sessions on this page
Why you should migrate
One integration for every use case. Legacy Payment Link only ever does one thing — a one-time payment link. Payment Sessions lets you declare a
session_typePAY,SAVE, orSUBSCRIPTION) and reuse the same endpoint and hosted-page experience for one-off payments, saving a payment method, and recurring billing — instead of stitching together separate legacy products (Invoice, Payment Method, Recurring).Full channel and region coverage. Legacy Payment Link is capped at a limited, older set of payment channels and specific regions. Payment Sessions gives you access to every channel Xendit supports, in every supported country, through the same
allowed_payment_channelsfield.Visibility into failed attempts. Legacy Payment Link never tells you when a customer tried and failed to pay — you only ever hear about success or expiry. Payment Sessions (via the underlying Payment v3 object) fires
payment.failurefor every failed attempt, which materially improves your reconciliation and drop-off analytics.Save-and-charge-later, done right. In the legacy payment link, saving a payment method for reuse required a separate Payment Method integration and non existent flow.
allow_save_payment_methodDISABLED/OPTIONAL/FORCED) is a native option on aPAYsession, andSAVEto store the token for future use.
Overal changes
The change is not just a new URL — it's a different model. In the legacy payment link you "create an invoice" and get back a PENDING object that becomes PAID. In the new session you "create a Session" and explicitly declare what you want the session to accomplish session_type) and how the customer should complete it (mode).
Before touching any code, a few things your team should know going in:
This is not a drop-in field rename. Some concepts don't map 1:1 —
feesbecomes a line item oftype: FEEinside items rather than its own field.amountbehaves differently depending onsession_type. ForPAYandSUBSCRIPTION,amountmust be greater than zero. ForSAVE, amount must be exactly 0. A validation errorINVALID_AMOUNT) will reject anything else — this trips up teams that copy aPAYpayload to build aSAVErequest.Webhook payloads and event names both changed. This isn't just
Invoice paid→Payment Session Completed— the new model also separates the session-lifecycle webhook from the underlying Payment/Payment Token status webhooks. Don't assume one webhook tells you everything; see Webhook event mapping below.customeris now closer to mandatory. Unlike legacy payment link (wherepayer_emailwas optional and loosely structured), the Session API requires acustomerobject orcustomer_idwhenever session_type isSAVE,allow_save_payment_methodisFORCED/OPTIONAL. Plan your data collection accordingly.modeis a new, required concept. Legacy Payment Link had no equivalent field — it was always a hosted redirect. Payment Sessions require you to explicitly declaremode: PAYMENT_LINK(hosted redirect, same UX as legacy) ormode: COMPONENTS(embedded fields via SDK).
Area | Legacy Payment Link | Payment Session |
|---|---|---|
Core object | Invoice | Payment Session (backed by Payment API v3 principle) |
Payment flows supported | One-time payment only |
|
Channel / region availability | Limited to legacy channels and specific regions | All channels and regions Xendit supports |
Failed-attempt visibility | None — silent until success or expiry |
|
Saving a payment method | None — was only supported for one time payment | Native via |
Recurring billing | Not supported by this product | Native via |
Primary Identifier |
|
|
Detailed changes
Endpoint URLs
Feature | Legacy Payment Link | Payment Session |
|---|---|---|
Create |
|
|
Get status |
|
|
Declaring intent: session_type (new, explicit field)
Legacy Payment Link had no equivalent — every Invoice implied the same one-time-payment intent. Payment Sessions requires you to declare it up front:
session_type | What it does | Typical use case |
|---|---|---|
| Collects exactly one payment. Optionally saves the payment method if allow_save_payment_method is set. | Standard checkout — the direct replacement for a legacy payment link. |
| Collects no payment (amount must be 0); only stores a payment method as a Payment Token. | Onboarding / registration flows where you need to store a card or account before any charge occurs. |
| Registers the customer onto a recurring billing schedule (interval, total_recurrence, retry rules). | Recurring plans — previously required a separate product; now unified into the same endpoint to display the registration |
allow_save_payment_method DISABLED / OPTIONAL / FORCED) is a modifier on PAY, not a separate flow — this is the "pay and save" pattern that legacy Payment Link couldn't express in a single call.
{
"reference_id": "order_12345_PAY",
"session_type": "PAY",
"mode": "PAYMENT_LINK",
"amount": 10000,
"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"
}
},
"items": [
{
"reference_id": "item_001",
"name": "Clothes",
"type": "PHYSICAL_PRODUCT",
"category": "CLOTHES",
"net_unit_amount": 5000,
"quantity": 1,
"currency": "PHP"
},
{
"reference_id": "item_002",
"name": "Pants",
"type": "PHYSICAL_PRODUCT",
"category": "CLOTHES",
"net_unit_amount": 5000,
"quantity": 1,
"currency": "PHP"
}
],
"capture_method": "AUTOMATIC",
"description": "Sample one-time payment using Payment Session",
"success_return_url": "https://yourcompany.com/success",
"cancel_return_url": "https://yourcompany.com/cancel"
}{
"external_id": "payment-link-example",
"amount": 510000,
"description": "Invoice Demo #123",
"invoice_duration": 86400,
"customer": {
"given_names": "John",
"surname": "Doe",
"email": "johndoe@example.com",
"mobile_number": "+6287774441111",
"addresses": [
{
"city": "Jakarta Selatan",
"country": "Indonesia",
"postal_code": "12345",
"state": "Daerah Khusus Ibukota Jakarta",
"street_line1": "Jalan Makan",
"street_line2": "Kecamatan Kebayoran Baru"
}
]
},
"success_redirect_url": "https://www.google.com",
"failure_redirect_url": "https://www.google.com",
"currency": "IDR",
"items": [
{
"name": "Air Conditioner",
"quantity": 1,
"price": 100000,
"category": "Electronic",
"url": "https://yourcompany.com/example_item"
}
],
"fees": [
{
"type": "ADMIN",
"value": 5000
}
],
"payment_methods": [
"CREDIT_CARD"
]
}Parameter / field mapping
Legacy Payment Link | Payment Session | Notes |
|---|---|---|
|
| Renamed. |
|
| Structured customer object; can be created up-front via the Customer API and referenced by |
|
| Now a specific ISO 8601 timestamp rather than a duration in seconds. |
|
| Defines which channels appear on the hosted page. Omit to show all activated channels. |
|
| Stored under the channel properties for cards |
|
| Stored under the channel properties for cards |
|
| Fees are now modeled as line items rather than a dedicated field. |
|
| New primary identifier, prefixed |
Status lifecycle
Legacy Payment Link Status | Payment Session Status | Notes |
|---|---|---|
|
| Awaiting customer action. |
|
| Session's underlying Payment succeeded; |
|
| Unchanged |
| New: no direct legacy equivalent (e.g. customer or merchant explicitly aborted the flow). |
Rule of thumb: Treat the Session's own status field as the lifecycle source of truth (did the customer complete checkout), and treat the underlying Payment object's status as the transaction source of truth (did the money move) — the same separation-of-concerns pattern used in the v2→v3 Payment API migration.
Webhook event mapping
Adjust the webhook URL to receive the information upon your payment sessions lifecycle.
Legacy Payment Link API
Invoices paid: Xendit will send webhook for paid invoicesInvoice expired: Xendit will send webhook for expired invoices
Payment Sessions API
Payment Session Completed: Xendit will send webhook when the payment session has completedPayment Session Expired: Xendit will send webhook when the payment session has expired
Optional but recommended
Payment v3 – Payment Status
Xendit sends webhooks whenever there is a status update on a Payment object.
payment.succeededIdentifies successful payments and includes full payment details.payment.failureIdentifies failed payment attempts, including failures that occur on the Xendit hosted page.
Payment Tokens v3 – Payment Token Status
Xendit sends webhooks whenever there is a status update on a Payment Token object.
This applies to PAY sessions with payment method saving enabled and SAVE session types.