Skip to content

Webhook Notifications ​

Whenever an order status changes (e.g., buyer payment received, trader proof uploaded, payout settled, or order cancelled), the SureLink Gateway posts an HTTP POST notification to the Webhook Callback URL configured in your Merchant Portal.


1. Delivery & Security Specifications ​

1.1 Outbound Request Headers ​

Every Webhook POST request dispatched by the gateway carries symmetric HMAC authentication headers:

HeaderDescription
Content-TypeFixed application/json
User-AgentFixed otc-gateway-webhook/1.0
X-Api-KeyMerchant API Key intended for this event
X-TimestampMillisecond Unix timestamp string
X-Nonce32-character hexadecimal random nonce
X-SignatureHMAC-SHA256 lowercase hex signature computed with the merchant's API Secret

1.2 Signature Verification ​

Merchants must verify the signature over the raw request payload before processing:

$$\text{Expected Signature} = \operatorname{HMAC-SHA256}_{\text{API Secret}}(\text{X-Timestamp} + \text{"."} + \text{X-Nonce} + \text{"."} + \text{RawRequestBody})$$

The computed signature must match the X-Signature header byte-for-byte in constant time.


2. Event Types & Payloads ​

Webhook payloads share a standard top-level envelope:

json
{
  "eventId": "c7a8b9d0-1234-5678-9abc-def012345678",
  "occurredAt": 1715000000000,
  "type": "order.completed",
  "data": { ... }
}

2.1 Event Catalog ​

Event Type (type)TriggerFlow
order.completedPay-in order completed; crypto credited to balancePay-in (DEPOSIT)
order.cancelledPay-in order expired, user cancelled, or dispute cancelledPay-in (DEPOSIT)
payout.proof_submittedTrader wired fiat and uploaded bank transfer receiptPayout (WITHDRAW)
payout.completedMerchant confirmed release or arbitrated completePayout (WITHDRAW)
payout.disputedPayout order entered dispute reviewPayout (WITHDRAW)
payout.cancelledPayout order cancelled; locked crypto refundedPayout (WITHDRAW)

2.2 Pay-in Completion Notification (order.completed) ​

json
{
  "eventId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "occurredAt": 1715000000000,
  "type": "order.completed",
  "data": {
    "orderId": "698c56fa-b0f1-460d-8302-39c09c916781",
    "orderNo": "ORD20261011000001",
    "merchantOrderNo": "MCH_PAYIN_20261011_001",
    "status": "COMPLETED",
    "direction": "DEPOSIT",
    "assetSymbol": "USDT",
    "assetAmount": "100.000000",
    "fiatCurrency": "CNY",
    "fiatAmount": "725.50",
    "fxRate": "7.2550",
    "paymentSubmittedAt": "2026-10-11T08:38:12.000Z",
    "completedAt": "2026-10-11T08:40:05.000Z",
    "chainTxKey": null,
    "createdAt": "2026-10-11T08:35:00.000Z",
    "updatedAt": "2026-10-11T08:40:05.000Z"
  }
}

2.3 Trader Proof Submitted Notification (payout.proof_submitted) ​

Notifies the merchant that the trader wired the fiat funds and submitted proof:

json
{
  "eventId": "8b9c0d1e-2345-6789-abcd-ef0123456789",
  "occurredAt": 1715000100000,
  "type": "payout.proof_submitted",
  "data": {
    "orderId": "3b2e5a7d-8f90-4c12-9e34-5a6b7c8d9e0f",
    "orderNo": "WD20261011000088",
    "merchantOrderNo": "MCH_PAYOUT_20261011_888",
    "status": "PAYMENT_SUBMITTED",
    "direction": "WITHDRAW",
    "fiatCurrency": "CNY",
    "lockedFiatAmount": "1000.00",
    "merchantActionDueAt": "2026-10-11T11:00:00.000Z",
    "paymentDueAt": "2026-10-11T09:30:00.000Z",
    "firstProofSubmittedAt": "2026-10-11T09:15:30.000Z",
    "completedAt": null,
    "reason": null,
    "createdAt": "2026-10-11T09:00:00.000Z",
    "updatedAt": "2026-10-11T09:15:30.000Z"
  }
}

3. Webhook Verification Example ​

typescript
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

const app = express();

app.post('/api/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const apiKey = req.headers['x-api-key'] as string;
  const timestamp = req.headers['x-timestamp'] as string;
  const nonce = req.headers['x-nonce'] as string;
  const receivedSig = req.headers['x-signature'] as string;

  const rawBody = req.body.toString('utf8');
  const myApiSecret = process.env.SURELINK_API_SECRET!;

  // 1. Recompute expected signature
  const source = `${timestamp}.${nonce}.${rawBody}`;
  const expectedSig = createHmac('sha256', myApiSecret).update(source).digest('hex');

  // 2. Constant-time comparison
  const isMatch = receivedSig.length === expectedSig.length &&
    timingSafeEqual(Buffer.from(receivedSig, 'hex'), Buffer.from(expectedSig, 'hex'));

  if (!isMatch) {
    console.error('Webhook signature mismatch; rejected');
    return res.status(401).send('Invalid signature');
  }

  // 3. Process event
  const payload = JSON.parse(rawBody);
  console.log(`Received event: ${payload.type}, ID: ${payload.eventId}`);

  // Return HTTP 2xx to acknowledge receipt
  res.status(200).send('OK');
});
python
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import json
import os

app = FastAPI()
API_SECRET = os.getenv("SURELINK_API_SECRET", "your_secret")

@app.post("/api/webhook")
async def handle_webhook(request: Request):
    api_key = request.headers.get("x-api-key")
    timestamp = request.headers.get("x-timestamp")
    nonce = request.headers.get("x-nonce")
    received_sig = request.headers.get("x-signature")

    raw_body = (await request.body()).decode("utf-8")

    source = f"{timestamp}.{nonce}.{raw_body}"
    expected_sig = hmac.new(
        API_SECRET.encode("utf-8"),
        source.encode("utf-8"),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(received_sig, expected_sig):
        raise HTTPException(status_code=401, detail="Invalid signature")

    event = json.loads(raw_body)
    # Business logic...

    return {"status": "success"}

4. Response Requirements, Retries & Deduplication ​

  1. Response Contract:
    • The merchant endpoint must return an HTTP 2xx status code (e.g., 200 OK) upon receipt.
    • The HTTP timeout threshold is 10 seconds.
  2. Retry Strategy:
    • In case of 5xx errors, 429 Too Many Requests, or connection timeouts, the gateway automatically retries with exponential backoff via an outbox worker.
    • Client errors (4xx) and redirects (3xx) are treated as terminal failures and moved to the dead-letter queue (DLQ) without retrying.
  3. Idempotent Handling:
    • Due to network retries, the same event may be delivered more than once. Merchants must record and deduplicate using eventId. If the eventId has already been processed, return 200 OK immediately without repeating actions.