Notion webhooks are at-most-once: reconcile against Sume job ids

Notion webhook events are delivered at most once with up to 8 retries, and carry IDs only. A reconcile loop that keeps Sume jobs from being missed or doubled.

4 min readSume
All posts

A Notion page webhook can miss your handler, so a flow that starts a Sume job from a Notion event needs a periodic reconcile step. Notion's delivery page (read 2026-10-10) describes at-most-once delivery, so an event that exhausts its retries is gone and nothing will replay it.

The same page says retries run up to 8 attempts with exponential backoff, the last about 24 hours after the trigger, and that most events arrive within a minute with a target of 5 minutes. Aggregated events, page.content_updated, page.created and page.deleted, may be delayed slightly, while page.locked, comment.created and similar events are real time. The payload carries identifiers and metadata, not the changed content.

Two different guarantees

Sume's webhooks promise something different. Per the webhooks doc, a job webhook is retried 10 times, 30 seconds apart, with a 10 second timeout per attempt, and you can redeliver by hand with POST /v1/jobs/{job_id}/webhook/redeliver. The job id is the idempotency key. So the Sume side of the flow is recoverable, while the Notion side is not.

The result is that the two ends need different handling. For events coming in from Notion, assume a miss is possible. For results coming back from Sume, assume a duplicate is possible.

Delivery guarantees compared (Notion page and Sume webhooks doc, read 2026-10-10)
PropertyNotion webhookSume job webhook
Delivery semanticsAt-most-onceRetried until acknowledged or exhausted
RetriesUp to 8, exponential backoff10 attempts, 30 s apart
Per-attempt timeoutNot stated on the page10 s
Manual replayNot offered on the pagePOST /v1/jobs/{job_id}/webhook/redeliver
PayloadIDs and metadataJob event with job_id

The reconcile loop

Keep a small table in the place you already store state, such as a Notion database, with one row per page and the Sume job_id you started for it. A scheduled sweep, say every 15 minutes, lists pages changed since the last sweep through the Notion API and compares them with that table. Any page with no job id gets a submit. The submit carries an Idempotency-Key built from the page id and its last edited time, so a webhook that arrives after the sweep already handled it returns the original Sume job rather than starting another.

When Sume reports the job finished, either through the webhook or by reading GET /v1/jobs/{id}/status, write the result link back to the row. If your handler was down for hours, the status read answers the question that the missing webhook could not.

  • Notion event: treat as a hint, then fetch the page.
  • Sweep: the source of truth for what needs a job.
  • Idempotency key: page id plus last edited time.
  • Sume result: write back by job_id, ignore duplicates.

What to avoid

Do not start paid work directly from a content-updated event without a key. Because these events are aggregated, a burst of edits may arrive as one event or several, and a retry from a slow handler could resubmit the same intent. Also do not rely on the order of events across event types. Fetch the current page state, then decide. I did not verify Notion's signature scheme for this post, so use the two verifiers post for that part and keep the Notion and Sume secrets separate.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume