Integration guide
Connecting your platform takes two pieces of work: your backend calls our API to open deposits and show the pay screen, and you add one endpoint we call to credit a player once their payment is confirmed. Player balances, bonuses and limits stay in your system.
- A signed-in player asks to deposit. Your backend calls
POST /internal/depositswith your player's ID, the amount in dollars and the coin. - We answer with an address in your wallet, the exact amount of the coin and a deadline. You show them to the player.
- The player pays. The money goes straight to your wallet; we only watch the blockchain.
- When the payment has enough confirmations, we call
POST /internal/fundson your platform to credit the player, once.
Everything is server to server, over HTTPS, in JSON. Players' browsers never talk to us directly.
Before you start
What we need from you
- Your platform's base URL for crediting players, for example
https://api.yourcasino.com. We call/internal/fundsunder it. - A credit key: a long random secret we send with every credit, so your endpoint knows the call is from us. We store it encrypted.
- The public IP addresses of your servers, so we can allow them to reach the API.
- Your wallets: for each coin, an extended public key (xpub, ypub or zpub) or a single address. Never a private key or seed phrase. See Wallets.
- Your limits: smallest and largest deposit, the most one player may deposit in 24 hours, and how long a quoted price holds (30 minutes by default).
What you get from us
- The API address:
https://pay.cryptocashier.solutionsif we host it, or your own domain if it runs on your server. - Your API key, starting
cc_live_. It is shown once when issued; if it's lost, we issue a new one and the old one stops working.
Authentication
Send your API key in the X-Api-Key header on every request. The key identifies your operator account by itself.
curl https://pay.cryptocashier.solutions/internal/cashier \
-H "X-Api-Key: cc_live_…"
Keep the key on your server. Anyone holding it can open deposits and act for any of your players. Never put it in a web page, a mobile app or a support ticket. Requests are also only accepted from the IP addresses you gave us.
Deposit API
Every call names the player by playerRef: your own ID for them, up to 64 characters, for example their username. We trust it as given, so only ever pass the player who is signed in, never a value taken from the request body. Player references are compared without regard to case.
/internal/cashier
What your players can pay with right now, and your limits. A coin only appears here once its wallet is set up.
{
"minDeposit": "10.00",
"maxDeposit": "5000.00",
"dailyMax": "10000.00",
"orderMinutes": 30,
"coins": [
{ "id": "usdt_trc20", "label": "Tether", "symbol": "USDT", "network": "TRON (TRC-20)", "confirmations": 20 },
{ "id": "btc", "label": "Bitcoin", "symbol": "BTC", "network": "Bitcoin", "confirmations": 2 }
]
}
/internal/deposits
Opens a deposit for one player. Opening a new one replaces that player's unpaid deposits; one that has already received a payment is kept, so a late payment still lands.
| Field | Type | Meaning |
|---|---|---|
playerRef | string | Your ID for the signed-in player. |
amountUsd | string | Dollars and cents, for example "50.00". |
coin | string | A coin id from /internal/cashier. |
curl -X POST https://pay.cryptocashier.solutions/internal/deposits \
-H "X-Api-Key: cc_live_…" -H "Content-Type: application/json" \
-d '{"playerRef":"player_1042","amountUsd":"50.00","coin":"usdt_trc20"}'
The answer, with status 201:
{
"id": "9f1c2e4a-6d0b-4a51-9a63-2f7d1c8e5b10",
"status": "awaiting",
"coin": "usdt_trc20",
"label": "Tether",
"symbol": "USDT",
"network": "TRON (TRC-20)",
"amountUsd": "50.00",
"amount": "50.00", // what the player sends, in the coin
"address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
"uri": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE", // put this in the QR code
"rate": "1", // dollars per coin used for the quote
"confirmations": 0,
"confirmationsNeeded": 20,
"received": "0",
"expiresAt": "2026-10-15T18:40:00.000Z",
"exactAmount": false, // true: the player must send this amount to the last digit
"reviewReason": null
}
For Bitcoin, Litecoin and Monero the uri is a payment link that wallets open with the amount filled in, such as bitcoin:bc1q…?amount=0.00071234. Bitcoin deposits up to a limit you choose need only one confirmation.
/internal/deposits/{id}?playerRef=…
The same object, current. Poll it every 5 seconds while the player is on the pay screen, and stop at a final status or when expiresAt has passed with nothing received. A deposit can only be read with the playerRef it was opened for.
/internal/deposits?playerRef=…&limit=25
The player's deposits, newest first, up to 100. Each has id, coin, symbol, network, amountUsd, amount, received, address, status, confirmations, confirmationsNeeded, createdAt, paidAt and expiresAt.
Deposit statuses
| Status | Meaning | Final |
|---|---|---|
awaiting | Waiting for the player's payment. | No |
confirming | Payment seen on the blockchain; counting confirmations. | No |
paid | Confirmed, and your platform accepted the credit. | Yes |
review | Paid, but something needs a person: too little sent, paid after the price expired, over the player's daily limit, or your platform refused or did not answer the credit. reviewReason says which. | Until someone acts |
expired | Nothing arrived before the deadline. We keep watching for a day: a payment up to 20 minutes late is credited as normal, a later one goes to review so the price can be checked. | Yes |
closed | Closed by your staff, with a reason. | Yes |
Your credit endpoint
This is the one endpoint you build. When a deposit is confirmed, we send:
POST {your base URL}/internal/funds
Content-Type: application/json
X-Admin-Key: {your credit key}
{
"login": "player_1042",
"amount": "50.00",
"kind": "deposit",
"direction": "credit",
"externalRef": "deposit:9f1c2e4a-6d0b-4a51-9a63-2f7d1c8e5b10",
"reason": "…"
}
login is the playerRef you gave, and amount is in dollars.
How to answer
| You answer | We do |
|---|---|
2xx with a JSON body, such as {"ok":true} | Mark the deposit paid. |
2xx with {"ok":true,"duplicate":true} | The same: you already had it. This is the right answer to a repeat. |
4xx with {"error":"why"} | A refusal (unknown player, closed account). The deposit goes to review with your reason. |
5xx, a timeout (15 seconds) or a body that isn't JSON | We can't know whether you credited it, so the deposit goes to review and is never sent again automatically. A person checks the balance before crediting it again. |
Credit each externalRef once. Store it with a unique constraint, in the same database transaction as the balance change, and answer a repeat with duplicate: true. A deposit can be sent to you again when your staff credit one from review; the reference is what keeps the player from being paid twice.
Example (Node.js, PostgreSQL)
import { timingSafeEqual } from 'node:crypto';
const KEY = Buffer.from(process.env.CRYPTOCASHIER_CREDIT_KEY);
const fromUs = (req) => {
const got = Buffer.from(req.get('X-Admin-Key') ?? '');
return got.length === KEY.length && timingSafeEqual(got, KEY);
};
app.post('/internal/funds', express.json(), async (req, res) => {
if (!fromUs(req)) return res.status(401).json({ error: 'unauthorized' });
const { login, amount, kind, externalRef } = req.body;
if (kind !== 'deposit' || !/^\d+(\.\d{1,2})?$/.test(amount)) {
return res.status(400).json({ error: 'bad request' });
}
const db = await pool.connect();
try {
await db.query('BEGIN');
const p = await db.query(
'SELECT id FROM players WHERE lower(login) = lower($1) FOR UPDATE', [login]);
if (!p.rows[0]) {
await db.query('ROLLBACK');
return res.status(404).json({ error: 'unknown player' }); // a refusal
}
// external_ref is UNIQUE: a repeat fails here and changes nothing.
await db.query(
'INSERT INTO crypto_credits (external_ref, player_id, amount) VALUES ($1, $2, $3)',
[externalRef, p.rows[0].id, amount]);
await db.query('UPDATE players SET balance = balance + $2 WHERE id = $1', [p.rows[0].id, amount]);
await db.query('COMMIT');
return res.json({ ok: true });
} catch (err) {
await db.query('ROLLBACK');
if (err.code === '23505') return res.json({ ok: true, duplicate: true }); // seen before
throw err; // a 500: we park it for a person rather than guess
} finally {
db.release();
}
});
Bonuses, wagering and deposit limits of your own belong in this handler, after the credit, so a problem with them never undoes a deposit.
The pay screen
- Show the amount above the address, with a copy button for each, and the network by name ("USDT on TRON (TRC-20) only"). Coins sent on the wrong network are lost.
- Draw a QR code from
uri. - When
exactAmountis true, say the player must send that amount to the last digit and add any wallet or exchange fee on top. That happens when a coin's wallet is a single address, and the amount is how we tell payments apart. - Show progress: "Waiting for your payment" while
awaiting, then "confirmationsofconfirmationsNeeded" whileconfirming, then "Credited". - Count down to
expiresAt. For Bitcoin and other non-stable coins this is how long the quoted price holds.
A player who sends slightly less because an exchange took its fee is still credited automatically for what arrived, as long as the payment can only belong to one deposit. Anything else goes to review rather than being lost.
Deposits that need a person
For your admin panel. All take and return JSON; the ones that change something ask for a short reason, which is recorded.
| Call | Does |
|---|---|
GET /internal/deposits/review | Deposits in review, with the reason. |
GET /internal/payments/unmatched | Payments that arrived but couldn't be tied to a deposit. |
POST /internal/deposits/{id}/credit {"note"} | Credit a deposit in review, after checking it. Uses the same externalRef, so a credit that had in fact arrived is answered as a duplicate. |
POST /internal/deposits/{id}/close {"note"} | Close it without crediting, for example after refunding the player yourself. |
POST /internal/payments/{id}/assign {"orderId"} | Attach an unmatched payment to the deposit it belongs to. Two staff members can't attach the same payment twice. |
POST /internal/payments/{id}/ignore {"note"} | Mark an unmatched payment as dealt with outside the system. |
Wallets
Deposits go to wallets you own. For each coin we need one of two things:
- An extended public key (xpub, ypub or zpub; for Monero, a view-only wallet). Every player or deposit gets its own address, and an amount that arrives a little short still credits. This is the better option.
- A single address, for wallets that can't export a public key. Every open deposit then gets a slightly different amount so payments can be told apart, and the player must send it exactly.
We never accept a private key or a seed phrase, and the service refuses one if it's pasted. Because it holds only public keys, CryptoCashier can see payments to your wallets but can't move a single coin from them.
Wallets are set up with you during onboarding. GET /internal/wallets shows each network's state (ready, or what is missing) without revealing the keys. Whoever can change a wallet decides where your players' money goes, so treat access to it like access to the wallet itself.
Withdrawals
CryptoCashier can't send money, so withdrawals are paid from your own wallet. Our tooling lets your staff approve a withdrawal, pay it from their wallet and paste the transaction ID, which is checked on the blockchain (right token, right address, enough of it, final) before the withdrawal is marked paid. It runs in production for USDT on TRON. Ask us about connecting it to your platform.
Errors
Errors come back as {"error": "…", "code": "…"}. The error text is written to be shown to a player as it is; use code in your own logic.
| HTTP | code | When |
|---|---|---|
| 400 | bad_amount | The amount isn't dollars and cents. |
| 400 | below_min, above_max | Outside your deposit limits. |
| 400 | daily_limit | Over the player's 24-hour limit. |
| 400 | coin_unavailable | Not a coin you take, or its wallet isn't set up. |
| 401 | unauthorized | Missing or wrong API key. |
| 404 | not_found | No such deposit for this player. |
| 409 | order_paid, order_closed, already_assigned | The deposit or payment was already settled. |
| 503 | prices_unavailable | No current price for that coin. Try again in a minute or offer another coin. |
| 503 | tenant_disabled, wallet_not_configured, busy | Deposits are paused, not set up yet, or too many are open at once. |
| 500 | Something failed on our side. Safe to try again. |
Going live
- We set up your account, your wallets and your limits, and allow your server IPs.
- You build the credit endpoint and the pay screen, using your API key.
- Make one real deposit of the smallest amount and watch it reach
paidand your player's balance. - Make a second one a cent short, and check that it shows up in review on your side. That's the screen your staff will need on a bad day, so it's worth using once before it matters.
Questions at any point: @gregory_ig on Telegram.