When a calendar cannot be read at all
Some failures are worth waiting out. A provider that times out at 3am is usually fine by 3:05, and an integration that treats every blip as a disaster spends its life crying wolf.
Some failures are not like that. A calendar that is too large to read will be too large to read on the next attempt, and the attempt after that. The condition is permanent in the strict sense: the same request returns the same bytes.
The limit
We read calendars up to 8 MiB and no further. That number is far beyond any real export — a calendar with every booking for a decade is a few hundred kilobytes — so a file over it is almost always a symptom rather than a calendar: an export that concatenated several properties, or a URL that returns a report instead of a feed.
The important part is what it is not: it is not something a retry can clear. Waiting cannot help, and telling a customer to wait is worse than telling them nothing.
Three events, not one event with a flag
A webhook that says "something went wrong" and leaves the reader to guess is the root of most integration bugs. We send three different events, because they call for three different responses:
| Event | Says | What you should do |
|---|---|---|
ical.changed |
A calendar moved | Update your copy |
ical.failure_rate |
Our polling is failing more than your thresholds allow | Watch; it may recover |
ical.error |
This subscription cannot work at all | Stop waiting and act |
The split matters because the actions are opposites. An alert is a request for patience; an error is a request for work. Collapsing them into one event with a severity field forces every consumer to branch on that field to find out which conversation it is in.
The payload
ical.error carries no calendar content — there is nothing to diff — and no
changes block, because three empty buckets would read as "nothing happened",
which is the opposite of the message:
{
"event": "ical.error",
"subscription_id": "3f9c…",
"external_id": "villa-42",
"timestamp": "2026-10-12T08:15:00Z",
"error": {
"code": "calendar_too_large",
"message": "the calendar is larger than our limit of 8 MiB, so it cannot be read at all; no retry will change this until the calendar is smaller",
"permanent": true,
"feed_id": "a18deb5c-1666-4503-98a8-c290204a66c7",
"provider": "Smoobu",
"limit_bytes": 8388608,
"detected_at": "2026-10-12T08:15:00Z"
}
}
permanent: true is the load-bearing field, and it is a promise: we only set it
when we can prove that retrying cannot help. code is the field to key on — the
message is for the human reading the alert, and it names the limit so nobody has
to file a ticket to find out what it is.
Sent once, on the first poll
The condition stays true on every poll, so repeating the event would turn one oversized calendar into an endless stream. We record that we have told you and stay quiet until the calendar becomes readable.
Only the first poll, though. A feed that was readable yesterday and is oversized today is a different story: the provider changed the calendar and may change it back, so that is a stale integration rather than a broken one, and it shows up in the feed's health and request log rather than in a webhook that claims the situation is final.
The full contract is in the API reference.