Operator Webhook Integration Guide
Overview
The system communicates with your backend through a single webhook endpoint. All events (deposits, withdrawal requests, withdrawal completions) are delivered as HTTP POST requests to the URL you configure, differentiated by an event field in the payload.
Configuration
Provide two environment variables to the API server:
| Variable | Description |
|---|---|
OPERATOR_CALLBACK_URL | Your webhook endpoint URL (e.g. https://your-server.com/api/webhook) |
OPERATOR_SECRET_KEY | Shared secret used to sign every request |
If OPERATOR_CALLBACK_URL is not set, no webhook requests are sent.
Request Format
All webhook requests are sent as:
POST <OPERATOR_CALLBACK_URL>
Content-Type: application/x-www-form-urlencodedThe body is a URL-encoded form containing the event-specific fields plus:
| Field | Type | Description |
|---|---|---|
event | string | Event type: DEPOSIT, WITHDRAW_REQUEST, or WITHDRAW_COMPLETE |
hash | string | MD5 signature for request verification |
⚠️
Every request includes a hash field. You must verify this signature — see Signature Verification.
Response Format
Your endpoint must return a JSON response with the following structure:
{
"error": 0,
"description": "ok"
}| Field | Type | Description |
|---|---|---|
error | number | 0 for success, non-zero to indicate an error |
description | string | Human-readable description |
transactionId | string (optional) | Your internal transaction ID |
Quick Start Example
A minimal Express webhook handler:
import express, { Request, Response } from 'express';
import { createHash } from 'crypto';
const app = express();
app.use(express.urlencoded({ extended: true }));
const SECRET_KEY = process.env.OPERATOR_SECRET_KEY!;
function verifySignature(params: Record<string, string>, secretKey: string): boolean {
const hash = params.hash;
if (!hash) return false;
const canonical = Object.entries(params)
.filter(([k, v]) => k !== 'hash' && v != null && v !== 'null' && v !== 'undefined' && `${v}` !== '')
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}=${v}`)
.join('&');
const expected = createHash('md5').update(canonical + secretKey, 'utf8').digest('hex');
return expected === hash;
}
app.post('/api/webhook', (req: Request, res: Response) => {
// 1. Verify signature
if (!verifySignature(req.body, SECRET_KEY)) {
return res.status(401).json({ error: 1, description: 'invalid signature' });
}
const { event } = req.body;
switch (event) {
case 'DEPOSIT':
// Process deposit notification
// Use req.body.idempotencyKey to deduplicate
console.log('Deposit confirmed:', req.body.depositId);
return res.json({ error: 0, description: 'ok' });
case 'WITHDRAW_REQUEST':
// Validate and approve/deny the withdrawal
// Return error: 0 to approve, non-zero to deny
console.log('Withdraw request:', req.body.externalUserId, req.body.fiatAmount);
return res.json({ error: 0, description: 'ok' });
case 'WITHDRAW_COMPLETE':
// Record the completed withdrawal
console.log('Withdrawal completed:', req.body.withdrawRequestId);
return res.json({ error: 0, description: 'ok' });
default:
return res.json({ error: 0, description: 'ok' });
}
});
app.listen(3001, () => console.log('Webhook server running on :3001'));