Developers

Webhooks and API

When something happens on an AOS Sites website (a new lead, a finished form, a publish), we post a signed JSON message to the addresses you choose. Connect Zapier or Make with no code, or your own system with a few lines.

Overview

  • • Add endpoints in Settings → Integrations (the owner or an admin, on any paid plan).
  • • Every message is signed with that endpoint's secret, so you can tell it came from us.
  • • If your endpoint doesn't answer with a 2xx, we try again for about 24 hours.
  • • Each event has an id. A retry sends the same id, so you can ignore one you've already handled.

Events

Webhook events
EventSent when
lead.createdSomeone gets in touch through your site: a form, AOS's chat or a call.
lead.updatedA lead moves to contacted, booked, won or lost, or its job value changes.
form.submittedSomeone finishes one of your smart forms, with every answer.
booking.createdSomeone books or requests a time on your booking calendar.
booking.updatedA booking is confirmed, moved, canceled, or marked done or no-show.
site.publishedYour changes go live.

lead.created

{
  "id": "3f0c2a8e-6b1d-4c2e-9f7a-5d4e3c2b1a00",
  "type": "lead.created",
  "created_at": "2026-11-12T15:04:05.000Z",
  "account_id": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
  "test": false,
  "data": {
    "id": "6f1d0c52-2b7a-4c1e-9a43-1f2e3d4c5b6a",
    "name": "Jordan Lee",
    "email": "jordan@example.com",
    "phone": "+16155550123",
    "message": "Do you do roof inspections after hail?",
    "channel": "form",
    "status": "new",
    "source": "google_search",
    "landing_path": "/",
    "page_path": "/contact",
    "device": "mobile",
    "created_at": "2026-11-12T15:04:05Z"
  }
}

lead.updated

{
  "id": "3f0c2a8e-6b1d-4c2e-9f7a-5d4e3c2b1a00",
  "type": "lead.updated",
  "created_at": "2026-11-12T15:04:05.000Z",
  "account_id": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
  "test": false,
  "data": {
    "id": "6f1d0c52-2b7a-4c1e-9a43-1f2e3d4c5b6a",
    "name": "Jordan Lee",
    "email": "jordan@example.com",
    "status": "booked",
    "previous_status": "contacted",
    "job_value_cents": 840000,
    "updated_at": "2026-11-13T09:30:00Z"
  }
}

form.submitted

{
  "id": "3f0c2a8e-6b1d-4c2e-9f7a-5d4e3c2b1a00",
  "type": "form.submitted",
  "created_at": "2026-11-12T15:04:05.000Z",
  "account_id": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
  "test": false,
  "data": {
    "id": "0b8e7d6c-5a4b-4f3e-8d2c-1b0a9f8e7d6c",
    "form_name": "Free estimate",
    "answers": {
      "service": "Roof repair",
      "timeline": "This month",
      "address": "12 Oak St"
    },
    "submitted_at": "2026-11-12T15:04:05Z",
    "lead": {
      "id": "6f1d0c52-2b7a-4c1e-9a43-1f2e3d4c5b6a",
      "name": "Jordan Lee",
      "email": "jordan@example.com"
    }
  }
}

booking.created

{
  "id": "3f0c2a8e-6b1d-4c2e-9f7a-5d4e3c2b1a00",
  "type": "booking.created",
  "created_at": "2026-11-12T15:04:05.000Z",
  "account_id": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
  "test": false,
  "data": {
    "id": "7c6b5a49-3e2d-4c1b-8a09-f8e7d6c5b4a3",
    "type": "Free estimate",
    "location": "visit",
    "start_at": "2026-11-14T15:00:00Z",
    "end_at": "2026-11-14T15:30:00Z",
    "timezone": "America/Chicago",
    "status": "booked",
    "name": "Jordan Lee",
    "email": "jordan@example.com",
    "phone": "+16155550123",
    "source": "site",
    "created_at": "2026-11-12T15:04:05Z"
  }
}

booking.updated

