Appearance
Complete payment flow
Checkout sequence
| Step | Endpoint | Next action |
|---|---|---|
| 1. Methods | GET /v2/payment-methods | Send customer ID; display available and saved payment methods. |
| 1a. Method options | GET /v2/payment-methods/{payment_method_code}/options | Call only when the selected method's payment_details_schema contains option_code; let the customer pick an option (currently FPX banks). |
| 2. Calculate | POST /v2/orders/calculate | Send items, method, and customer fee percent; confirm the returned total. |
| 3. Order | POST /v2/orders | Create once; save order_id. No charge starts. |
| 4. Payment | POST /v2/payments | Send order ID, payer, return URL, method, and confirmed amount. For FPX or new CARD, add payment_method_details; for saved CARD, send top-level saved_payment_method_id instead. New CARD can request saving with top-level save_payment_method. Follow the returned action. |
| 5. Results | Webhooks | Track collection, per-unit fulfillment, and order-level refunds. |
Send requests from your backend. Confirm payment using webhooks or a status request, not the browser redirect.
Other order and payment operations
| Purpose | Endpoint |
|---|---|
| Calculate before retrying payment | POST /v2/orders/calculate with order_id instead of items |
| Cancel an eligible pending payment | POST /v2/payments/{payment_id}/cancel |
| Read collection status | GET /v2/payments/{payment_id} |
| Read fulfillment and refunds | GET /v2/orders/{order_id} |
| Retrieve order history | GET /v2/orders |
See Calculate Order, cancellation, and result reads. There is no order cancellation or public refund-submission endpoint.
Retries and idempotency
Send a unique Idempotency-Key when creating an order or payment; a UUID is recommended. Reuse the same key and request for network retries; never reuse a key for a different operation. Keep the returned order and payment IDs.
Retry responses are retained for at least seven days. Payment-create responses remain available for at least seven days after the payment's final update; unresolved attempts defer removal. Replay is not guaranteed forever. For older requests, check the existing order/payment rather than resubmitting creation. Your order's external_reference must remain unique within your account.
While the idempotency record is retained, retrying with the same key and request returns the original creation response without submitting another payment, including a committed 400 provider_validation_error rejection. If creation has not yet recorded a response, 409 payment_creation_in_progress includes error.order_id and error.payment_id so you can track that same attempt. Do not create a new key to poll; use GET /v2/payments/{payment_id} or payment webhooks for current status.
| Situation | Action |
|---|---|
| A creation response is lost | Retry with the same key and request, using fresh authentication headers. See the card-specific retry guidance below. |
idempotency_conflict | The key was used with different data. Check the existing order/payment. |
total_mismatch | Confirm the newly calculated total, then submit the corrected request. |
| Payment is pending, processing, or cancellation is pending | Check its status; do not create another payment. |
| Payment is failed, cancelled, or expired | If the order is still unpaid and unexpired, recalculate and create a new payment with a new key. |
A new key does not bypass payment safety: an unresolved attempt returns 409 active_payment_exists; an order with a successful collection returns 409 payment_already_successful. An eligible new attempt keeps the same order_id but receives a new payment_id.
For card retries, do not store card details.
Payment expiry
Create payments within 24 hours of order creation. Each payment has a 30-minute customer-action window; use its expires_at value.
Always check the payment's status before retrying. An elapsed deadline alone does not mean the payment failed.
Shared errors
Error responses use this envelope; endpoint-specific tables describe additional codes.
Status codes follow one rule: 400 means fix the request, 404 means not found, 409 means the request conflicts with the current order, payment, or Idempotency-Key state, 429 means slow down, and 500/503 mean retry the same request later. Branch on error.code for the specific case.
| Field | Type | Description |
|---|---|---|
error.code | string | Machine-readable error code. |
error.message | string | Safe explanation. |
error.request_id | string | Reference for support. |
error.order_id | string | Related order ID on payment-creation and cancellation conflicts, such as payment_creation_in_progress or payment_already_successful. |
error.payment_id | string | Existing attempt ID when creation is in progress or an attempt-specific rejection supplies it. Use it to read payment status. |
error.details | object or array | Returned only where documented: validation, item failures, duplicate reference, or total mismatch. |
| HTTP | Code | Action |
|---|---|---|
| 400 | validation_error | Fix the fields named in error.details; see validation details. |
| 401 | Authentication error | Check the signing headers. |
| 429 | rate_limit_exceeded | Too many requests from your account. Retry with bounded backoff. |
| 500 | internal_error | Retry with bounded backoff; preserve creation keys and follow the retry rules above. |
Validation details
| Field | Type | Description |
|---|---|---|
error.details[].field | string | Field path, including array position when relevant. |
error.details[].code | string | What is wrong with the field, such as required, max_length, invalid_amount, invalid_quantity, invalid_order_id, invalid_customer, invalid_return_url, invalid_idempotency_key, items_required, unsupported_field, or invalid_json. |
error.details[].message | string | How to correct the field, for example order_id must be a valid 26-character ULID. Never echoes submitted values. |
json
{
"error": {
"code": "validation_error",
"message": "The request is invalid.",
"request_id": "req_01M3KC2PHYKJ1FMVA0MZGXECRN",
"details": [
{
"field": "items",
"code": "items_required",
"message": "items must contain at least one item."
}
]
}
}If an item fails product validation rather than field-shape validation, the error is 400 invalid_items and error.details.items lists each failing item with its index, code (invalid_amount, invalid_extras, invalid_product, invalid_account, invalid_topup, or invalid_item) and message. Correct the listed items and calculate again.

