Webhooks
Overview & Config

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:

VariableDescription
OPERATOR_CALLBACK_URLYour webhook endpoint URL (e.g. https://your-server.com/api/webhook)
OPERATOR_SECRET_KEYShared 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-urlencoded

The body is a URL-encoded form containing the event-specific fields plus:

FieldTypeDescription
eventstringEvent type: DEPOSIT, WITHDRAW_REQUEST, or WITHDRAW_COMPLETE
hashstringMD5 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"
}
FieldTypeDescription
errornumber0 for success, non-zero to indicate an error
descriptionstringHuman-readable description
transactionIdstring (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'));