Skip to content

POST /v1/payments

The primary endpoint. Creates a payment, picks a PSP via routing, returns a redirect URL.

POST /v1/payments
Host: payment.uncle-z.com
X-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 }
FieldRequiredNotes
external_user_idyesStable 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.
emailyes (in practice)Buyer’s email. NICEPAY rejects QRIS/VA without it (9018); Polar / PayPal use it to pre-fill the buyer form.
amount_minoryesSmallest 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.
currencyyesISO 4217, uppercase. IDR, USD, EUR, GBP.
methodyesMethod 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_urlyesWhere 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.
metadatanoOpaque JSON the gateway echoes back on outbound webhooks. Some PSPs need specific keys (Polar wants polar_product_id); they’re documented per guide.
invoice_prefixnoOptional 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.
{
"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.

StatusBodyMeaning
401missing auth headersOne of X-PAY-Key / X-PAY-Timestamp / X-PAY-Signature absent.
401bad timestampX-PAY-Timestamp not parseable as integer.
401timestamp out of range|now − ts| > 300s. NTP your machine.
401bad signatureHMAC mismatch. See Authentication common-mistakes table.
400missing required fields: …A required body field is empty.
409idempotency conflict — different body for same keyYou reused an Idempotency-Key with a different body.
422no provider supports method=… for currency=…Routing found no PSP. Check Routing.
422provider X not allowed for this appApp’s allowed_providers blocks the picked PSP.
422method X not enabled for provider Y on this appApp’s allowed_methods doesn’t include the (PSP, method) pair.
502provider checkout failed: …The PSP rejected the registration. The error from the PSP is included verbatim — common causes documented per-PSP guide.
500payment create failedDB or other gateway-internal failure. Retryable.

Always send Idempotency-Key. Same key + same body within 24h returns the original response without re-processing. Same key + different body → 409. See Idempotency.

Terminal window
PK=pk_xxx
SK=xxx
BODY='{"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"