Payment statuses, balance and widget URL

The three things every integration eventually asks about: what each payment status means and which ones are final, how fees hit your balance, and what the checkout URL you get from a widget payment actually does.

Payment statuses

A payment moves through these statuses. The terminal ones never change again, and each of them fires your webhook once.

StatusMeaningTerminal
createdWaiting for the buyer to pick a cryptocurrency on the checkout page. No crypto amount or wallet address yet.no
pendingA currency and a wallet are assigned - the buyer picked one in the widget, or you named target_currency on the h2h endpoint - and the transfer is awaited.no
confirmedThe transfer was found on-chain and confirmed. Deliver.yes
confirmed_manualConfirmed from a transaction hash submitted by hand in the cabinet, for transfers the scanner missed. Treat exactly like confirmed.yes
failedThe buyer picked a currency but did not pay within the payment's time window.yes
expiredNobody picked a currency within 48 hours of creation.yes

Two clocks drive the two failure states:

  • A created payment expires 48 hours after creation.
  • A pending payment fails ttl seconds after the currency was picked - ttl is a merchant setting, 300 seconds by default. The checkout page shows this countdown to the buyer, because the crypto amount is fixed at that moment from the exchange rate and cannot stay open indefinitely.

Only a payment past created carries amount, currency and wallet_address: they appear together the moment a currency is assigned. GET /v1/payments/{id} documents this as two shapes in the API reference.

expired payments release their external_id: creating a new payment with the same id after expiry makes a new payment, while repeating the id of a live one returns that payment - see the idempotency note on the creation endpoints.

Balance and fees

Your account holds a balance, in USD. Fees are charged against it - never deducted from the crypto the buyer sends, which goes straight to your wallet.

  • Each payment carries a fee computed at creation: amount × percentage + fix, from the rates on your merchant.
  • fee_payer decides who covers it. payee (the default): the fee is charged to your balance when the payment is confirmed. payer: the fee is added on top of the amount the buyer pays, and still settles through your balance in the same movement - the net effect is that the buyer covered it.
  • The charge happens once, on confirmed or confirmed_manual. Failed and expired payments cost nothing.
  • Test payments never touch the balance. A payment created with is_test: true (or through an SDK sandbox client) carries its fee for display only and is excluded from statistics.
  • Top up the balance from the cabinet by paying a top-up invoice in crypto. When the balance runs low you are warned in Telegram if you linked your account there.

The widget URL

POST /v1/payments/widget returns url - the checkout page for that one payment:

https://pay.cryptunnel.io/<payment id>

The page is formed from the payment id alone, so it can be sent anywhere: a redirect, a chat message, a QR code, a bot button. It is not signed and does not expire on its own - it follows the payment's status.

What the buyer sees:

  • While the payment is created: the list of cryptocurrencies your merchant can receive, only from the network family matching the payment's is_test - mainnet coins for a real payment, testnet coins for a sandbox one. A sandbox payment on a merchant with no testnet wallet shows an empty list; attach one first - see Sandbox and faucets.
  • Once a currency is picked: the wallet address, the exact crypto amount, a QR code and the countdown. The payment is now pending.
  • After the outcome: the page shows it and, if you gave it somewhere to go, sends the buyer there.

success_url and fail_url are automatic redirects from that last screen: the buyer is sent to success_url after confirmed or confirmed_manual, and to fail_url after failed. An expired payment stays on the page - nobody was mid-checkout to redirect. Both are optional; without them the page simply shows the outcome. They are a convenience for the buyer, not a signal for you: a buyer can close the tab before the redirect fires, so your system learns the outcome from the webhook or from GET /v1/payments/{id}, never from the redirect. For Telegram bots the return link is a deep link back into the bot - see the Telegram bot recipe.


Did this page help you?