How delivery works
- Ordered: each webhook receives events in project-position order, one batch at a time. A batch holds up to 100 events and about 256 KB.
- At least once: OAO advances the cursor only after a
2xxresponse. A failed batch is resent with the samewebhook-idheader and the same events, so deduplicate by eventid. - Retries: failures back off from 2 seconds, doubling to at most 5
minutes. A
Retry-Afterheader is honored within that range. Retries continue until the endpoint recovers; events are never dropped. - 410 Gone: the endpoint is disabled until you re-enable it. Its cursor is kept, so re-enabling resumes where delivery stopped.
- Filtered events: events outside the webhook’s filter advance the cursor without a request.
- Latency: each event commit wakes the runtime worker, so a batch usually leaves within tens of milliseconds. The worker also polls every second, so a missed notification delays delivery by at most that long.
OAO_CREDENTIAL_ENCRYPTION_KEY, because the signing secret is stored
encrypted.
Create a webhook
1
Open Webhooks
In the console, open Webhooks under Configure and select Add
webhook. Creating and managing webhooks requires
project:admin.2
Describe the endpoint
Enter a display name and the HTTPS endpoint URL. Choose All events or
the event families you need, such as
run.* and message.created.3
Save the signing secret
The console generates a Standard Webhooks secret that starts with
whsec_.
Copy it into your receiver’s configuration now. OAO stores it encrypted and
never shows it again.4
Choose where delivery starts
New events only starts after the latest committed event. Replay
history delivers the project’s full event history first.
echo "whsec_$(openssl rand -base64 32)":
eventKinds accepts exact event kinds and family wildcards such as run.*.
Omit it, or send null, to receive every event.
Delivery format
Every request is aPOST with a JSON body and three
Standard Webhooks headers:
fromPosition is
exclusive and throughPosition is inclusive; together they cover filtered
events too. cursor uses the same opaque format as the SSE id, so a
receiver can hand it to Last-Event-ID if it ever needs to backfill over SSE.
Respond with any 2xx status within 15 seconds. OAO ignores the response
body and does not follow redirects.
Verify signatures
Verify every request before parsing its body. The signed content is${webhook-id}.${webhook-timestamp}.${rawBody}, signed with HMAC-SHA256 using
the base64-decoded part of the secret after whsec_. Reject timestamps more
than 5 minutes from your clock.
This verifier uses only Web Crypto, so it runs unchanged in Node.js 20+,
browsers, edge runtimes, and Convex:
oaoSignature.ts
verifyEventWebhookSignature from @oao/sdk-js.
Any Standard Webhooks library also verifies OAO deliveries.
Receive events in Convex
A Convex HTTP action is a natural receiver: it verifies the batch, and one mutation applies it transactionally. Your Convex queries then update every subscribed client, with no connection between your UI and OAO.1
Store the secret
convex/oaoSignature.ts.2
Add tables for events and messages
convex/schema.ts
3
Apply a batch idempotently
convex/oao.ts
4
Expose the HTTP action
convex/http.ts
5
Point the webhook at Convex
Use your deployment’s HTTP actions domain, which ends in
.convex.site,
plus the route path: https://happy-animal-123.convex.site/oao/events.Include message content
By default,message.created carries only metadata such as the role. With
includeMessageContent: true, OAO adds a message object with the text of
user messages and of the assistant’s final reply. That is the same transcript
text an authorized reader gets from GET /sessions/{sessionId}.
Rotate the signing secret
Send a new secret toPUT /event-webhooks/{webhookId}/credential. For 24
hours, OAO signs every delivery with both the new and the previous secret, so
you can update your receiver without rejecting deliveries. Set
previousCredentialTtlSeconds to change that window, from 0 for immediate
revocation up to 7 days.
Monitor delivery
The webhook resource reports delivery health:lastErrorCode is one of http_status, timeout, connection_failed,
destination_blocked, endpoint_gone, or signing_key_unavailable. The last
code means the worker cannot decrypt the secret; check that the API and worker
share the same OAO_CREDENTIAL_ENCRYPTION_KEY.
Changing the endpoint URL, event filter, or message-content setting discards a
pending batch and forms the next one under the new configuration. Enabling or
disabling the webhook keeps a pending batch, which is resent under the same
webhook-id. After any of these changes, a request already in flight is not
recorded, even if the old endpoint answered 2xx or 410, and its events are
delivered again. Delivery resumes immediately, or within 20 seconds when a
request was in flight, so batches never overlap. To replay history into a new
receiver, create another webhook with deliverFrom: "beginning".
Security
- Destinations: endpoints must use HTTPS and resolve only to public addresses. OAO pins the connection to the checked address, does not follow redirects, and times out after 15 seconds.
- Secrets: the signing secret is write-only and stored with AES-256-GCM. Responses, audit entries, events, and logs carry only its fingerprint and version.
- Endpoint URLs: project admins can read the full URL, and audit entries record only its origin. Authenticate with the signature, and never put credentials in the URL.

