DuckyPayเอกสาร

Charges

A charge is a signed, single-use invoice: one buyer, one amount, one deadline. Create it server-side, send the buyer to the hosted checkout, and confirm via webhook or polling.

Create a charge#

POST/v1/invoices

chainIdintegerrequired

The network to charge on — see Chains & tokens. Must match your key’s mode (test ↔ testnet).

amountstringoptional

Amount in the token’s smallest unit ("25000000" = 25 USDT). Provide exactly one of amount or fiat.

fiatobjectoptional

Price in fiat instead of token units; DuckyPay converts at a live rate. See Fiat pricing for the rate mechanics.

currency'THB' | 'USD'required

Fiat currency to price in — THB and USD are the complete list today; any other code is rejected with 400.

amountstringrequired

Decimal string, e.g. "350" or "19.99".

tokenstringoptional

Token symbol ("USDT", "USDC"). Defaults to the chain’s default token.

paymentMode'wallet' | 'transfer' | 'both'optional

Which rail(s) to arm. Default both — the buyer picks at checkout (falls back to wallet on chains without the transfer rail). See Payment modes.

referencestring ≤ 255optional

Your order id. Echoed back on the charge and in the invoice.paid webhook.

returnUrlstring (https)optional

Where the checkout sends the buyer after payment (your success_url). The invoice id and your reference are appended as query params.

expiresInSecondsinteger 1–86400optional

How long the charge is payable. Defaults to your project's payment window (1 hour unless changed).

merchantaddressoptional

Optional safety check: if provided, must equal the project’s verified payout address, else 403.

terminal
curl https://api.duckypay.co/v1/invoices \
  -H "x-api-key: $DUCKYPAY_API_KEY" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: order_1001" \
  -d '{
    "chainId": 137,
    "fiat": { "currency": "USD", "amount": "25" },
    "paymentMode": "both",
    "reference": "order_1001",
    "returnUrl": "https://shop.example/thanks"
  }'
201 response (both mode)
{
  "success": true,
  "data": {
    "invoiceId": "0x2b7e…c4a1",
    "chainId": 137,
    "mode": "both",
    "checkoutUrl": "https://pay.duckypay.co/c/0x2b7e…c4a1",
    "depositAddress": "0x9F62…7EBB",
    "token": "0xc213…58e8F",
    "amount": "25012500",
    "feeBps": 0,
    "deadline": "1789552800",
    "intent": {
      "invoiceId": "0x2b7e…c4a1",
      "merchant": "0xYourWallet…",
      "token": "0xc213…58e8F",
      "amount": "25012500",
      "feeBps": 0,
      "deadline": "1789552800"
    },
    "signature": "0x…",
    "fiat": {
      "currency": "USD",
      "amount": "25",
      "tokenAmount": "25012500",
      "…": "rate breakdown"
    },
    "reference": "order_1001"
  },
  "error": null
}

The response shape follows the mode: wallet returns the signed intent + signature (what the buyer’s wallet submits to the router), transfer returns the per-invoice depositAddress, and both returns everything. In every mode, checkoutUrl is the only thing most integrations need.

Amounts: token or fiat#

Token amounts are decimal-integer strings in the token’s smallest unit — never floats. With fiat pricing, DuckyPay fetches a live rate, applies a small buffer in your favor, rounds the token amount up, and returns the full breakdown in data.fiat so the buyer sees exactly how the price was formed.

Idempotency#

Send an Idempotency-Key header (1–255 chars, scoped to your project) on charge creation. Replays return the original 201 response instead of minting a duplicate invoice. Only successful responses are cached — a failed attempt can be retried with the same key.

Retrieve a charge#

GET/v1/charges/:invoiceId

Returns the full charge — status, amount, token metadata, deposit address, fiat breakdown, branding, and a freshly signed intent. This is the same route the hosted checkout renders from, so anything the buyer can see, you can read.

Poll the status#

GET/v1/charges/:invoiceId/status

A lightweight route returning only the status — use this for polling loops instead of the full read.

StatusMeaning
pendingAwaiting payment
paidPayment confirmed on-chain — funds are in (or on their way to) your wallet
underpaidTransfer mode: a deposit arrived below the invoice amount
expiredThe deadline passed without full payment

Prefer webhooks over polling in production — you get the same truth pushed to you, signed.

terminal
curl https://api.duckypay.co/v1/charges/0x2b7e…c4a1/status \
  -H "x-api-key: $DUCKYPAY_API_KEY"
response
{
  "success": true,
  "data": { "invoiceId": "0x2b7e…c4a1", "status": "paid" },
  "error": null
}

Expiry#

  • Wallet mode deadlines are enforced by the contract itself — a transaction after the deadline reverts, so “paid after expiry” cannot happen on this rail.
  • Transfer mode keeps watching the deposit address for a grace period after the deadline (hours on mainnet — sized for slow exchange withdrawals) before marking the charge expired. A late deposit is still recoverable: funds sit at the deposit address and settlement is permissionless, so nothing is ever stranded.