Atribu
API Reference

Webhooks

Subscribe to what Atribu finishes, instead of polling it — including the platform lifecycle events an agent waits on.

Atribu fans out events to HTTPS endpoints you register. Before #1049 the whole platform was poll-only: an agent that started an OAuth connect, queued a recompute or fired an export had no way to learn it landed except by asking again.

Subscribing

Endpoints
GET    /api/v1/webhooks/subscriptions
POST   /api/v1/webhooks/subscriptions
PATCH  /api/v1/webhooks/subscriptions/{id}
DELETE /api/v1/webhooks/subscriptions/{id}

A subscription names a url, a list of events, and a list of providers. A delivery is matched on both lists — an event whose provider your subscription does not name is not delivered, whatever its event type.

Platform lifecycle events are opt-in, and the trap is the provider list

Every lifecycle event below carries the provider atribu, not a channel — a recompute is not WhatsApp, and a connection.connected for meta_ads is not any messaging channel either. So an existing subscription whose providers is ["whatsapp"] receives none of them until it PATCHes atribu in. The connector's real provider rides in data.provider.

The platform lifecycle events

eventfires when
connection.connecteda provider connect completed
connection.reconnect_requireda connection stopped delivering and needs re-authorizing
connection.revokeda connection was removed
handoff.completeda hand-off you minted was finished by a human
recompute.completedan attribution recompute finished
conversion.attributedonce per conversion definition, on its first attributed conversion
export.completed / export.faileda Conversion Sync export run settled
profile.freshness.changedthe profile's attribution freshness moved
outcome.recordeda GoHighLevel or MercadoPago outcome was recorded on a profile your app provisioned

conversion.attributed firing exactly once, durably, is what makes it usable as "the setup worked" — it is stamped on the definition row by the projector, never recomputed by a scan.

Provider outcomes: outcome.recorded

Each GoHighLevel outcome (a lead, a booking, a won or lost deal — whatever the stage mapping says) and each MercadoPago payment recorded on a profile your app provisioned arrives as one outcome.recorded event:

outcome.recorded
{
  "id": "00000000-0000-4000-8000-00000000000a",
  "type": "outcome.recorded",
  "occurred_at": "2026-09-29T10:00:00.000Z",
  "app_id": "<your app id>",
  "provider": "atribu",
  "connection_id": null,
  "data": {
    "profile_id": "<profile id>",
    "provider": "gohighlevel",
    "outcome_type": "lead_created",
    "occurred_at": "2026-09-29T10:00:00.000Z",
    "recorded_at": "2026-09-29T10:00:04.000Z",
    "external_id": "<provider's id for the outcome>",
    "customer_key": "<provider's customer id>",
    "value": null,
    "contact": { "first_name": "…", "last_name": "…", "email": "[email protected]", "phone": "+15555550100" },
    "contact_withheld": false
  }
}
  • id is stable per outcome. Retries, a re-sync or an attribution recompute never produce a second event for the same outcome. De-dupe on it.
  • Delivery needs a live grant for your app on the profile.
  • contact is included only when that grant carries customers:read. Without it, contact is null, contact_withheld is true, and a customer_key that is not the provider's own opaque id (an email, any phone notation) is null. The person is read at each delivery attempt, so a retry after the customer was erased carries no person.
  • Healthcare profiles never produce it: a healthcare workspace, or a profile in Platform-Safe or HIPAA privacy mode.
  • A first sync records historical outcomes too: filter on data.occurred_at to skip them.

Order and replay. Order is guaranteed only for the replay: GET /api/v1/outcomes/recorded (conversions:read or customers:read) returns the same events with the same ids, in one stable order, from an opaque cursor. The webhook itself is at-least-once and can arrive out of order: drains can overlap, a failed row is retried later, and each delivery has its own retry schedule. De-dupe on id, and reconcile through the replay. Store pagination.cursor (it is present on the last page too) and pass it back later; has_next: false means you are caught up. Use it to backfill a new subscriber or to reconcile after downtime. Events stay replayable for 90 days after delivery; an erased customer's events are not replayed.

The messaging and commerce event types (message.received, order.status.changed, catalog.updated, …) are documented with the surfaces that produce them — see the Node SDK reference.

Secrets, tests and replays

Endpoints
POST /api/v1/webhooks/subscriptions/{id}/rotate-secret
POST /api/v1/webhooks/test/{id}
POST /api/v1/webhooks/deliveries/{id}/replay

rotate-secret is an atomic dual-key rotation with a grace window: the old secret keeps verifying while you deploy the new one, so a rotation is not an outage. test/{id} fires a synthetic, correctly-signed event — the right way to prove your verifier works before real traffic depends on it. replay re-enqueues an existing delivery payload rather than fabricating a new one, so a consumer that was down gets the actual bytes it missed.

Every delivery is HMAC-signed; verify the signature before trusting the body. The runbook for adding a consumer is docs/external-webhooks.md.

MethodPathWhat it does
GET/api/v1/outcomes/recordedReplay outcome.recorded events
POST/api/v1/webhooks/deliveries/{id}/replayReplay a webhook delivery
GET/api/v1/webhooks/subscriptionsList webhook subscriptions
POST/api/v1/webhooks/subscriptionsCreate a webhook subscription
PATCH/api/v1/webhooks/subscriptions/{id}Update a webhook subscription
DELETE/api/v1/webhooks/subscriptions/{id}Delete a webhook subscription
POST/api/v1/webhooks/subscriptions/{id}/rotate-secretRotate a subscription's HMAC secret
POST/api/v1/webhooks/test/{id}Fire a synthetic test event

Generated from openapi.json. The full request and response schema for every operation is in the OpenAPI document.

Next steps

On this page