Quickstart
Take your first payment from your server in four steps, and learn the result.
You need a secret key (Authentication) and a URL on your server that can receive webhooks (Webhooks). Set the key as an environment variable so it never appears in your code:
export ZENY_SECRET_KEY=sk_…
export ZENY_API_URL=https://api.usezeny.com
Use the key and the address of the same environment: sk_live_… with https://api.usezeny.com, or sk_test_… with the sandbox address you were given. See Test and live.
1. Create the payment
A payment is the amount you ask a payer for. Nothing is charged yet. Use your own order id as reference: it must be unique in your project.
curl $ZENY_API_URL/api/merchant/v1/payments \
-H "Authorization: Bearer $ZENY_SECRET_KEY" \
-H "Idempotency-Key: order-1042-create" \
-H "Content-Type: application/json" \
-d '{
"reference": "order-1042",
"amount": { "value": "150000.00", "currency": "IDR" },
"description": "Order 1042",
"payer": { "name": "Siti Rahma", "email": "siti@example.com" }
}'
The answer (201) is the payment, with status: "pending" and an id that starts with pay_. Save that id next to your order. The payment stops taking attempts at expiresAt: 24 hours from now unless you send your own expiresAt (between 5 minutes and 30 days).
2. Start an attempt with the method the payer chose
An attempt is one try at paying, with one payment method. Send the instrument code the payer picked, such as a bank for a virtual account, QRIS or an e-wallet. Use a new Idempotency-Key.
curl $ZENY_API_URL/api/merchant/v1/payments/pay_…/attempts \
-H "Authorization: Bearer $ZENY_SECRET_KEY" \
-H "Idempotency-Key: order-1042-attempt-create" \
-H "Content-Type: application/json" \
-d '{ "instrument": "BCA" }'
The answer (201) is the attempt. Always read its status: when the payment provider refuses the attempt the answer is still 201, with status: "failed" and a failure. When the status is requires_action, the nextAction tells you what to show the payer. It is a different object depending on its type:
nextAction.type | What you do |
|---|---|
display_va | Show displayVa.number (and billerCode when it is not empty) with the bank name. The payer transfers the exact charge to it. |
display_qr | Render displayQr.data as a QR code, unchanged. The payer scans it with any banking or e-wallet app. |
redirect | Send the payer to redirect.url. Pass a returnUrl (an https address) in the request to bring them back to your site afterwards. |
| anything else | A type your code does not know yet: do not guess. Tell the payer to choose another method. |
Show the payer the attempt's charge, not the payment's amount: it is what they must pay, including a fee they bear or a unique code. Their instructions stop working at the attempt's expiresAt.
3. Wait for the webhook
When the payer has paid, Zeny sends a payment.paid event to your endpoint. Verify its signature, then mark the order paid. Do not mark an order paid because the payer came back to your returnUrl: that only means they were sent back, not that the money arrived.
4. Read the payment if you need to
curl $ZENY_API_URL/api/merchant/v1/payments/pay_… \
-H "Authorization: Bearer $ZENY_SECRET_KEY"
Use this to check a payment whose webhook did not arrive, not to poll in a loop. See Rate limits.
If something goes wrong
- The payer's bank refuses, or they do not pay in time: the attempt becomes
failedorexpiredand the payment stayspending. Start a new attempt (with a new key), perhaps with another method. See Errors. - Your request times out: send it again with the same
Idempotency-Key. See Idempotency. - Nobody pays before
expiresAt: the payment becomesexpiredand you receivepayment.expired.
Ready for real money? Follow the go-live checklist.