POST /v1/payments
The primary endpoint. Creates a payment, picks a PSP via routing, returns a redirect URL.
Request
Section titled “Request”POST /v1/paymentsHost: payment.uncle-z.comX-PAY-Key: pk_<your app's public key>X-PAY-Timestamp: <unix seconds>X-PAY-Signature: <hex hmac-sha256, see /concepts/auth>Idempotency-Key: <optional uuid>Content-Type: application/json
{ … body … }Body fields
Section titled “Body fields”| Field | Required | Notes |
|---|---|---|
external_user_id | yes | Stable per-buyer id from your side. Don’t put PII here. The gateway upserts a customers row keyed on (app_id, external_user_id) so the customer is tracked across payments. |
email | yes (in practice) | Buyer’s email. NICEPAY rejects QRIS/VA without it (9018); Polar / PayPal use it to pre-fill the buyer form. |
amount_minor | yes | Smallest unit. Cents for USD/EUR/GBP (2900 = $29). Rupiah for IDR — IDR has no minor unit, so 50000 means Rp 50,000 not Rp 500.00. |
currency | yes | ISO 4217, uppercase. IDR, USD, EUR, GBP. |
method | yes | Method slug. See Routing for the full list. IDR rails: qris, va_bca, va_mandiri, va_bni, va_bri, va_permata. Method overrides: paypal, polar. |
return_url | yes | Where the PSP redirects the buyer after success/failure. Don’t trust the redirect’s query params for status; use the webhook or GET /v1/payments/{id} for canonical state. |
metadata | no | Opaque JSON the gateway echoes back on outbound webhooks. Some PSPs need specific keys (Polar wants polar_product_id); they’re documented per guide. |
invoice_prefix | no | Optional uppercase tag for the PSP-facing invoice number. When set, the gateway constructs <prefix>-<6-digit zero-padded global seq>. Format: ^[A-Z][A-Z0-9-]{0,11}$. See Invoice numbers. |
Response (201)
Section titled “Response (201)”{ "payment_id": "abc123…", // 32 hex chars — gateway internal id "status": "initiated", "checkout_url": "https://…", // redirect the buyer here "invoice_number":"FTY-PRO-000042", // present when invoice_prefix supplied "qr_string": "00020101...", // QRIS only, V2 Direct only "qr_image_url": "https://…/qr.png" // QRIS only, V2 Direct only}payment_id is the canonical gateway identifier (always returned, always 32-char hex). invoice_number is the PSP-facing display string, populated when you sent invoice_prefix. Use payment_id for GET /v1/payments/{id} lookups; surface invoice_number to operators / customer support.
qr_string and qr_image_url are populated only when method=qris AND the operator has enabled NICEPAY V2 Direct on the gateway (see POS retail). Otherwise omitted; products must handle both response shapes.
Errors
Section titled “Errors”| Status | Body | Meaning |
|---|---|---|
401 | missing auth headers | One of X-PAY-Key / X-PAY-Timestamp / X-PAY-Signature absent. |
401 | bad timestamp | X-PAY-Timestamp not parseable as integer. |
401 | timestamp out of range | |now − ts| > 300s. NTP your machine. |
401 | bad signature | HMAC mismatch. See Authentication common-mistakes table. |
400 | missing required fields: … | A required body field is empty. |
409 | idempotency conflict — different body for same key | You reused an Idempotency-Key with a different body. |
422 | no provider supports method=… for currency=… | Routing found no PSP. Check Routing. |
422 | provider X not allowed for this app | App’s allowed_providers blocks the picked PSP. |
422 | method X not enabled for provider Y on this app | App’s allowed_methods doesn’t include the (PSP, method) pair. |
502 | provider checkout failed: … | The PSP rejected the registration. The error from the PSP is included verbatim — common causes documented per-PSP guide. |
500 | payment create failed | DB or other gateway-internal failure. Retryable. |
Idempotency
Section titled “Idempotency”Always send Idempotency-Key. Same key + same body within 24h returns the original response without re-processing. Same key + different body → 409. See Idempotency.
Reference signer
Section titled “Reference signer”PK=pk_xxxSK=xxxBODY='{"external_user_id":"u-1","email":"buyer@example.com","amount_minor":2900,"currency":"USD","method":"polar","metadata":{"polar_product_id":"<uuid>"},"return_url":"https://x","invoice_prefix":"PROD-PRO"}'TS=$(date +%s)H=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')S=$(printf '%s' "$TS.POST./v1/payments.$H" | openssl dgst -sha256 -hmac "$SK" -hex | awk '{print $NF}')curl -sL -X POST https://payment.uncle-z.com/v1/payments \ -H "X-PAY-Key: $PK" -H "X-PAY-Timestamp: $TS" -H "X-PAY-Signature: $S" \ -H "Content-Type: application/json" -d "$BODY"