Production checklist
Go through it before you accept real payments. Each item closes a known way to lose money. The order of the integration work is in turnkey integration.
Keys and access
- The API key is kept in a secrets manager and used only on the server. It is not in a browser, a mobile app, a repository, or logs.
- The webhook secret is kept the same way, separately from the API key. The same secret signs webhooks and the redirects from the checkout page.
- The product calls the API only over
https://: overhttp://the key would travel in plain text before the redirect happens — see authentication. - The webhook URL in the project settings starts with
https://: the cabinet (Dashboard) also acceptshttp://, and then events travel in plain text.
Webhooks
- The signature is verified against the raw request body, before JSON parsing — see signature.
- Signatures are compared in constant time.
- Webhooks whose
X-Timestampdiffers from the current time by more than 5 minutes are rejected. - The decision to fulfill an order is based on the API response —
GET /v1/invoices/{id}— not on the webhook body. - A
per_usertop-up is credited to the user only afterGET /v1/deposits/{tx_hash}shows its transfer withstatus=confirmedandcredited=true— see verify a transfer. - Duplicates are dropped by the event
id, and a redelivery of the same event does not fulfill the order a second time. - The handler does not depend on the order of events:
invoice.confirmedcan arrive beforeinvoice.seen. - The handler responds
2xxwithin 10 seconds, including to event types it does not process, and does heavy work asynchronously — see delivery.
Payments
- An order is fulfilled only when the invoice status is
confirmedorsettled, not onseenand not when the transaction first appears — see statuses. - The product has defined what it does on
underpaid, onpaid_late, and on an overpayment —confirmedwithamount_receivedgreater thanamount_expected. - An order is not fulfilled on
partially_paid. If the product shows the payment itself, the customer seesamount_remainingand the same address, see partial payment. - Amounts are handled as strings or as integers in minimum units, never as floating-point numbers.
- The number of decimal places is taken for the specific asset and is not hardcoded: USDT on BNB Chain has 18, not 6. An amount in an API response has exactly as many digits after the point as its asset has decimals.
- The product compares its price with
amount_requestedand asks the customer foramount_expected— see fees. - Each order creates an invoice with its own
order_id, and a retry after a timeout uses the sameorder_idwith the same fields.
Display
- The address is shown to the user in full, without shortening, together with the network and the asset — see displaying the address.
- The user is warned that a transfer on another network or of another token is not credited automatically.
- The integration accepts more than one transfer in a transaction and a negative
log_indexother than-1: a payment in the native coin of an EVM network sent by a smart contract, such as ETH or BNB from a smart-contract wallet, a multisig or an exchange's withdrawal, comes with-2,-3and so on — see what arrives at an address. - An invoice created without
assetis paid only throughcheckout_url: until the customer chooses a token, it has no address.
Checkout page
- If the checkout page is used, the return and cancel URLs are set in the project settings: without them the page does not send the customer back — see return URLs.
- The redirect signature and the 5-minute window of
tsare verified, and the order is never fulfilled on a redirect — see signature. - A visit to the cancel URL with
status=created,status=seen, orstatus=partially_paiddoes not cancel the order: the invoice can still be paid.
Payouts
- Payout addresses are on the whitelist in advance: the operator adds them, and a new address becomes usable only after a delay, 24 hours by default.
- A payout address is taken from the whitelist and never from transaction history.
- Each payout is created with its own
reference, and a retry uses the samereference. -
pending_reviewis handled as an accepted payout waiting for a person, not as an error, and the outcome is taken fromGET /v1/payouts/{id}.
Testing
- The whole flow — creating an invoice, paying, receiving the webhook, checking the status through the API, and fulfilling the order — has been run with a small amount before accepting real payments.