Taking money in, both ways: driving the flow yourself or handing the player a link.
Before you start
Two ways to take a deposit through a session, with complete code on
Choose a flow. Both settle the same way and both notify you the same way;
what differs is who renders the payment. Sessions run on EVM networks.
Self-hosted — your backend drives every step, your frontend renders it.
Hosted page — one signed call gives you a link; we render the rest.
A third way needs no session at all: a standing wallet address per player,
held by the platform. It is the only option on networks without a payment router, such as Tron.
Everything below is signed as described in Signing requests, except where
an endpoint is marked no signature — those are called by the hosted page in the player's
browser, authenticated by the one-time token in its URL. You never call them yourself.
In the hosted flow that leaves you exactly one call:
POST /v1/hpp/sessions for the link.
/p/{token} is the page that link opens — the address you
redirect to or set as an iframe src — and
/hpp/sdk.js is the script that opens it in a modal for you.
/v1/hpp/step and
/v1/hpp/status are what that page calls on itself
while the player pays; they are documented so nothing about the flow is hidden, not because an
integration has to touch them.
external_order_id is your idempotency key. Send it. A create repeated with the same
reference and the same parameters returns the order you already have; without it, a retry after a
timeout opens a second session against the same deposit.
Deposits · self-hosted
Server-to-server deposit flow. Your backend drives every step and your frontend renders the returned UI schema. The API secret never reaches a browser.
POST/v1/cashier/initsigned
Open a deposit session
Starts a session and returns its first step. Everything in the body is optional, but sending external_order_id is what makes a retry safe.
Prefilled values are validated here, because the steps that would normally check them are the ones being skipped. With no prefill the session starts at network_select; with network at connect_wallet, and the coin picker follows once a wallet is connected; with both at connect_wallet and no picker; add amount and the player's only remaining action is connecting a wallet. See prefilling.
Request body
Field
Type
Required
Description
player_id
string
no
Your player reference. Echoed back in the settlement webhook.
external_order_id
string
no
Your order reference, and the idempotency key: repeating a create with the same reference and the same parameters returns the original session instead of opening a second one. Reusing it with different parameters is a conflict.
callback_url
string (uri)
no
Per-session override of the webhook target configured for your merchant. Empty falls back to the merchant default.
amount
string
no
Optional prefill in whole units, e.g. "125.50". Skips the enter_amount step.
network
string
no
Optional prefill of the network, when you already know which one the player is paying on. Skips the network_select step.
token
string
no
Optional prefill of the asset. Requires network; sending both starts the session at connect_wallet.
language
string
no
The language the step views are written in, as an ISO 639-1 code. A region is ignored, so "en-GB" is English. Unsupported or missing falls back to English rather than failing the call. One of: en, ru, es
400unsupportedMalformed JSON, an unknown network or token, a token sent without a network, or an amount that is not a positive number. Codes: validation, unsupported.
401unauthorizedThe signature, key, timestamp or nonce did not check out: a missing header, an unknown key id, a timestamp more than 5 minutes from ours, a signature that does not match, or a nonce already used inside the window. Do not retry without rebuilding the request — a retry needs a new timestamp, a new nonce and a new signature.
403forbiddenThe credential is valid but the caller's IP is not in the allowlist configured for your merchant. Not retryable.
{
"error": {
"code": "forbidden",
"message": "client IP not allowed for this merchant"
}
}
409conflictThis external_order_id already belongs to a session created with different parameters. Repeating a create with identical parameters is safe and returns the original session; changing the amount, player or token under a reference you already used is not, and is refused rather than silently resolved.
{
"error": {
"code": "conflict",
"message": "external_order_id \"deposit-1001\" already belongs to session sess_9f2c1e with different parameters"
}
}
503unavailableA dependency we need to answer safely is degraded, so the request was refused rather than served wrongly. Transient: retry with backoff, using a fresh timestamp, nonce and signature.
Submits the action the current step asked for and returns the next step. The action name and the shape of its value come from the step you are on — see the step machine.
Request body
Field
Type
Required
Description
session
string
yes
Session id returned by init.
action
string
yes
The action the current step expects, e.g. select_network.
value
string
no
The action's value. Empty for actions that carry none, such as poll.
400invalid_actionAn action that does not belong to the current step, a terminal session being advanced, a malformed value such as an address or transaction hash of the wrong shape, or a payer wallet that does not hold enough of the token to cover the deposit. Codes: validation, invalid_action, insufficient_funds, insufficient_allowance, tx_reverted.
{
"error": {
"code": "invalid_action",
"message": "action \"submit_amount\" is not valid at step \"connect_wallet\""
}
}
401unauthorizedThe signature, key, timestamp or nonce did not check out: a missing header, an unknown key id, a timestamp more than 5 minutes from ours, a signature that does not match, or a nonce already used inside the window. Do not retry without rebuilding the request — a retry needs a new timestamp, a new nonce and a new signature.
503unavailableA dependency we need to answer safely is degraded, so the request was refused rather than served wrongly. Transient: retry with backoff, using a fresh timestamp, nonce and signature.
Read-only and safe to poll while the chain listener settles in the background. Each poll is a separate signed request and therefore needs its own nonce.
401unauthorizedThe signature, key, timestamp or nonce did not check out: a missing header, an unknown key id, a timestamp more than 5 minutes from ours, a signature that does not match, or a nonce already used inside the window. Do not retry without rebuilding the request — a retry needs a new timestamp, a new nonce and a new signature.
503unavailableA dependency we need to answer safely is degraded, so the request was refused rather than served wrongly. Transient: retry with backoff, using a fresh timestamp, nonce and signature.
Your backend creates a payment link; the player pays on a page we host, reached by redirect or in an iframe. Only the link-creating call is signed — the browser calls are scoped to the one-time token in the URL.
The token is returned once and stored only as a hash, so it can never be read back. A repeat of the same order therefore carries a new token on the same session: a caller retrying a create never received the first link, so replacing it costs nothing.
Request body
Field
Type
Required
Description
player_id
string
no
Your player reference. Echoed back in the settlement webhook.
external_order_id
string
no
Your order reference, and the idempotency key: a repeat with the same parameters returns the same session carrying a fresh link. See the 409 response for the cases that are refused.
return_url
string (uri)
no
Where the hosted page sends the player after a terminal step.
amount
string
no
Optional prefill: fixes the deposit amount so the hosted page never asks for it.
network
string
no
Optional prefill of the network, when you already know which one the player is paying on.
token
string
no
Optional prefill of the asset. Requires network.
callback_url
string (uri)
no
Per-session override of the merchant's configured webhook target.
language
string
no
The language the hosted page is shown in, as an ISO 639-1 code. A region is ignored, so "en-GB" is English. Send the language your player is playing in: the page is reached by a redirect that carries nothing about them, so this is the only thing that decides it. Never a reason to refuse a deposit — an unsupported or missing language opens the page in English. One of: en, ru, es
401unauthorizedThe signature, key, timestamp or nonce did not check out: a missing header, an unknown key id, a timestamp more than 5 minutes from ours, a signature that does not match, or a nonce already used inside the window. Do not retry without rebuilding the request — a retry needs a new timestamp, a new nonce and a new signature.
403forbiddenThe credential is valid but the caller's IP is not in the allowlist configured for your merchant. Not retryable.
{
"error": {
"code": "forbidden",
"message": "client IP not allowed for this merchant"
}
}
409conflictEither this external_order_id already belongs to a session created with different parameters, or the order it names is no longer pending — a payment is already on the chain for it, and handing out a new link would strand the player who is paying. Create a new order under a new reference.
503unavailableA dependency we need to answer safely is degraded, so the request was refused rather than served wrongly. Transient: retry with backoff, using a fresh timestamp, nonce and signature.
Called by the hosted page itself, authenticated by the token in the URL rather than by your API key. You do not need to call this — it is documented because it is what the page in the player's browser does.
400validationA missing token, an action that does not belong to the current step, a malformed value, or a payer wallet that does not hold enough of the token to cover the deposit. Codes: validation, invalid_action, insufficient_funds.
A small script that opens the hosted page in a modal iframe and reports lifecycle events to the parent window. See the iframe flow for the snippet and the events it posts.
Request
curl -X GET "$BASE_URL/hpp/sdk.js"
Response 200
JavaScript.
Prefilling
network, token and amount are optional on both create
calls and mean the same thing in each: when you already know what the player is depositing, the
matching step is skipped and the session starts further along.
Prefilling token does more than skip a screen. The coin picker runs after the wallet
connects and lists what that wallet holds, so a session without a prefilled token gives the player
the coins they can actually pay with — and lets them pick a different one if they change wallets.
Prefill the token only when the order really is denominated in it.
You send
Session starts at
nothing
network_select
network
connect_wallet, then token_select
network + token
connect_wallet, and the coin picker is skipped
network + token + amount
connect_wallet, and the player is never asked for a coin or an amount
Prefilled values are validated when the session is created, because the steps that would normally
check them are exactly the ones being skipped. An unknown network, a token that does not exist on
that network, a token without a network, or an amount that is not a positive number are all
rejected at create time rather than surfacing at settlement.
Deposit statuses
Status
Set by
Meaning
pending
the session
Created; no payment transaction submitted yet.
processing
the session
Payment transaction submitted; waiting for confirmations.
completed
the chain listener
Reached the required confirmation depth. This is when you credit.
failed
the chain listener
Settlement was unsuccessful.
Confirmation depth is amount-based: larger deposits wait for more blocks. The listener matches a
payment by orderRef (which is keccak256(session_id)) and falls back to
the transaction hash, so a deposit reconciles even if the browser never reported the hash.
Webhooks are the way to learn the outcome. If one never arrives — your endpoint was down through
every retry — read the session with /v1/cashier/status
and credit from its status, deduplicating on external_order_id exactly
as the webhook handler does.