Receiver scaffold

Production-ready webhook receivers in Node.js, Python, and Go.

A correct webhook receiver does five things: read the exact request body bytes before any middleware re-serialises them; verify HMAC-SHA256 with a constant-time compare; check that X-Partner-Webhook-Timestamp is within the 5-minute window; deduplicate by an event-specific key; and return any 2xx HTTP status inside the 10-second budget. The receivers below cover all five for the two event types your partner record may be subscribed to: order.status_changed, partner.paid_out. They use an in-process Set for idempotency. Replace it with a persistent store (a key-value cache with TTL, or a database table with a unique constraint). Disable response compression and any body-rewriting middleware on the webhook path.

What the scaffolds share

  • Raw body read (no JSON-parse before signature check).
  • Constant-time HMAC-SHA256 compare against X-Partner-Webhook-Sign.
  • 5-minute timestamp window: -60 ≤ now − ts ≤ 300 seconds.
  • Per-event idempotency key (order:<id>:<status>, paid_out:<paidOutAt>, cleared:<clearedAt>).
  • Fast 2xx (≤ 10 s) with async dispatch of business logic.

Receivers

Pick a language. The receiver listens on PORT (default 4242) and expects the secret in the PARTNER_WEBHOOK_SECRET environment variable.

import express from 'express';
import crypto from 'node:crypto';

const PORT = process.env.PORT || 4242;
const WEBHOOK_SECRET = process.env.PARTNER_WEBHOOK_SECRET;
if (!WEBHOOK_SECRET) {
  console.error('PARTNER_WEBHOOK_SECRET is required');
  process.exit(1);
}

const app = express();

// Capture the raw body BEFORE express.json mutates it.
// The HMAC is computed over these exact bytes.
app.post(
  '/webhook',
  express.raw({ type: 'application/json', limit: '256kb' }),
  (req, res) => {
    const rawBody = req.body; // Buffer
    const sigHeader = String(req.headers['x-partner-webhook-sign'] ?? '');
    const tsHeader = String(req.headers['x-partner-webhook-timestamp'] ?? '');

    if (!verifySignature(rawBody, sigHeader, WEBHOOK_SECRET)) {
      return res.status(401).end();
    }
    if (!verifyTimestamp(tsHeader)) {
      return res.status(401).end();
    }

    let payload;
    try {
      payload = JSON.parse(rawBody.toString('utf8'));
    } catch {
      return res.status(400).end();
    }

    if (isReplay(payload)) {
      return res.status(200).end(); // idempotent re-acknowledge
    }

    // 2xx within 10 s. Do the actual work asynchronously.
    res.status(202).end();
    queueMicrotask(() => dispatch(payload).catch(console.error));
  },
);

function verifySignature(rawBody, sigHex, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(expected, 'hex');
  let b;
  try {
    b = Buffer.from(sigHex, 'hex');
  } catch {
    return false;
  }
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function verifyTimestamp(tsStr) {
  const ts = Number(tsStr);
  if (!Number.isFinite(ts)) return false;
  const now = Math.floor(Date.now() / 1000);
  return now - ts >= -60 && now - ts <= 300;
}

const seen = new Set(); // production: persistent key-value cache, SET-IF-NOT-EXISTS with 24h TTL
function isReplay(payload) {
  const key = idempotencyKey(payload);
  if (!key) return false;
  if (seen.has(key)) return true;
  seen.add(key);
  return false;
}

function idempotencyKey(payload) {
  switch (payload.event) {
    case 'order.status_changed':
      return `order:${payload.order?.id}:${payload.order?.status}`;
    case 'partner.paid_out':
      return `paid_out:${payload.payout?.paidOutAt}`;
    default:
      return null;
  }
}

async function dispatch(payload) {
  switch (payload.event) {
    case 'order.status_changed':
      return onOrderStatusChanged(payload.order);
    case 'partner.paid_out':
      return onPartnerPaidOut(payload.payout);
    default:
      console.warn('Unknown event', payload.event);
  }
}

async function onOrderStatusChanged(order) {
  // your business logic here
  console.log('order', order.id, order.status);
}
async function onPartnerPaidOut(payout) {
  console.log('paid', payout.totalEarnedUsd, 'at', payout.paidOutAt);
}

app.listen(PORT, () => console.log(`webhook receiver on :${PORT}`));

Production notes

  • Idempotency store. Replace the in-process Set with a persistent key-value store (set-if-not-exists with a 24-hour TTL) or a database table with a unique constraint on eventId. A 24-hour TTL is comfortable: re-deliveries never span more than the retry ladder (~4 h 36 m).
  • Async dispatch. If your business handlers can take longer than the 10-second budget, enqueue and return 2xx immediately. The retry contract is documented on the Webhooks page.
  • Logging. Log the event, ts, and an identifying field (order.id / payout.paidOutAt). Do not log the raw signature or secret.
  • TLS termination. Serve the receiver over HTTPS in production. The signature alone authenticates the body bytes, but TLS protects against passive eavesdropping of payloads in transit.

Partner API.
Same engine as 0trace.

A private partner integration surface. Signed quotes, server-side pricing, webhook delivery, multiple reference codes, and a self-serve cabinet — all backed by the production exchange engine.

Need help?

Questions? Answers.

A partner integration surface on top of the same exchange engine that powers 0trace.io. You connect with a signed REST contract, query quotes, open orders, and receive webhook notifications. Our liquidity, our pricing, our payouts. You focus on your product.
No. The API is private and invite-only, aligned with the privacy posture of 0trace itself. There is no KYC, no identity collection, and no source-of-funds reporting required of partners or their end users.
Submit a request through our partner application form at 0trace.io/api/contact-sales and we’ll reply within three business days.
Traffic is encrypted in transit (TLS). Every request is signed with HMAC-SHA256 over the exact request body bytes and gated by a nonce-based replay window. Pricing, fees, and payout amounts are computed server-side. Payouts run on an isolated service that re-verifies each transfer against the on-chain deposit before broadcasting.
A per-partner sliding-window weight budget, default 2500 wu/min (≈50 creates/minute). Endpoint weights: /api/v1/create is 50, /api/v1/qr is 5, every other endpoint is 1. XML feeds run on a separate public bucket. Exceeding the budget returns 429 with a Retry-After header. To raise your cap, send your projected per-endpoint call rate to your operator contact.
Two ways to earn. Revenue share: send us traffic with your referral link and earn a share of our service fee on every order — the visitor sees our live rate. Markup: integrate through the API and add your own margin on top of our rate, shown in your own interface. Use either, or both.
Yes. 0trace operates its own liquidity pool across every supported asset and network. Quotes are recomputed server-side at order creation against the live feed. Payouts are direct, with no third-party intermediary.
Bitcoin, Ethereum, BSC, Solana, Tron, Monero, and Arbitrum One — covering native coins plus the major stablecoins on each network (USDT, USDC, USDC.e).
Yes. We push signed events for order.status_changed and partner.paid_out; you subscribe to the ones you want in your cabinet.

Follow us on X for the latest updates.

Subscribe