MegTrustDevelopers

Webhooks

Optional — status is always readable from the API, so build on polling first.

Webhooks tell you that something may have changed. The API stays the source of truth: re-read GET /api/v1/withdrawals/{id} for a payout and GET /api/v1/transactions for movements, and use polling for recovery rather than relying on events alone.

Register a receiver

Register a public HTTPS receiver in the console under Developers → Webhooks, and save its one-time secret separately from your API key.

Events

Every delivery has the same envelope: { id, type, created_at, data }.

EventMeaning
withdrawal.status_changedA payout moved. data matches GET /withdrawals/{id}.
deposit.receivedFunds arrived and are being processed.
deposit.completedThe deposit is settled and spendable.
example delivery
{
  "id": "evt_dep_9c21…_Released",
  "type": "deposit.completed",
  "created_at": "2026-07-30T09:20:00.000Z",
  "data": {
    "id": "9c21…",
    "status": "completed",
    "wallet_id": "acct_7f3a…",
    "asset": "USDT",
    "amount": "500",
    "from_address": "0xfa11…cee7",
    "tx_hash": "0x24de…abcd",
    "received_at": "2026-07-30T09:18:42.000Z"
  }
}

The event id is stable per subject and state, so it is a safe deduplication key for your handler. The full event schemas are in the API reference.

Verify every delivery

Check the signature before trusting a payload. Use the raw body — a re-serialized object will not match.

HeaderNotes
x-webhook-timestampUnix seconds. Reject if more than 5 minutes old.
x-webhook-signatureLowercase hex HMAC-SHA256 over timestamp + "." + rawBody, using your endpoint secret.
verify.mjs
import crypto from "node:crypto";

// Verify a webhook delivery. Use the RAW request body — a re-serialized
// object will not match.
export function verify(rawBody, headers, secret) {
  const timestamp = headers["x-webhook-timestamp"];
  const signature = headers["x-webhook-signature"];
  if (!timestamp || !signature) return false;

  // Reject anything older than 5 minutes (replay protection).
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = crypto.createHmac("sha256", secret)
    .update(timestamp + "." + rawBody).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(signature, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

The supplied client.mjs exports the same check as verifyWebhook:

import { verifyWebhook } from "./client.mjs";
const valid = verifyWebhook(rawBody,
  request.headers.get("x-webhook-timestamp"),
  request.headers.get("x-webhook-signature"),
  process.env.MEGTRUST_WEBHOOK_SECRET);
if (!valid) return new Response("Invalid signature", { status: 401 });
const event = JSON.parse(rawBody.toString("utf8"));
// Durably record event.id and queue processing in one transaction, ignoring duplicates.
// Return 2xx only after that write succeeds; process the queue outside the request.

Handle deliveries safely

  • Deliveries may be duplicated, delayed or arrive out of order. Deduplicate by event.id.
  • Record the event and return 2xx quickly — within 10 seconds — then process it outside the request.
  • For a payout, re-read GET /withdrawals/{id} before acting on a status.
  • Never credit an unsettled deposit as completed.

Retries

Anything other than a 2xx is retried with exponential backoff: up to eight attempts over about eight hours. After that the delivery is left recorded as failed — visible to you on the console's Webhooks tab with its last error.

On this page