Draw the checkout inside your own app — the QR, a live stream of the payment, and the rules your screen has to keep.
Sending the customer to checkout_url is one way to take a payment. The other is to draw the checkout yourself, in your own app and your own design. You get the same data the hosted page draws from, and the gateway decides the parts a screen most easily gets wrong.
checkout_stream_url, checkout_data_url, and each route's qr_url from the response.payment.paid webhook. Never from the app.The three URLs are safe on the customer's device. Each carries the payment's checkout token, which opens the payer's view of this one payment — what checkout_url already shows them — and nothing about your account. Your secret key stays on your server.
const stream = new EventSource(payment.checkout_stream_url);
stream.onmessage = (event) => {
const checkout = JSON.parse(event.data); // the whole object, every time
render(checkout);
};
Every frame is the whole checkout — the object checkout_data_url returns — so draw from the latest and keep no state of your own. When EventSource reconnects, its first frame brings you up to date.
Status changes arrive as they commit. Confirmation counts are re-read every 25 seconds, so they can trail the chain by that much. After 30 minutes the server sends event: reconnect and closes, and EventSource reconnects on its own.
Some networks cut long-lived connections. While the stream is down, poll checkout_data_url every few seconds — it is the same object. The hosted page does exactly this.
Outside a browser, any Server-Sent Events client works. All three URLs answer from any origin with no API key.
This is the part that costs a payer money when it is wrong, so the gateway decides it.
payable on the checkout says whether to show anywhere to send money at all. When it is false, show the status instead of an address.payable on a route says whether to show *that* address and its QR code. One chain can close while another stays open: a chain the gateway cannot currently watch is marked unavailable, and unavailable_reason is a sentence you can show the payer.qr_url is present only while its route is payable. Draw it when you have it, and take it down when it disappears.payable turns false the moment anything arrives — including an underpayment. Do not ask for the difference: on an escrow route the gateway collects a payment's money once, and a top-up that lands after that is never collected — it stays at the deposit address. Show what arrived, and ask the payer to contact you.
It also turns false at expires_at, by the clock, before anything else has moved. The stream sends a frame when it does.
qr_url is an image: PNG, or SVG with ?format=svg. It is black on white with its full quiet zone, so it scans on a dark page too.
<img src="https://pay.paygatehq.com/c/cs_…/qr/bsc" width="240" height="240" alt="Scan to pay">
It encodes the route's uri — the wallet link, with the exact amount in it. To draw the code yourself, encode uri with any QR library; it is the same string. Once the route closes the image answers 409 payment_not_payable, so a screen left open on an old frame cannot reload it into a code somebody scans.
When a route has no uri, the gateway cannot name the network for certain. Offer the address to copy, with no QR code and no wallet button.
strategy unique_amount the address is shared and the amount is what identifies the payment, so say "send exactly". On escrow_contract and derived_address the address belongs to this payment alone.chain_id (BSC) or a network_id (Tron) to check it against.refund_window_seconds is how long the money sits on the escrow before it is yours, and the payer may *ask* you for a refund in that time. Do not describe it as a refund they are owed.merchant carries your public name, logo and support address. Somebody sending an irreversible transfer should be able to see that it is you.In test mode a payment has a sim route. Its qr_url encodes the bare address — the simulated chain has no wallet — so you can lay out the screen before you go live. Pay it from your server and watch the frames arrive:
curl -X POST $PAYGATE/v1/test/payments/{id}/pay \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $(uuidgen)" -d '{}'
Pass {"amount":10000000} instead — micro-units, so 10 USDT — to see what an underpayment does to payable. The rest of the cases are in Test mode.