diff --git a/.changeset/verify-webhook-timestamp.md b/.changeset/verify-webhook-timestamp.md new file mode 100644 index 00000000000..06d87a371f7 --- /dev/null +++ b/.changeset/verify-webhook-timestamp.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': minor +--- + +`verifyWebhook()` now returns the event's top-level `timestamp` (milliseconds since epoch when the event occurred) and `instance_id`, so webhook handlers can order events and identify the instance that sent them. diff --git a/packages/backend/src/__tests__/webhooks.test.ts b/packages/backend/src/__tests__/webhooks.test.ts index b1ce678a927..353cd554e15 100644 --- a/packages/backend/src/__tests__/webhooks.test.ts +++ b/packages/backend/src/__tests__/webhooks.test.ts @@ -248,4 +248,31 @@ describe('verifyWebhook', () => { expect(result).toHaveProperty('event_attributes.http_request.client_ip', '127.0.0.1'); expect(result).toHaveProperty('event_attributes.http_request.user_agent', 'Mozilla/5.0 (Test)'); }); + + it('should parse timestamp and instance_id', async () => { + const clerkPayload = JSON.stringify({ + type: 'user.created', + data: { id: 'user_123', email: 'test@example.com' }, + timestamp: 1654012591835, + instance_id: 'ins_123', + }); + const svixId = 'msg_123'; + const svixTimestamp = (Date.now() / 1000).toString(); + const validSignature = createValidSignature(svixId, svixTimestamp, clerkPayload); + + const mockRequest = new Request('https://clerk.com/webhooks', { + method: 'POST', + body: clerkPayload, + headers: new Headers({ + 'svix-id': svixId, + 'svix-timestamp': svixTimestamp, + 'svix-signature': validSignature, + }), + }); + + const result = await verifyWebhook(mockRequest, { signingSecret: mockSecret }); + expect(result).toHaveProperty('type', 'user.created'); + expect(result).toHaveProperty('timestamp', 1654012591835); + expect(result).toHaveProperty('instance_id', 'ins_123'); + }); }); diff --git a/packages/backend/src/api/resources/Webhooks.ts b/packages/backend/src/api/resources/Webhooks.ts index 5d0bcf927a9..f9f3ea08d4d 100644 --- a/packages/backend/src/api/resources/Webhooks.ts +++ b/packages/backend/src/api/resources/Webhooks.ts @@ -25,7 +25,14 @@ type WebhookEventAttributes = { }; }; -type Webhook = { type: EvtType; object: 'event'; data: Data; event_attributes: WebhookEventAttributes }; +type Webhook = { + type: EvtType; + object: 'event'; + data: Data; + event_attributes: WebhookEventAttributes; + timestamp: number; + instance_id: string; +}; export type UserWebhookEvent = | Webhook<'user.created' | 'user.updated', UserJSON> diff --git a/packages/backend/src/webhooks.ts b/packages/backend/src/webhooks.ts index 3f1e7c69392..4f8976cbc8c 100644 --- a/packages/backend/src/webhooks.ts +++ b/packages/backend/src/webhooks.ts @@ -135,6 +135,8 @@ export async function verifyWebhook(request: Request, options: VerifyWebhookOpti object: 'event', data: payload.data, event_attributes: payload.event_attributes, + timestamp: payload.timestamp, + instance_id: payload.instance_id, } as WebhookEvent; } catch (e) { return errorThrower.throw(`Unable to verify incoming webhook: ${e instanceof Error ? e.message : 'Unknown error'}`);