A webhook is an address in another tool that we call the moment something happens. Instead of your tool asking us again and again whether anyone new registered, we send it a message right away. Webhooks never email anyone.
Add a webhook
- Open the API page and find Webhooks. Only owners and admins can see it.
- Under Address (URL), paste the address your tool gave you. Zapier's "Catch Hook" and Make's "Custom webhook" both give you one.
- Under Send When, tick what you want to hear about (Someone Registers and Someone Checks In are ticked to start). Pick one event, or leave All Events to cover every event in your organization.
- Choose Add Webhook, then Send Test. Your tool should receive a "ping" message.
- You can have up to 10 webhooks. Each has its own Signing Secret, which your tool can use to check a message really came from us.
- Later, use the On/Off switch to pause a webhook, Edit Webhook to change its address or what it sends, and the trash icon to delete it. Recent deliveries are listed under each webhook with whether they arrived.
What we send
There are four changes: attendee.registered (someone registers), attendee.waitlisted (someone joins the waitlist), attendee.cancelled (someone cancels) and attendee.checked_in (someone checks in). Send Test sends a "ping" so you can see it arrive.
Each message is a POST with a JSON body. The attendee is the same object the API reference describes.
{
"id": "DELIVERY_ID",
"type": "attendee.registered",
"created_at": "2026-11-20T09:00:00Z",
"event": { "id": "EVENT_ID", "name": "Tech Summit", "slug": "tech-summit" },
"attendee": {
"id": "ATTENDEE_ID", "email": "sam@example.com", "first_name": "Sam", "last_name": "Lee",
"job_title": "Designer", "company": "Acme", "status": "registered", "registered_at": "2026-11-20T09:00:00Z",
"checked_in_at": null, "table": null, "seat": null, "track": "Design", "ticket_type": null,
"ticket_url": "https://shoalevents.com/t/...", "answers": { "Dietary Needs": "Vegetarian" },
"created_at": "2026-11-01T15:30:00Z"
}
}Headers: Content-Type: application/json, User-Agent: Shoal-Webhooks/1.0, Shoal-Event (the type), Shoal-Delivery (the id) and Shoal-Signature.
Check the signature
Shoal-Signature looks like t=1768900000,v1=abc123. The t part is the time in unix seconds. The v1 part is an HMAC-SHA256 in hex of the text "<t>.<raw body>", made with your Signing Secret. Use the raw body exactly as it arrived, before any parsing. In Node.js:
const crypto = require("crypto");
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || !parts.v1 || age > 300) return false; // older than 5 minutes
const expected = crypto.createHmac("sha256", secret).update(parts.t + "." + rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Retries and limits
- Answer with any 2xx within 10 seconds. Anything else counts as a failure.
- After a failure we try again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, 7 tries in all.
- The same delivery can arrive twice. Use its id to skip ones you've already handled.
- A webhook that has failed for 3 days straight is switched off. Fix the problem, then switch it back on from the API page.
- We only send to https addresses on the normal port (443) that anyone can reach on the internet. Addresses on private networks are refused.
- If you change a Signing Secret with New Secret, tools checking the old one will reject deliveries until you update them.