Hosted checkout

Redirect customers to Payzum's hosted payment page.

For merchants who don't want to render their own deposit page, payzum ships a buyer-facing hosted checkout. Every invoice gets a clean URL that displays the deposit address, QR code, a countdown to expiration, and a live status indicator.

URL pattern

https://<host>/pay/<invoiceId>

When you create an invoice with POST /v1/payment, the response includes an invoice_url field pointing at the hosted checkout. Redirect your customer to that URL.

Create + redirect

Pull the invoice_url out of the create-invoice response and use it as a redirect target on your checkout screen.

curl -X POST "$PAYZUM_BASE/v1/payment" \
  -H "x-api-key: $PAYZUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "price_amount": 49.99,
    "price_currency": "usd",
    "pay_currency": "usdttrc20",
    "order_id": "ORDER-12345",
    "success_url": "https://merchant.example.com/thanks",
    "cancel_url": "https://merchant.example.com/checkout",
    "ipn_callback_url": "https://merchant.example.com/payzum/ipn"
  }' | jq -r '.invoice_url'

Redirect the customer to the returned URL. No further integration is needed for the payment flow.

Creation parameters

The create endpoint accepts the following body parameters (full details in the API reference):

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | price_amount | number (> 0) | yes | Invoice amount in the price currency. | | price_currency | string 2–8 | yes | Pricing currency (e.g. usd). | | pay_currency | string 2–32 | yes | Currency code, a bare symbol (with network), or "all" for buyer choice. | | network | string | no | When sent, pay_currency is interpreted as a bare symbol on this network. | | order_id | string ≤ 255 | no | Your order identifier. Uniqueness is not enforced — send a unique value. | | order_description | string ≤ 2000 | no | Free-text shown on the checkout. | | ipn_callback_url | URL | no | Where signed IPN events are POSTed. | | purchase_id | string | no | Accepted but ignored; the response always returns purchase_id = payment_id. | | success_url | http(s) URL ≤ 2048 | no | Where the checkout redirects the buyer after payment — key to the redirect flow this page describes. | | cancel_url | http(s) URL ≤ 2048 | no | Where the checkout sends the buyer after cancellation/expiry. | | pricing_mode | "fiat" (default) | "direct" | no | "direct" treats price_amount as denominated in the pay currency (then price_currency must equal the pay symbol). |

Validation rule: pricing_mode: "direct" is incompatible with pay_currency: "all" — the API rejects that combination with 400 INVALID_REQUEST.

The endpoint also accepts an optional Idempotency-Key header (per-merchant scope, 24 h TTL) so a retried create returns the original invoice instead of minting a second one — see Idempotency.

What the hosted page shows

  • The chain and pay currency.
  • The exact amount to send — after a partial payment, only what is still due — and the deposit address (with copy buttons).
  • A QR code with the full payment request — network, token, amount still due and deposit address — that the buyer's wallet pre-fills on scan: BIP-21 for Bitcoin (and the same form for Litecoin and Dogecoin), EIP-681 for ETH and ERC-20 tokens on EVM chains, Solana Pay for Solana. Other chains, native POL, BNB and AVAX, and Solana Token-2022 tokens (such as PYUSD) show the bare address, and the Wallet can't read it? Show the address only link under the QR covers wallets that can't read the request.
  • A countdown to invoice expiration.
  • A live status indicator that updates as confirmations arrive.

When the invoice was created with pay_currency: "all", the page first shows a currency selector (searchable, grouped by chain, limited to the merchant's accepted-tokens allowlist). Once the buyer picks, the deposit address and exact amount appear and the flow continues as above — same payment_id throughout.

Pay with wallet

On EVM chains (Ethereum, Polygon, Arbitrum, Optimism, BNB Smart Chain, Avalanche, Base) and Solana, the page also offers a Pay with wallet button. It connects a browser wallet (MetaMask, Trust, Phantom, Rabby, Coinbase Wallet…) or any mobile wallet through WalletConnect, and asks it to sign exactly the transfer the invoice expects — amount, token, network and destination are filled in by payzum.

Nothing changes on your side: the funds land on the same deposit address, are detected the same way, and the invoice moves to paid through the same status flow and IPN. The wallet is just another way to send the same payment; the address and QR stay available.

The button is not shown for Bitcoin, Litecoin, Dogecoin, Tron or TON invoices — those wallets cannot sign from a web page.

Polling status yourself

If you prefer to render your own checkout UI instead of using the hosted page, you can poll the public status endpoint directly without an API key.

curl -s "$PAYZUM_BASE/v1/invoices/pzi_viawy8vaio26d82n023epiq4/status" | jq .

The path parameter is the payment_id returned when you created the invoice (pzi_ followed by 24–32 lowercase alphanumerics). The endpoint:

  • requires no authentication — the unguessable id is the bearer;
  • is rate limited at 60 requests/min per IP (separate from the merchant API budget);
  • returns Cache-Control: no-store and CORS Access-Control-Allow-Origin: *, so it is safe to call directly from a browser.

Status response schema

The response uses camelCase and the buyer-facing status vocabulary — deliberately different from the merchant API's payment objects:

{
  "id": "pzi_viawy8vaio26d82n023epiq4",
  "status": "pending",
  "payAddress": "0x1a2b3c4d5e6f7890a1b2c3d4e5f60718293a4b5c",
  "payAmount": "49.99",
  "payCurrency": "USDT",
  "payChain": "tron",
  "priceAmount": "49.99",
  "priceCurrency": "USD",
  "amountReceived": "0",
  "amountRemaining": "49.99",
  "expiresAt": 1753100096000,
  "paidAt": null,
  "createdAt": 1753096496000,
  "timeRemainingSeconds": 3600
}

| Field | Type | Description | |-------|------|-------------| | id | string | The payment_id. | | status | string | Buyer vocabulary: pending, partial, paid, overpaid, expired, cancelled. | | payAddress | string | Deposit address. | | payAmount | string | Amount to pay, as a decimal string. | | payCurrency | string | Raw symbol (e.g. "USDT"). | | payChain | string | Chain identifier. | | priceAmount | string | Invoice price, as a decimal string. | | priceCurrency | string | Pricing currency, uppercase. | | amountReceived | string | Amount received so far, decimal string. | | amountRemaining | string | Amount still owed, decimal string. | | expiresAt | number | Expiry as epoch milliseconds. | | paidAt | number | null | Paid-at as epoch milliseconds, or null. | | createdAt | number | Creation as epoch milliseconds. | | timeRemainingSeconds | number | Seconds until expiry. |

Errors use the envelope { "statusCode", "code", "message", "error" } where error is the legacy slug:

| error slug | HTTP | code | |--------------|------|--------| | invalid_invoice_id | 400 | INVALID_REQUEST | | rate_limited | 429 | RATE_LIMIT_EXCEEDED (with Retry-After: 60) | | not_found | 404 | PAYMENT_NOT_FOUND |

Customizing the checkout

Logo and accent-color theming for the hosted checkout is on the roadmap. In the meantime, the page renders with neutral payzum branding.