{
  "id": "3f0c2a8e-6b1d-4c2e-9f7a-5d4e3c2b1a00",
  "type": "booking.updated",
  "created_at": "2026-11-12T15:04:05.000Z",
  "account_id": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
  "test": false,
  "data": {
    "id": "7c6b5a49-3e2d-4c1b-8a09-f8e7d6c5b4a3",
    "type": "Free estimate",
    "start_at": "2026-11-15T16:00:00Z",
    "status": "booked",
    "previous_status": "booked",
    "previous_start_at": "2026-11-14T15:00:00Z",
    "name": "Jordan Lee",
    "updated_at": "2026-11-13T09:30:00Z"
  }
}

site.published

{
  "id": "3f0c2a8e-6b1d-4c2e-9f7a-5d4e3c2b1a00",
  "type": "site.published",
  "created_at": "2026-11-12T15:04:05.000Z",
  "account_id": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
  "test": false,
  "data": {
    "site_id": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a98",
    "slug": "vanguard-roofing",
    "url": "https://vanguard-roofing.site.agency-os.ai",
    "published_at": "2026-11-12T15:04:05Z"
  }
}

What we send

A POST with a JSON body (above) and these headers. Answer with any 2xx within 10 seconds; do slow work after you answer.

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: AOS-Sites-Webhooks/1.0
AOS-Event: lead.created
AOS-Event-Id: 3f0c2a8e-6b1d-4c2e-9f7a-5d4e3c2b1a00
AOS-Delivery: 9d8c7b6a-5f4e-4d3c-8b2a-1f0e9d8c7b6a
AOS-Signature: t=1794495845,v1=5b7e…

Check the signature

Each endpoint has its own secret (whsec_…), shown when you add it and from its Secret button. Compute an HMAC-SHA256 of the timestamp, a dot and the raw body, and compare. Reject anything older than 5 minutes.

import { createHmac, timingSafeEqual } from "node:crypto";

// AOS-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
export function fromAosSites(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=", 2)));
  const t = Number(parts.t);
  if (!parts.v1 || !Number.isFinite(t)) return false;
  if (Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 minutes: a replay
  const want = Buffer.from(createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"));
  const got = Buffer.from(parts.v1);
  return want.length === got.length && timingSafeEqual(want, got);
}

Retries

A timeout, 408, 429 or 5xx is tried again, 8 tries in all over about 24 hours: after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 6 hours. Any other 4xx, or a redirect, isn't retried (redirects aren't followed: use the final address). Answering 410 Gone switches the endpoint off, and 25 failed messages in a row do the same. Settings → Integrations keeps a 30-day log with every answer, and a “Try again now” button.

Zapier and Make

  1. In Zapier, start a Zap with Webhooks by Zapier → Catch Hook (in Make: Webhooks → Custom webhook) and copy the address it gives you.
  2. In AOS Sites, go to Settings → Integrations, add that address and pick the events.
  3. Press Send a test; Zapier or Make now sees every field to map.

Building your own Zapier app, or a tool that subscribes for itself? Use the REST hooks API below with an API key.

API

Base address https://site.agency-os.ai/api/v1. Make a key in Settings → Integrations and send it as Authorization: Bearer aos_live_…. Answers are { ok: true, data } or { ok: false, code, message }; 120 requests a minute per key.

# Who the key belongs to (use it to test a connection)
curl https://site.agency-os.ai/api/v1/me -H "Authorization: Bearer $AOS_KEY"

# Subscribe: post this event to target_url  → 201 { ok, data: { id } }
curl -X POST https://site.agency-os.ai/api/v1/hooks -H "Authorization: Bearer $AOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target_url":"https://hooks.zapier.com/hooks/standard/123/abc","event":"lead.created"}'

# This key's subscriptions
curl https://site.agency-os.ai/api/v1/hooks -H "Authorization: Bearer $AOS_KEY"

# Unsubscribe
curl -X DELETE https://site.agency-os.ai/api/v1/hooks/<id> -H "Authorization: Bearer $AOS_KEY"

# Recent events in the webhook shape (Zapier's sample data), a JSON array
curl "https://site.agency-os.ai/api/v1/samples?event=lead.created" -H "Authorization: Bearer $AOS_KEY"

Revoking a key ends its subscriptions too. There's no write access to leads or the site through the API.