Events
You get
post.generation.* events for tracked post generation jobs, including the
ones you start with POST /v1/posts/generate, and the first terminal result for
each tracked generation job wins.
Post lifecycle
Post lifecycle events all have the same data shape:{ "postId": "post_123" }.
post.created fires when a post is inserted. post.updated fires when persisted
post fields actually change, including status changes, but not for timestamp-only
updates. post.published fires when a post is inserted as published and every time
a draft becomes published, including after unpublishing. Leaving published status
emits post.unpublished, and any status transition also emits post.updated.
Deleting a post emits post.deleted, including when deleting its collection
cascades to the post. Deleting the organization does not emit post deletion events.
Database triggers write these events in the same transaction as the post change,
which covers API, dashboard, AI and GitHub writes, so a failed event insert rolls
back the post change.
GEO scan results
The first persisted terminal result for a scan ID emits eithergeo.scan.completed
or geo.scan.failed, whether the scan is inserted as terminal or transitions to
terminal. Later metadata changes and repeated terminal updates do not emit another
event, even if the reported outcome changes.
Both events contain string scanId and projectId fields. Completed events also
contain runId (string | null) and checksTotal, checksFailed, mentions and
durationMs (each number | null). Failed events also contain errorCode, error
and failedStage (each string | null) and retryable (boolean | null). Every
field is always present, with null for unavailable values. Notra writes the event
in the same transaction as the scan result. Failure text is bounded, with errorCode
limited to 256 characters and error to 4096 characters. Polling remains available
for scan progress.
Payload version
New events useapiVersion: "2026-10-06" to identify the repeat-publish semantics.
The post.published data payload remains { "postId": "post_123" }, and existing
stored events keep their original payload and version when retried.
Subscribe through the API
Use an organization API key with thewebhooks.write scope:
endpoint and a one-time secret. Save the secret somewhere
safe because you won’t see it again. GET /v1/webhooks lists your subscriptions.
An endpoint can subscribe to between one and twelve event types. Subscriptions
receive only future matching events, so Notra does not backfill existing posts,
jobs and scans.
DELETE /v1/webhooks/{endpointId} removes one and cancels anything not sent yet,
although a request that’s already on its way can still reach the deleted endpoint.
Verify requests
Read the body as raw text before you parse the JSON. Every request has these headers:x-notra-event: the event typex-notra-event-id: a stable event IDx-notra-delivery-id: a stable delivery ID for this endpointx-notra-timestamp: when we signed it, in Unix secondsx-notra-signature:v1,followed by a base64 HMAC-SHA256 signature
whsec_ prefix from your secret, base64-decode the rest and check the
HMAC over this UTF-8 message:
Logs and retries
The delivery table shows the status, response code, attempt count and when each delivery was created. Click a row to see the payload, the destination, the event and delivery IDs, how each attempt went and when the next retry is due. You can retry a failed delivery after a one-minute cooldown, and earlier attempts stay in the history. For API access, usewebhooks.read:
GET /v1/webhooks/deliveries?offset=0&status=allreturns 25 rows andhasMore.GET /v1/webhooks/deliveries/{deliveryId}returns the payload and attempts.POST /v1/webhooks/deliveries/{deliveryId}/retryqueues a failed delivery and needswebhooks.write.
Retry-After and goes up
to six hours. Dispatch and recovery run once a minute, so a delivery or retry can
start a little after its scheduled time.
Finished deliveries stay in the history for 30 days, and anything still in progress
stays until it’s done. We don’t store response bodies. Your endpoint has to use a
public HTTPS hostname on port 443.