Webhooks

Webhooks let ClientLoop notify your systems when something happens — for example, when an application's status changes — by sending an HTTP request to a URL you control, instead of you having to poll the API.

A webhook is a subscription owned by an agency or an org. It has:

  • a URL — the HTTPS endpoint ClientLoop delivers to;
  • a list of events it is subscribed to (see Events);
  • an enabled flag controlling whether deliveries are sent;
  • a signing secret, returned once when the webhook is created, that you use to verify a delivery's signature and confirm it genuinely came from ClientLoop.

Delivery

When a subscribed event occurs, ClientLoop sends an HTTP POST to the webhook's URL with the event payload as a JSON body. Each request is signed — verify the signature before trusting it — and you respond with 200 or 201 to acknowledge the delivery.

If your endpoint returns any other status (or is unreachable), the delivery is retried with exponential backoff — growing up to one hour between attempts — for up to 7 days. After that the delivery is marked failed and no further attempts are made.

Every delivery is recorded, retained for 90 days, and can be retrieved for a webhook through the API so you can inspect attempts, response codes, and the delivered payload.

Resending a delivery

A delivered or failed delivery can be resent from the webhook's page in the portal, or with the webhookRequestReplay mutation. The delivery is put back in the queue and sent again unchanged — the same payload, URL and eventId — and is retried as usual if your endpoint does not accept it. A delivery that is still pending is already queued and can't be resent, and a suspended webhook can't be resent to.

Because the eventId is unchanged, a handler that deduplicates on it treats a resent delivery as an event it has already seen. To have it processed again, clear that event from your deduplication store first.

Payload format

Every payload is a JSON object. Alongside the event-specific fields it always carries three common fields:

  • type — identifies the event and its payload version (e.g. application.status.changed.v1), so a single endpoint can dispatch on it. It matches the entry in the webhook's events list. See Versioning.
  • eventId — a stable identifier for the event. It is identical across every retry and every resend, so use it to deduplicate (deliveries are retried for up to 7 days, so the same event can reach you more than once).
  • requestId — a fresh identifier minted for each individual delivery attempt, useful for tracing a specific request in your logs. It is also sent as the cl-request-id header.
{
  "eventId": "2cVQF8s0u9c2bq5b3pTn1mY7kde",
  "requestId": "2dWRg9t1v0d3cr6c4qUo2nZ8lef",
  "type": "application.status.changed.v1",
  "...": "event-specific fields"
}

See Events for the catalog of events and their payloads.

Versioning

Each payload type is versioned with a trailing .v<n> (e.g. application.status.changed.v1). Within a version, changes are only ever additive: new fields may be added to an existing version, but a field is never renamed or removed without publishing a new payload version (e.g. application.status.changed.v2). Parse defensively and ignore unknown fields so an added field does not break your handler.

You subscribe to a specific version by listing its type in the webhook's events, so moving to a new version is always an explicit choice on your part.

When a new version is published, the previous one is deprecated rather than removed immediately:

  • These docs are updated as versions are deprecated and new versions are added.
  • An email notice is sent to the technical contact listed on the organization.
  • A deprecated version generally remains available for 180 days after its deprecation, giving you time to migrate.