|
| 1 | +import { createMetadata, Details } from "@/components/Document"; |
| 2 | +import { DocImage, FeatureCard, OpenSourceCard, Callout } from "@doc"; |
| 3 | +import { ArrowLeftRightIcon, UserLockIcon, UsersIcon, WalletIcon } from "lucide-react"; |
| 4 | + |
| 5 | + |
| 6 | +export const metadata = createMetadata({ |
| 7 | + title: "thirdweb Webhooks", |
| 8 | + description: "Receive real-time updates for onchain and offchain events.", |
| 9 | +}); |
| 10 | + |
| 11 | +# Webhooks |
| 12 | + |
| 13 | +Webhooks are a way to receive real-time updates for onchain and offchain events. |
| 14 | + |
| 15 | +An **event** is a real-time update or action that occured for your team or project. |
| 16 | + |
| 17 | +A **webhook** is a configured endpoint on your backend that receives events. |
| 18 | + |
| 19 | +A **topic** is an identifier that groups events (example: `engine.transaction.sent`). |
| 20 | +- The thirdweb product ("engine") |
| 21 | +- The object ("engine.transaction") |
| 22 | +- The event that occurred ("sent") |
| 23 | + |
| 24 | +Each webhook can subscribe to multiple topics. |
| 25 | + |
| 26 | +## Quickstart |
| 27 | + |
| 28 | +1. Configure an HTTP endpoint on your backend to receive events. |
| 29 | +1. Create a webhook in the thirdweb dashboard to subscribe to topics. |
| 30 | +1. (Recommended) Verify the webhook signature to secure your endpoint. |
| 31 | + |
| 32 | +## Manage your webhooks |
| 33 | + |
| 34 | +Manage your webhooks from the thirdweb dashboard. |
| 35 | + |
| 36 | +1. Select your team and project. |
| 37 | +1. Select **Webhooks** in the left sidebar. |
| 38 | + |
| 39 | +### Create a webhook |
| 40 | + |
| 41 | +Select **Create Webhook** to create a new webhook. |
| 42 | + |
| 43 | +Provide the following details: |
| 44 | +- Description: A name for your webhook. |
| 45 | +- Destination URL: The URL to send the webhook to. Only HTTPS URLs are supported. |
| 46 | +- Topics: The thirdweb topics to subscribe this webhook to. |
| 47 | +- Start Paused: Whether the webhook should immediately start receiving events. |
| 48 | + |
| 49 | +### Update or delete a webhook |
| 50 | + |
| 51 | +Find your webhook in the list. |
| 52 | +- Select **... > Edit** to update your webhook details or subscribed topics. |
| 53 | +- Select **... > Delete** to delete it. |
| 54 | + |
| 55 | +### View analytics |
| 56 | + |
| 57 | +Monitor your webhooks' requests over time to identify errors or latency issues. |
| 58 | + |
| 59 | +## Retries |
| 60 | + |
| 61 | +Webhook delivery attempts only consider a **2xx status code** returned within the **10-second timeout** as successful. |
| 62 | + |
| 63 | +Otherwise the delivery will retry multiple times over the next 24 hours with exponential backoff. |
| 64 | + |
| 65 | +### Automatic suspension of failing webhooks |
| 66 | + |
| 67 | +Webhooks experiencing high error rates (non-2xx status codes) sustained over several hours will be paused automatically. |
| 68 | +A paused webhook cannot receive any webhook events until it is manually resumed from the dashboard. |
| 69 | + |
| 70 | +You will be notified via email and a dashboard notification when your webhook is paused. |
| 71 | + |
| 72 | +## Handle webhook events |
| 73 | + |
| 74 | +Your HTTP endpoint should handle webhook events and return a 200 status code quickly. Avoid slow or long-running synchronous operations, or move them to a queue in your backend. |
| 75 | + |
| 76 | +### HTTP format |
| 77 | + |
| 78 | +Webhooks are sent as a `POST` request to your configured endpoint. |
| 79 | + |
| 80 | +**Headers** |
| 81 | + |
| 82 | +- `content-type`: `application/json` |
| 83 | +- `x-webhook-id`: A unique identifier for this webhook |
| 84 | +- `x-webhook-signature`: HMAC-SHA256 signature |
| 85 | + - See *Verify webhook signature* below |
| 86 | +- `x-webhook-timestamp`: Timestamp of delivery attempt in Unix seconds |
| 87 | + - See *Reject expired webhooks* below |
| 88 | + |
| 89 | +**Request body** |
| 90 | + |
| 91 | +```json |
| 92 | +{ |
| 93 | + "id": "evt_cllcqqie908ii4q0ugld6noeu", |
| 94 | + "type": "engine.transaction.sent", |
| 95 | + "triggered_at": 1752471197, |
| 96 | + "object": "engine.transaction", |
| 97 | + "data": { |
| 98 | + // ...engine.transaction fields |
| 99 | + }, |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +- `id`: A unique identifier for the event for this topic. Multiple delivery attempts for the same event will use the same ID. |
| 104 | +- `type`: The topic that an event was triggered for. |
| 105 | +- `triggered_at`: The timestamp the event was triggered in Unix seconds. |
| 106 | + - Note: This value does not change for each delivery attempt, but the `x-webhook-timestamp` header does. |
| 107 | +- `object`: The object that defines the shape of `data`. |
| 108 | +- `data`: The object payload for the event. |
| 109 | + |
| 110 | +### Secure your webhook endpoint |
| 111 | + |
| 112 | +The `x-webhook-signature` header is a signature that hashes the raw request body and the delivery timestamp. |
| 113 | +This signature ensures that neither the request body nor the timestamp were modified and can be trusted as sent from thirdweb. |
| 114 | + |
| 115 | +Follow these steps to verify the webhook signature: |
| 116 | +1. Concatenate `{TIMESTAMP_IN_UNIX_SECONDS}.{REQUEST_JSON_BODY}`. |
| 117 | +1. Hash the result with the webhook secret using SHA256. |
| 118 | +1. Compare the result with the `x-webhook-signature` header. |
| 119 | + |
| 120 | +**Code examples** |
| 121 | + |
| 122 | +<Details summary="Verify webhook signature in TypeScript"> |
| 123 | +```typescript |
| 124 | +import { createHmac, timingSafeEqual } from "crypto"; |
| 125 | + |
| 126 | +const webhookSecret = "whsecret_..."; // Your webhook secret from the dashboard |
| 127 | +const actualSignature = req.headers["x-webhook-signature"]; |
| 128 | +const timestamp = req.headers["x-webhook-timestamp"]; |
| 129 | +const body = "..." // raw HTTP body as string |
| 130 | + |
| 131 | +// Generate the expected signature. |
| 132 | +const expectedSignature = createHmac("sha256", webhookSecret) |
| 133 | + .update(`${timestamp}.${body}`) |
| 134 | + .digest("hex"); |
| 135 | + |
| 136 | +// Use `timingSafeEqual` to compare the signatures safely. |
| 137 | +const expected = Buffer.from(expectedSignature, "hex"); |
| 138 | +const actual = Buffer.from(actualSignature, "hex"); |
| 139 | +const isValidSignature = |
| 140 | + expected.length === actual.length && |
| 141 | + timingSafeEqual(expected, actual); |
| 142 | + |
| 143 | +if (!isValidSignature) { |
| 144 | + throw new Error("Invalid webhook signature"); |
| 145 | +} |
| 146 | +``` |
| 147 | +</Details> |
| 148 | + |
| 149 | +### Reject expired webhooks (optional) |
| 150 | + |
| 151 | +You can reject webhook attempts that are received after a certain duration. This prevents requests from being replayed. |
| 152 | + |
| 153 | +```typescript |
| 154 | +const MAX_AGE_SECONDS = 10 * 60 * 1000; // 10 minutes |
| 155 | +const timestamp = req.headers["x-webhook-timestamp"]; |
| 156 | +if (Date.now() / 1000 - timestamp > MAX_AGE_SECONDS) { |
| 157 | + throw new Error("Webhook expired"); |
| 158 | +} |
| 159 | +``` |
| 160 | + |
0 commit comments