Documentation
Everything you need to send, receive, and verify webhooks with HookRelay.
Quickstart
Get your first event delivered in under 5 minutes.
Sign in to your dashboard and click + New project. Give it a name and you'll land on your project page.
Go to the API Keys tab, enter a label, and click Generate key. Copy the key immediately — it's only shown once.
Go to the Endpoints tab and add the URL HookRelay should deliver events to. Use webhook.site for testing.
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}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
POSTrequests with a JSON body - Return a
2xxstatus 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")
);
}Retries & failures
Failed deliveries are retried automatically with exponential backoff:
After 5 failed attempts the delivery is marked failed. You can manually replay any event from the Events tab.