This guide explains how to migrate existing Xendit Payment API v2 integrations (Payment Method, Payment Request, and Payment objects) to Payment API v3 (/v3/payment_requests and /v3/payment_tokens).
Scoped to the generic Payment API v2. If you're integrating directly per-channel (e.g.
/ewallets,/qr_codes,/credit_card_charges), see the separate Legacy direct-channel APIs → Payments API v3 guide.
Why you should migrate
Faster time-to-market for new channels. Because every channel now shares the same
channel_code+channel_propertiesshape instead of apayment_methodsub-object, adding a new payment channel is only a configuration change on your end.Use case-centric model. In v3, you can define explicitly the use case of your Payment Request (
PAY,PAY_AND_SAVE,REUSABLE_PAYMENT_CODE) while saving end user payment method account separately handled by Payment Token. This structure will make the flow more predictable.v2 required per-channel logic to interpret
url_type(API/WEB/MOBILE/DEEPLINK) andaction(AUTH/RESEND_AUTH) fields. v3 standardizes this into two universal action types, so your checkout UI logic no longer needs an implicit channel-specific switch statement.Clearer separation of concerns for saved payment methods. Saving a payment method (
Payment Token) is now explicitly decoupled from charging (Payment Request), instead of being entangled in one object.v2 treats the Payment Method as the mandatory gate every transaction must pass through. A
Payment Requestcannot exist without aPayment Method— either created inline or referenced by ID. This means the "instrument" object and the "charge intent" object are tightly coupled, even when you don't need reusability.v3 treats the Payment Request as self-sufficient. It carries its own
channel_code,channel_properties, and an explicittypedescribing the intended flow. A Payment Token is an optional, independent object that you only create when you genuinely need to save a payment method for future use.
Overall changes
The change is not just a new URL — it's a different way of modeling a transaction. In v2, every payment is created through a Payment Method. In v3, a Payment Request stands on its own: you pass channel_code and channel_properties directly, tell Xendit what you want with type, and only bring a Payment Token into the picture if you actually need to charge the customer again later.
Before touching any code, there are a few things engineers should be aware of going in:
This is not a drop-in field rename. Some code paths won't just need renamed fields — they'll need to be restructured or removed entirely (e.g. any logic that creates a Payment Method purely to immediately charge it once).
The
typefield is reused with a different meaning. v2 had atypefield on the Payment Method (EWALLET,CARD, etc.). v3 has atypefield on the Payment Request (PAY,PAY_AND_SAVE,REUSABLE_PAYMENT_CODE). These are not the same concept.Payment Request in v3 is an independent object, creating a Payment Request won’t need Payment Method object as v2 concept.
Webhook payloads and event names both changed. This isn't just
payment.failed→payment.failure— thepayment_method.expiredevent that v2 sends after every successful one-time payment has no v3 equivalent, and any logic (alerts, retries, reconciliation) built around it needs to be removed, not remapped.In v3, everything specific to a channel sits inside
channel_properties, and which fields are required there depends on thechannel_codeandtypeyou're using.
Area | v2 | v3 |
|---|---|---|
Create a payment request — URL |
|
|
Save end user payment method — URL |
|
|
Core objects |
|
|
Relationship between objects | Payment Request requires a Payment Method (inline or by reference) | Payment Request is self-contained; Payment Token is optional |
Tokenization | Implicit — happens for every payment, even one-off ones | Explicit/opt-in — only via |
How you select a channel | Nested inside a | Top-level |
How you declare intent | Implied by | Explicit |
End-user action model | Channel-specific | Standardized |
Webhooks |
|
|
API versioning | No | Requires |
Detailed changes
Endpoint URLs
Action | v2 | v3 |
|---|---|---|
Create a payment |
|
|
Get a payment request |
|
|
List payment requests |
|
|
Save end user payment method |
|
|
Get a saved payment method/token |
|
|
List saved payment methods/tokens |
|
|
Update a saved payment method/token |
|
|
Mandatory Payment Method (v2) vs. self-defined flow (v3)
In v2, POST /payment_requests always requires a Payment Method — either inline or by payment_method_id while on v3, POST /v3/payment_requests have a type to identify the flow explicitly
// v2 — POST https://api.xendit.co/payment_requests
{
"currency": "IDR",
"amount": 100000,
"payment_method": {
"type": "EWALLET",
"reusability": "ONE_TIME_USE",
"ewallet": {
"channel_code": "SHOPEEPAY",
"channel_properties": {
"success_return_url": "https://your-redirect-website.com/success"
}
}
},
"customer_id": "fc4c060b-3c41-4707-b7b2-df9c3376edde"
}// v3 — POST https://api.xendit.co/v3/payment_requests
// header: api-version: 2024-11-11
{
"reference_id": "order-id-123",
"type": "PAY",
"country": "ID",
"currency": "IDR",
"request_amount": 100000,
"channel_code": "SHOPEEPAY",
"channel_properties": {
"success_return_url": "https://redirect.me/payment"
}
The type field — declaring intent explicitly
Refer to Payment API v3 integration guideline to see supported use cases and type mapping.
channel_code and channel_properties reference
Available
channel_codevalues and supportedchannel_propertiesfor a givenchannel_codecan be find here
Standardized action handling
v2's actions array used channel-specific url_type values (API, WEB, MOBILE, DEEPLINK) and action purposes (AUTH, RESEND_AUTH), requiring per-channel client logic.
v3 collapses this into two universal action types:
v3 | Meaning | Roughly maps to v2's... |
|---|---|---|
| Send the customer to a URL to authenticate |
|
| Show a code/value to the customer directly (no redirect) | Values previously read off channel-specific sub-objects (e.g. |
⚠️ The misleading payment_method.expired webhook (v2) — and how v3 removes it
In v2: for a one-time-use payment, Xendit creates a Payment Method under the hood with reusability: ONE_TIME_USE. Once that payment succeeds, the Payment Method has served its purpose and Xendit immediately expires it — firing a payment_method.expired webhook, essentially simultaneously with the payment.succeeded webhook for the same transaction.
This is expected behaviour, but it reads as an error: engineers frequently see payment_method.expired land right after a successful payment and assume something failed, when the payment actually succeeded and the "expiry" is just the one-time-use object being retired.
Rule of thumb: treat payment.capture / payment.failed (the Payment object status) as the single source of truth for transaction outcome.
In v3: this ambiguity is removed structurally. A type: "PAY" request never creates a Payment Token, so there is no expiry webhook to misinterpret. payment_token.* events only fire when a token was explicitly requested via PAY_AND_SAVE or Payment Token creation, and such tokens don't auto-expire immediately after use.
Field mapping
Payment Request object — what’s change
v2 field | v3 field | Notes | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
| Unchanged format. | ||||||||||||||||||||||||
|
| Renamed. | ||||||||||||||||||||||||
|
| Status lifecycle changes
| ||||||||||||||||||||||||
| (removed — replaced by | Can be found here | ||||||||||||||||||||||||
|
| Moved to the top level | ||||||||||||||||||||||||
|
| Moved to the top level. | ||||||||||||||||||||||||
|
| Renamed — only present when charging a previously saved token. Prefix changed from | ||||||||||||||||||||||||
(implied by |
| New, explicit field. Note: v2 also had a field called | ||||||||||||||||||||||||
|
| Restructured. | ||||||||||||||||||||||||
|
| Unchanged ( | ||||||||||||||||||||||||
|
| Specifically only for | ||||||||||||||||||||||||
|
| No longer an "override" — it's the only place these fields live. |
Payment Method (v2) → Payment Token (v3) — full field mapping
v2 field ( | v3 field ( | Notes |
|---|---|---|
|
| Renamed field and prefix. |
(not present at top level — implied by channel) |
| New top-level field in v3. |
|
| Same general lifecycle ( |
|
| Restructured. |
| (removed) | No longer needed — |
|
| Moved to the top level. |
|
| Moved to the top level. |
| Channel-specific fields returned directly on the token response object | Structure simplified — confirm exact response shape per channel via the Get Payment Token reference. |
Migrate existing saved Payment Methods to Payment Tokens. You can reuse the same identifier — just swap the prefix from
pm-topt-(e.g.pm-6ff0b6f2-f5de-457f-b08f-bc98fbae485abecomespt-6ff0b6f2-f5de-457f-b08f-bc98fbae485a). Update any stored references (database columns, customer profiles, recurring billing schedules) accordingly before switching charge calls over topayment_token_id.
Webhook event mapping
v2 event | v3 event | Notes |
|---|---|---|
|
| Same purpose; payload shape updated. |
|
| Renamed. |
| For payment with AUTHORIZED status. Supported only for CARDS with manual capture method. | |
|
| Only fires in v3 when a token was actually requested ( |
|
| For explicitly deactivated tokens, check |
|
| Renamed. |
n/a |
| To identify that payment request can no longer accepting payment. For example virtual account number expired, or redirection to partner is no longer valid. |