Webhooks
We send an HTTPS POST to your endpoint whenever a deposit or payout changes. Your endpoint URL and signing secret (whsec_…) are set up during onboarding.
Delivery
- Reply with any
2xxstatus within 10 seconds. The response body is ignored; redirects count as failures. - Failed deliveries are retried with backoff (10s, 20s, 40s … up to 1 hour apart) for 24 hours.
- Delivery is at least once and not ordered. Deduplicate on
event_idand trust thestatusinsidedata.
Headers
| Header | Value |
|---|---|
X-Event-Id | Same as event_id in the body |
X-Event-Type | Same as type in the body |
X-Timestamp | Unix time in milliseconds, new on every attempt |
X-Signature | Hex HMAC-SHA256, see below |
Events
| type | When |
|---|---|
deposit.detected | A payment to a player's address was seen on-chain. Not final yet: do not credit. |
deposit.confirmed | The deposit is final. Credit the player with data.net (amount minus fee). |
deposit.below_min | The deposit is below the minimum amount and was not credited. |
deposit.flagged | The deposit needs review, for example an asset not enabled on your account. |
deposit.orphaned | A detected deposit's block was reorganized away. A later deposit.confirmed may still follow. |
deposit.swept | The deposit was moved into your vault. Informational: it was already credited at deposit.confirmed. |
payout.pending_approval | The payout is waiting for manual approval. |
payout.completed | The payout is confirmed on-chain. data.tx_hash is set. |
payout.failed | The payout could not be sent. data.reason explains why. |
payout.rejected | The payout was rejected during approval. The reserved amount is released. |
vault.topup | Funds were sent straight into your vault (not via a deposit address). Credited to your balance, no fee. |
vault.withdrawal | Funds left your vault outside a payout, for example your own withdrawal. Debited from your balance. |
Deposit events carry the deposit (deposit_id, chain, asset, decimals, address, player_ref, from, amount, tx_hash, …), see Deposits. Payout events carry the full payout object. Vault events carry vault_event_id, asset, vault, counterparty, amount and tx_hash.
deposit.confirmed
{
"event_id": "9d4f7a12-3b6e-4c8d-a1f0-5e2b7c9d0a14",
"type": "deposit.confirmed",
"created_at": "2026-10-10T08:01:12.000Z",
"data": {
"deposit_id": "c1a5e8f2-7b3d-4e9a-8c6f-1d2e3f4a5b6c",
"status": "confirmed",
"chain": "tron",
"asset": "tron:USDT",
"decimals": 6,
"address": "TQ9xG7c2...7kLm",
"player_ref": "player_1024",
"from": "TFp3...Wq8",
"amount": "50000000",
"fee": "1000000",
"net": "49000000",
"tx_hash": "7c3e...b91d",
"log_index": 0,
"block_number": "68421337",
"finality": "finalized"
}
}Verifying signatures
The signature uses the same scheme as API requests, with your webhook secret as the key:
hex(HMAC-SHA256(whsec_secret, "{X-Timestamp}\nPOST\n{path + query of your webhook URL}\n{raw body}"))- Use the raw request body, before any JSON parsing.
- Use your secret exactly as issued, including the
whsec_prefix. - Reject timestamps older than 5 minutes, and compare signatures in constant time.
import crypto from "node:crypto"; import express from "express"; const WEBHOOK_SECRET = process.env.ICN_WEBHOOK_SECRET; // whsec_... (use as-is) const app = express(); // Keep the raw body: the signature covers the exact bytes we sent. app.post("/webhooks/icryptonow", express.raw({ type: "application/json" }), (req, res) => { const ts = req.get("X-Timestamp"); const raw = req.body.toString("utf8"); const expected = crypto .createHmac("sha256", WEBHOOK_SECRET) .update(`${ts}\nPOST\n${req.originalUrl}\n${raw}`) .digest("hex"); const given = Buffer.from(req.get("X-Signature") ?? "", "utf8"); const fresh = Math.abs(Date.now() - Number(ts)) < 5 * 60 * 1000; if (!fresh || given.length !== 64 || !crypto.timingSafeEqual(given, Buffer.from(expected))) { return res.sendStatus(401); } const event = JSON.parse(raw); // Deliveries are at-least-once: skip event_ids you have already processed. queueForProcessing(event); res.sendStatus(200); // reply fast; do the work asynchronously });
import hashlib, hmac, json, os, time from flask import Flask, abort, request WEBHOOK_SECRET = os.environ["ICN_WEBHOOK_SECRET"] # whsec_... (use as-is) app = Flask(__name__) @app.post("/webhooks/icryptonow") def icryptonow_webhook(): ts = request.headers.get("X-Timestamp", "") raw = request.get_data(as_text=True) # exact bytes we sent path = request.full_path.rstrip("?") # path + query of your webhook URL msg = f"{ts}\nPOST\n{path}\n{raw}".encode() expected = hmac.new(WEBHOOK_SECRET.encode(), msg, hashlib.sha256).hexdigest() fresh = ts.isdigit() and abs(time.time() * 1000 - int(ts)) < 5 * 60 * 1000 if not fresh or not hmac.compare_digest(expected, request.headers.get("X-Signature", "")): abort(401) event = json.loads(raw) # Deliveries are at-least-once: skip event_ids you have already processed. queue_for_processing(event) return "", 200