Webhooks and email alerts
Instead of polling the changes feed, let us tell you. Set up webhook endpoints and email recipients in your dashboard; each one has its own calendars and its own choice of all changes or confirmations only.
| Starter | Pro | Business | Enterprise | |
|---|---|---|---|---|
| Webhook endpoints | — | 3 | 20 | Unlimited |
| Email recipients | 2 | 5 | 20 | 50 |
Events
Section titled “Events”holiday.confirmed
Section titled “holiday.confirmed”Sent when an estimated holiday is confirmed, whether its dates stayed or moved. Islamic holidays such as Eid are
estimated months ahead and confirmed by announcement; this is the event that tells you. A confirmation that arrives
more than 7 days after the holiday began, such as a correction to a past year, appears only in dates.changed.
{ "id": "evt_9f3c1b2a4d5e6f70", "type": "holiday.confirmed", "created_at": "2027-03-08T19:42:10Z", "release": { "id": "20270308T193500Z", "previous_id": "20270301T030000Z" }, "calendar": { "id": "SA", "name": "Saudi Arabia" }, "data": { "name": "Eid al-Fitr", "dates": ["2027-03-10", "2027-03-11", "2027-03-12"], "previous_dates": ["2027-03-09", "2027-03-10", "2027-03-11"], "moved": true }}dates.changed
Section titled “dates.changed”One per calendar per release, listing every date the release added, removed or modified, with the entry before and
after (the same fields as the holidays endpoint). Confirmations appear here too. When a calendar is new in the
release, its event also has "new_calendar": true and "holidays" (how many holidays it has): every entry is listed
as added, but nothing about an existing calendar changed.
The dashboard’s Send test event sends one to a single endpoint straight away.
Receiving webhooks
Section titled “Receiving webhooks”We POST each event as JSON to your URL with these headers:
| Header | Value |
|---|---|
FC-Event-Id |
The event’s id; use it to ignore duplicates |
FC-Event-Type |
holiday.confirmed, dates.changed or ping |
FC-Signature |
t=<unix seconds>,v1=<hex HMAC-SHA256> |
Answer with any 2xx within 10 seconds. Anything else, including a redirect, is a failure and is retried after 1, 5
and 15 minutes, then 1, 3, 6 and 12 hours. An endpoint that fails for 3 days is disabled and you get an email; turn
it back on in the dashboard.
Events can arrive out of order. Each names its release: order by release.id, or simply refetch the calendar.
Verifying the signature
Section titled “Verifying the signature”Compute HMAC-SHA256 of <t>.<raw body> with your endpoint’s signing secret (whsec_…, in the dashboard), compare it
with v1 in constant time, and reject a t more than 5 minutes old.
Node:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret) { const parts = Object.fromEntries(header.split(",").map((p) => p.split("="))); if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex"); return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));}Python:
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) if abs(time.time() - int(parts["t"])) > 300: return False expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get("v1", ""))Use the raw request body, before any JSON parsing.
Email alerts
Section titled “Email alerts”Each recipient confirms their address from an email before alerts start, and every alert has a one-click unsubscribe. A recipient gets one email per release, leading with confirmations: “Eid al-Fitr confirmed in Saudi Arabia: 10 March 2027”.