Errors & envelope
Every response — success or failure — uses one predictable envelope, so error handling is a single code path.
The envelope#
{
"success": true,
"data": { "invoiceId": "0x2b7e…c4a1", "…": "…" },
"error": null
}{
"success": false,
"data": null,
"error": "too many requests — slow down",
"meta": { "retryAfterMs": 60000 }
}success tells you which branch you are in; error is a human-readable message; some failures add a meta object with machine-readable hints (retryAfterMs, upgradeUrl, code). Every response also carries an x-request-id header — include it when contacting support.
Status codes#
| Status | Meaning | Typical causes |
|---|---|---|
400 | Invalid request | Missing amount/fiat (exactly one is required), unsupported token or chain, test key on a mainnet chain, invalid returnUrl |
401 | Unauthenticated | Missing or unknown API key |
402 | Plan limit | Free-plan monthly charge quota reached, or the project’s subscription lapsed — meta.upgradeUrl points at Billing. Existing charges keep settling. |
403 | Refused | merchant doesn’t match the project’s payout address; payments disabled for the project; sanctions screening |
404 | Not found | Unknown invoice id |
409 | Conflict | No payout wallet set; the charge can no longer be modified |
429 | Rate limited | Plan rate exceeded — wait meta.retryAfterMs and retry |
503 | Not configured | The requested rail isn’t available on that chain yet |
Common errors#
- “merchant must match the project payout address” — the optional
merchantfield exists as a safety check; if you pass it, it must equal the verified payout wallet. Easiest fix: omit it. - “monthly charge limit reached on the Free plan” (
402) — new charges are blocked until the window resets or you upgrade; nothing in-flight is affected. - Test/live mismatch (
400) — see Authentication; the key’s mode must match the chain.
Errors in the SDK#
@duckypay/node unwraps the envelope for you: a failed request throws DuckyPayError carrying the message, HTTP status, and the meta fields. Retryable failures (network, timeout, 429, 5xx) are retried automatically before the error ever reaches you — see the SDK’s reliability section.
import { DuckyPay, DuckyPayError } from '@duckypay/node';
try {
await duckypay.charges.create({ chainId: 137, amount: '10000000' });
} catch (err) {
if (err instanceof DuckyPayError) {
err.status; // 429
err.message; // 'too many requests — slow down'
err.code; // meta.code when present
err.retryAfterMs; // meta.retryAfterMs on 429s
err.requestId; // the x-request-id header — quote it to support
} else {
throw err;
}
}