Webhook payload shape and signing
See Webhooks for the full conceptual overview. In brief:- Open Notes sends an HTTP POST to your registered endpoint with a JSON body.
- The
X-OpenNotes-Signatureheader contains an HMAC-SHA256 of the raw request body, keyed with thesecretyou supplied when registering the webhook. - Verify the signature before processing the payload.
POST /api/public/v1/webhooks (not shown in this walkthrough — see the API Reference).
Retry schedule
Open Notes retries failed webhook deliveries (any non-2xx response or connection timeout) using exponential backoff:| Attempt | Delay before retry |
|---|---|
| 1st | immediate |
| 2nd | ~30 s |
| 3rd | ~5 min |
| 4th | ~30 min |
| 5th | ~2 h |
RETRYABLE_STATUSES = [429, 500, 502, 503, 504]
Source: OpenNotes::Client#request_with_retries
Polling fallback
The Discourse plugin does not use webhooks today — it pollsGET /api/public/v1/moderation-actions?action_state=proposed on a background schedule. This is a valid production strategy and guarantees eventual delivery even if your webhook endpoint is temporarily unreachable.
Combine both approaches for the most resilient integration: process webhook deliveries as they arrive, then run a periodic poll to catch anything missed.
Idempotency — preventing double-apply
Open Notes delivers each action at least once. Your endpoint may receive the same action ID more than once (re-delivery after a timeout, a duplicate webhook fire, or a poll that races with a push). Guard every action application with a check against the action state you last recorded:- On receipt of an action, look up the action ID in your local store.
- If the state is already
applied(or later), discard the duplicate silently and return HTTP 200. - Otherwise, apply the action and update local state atomically before returning 200.
ActionExecutor demonstrates this pattern at the platform level:
PATCH /moderation-actions/{id} with action_state=applied, any subsequent delivery of that action ID is safe to discard.
Ordering guarantees
Open Notes does not guarantee that moderation actions arrive in the order they were created. If a post receives two sequential actions (hide then unhide), the second action may arrive before the first under high load or after a re-delivery.
To handle out-of-order delivery:
- Compare
action_statetimestamps when processing actions for the samerequest_id. - Prefer the action with the later
updated_attimestamp. - If timestamps are equal, treat the action with the later
id(UUID v7, lexicographically sortable) as the more recent one.
TASK-1453 — evolving toward exactly-once delivery
Today’s guarantee is at-least-once. TASK-1453 tracks the work to add exactly-once delivery semantics. Until that ships, integrations must assume that any action may be re-delivered and apply the idempotency guards described above. When TASK-1453 lands, the webhook payload will include adelivery_id field. Integrations can use that ID as a deduplication key in addition to the action_id.
Summary checklist
- Verify the
X-OpenNotes-Signatureheader before processing any webhook payload. - Return HTTP 200 immediately on receipt; apply the action asynchronously if needed.
- Store the
action_idof every processed action and discard duplicates. - Run a periodic poll as a fallback to catch missed webhook deliveries.
- Use
updated_at(or UUID v7 ordering) to resolve out-of-order delivery for the same request.
You have now walked through the complete integration lifecycle. Return to the walkthrough overview or consult the Integration Guide overview for the full list of integration obligations before shipping to production.