GET /v1/payments/{id}
The source of truth for “is this payment succeeded yet?” Use this when you can’t / don’t want to rely on the webhook (polling fallback, post-redirect verification, etc.).
Request
Section titled “Request”GET /v1/payments/{payment_id}Host: payment.uncle-z.comX-PAY-Key: pk_<your app's public key>X-PAY-Timestamp: <unix seconds>X-PAY-Signature: <hex hmac-sha256 of "<ts>.GET./v1/payments/<id>.<sha256("")>">payment_id is the 32-char hex returned by POST /v1/payments.
For GETs the body hash is the SHA-256 of the empty string: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
Response (200)
Section titled “Response (200)”{ "payment_id": "abc123…", "status": "succeeded", "amount_minor": 2900, "currency": "USD", "method": "polar", "provider": "polar", "provider_ref": "polar_chk_…", "metadata": { /* echoed verbatim from create + PSP enrichment */ }, "invoice_number": "FTY-PRO-000042", "last_error": "", "created_at": "2026-05-09T12:34:56Z", "updated_at": "2026-05-09T12:35:01Z"}Status states
Section titled “Status states”status | Meaning |
|---|---|
initiated | Payment created, buyer hasn’t completed checkout yet (or PSP hasn’t notified us). |
succeeded | PSP confirmed the buyer paid. Terminal. |
failed | PSP rejected the payment, or buyer abandoned and the gateway recorded a terminal failure. |
canceled | Payment was canceled by buyer / admin / PSP before completion. Terminal. |
succeeded, failed, canceled are terminal — once a payment hits one, it doesn’t move again.
last_error
Section titled “last_error”When set, contains the verbatim PSP-side error string from the most recent failure. Useful for support tickets — “why did this payment fail?” returns a NICEPAY error code or Polar error message directly.
Polling pattern
Section titled “Polling pattern”async function waitForTerminal(paymentId: string, intervalMs = 1000, timeoutMs = 1200000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const r = await getPayment(paymentId); if (['succeeded', 'failed', 'canceled'].includes(r.status)) return r; await new Promise(r => setTimeout(r, intervalMs)); } throw new Error('payment never reached terminal state');}20 minutes is a reasonable upper bound for QRIS (which has a hard 20-min expiry); shorter for cards (Polar/PayPal usually settle in seconds).
Errors
Section titled “Errors”| Status | Body | Meaning |
|---|---|---|
401 | missing auth headers / bad signature / etc. | Same auth errors as POST. |
404 | payment not found | The id doesn’t exist OR doesn’t belong to your app. (Tenant scoping is enforced — you can’t read another product’s payments by guessing ids.) |