Skip to main content
REST API

Signed Webhook + Capturing Inbox

RequestValidateHard

What you will learn

  • Verify an HMAC-SHA256 signature over the RAW request body, never a re-serialized object
  • Use a capturing inbox to assert webhook delivery + signature in CI without a public receiver
  • Reject stale timestamps to defend against replay — signature validity alone is not enough

Description

Verify an HMAC-SHA256 webhook signature over the raw body, with a built-in inbox so you can test it in CI without ngrok.

Test Scenario

import crypto from 'node:crypto';

const subId = 'ci-1';
await request.post('/api/v1/webhooks/signed', { data: { subId, event: 'order.created', payload: { id: 7 } } });

const inbox = await (await request.get('/api/v1/webhooks/signed?subId=' + subId)).json();
const d = inbox.deliveries[0];

const expected = 'sha256=' + crypto.createHmac('sha256', 'whsec_practice_signed_demo_secret')
  .update(d['X-Timestamp'] + '.' + d.rawBody)
  .digest('hex');
expect(d['X-Signature']).toBe(expected);
expect(d.fresh).toBe(true); // reject if false (replay window exceeded)
Endpoint/api/v1/webhooks/signed

Testing Tips

  • •POST to capture a delivery, then GET ?subId= to read its rawBody, X-Signature and X-Timestamp
  • •Recompute HMAC-SHA256 over the literal `${X-Timestamp}.${rawBody}` with the published secret and compare
  • •Sign/verify the RAW body bytes (never a re-serialized object) and reject stale timestamps to stop replay
// Playwright API Testing - REST
import { test, expect } from '@playwright/test';
test('REST API request', async ({ request }) => {
const response = await request.get('/api/v1/resource');
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const data = await response.json();
expect(data).toBeDefined();
});

Challenge ID: rest-webhook-hmac

2 frameworks available

API Playground

Send requests and inspect responses

Endpoint: /api/v1/webhooks/signed

Press Enter to send