Holiday webhooks
Instead of polling the changelog, Festivo can call your HTTPS endpoint whenever a holiday is added, changed or removed. Each delivery is one change, with exactly the same content as its changelog entry.
Available on Growth, Pro and Titan. Set up endpoints in the portal under Integrations → Webhooks: pick the events you want, optionally limit them to some countries, and use Send test to check your endpoint before real changes arrive.
Events
| Event | Sent when |
|---|---|
holiday.created | A holiday appears: newly added, restored after a withdrawal, or published after review |
holiday.updated | A holiday's date, observed date, type, public, regions, name or substitute status changes |
holiday.removed | A holiday is withdrawn (it's then served with deprecated: true, see soft-removed holidays) or taken down for review |
Payload
Every delivery is a POST with a JSON body:
1{2 "id": "evt_hc_981204",3 "type": "holiday.updated",4 "createdAt": "2026-10-12T17:01:12Z",5 "dataVersion": "3.1.0",6 "data": {7 "changeId": "981204",8 "occurrenceId": "20260502_3f6c0d2a9b1e47c58d0e6a71b2c4f9e3",9 "country": "ES",10 "variant": "madrid-community-day",11 "name": "Madrid Community Day",12 "date": "2026-05-02",13 "revision": 1,14 "kind": "updated",15 "fields": {16 "observed": {17 "old": "2026-05-02",18 "new": "2026-05-04"19 }20 },21 "changedAt": "2026-10-12T17:01:12Z"22 }23}
datais the changelog entry for the change:kindiscreated,updated,deprecatedorrestored, andfieldsholds each changed field's old and new value.dataVersionis the contract version the payload is shaped for: your endpoint's pinned version.occurrenceIdis theidof the holiday in list and check responses; on Pro and Titan,/occurrences/{id}returns it with its full history.
Versioning
Webhook payloads are versioned the same way as API responses. Each endpoint is
pinned to a contract version, chosen
when you create it (default: the API's current default), and every payload it
receives is shaped for that version. For example, under 3.1.1 and later
public means "statutory within the holiday's scope", so a holiday changing
from public to regional shows no public change there, while 3.1.0
endpoints see public go from true to false.
- The version is in
dataVersionand theX-Festivo-Data-Versionheader of every delivery. - An endpoint stays on its version until you change it in the portal. New contract versions never change what an existing endpoint receives.
- Any live contract version can be chosen. Patch versions (3.1.x) are available on every plan that has webhooks.
- When you move an endpoint to another version, events queued from then on use the new version. Deliveries already queued, and resends of past deliveries, keep the payload they were created with.
Headers and signature
| Header | Value |
|---|---|
X-Festivo-Event | The event type, e.g. holiday.updated |
X-Festivo-Data-Version | The contract version of the payload, e.g. 3.1.0 |
X-Festivo-Signature | sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your endpoint's signing secret |
User-Agent | Festivo-Webhooks/1.0 |
Verify the signature over the raw body before parsing it, and compare in constant time:
1import { createHmac, timingSafeEqual } from "crypto";23function verifyFestivoSignature(rawBody, header, secret) {4const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");5const a = Buffer.from(expected);6const b = Buffer.from(header || "");7return a.length === b.length && timingSafeEqual(a, b);8}
The secret is shown once when you create the endpoint; you can rotate it in the portal.
Delivery, retries and ordering
- Respond with any 2xx within 15 seconds. Do slow work after responding.
- Retries: any other response, or a timeout, is retried after 1 minute, 5 minutes, 15 minutes, 1 hour, 2 hours, 4 hours, 8 hours and 8 hours (9 attempts over about 23 hours). After that it's marked failed: you can resend it from the delivery log.
- At least once: you may occasionally receive an event twice.
idis the same on every attempt, so ignore ids you've already processed. - Order isn't guaranteed across events. For one holiday,
data.revisiononly goes up: if you've already applied a higher revision, skip the event. - Changes reach webhooks a few seconds after they reach Festivo. If you miss some (an outage on your side longer than the retries), catch up from the changelog.
Disabling or deleting an endpoint, or moving to a plan without webhooks, stops deliveries, including retries already queued.
Delivery log and resending
In the portal, Deliveries on each endpoint lists every delivery from the last 30 days: time, event, holiday, status (queued, retrying, delivered or failed), attempts, the last HTTP result and when the next retry is due. Open a row to see the exact payload.
Resend queues a delivered or failed event again, with the same id, for
example after you've fixed your endpoint. It gets a fresh set of retries.
If an event still fails after its last retry, we email the endpoint's owner, at most once a day per endpoint, with a link to its delivery log.