DuckyPayDocs

Errors & envelope

Every response — success or failure — uses one predictable envelope, so error handling is a single code path.

The envelope#

success
{
  "success": true,
  "data": { "invoiceId": "0x2b7e…c4a1", "…": "…" },
  "error": null
}
failure
{
  "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#

StatusMeaningTypical causes
400Invalid requestMissing amount/fiat (exactly one is required), unsupported token or chain, test key on a mainnet chain, invalid returnUrl
401UnauthenticatedMissing or unknown API key
402Plan limitFree-plan monthly charge quota reached, or the project’s subscription lapsed — meta.upgradeUrl points at Billing. Existing charges keep settling.
403Refusedmerchant doesn’t match the project’s payout address; payments disabled for the project; sanctions screening
404Not foundUnknown invoice id
409ConflictNo payout wallet set; the charge can no longer be modified
429Rate limitedPlan rate exceeded — wait meta.retryAfterMs and retry
503Not configuredThe requested rail isn’t available on that chain yet

Common errors#

  • “merchant must match the project payout address” — the optional merchant field 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.

server.ts
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;
  }
}