Learn

Webhook payloads without a diff cost double.

Notion’s webhooks are signal-only: the payload tells you that something changed — a page id, a list of property ids — never the new values. Every event forces a follow-up GET, and those follow-ups burn the same 3 requests/second budget your automation is already rationing. Here is the failure mode in detail, and what a payload should carry instead.

What a Notion webhook actually sends

A typical delivery for a property change looks like this (July 2026, per Notion’s webhook reference):

Notion delivery (abridged)
{
  "type": "page.properties_updated",
  "entity": { "type": "page", "id": "<page_uuid>" },
  "data": {
    "parent": { "type": "data_source_id", "data_source_id": "..." },
    "updated_properties": ["<prop_id_1>", "<prop_id_2>"]
  }
}

updated_properties is a list of property ids. No old value, no new value, no card title, no column name. To learn what happened you call the API again. A few more contract details that matter when you build on it:

  • Delivery is at-most-once: Notion retries a failed POST (up to 8 attempts within roughly 24 hours) but events can drop under load, and there is no replay API.
  • High-frequency event types are aggregated — batched within a short window, typically arriving inside a minute.
  • Events can arrive out of order; consumers are expected to reorder by timestamp.

The follow-up-fetch death spiral

Now put those properties together on a busy board. A batch step moves 50 cards. You receive ~50 webhooks that each say “something changed on page X.” Your handler dutifully issues 50 follow-up reads — plus block fetches for the cards with content — straight into a 3 req/s limiter. The queue backs up, some reads 429, your retries collide with the next batch’s events, and if a delivery drops mid-storm there is no replay to recover it.

The perverse part: the busier your board, the more your notifications consume your API budget. Teams end up standing up a caching proxy just to answer “what changed?” — infrastructure whose only job is to work around the payload shape.

What a payload should carry

Novum OS events carry the change inline. The same card move looks like this (the envelope is a locked contract, FUNCTIONAL_SPEC §10.5):

Novum OS delivery
{
  "object": "event",
  "event_id": "1d4f3c2a-7a06-4b3a-b3f0-2e9a47b5d1c3",
  "event_type": "card.moved",
  "delivered_at": "2026-05-12T16:00:01.123456Z",
  "entity": { "type": "page", "id": "0192a4b2-..." },
  "data": {
    "card_id": "0192a4b2-...",
    "board_id": "0192a4b2-...",
    "column_id": "0192a4b2-...",
    "from_column_id": "0192a4b2-...",
    "to_column_id": "0192a4b2-..."
  }
}
  • The diff is in data: a move carries from/to columns; an update carries a diff map of the fields that changed. Most consumers never make a follow-up call.
  • At-least-once delivery with an idempotency key: dedupe on event_id and a retry can never double-apply. Failed deliveries retry on a backoff schedule, land in a dead-letter queue, and can be replayed with the same event_id.
  • Every delivery is HMAC-signed (a timestamped, replay-safe scheme plus a compatibility header), so receivers authenticate the exact bytes.
  • Subscriptions filter by event type and board, so a handler only wakes for events it actually wants.

If you're staying put

The pattern that suffers least on sparse payloads: subscribe to the narrowest event types available, dedupe aggregated deliveries before fetching, keep a local last-known-state cache so unchanged fields don’t trigger reads, and give webhook handlers their own slice of the rate budget so a notification storm can’t starve your writes.

Point your webhook receiver at a board that sends the diff — free tier, no card, webhooks included.

Create account

Notion is a trademark of Notion Labs, Inc. Novum OS is an independent product and is not affiliated with or endorsed by Notion Labs. Third-party behavior reflects public documentation as of July 2026 and may change.