← All guides

Send test webhooks to any URL β€” free, signed, no backend

You're building the receiving side of a webhook integration β€” the endpoint that will one day catch events from Stripe, GitHub, or your own platform's event bus. To test it, you need someone to actually send you webhooks: real HTTP POSTs, with real signatures, on demand. Tools like webhook.site solve the opposite problem (inspecting webhooks you receive); hand-crafting curl commands gets old the moment signatures are involved.

Mockbird's mock APIs can fire HMAC-SHA256-signed webhooks at any URL every time a record changes β€” which turns any project into a free, scriptable webhook sender. No signup required. Here's the whole flow.

1. Create a project with some data (20 seconds)

curl -X POST https://HOST/api/projects \
  -H 'content-type: application/json' -d '{"name":"webhook-lab"}'
# β†’ {"id":"abc123","adminKey":"...","baseUrl":"https://HOST/m/abc123"}

curl -X POST https://HOST/api/projects/abc123/resources \
  -H 'content-type: application/json' -H 'x-admin-key: YOUR_KEY' \
  -d '{"name":"orders","template":"orders","seed":10}'

2. Point the webhook at your endpoint

curl -X PUT https://HOST/api/projects/abc123/webhook \
  -H 'content-type: application/json' -H 'x-admin-key: YOUR_KEY' \
  -d '{"url":"https://your-app.example.com/hooks/orders"}'
# β†’ {"ok":true,"webhook":{"url":"...","secret":"whsec_...","events":["created","updated","deleted"]}}

Save the secret β€” that's your signing key. You can also restrict events to a subset, e.g. {"url":"...","events":["created"]}, and manage all of this from the dashboard's Webhook card instead of curl.

3. Send a test delivery right now

curl -X POST https://HOST/api/projects/abc123/webhook/test -H 'x-admin-key: YOUR_KEY'
# β†’ {"ok":true,"status":200,"duration_ms":143}

The response is synchronous, so you immediately know whether your endpoint answered, what status it returned, and how long it took β€” no digging through logs to find out whether anything arrived.

4. Fire real events on demand

Every write to the mock API produces a delivery. Create, update, or delete records β€” from curl, from your app, from a test script, or by clicking around the dashboard's data editor:

# fires "orders.created" at your endpoint
curl -X POST https://HOST/m/abc123/orders \
  -H 'content-type: application/json' \
  -d '{"product":"Test Plan","quantity":3,"status":"pending"}'

# fires "orders.updated"
curl -X PATCH https://HOST/m/abc123/orders/1 \
  -H 'content-type: application/json' -d '{"status":"shipped"}'

# fires "orders.deleted" (payload includes the removed record)
curl -X DELETE https://HOST/m/abc123/orders/2

Each delivery is a POST with a JSON payload and three headers:

POST /hooks/orders HTTP/1.1
X-Mockbird-Event: orders.created
X-Mockbird-Delivery: evt_k2j9x8a1
X-Mockbird-Signature: sha256=8f3a2c...

{
  "id": "evt_k2j9x8a1",
  "event": "orders.created",
  "project": "abc123",
  "resource": "orders",
  "action": "created",
  "record": { "id": 11, "product": "Test Plan", "quantity": 3, "status": "pending" },
  "ts": 1753430000
}

5. Verify the signature (the part worth practicing)

The signature is HMAC-SHA256 over the raw request body, keyed with your whsec_ secret β€” the same scheme Stripe and GitHub use, so the verification code you write against Mockbird transfers directly. Node:

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret) {
  const expected = "sha256=" +
    createHmac("sha256", secret).update(rawBody).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
// Express: app.post("/hooks/orders", express.raw({type:"*/*"}), (req,res) => {
//   if (!verify(req.body, req.get("x-mockbird-signature"), SECRET)) return res.sendStatus(401);
//   ... })

Python:

import hashlib, hmac

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)

Classic bugs this setup will catch for you: verifying against the parsed-and-re-serialized JSON instead of the raw bytes, comparing with == instead of a constant-time compare, and forgetting that header prefixes like sha256= are part of the header, not the digest.

6. Debug failed deliveries

curl https://HOST/api/projects/abc123/webhook/deliveries -H 'x-admin-key: YOUR_KEY'

The last 20 delivery attempts are logged with status, error (timeout, non-2xx, connection refused…), and duration β€” also visible live in the dashboard. If your endpoint returned a 500 or took longer than the 5-second timeout, you'll see exactly that.

7. Testing against localhost

Deliveries come from the internet, so http://localhost:3000 won't work (private and loopback addresses are refused). Two easy options:

Limits while free: 100 webhook deliveries per project per day, 5s timeout, no automatic retries (use the test endpoint or another record write to re-fire). Full reference in the docs. Everything above works without an account β€” create a project on the landing page and you're sending signed webhooks in under a minute.