Documentation

Everything you need to send, receive, and verify webhooks with HookRelay.

Quickstart

Get your first event delivered in under 5 minutes.

1
Create a project

Sign in to your dashboard and click + New project. Give it a name and you'll land on your project page.

2
Generate an API key

Go to the API Keys tab, enter a label, and click Generate key. Copy the key immediately — it's only shown once.

3
Add an endpoint

Go to the Endpoints tab and add the URL HookRelay should deliver events to. Use webhook.site for testing.

4
Send your first event
curl -X POST https://hook-relay-chi.vercel.app/api/ingest/YOUR_PROJECT_ID \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-HookRelay-Event: order.created" \
  -d '{"order_id": "123", "amount": 49.99}'

You should get back {"id":"...","accepted":true} and see the event appear in your dashboard.

Ingest URL

Every project has a unique ingest URL. POST your events here and HookRelay will fan them out to all your enabled endpoints.

https://hook-relay-chi.vercel.app/api/ingest/{projectId}
The ingest endpoint accepts any valid JSON body. It returns 202 Accepted immediately — delivery happens asynchronously.

Authentication

All ingest requests must include an API key in the Authorization header:

Authorization: Bearer hr_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

API keys are scoped to a project. A key from one project cannot send events to another. Keys are hashed on creation and never stored in plaintext — if you lose a key, delete it and generate a new one.

Sending events

Tag each event with a type using the X-HookRelay-Event header. Use dot-notation by convention.

X-HookRelay-Event: payment.failed

Node.js / fetch:

await fetch("https://hook-relay-chi.vercel.app/api/ingest/YOUR_PROJECT_ID", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
    "X-HookRelay-Event": "payment.failed"
  },
  body: JSON.stringify({
    payment_id: "pay_123",
    amount: 49.99,
    reason: "insufficient_funds"
  })
});

Python / requests:

import requests

requests.post(
    "https://hook-relay-chi.vercel.app/api/ingest/YOUR_PROJECT_ID",
    headers={
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "application/json",
        "X-HookRelay-Event": "payment.failed"
    },
    json={"payment_id": "pay_123", "amount": 49.99}
)

Receiving events

HookRelay delivers events to your endpoints via HTTP POST. Your endpoint should:

  • Accept POST requests with a JSON body
  • Return a 2xx status code to acknowledge receipt
  • Respond within 10 seconds — requests that time out are treated as failures
  • Be idempotent — the same event may be delivered more than once on retry

Express.js receiver:

app.post("/webhooks", express.json(), (req, res) => {
  const eventType = req.headers["x-hookrelay-event"];
  const deliveryId = req.headers["x-hookrelay-delivery"];

  // Always return 200 quickly, process async
  res.sendStatus(200);

  processEvent(eventType, req.body).catch(console.error);
});

Verifying signatures

Every delivery includes an X-HookRelay-Signature header. Verify it to confirm the request came from HookRelay.

X-HookRelay-Signature: t=1714000000000,v1=abc123...

The signed string is {timestamp}.{body}. Verification:

import { createHmac, timingSafeEqual } from "crypto";

function verifySignature(secret, payload, header) {
  const parts = Object.fromEntries(
    header.split(",").map(p => p.split("="))
  );
  const timestamp = parseInt(parts["t"], 10);

  // Reject requests older than 5 minutes
  if (Math.abs(Date.now() / 1000 - timestamp / 1000) > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${payload}`)
    .digest("hex");

  return timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(parts["v1"], "hex")
  );
}
Your endpoint secret is unique per endpoint. Find it in the Endpoints tab on your project page.

Retries & failures

Failed deliveries are retried automatically with exponential backoff:

1st retry10 seconds
2nd retry30 seconds
3rd retry1 minute
4th retry5 minutes
5th retry30 minutes

After 5 failed attempts the delivery is marked failed. You can manually replay any event from the Events tab.

Error codes

202Event accepted — delivery is in progress
400Invalid JSON body
401Missing or invalid API key
403Plan limit reached for endpoints
404Project not found
429Monthly event limit reached for your plan
500Internal error — event was not stored