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 pay amount and the deposit address (with copy button).
  • A QR code targeting the deposit address.
  • 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.

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.