Skip to main content
An AO Webhook gives your organization its own signed ingest endpoint. Any system that can make an HTTPS request — a deploy script, a CI job, an internal service — can send events to AO through it. No client library is required: a request is a JSON body plus two headers. You can create as many AO Webhooks as you like, for example one per source system or environment. Each has its own install ID, secret, and deduplication namespace.

Create a webhook

In AO Cloud, open Integrations, choose AO Webhook, and create it. AO shows three values: You can reveal or rotate the secret later from the installed integrations list. Rotating takes effect immediately: requests signed with the previous secret are rejected, so update your sender at the same time.

Sign each request

Every request carries a timestamp and a signature. The secret itself is never sent. To compute the signature:
  1. Build the string to sign: the X-AO-Timestamp value, a literal ., then the exact bytes of the request body.
  2. Compute HMAC-SHA256 over that string, keyed with the full signing secret as-is, including its whsec_ prefix. Do not decode or trim it.
  3. Hex-encode the digest in lowercase and prefix it with sha256=.
AO verifies the signature against the raw body it receives. Sign the same bytes you send: serialize the JSON once, then sign and send that string. Re-serializing after signing changes the bytes and the signature will not match. AO rejects a request whose timestamp is more than 5 minutes from its own clock, in either direction. Keep your sender’s clock in sync and sign each request, including retries, with a fresh timestamp.

Check your implementation

Your signing code should reproduce this signature exactly:

Examples

Each example sends one event. Set AO_WEBHOOK_URL to your endpoint URL and AO_WEBHOOK_SECRET to your signing secret. The Node.js example is an ES module: save it as a .mjs file.

The request body

The body is an ao.webhook.v1 envelope holding 1 to 500 records:
schema must be exactly ao.webhook.v1. The only record type accepted today is event: Unknown fields in a record are rejected rather than ignored, so a typo surfaces as an invalid_schema error instead of silently dropped data.

Updates and duplicates

Events are identified by source_event_id within a webhook. Sending a record with an ID the webhook has already sent updates that event instead of creating a new one, which makes retries safe. AO keeps the version with the latest observed_at, so when you update an event — for example to add closed_at — set observed_at to the time of the change. Two different webhooks can use the same source_event_id without colliding.

Responses

A successful request returns 200 with the number of records ingested:
If any record fails validation, the whole request is rejected and none are ingested. Errors return a JSON body with a stable code:
Retry internal_error and network failures. Retries are safe because records are deduplicated by source_event_id. Other errors will not succeed unchanged. Re-sign each retry with a fresh timestamp.

Limits