Creating a payment
The one endpoint that creates every payment, the fields it takes, what comes back, and the statuses a payment moves through.
Creating a payment
One endpoint creates every payment — POST /v1/payments — and one field decides how it behaves:
method— one ofCARD_CREDIT,CARD_DEBIT,QRorEWALLET. It says how the customer pays, and it is the only choice you make about the payment itself.customerEmail— required. The receipt and any payment instructions go there.
We answer with a paymentLinkUrl to send the customer to — or, for a QR payment, the code itself, because a QR is completed by scanning and the customer never has to leave.
The body
Four required fields — referenceId, method, amount and customerEmail — and the rest is polish:
{
"referenceId": "order-1001",
"method": "EWALLET",
"amount": 1500,
"currency": "PHP",
"customerEmail": "payer@example.com",
"description": "Order #1001",
"successReturnUrl": "https://your-shop.example/orders/1001/paid",
"cancelReturnUrl": "https://your-shop.example/orders/1001/cancelled"
}referenceId— your order id. Required, and unique per payment; see below.amount— in the major unit.1500is ₱1,500.00, not ₱15.00. There are no minor units anywhere in this API.currency— ISO-4217. Defaults to your account's currency; send it anyway, so the intent is in your own logs.description— shown to the customer during checkout. Write it for them, not for your database.successReturnUrl/cancelReturnUrl— where the customer lands afterwards. Read Confirming the outcome before you rely on either.
method and customerEmail are required alongside referenceId and amount. There is no flow to choose and no rail to name: those were fields once, and sending them now changes nothing.
What you get back
A payment comes back in one of two shapes, and which one you get follows from the method — so branch on the field that is present, never on the method itself.
{
"id": "9f2b41c7-de8a-4f05-9c3e-1a2b3c4d5e6f",
"referenceId": "order-1001",
"status": "PENDING",
"method": "EWALLET",
"amount": 1500,
"currency": "PHP",
"qrString": null,
"paymentLinkUrl": "https://checkout.ecashpay.example/9f2b41c7"
}Redirect the customer to paymentLinkUrl — do not iframe it, because the pages behind it redirect to banks and wallet apps that refuse to be framed.
{
"id": "3d7c9a10-5b2e-4c81-9f40-7e6d5c4b3a21",
"referenceId": "order-1002",
"status": "PENDING",
"method": "QR",
"amount": 1500,
"currency": "PHP",
"qrString": "00020101021228670011ph.ppmi.p2m…6304ABCD",
"paymentLinkUrl": null
}A QR payment is completed by scanning, so it publishes qrString and sets paymentLinkUrl to null. Render the payload as a QR code on your own page; the customer never leaves it. The payload is a string to draw, not an address to fetch.
Either way the payment starts PENDING. Nothing about the response tells you it succeeded — read Confirming the outcome for the one source of truth on that.
referenceId and double charges
referenceId is the double-charge guard, and it is the one field worth designing deliberately. Sending one you have used before is refused with DUPLICATE_REFERENCE_ID (409) rather than creating a second payment.
That makes the safe pattern for an unreliable network: reuse the same reference when you retry. Either you get the payment, or you get a 409 that proves it already exists — and in both cases your customer is charged once. Generating a fresh reference per attempt gives away exactly this protection.
Payment statuses
status is what you act on. Everything else on a payment is detail:
| Status | Meaning |
|---|---|
PENDING | Created, not yet paid. Every payment starts here. A customer who has not finished checkout sits here. |
REQUIRES_ACTION | The rail needs something from the customer — a redirect, an app confirmation, an OTP. Not a failure. |
AUTHORIZED | Funds are held but not taken. Only appears on flows that separate authorization from capture; call capture to settle it or cancel to release it. |
SUCCEEDED | Paid. This is the only status that means money is yours, and the only one to fulfil on. |
FAILED | The rail refused it — insufficient funds, a declined card, a rejected mandate. Terminal; create a new payment to try again. |
EXPIRED | The customer never completed it in time. Terminal, and the most common non-success outcome for over-the-counter and e-wallet rails. |
CANCELLED | Abandoned by the customer, or voided by you while it was authorized. Terminal. |
REFUNDED | Succeeded, then returned in full. |
PARTIALLY_REFUNDED | Succeeded, then partly returned. Still a payment you were paid for — the remainder is yours. |
Treat this list as open. A rail we add later may introduce a status you have not seen, so write the check as "is it SUCCEEDED" and "is it one of the terminal failures I know" — never as an exhaustive switch that throws on the unexpected.

