Webhook Payload

Payload Format

{
topic: string, // topic of the data
timestamp: string, // timestamp of when the payload was sent in seconds
data: [
{
data: object, // data of the event or transaction
status: "new" | "reverted",
type: "event" | "transaction",
id: string // unique id of the data
}
]
}

Example Response:

{
"timestamp": 1743164112,
"topic": "v1.events",
"data": [
{
"data": {
"chain_id": "1",
"block_number": 22140383,
"block_hash": "0x85bd4045d947c34b6b568fc84c7550c0efa741f71c834bbb3d3950e9da27842e",
"block_timestamp": 1743104207,
"transaction_hash": "0x460ced3718d0e09145eae8b63fd985dc366adfc4523c791116f24dc051e8363a",
"transaction_index": 93,
"log_index": 230,
"address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
"data": "0x000000000000000000000000000000000000000000000000009440c61e928cca",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000d360131b77ead72f1f23fb185b4896fe01dc8cb5",
"0x00000000000000000000000085cd07ea01423b1e937929b44e4ad8c40bbb5e71"
],
"decoded": {
"name": "Transfer",
"indexed_params": {
"from": "0xd360131b77ead72f1f23fb185b4896fe01dc8cb5",
"to": "0x85cd07ea01423b1e937929b44e4ad8c40bbb5e71"
},
"non_indexed_params": {
"amount": "41729516213800138"
}
}
},
"status": "new",
"type": "event",
"id": "76b81da4ec46486f0a6e325596f506cad13ebd629520aded104efcaa37a419d5"
}
]
}

Headers

Each webhook request includes:

  • x-webhook-id - ID of the webhook configuration to know which one triggered the send
  • x-webhook-signature - hash of the payload signed with the webhook's secret. Used to validate the payload

To verify that the request is from thirdweb webhooks it is highly recommended to verify the payload. Each webhook has a webhook_secret which is used to sign the raw payload and is then attached to the headers.

To verify the webhook:

const generateSignature = (
rawBody: string,
secret: string,
): string => {
return crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
};
const isValidSignature = (
rawBody: string,
signature: string,
secret: string,
): boolean => {
const expectedSignature = generateSignature(body, secret);
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature),
);
};
// extract the signature from request headers
const signature = req.headers["x-signature"];
// get the raw body of the request
const rawBody = req.rawBody;
const isValid = isValidSignature(rawBody, signature, secret);

You need to use the raw request body when verifying webhooks, as the cryptographic signature is sensitive to even the slightest changes. You should watch out for frameworks that parse the request as JSON and then stringify it because this too will break the signature verification.

Payload Age

You can optionally also verify the age of the payload.
After you have verified the signature, you can be sure the timestamp in the payload is correct and apply any time limit you wish.

export const isExpired = (
timestamp: string,
expirationInSeconds: number,
): boolean => {
const currentTime = Math.floor(Date.now() / 1000);
return currentTime - parseInt(timestamp) > expirationInSeconds;
};

Reorged Data

If a blockchain reorganization occurs:

  • You'll receive an event with status: "reverted" instead of status: "new"
  • You should handle this by reverting any actions taken for the original event