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 }.
| Event | Meaning |
|---|---|
withdrawal.status_changed | A payout moved. data matches GET /withdrawals/{id}. |
deposit.received | Funds arrived and are being processed. |
deposit.completed | The deposit is settled and spendable. |
{
"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.
| Header | Notes |
|---|---|
x-webhook-timestamp | Unix seconds. Reject if more than 5 minutes old. |
x-webhook-signature | Lowercase hex HMAC-SHA256 over timestamp + "." + rawBody, using your endpoint secret. |
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
2xxquickly — 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.