Skip to main content
Webhooks live on the Orchestration service and cover events from the whole platform, so one subscription endpoint serves every service.
Give this to your AI agent with your endpoint URL:“On gtm-api (MCP connector at https://mcp.gtm-api.com/mcp, or REST at app.gtm-api.com with my key): register a webhook for my endpoint URL with the events I name. The event vocabulary is the enum on the create-webhook schema, so read it there rather than looking for a catalog call, and use events: [\"*\"] if I say I want everything. Then watch the delivery log and show me the first delivery’s status and response code.”

1. Register an endpoint

POST /api/webhooks takes a name, your target_url and the events[] you want:
  • name is required (1 to 255 characters). Omitting it fails validation.
  • target_url must be https:// and must not resolve to a loopback, private or link-local address. DNS names are resolved and checked too, not just IP literals.
  • Each events[] value is validated against the platform event catalog, which currently carries 94 types across the services.
  • The same target_url cannot be registered twice in one team.
  • events: ["*"] subscribes to the entire catalog, including event types added later.
The response returns the webhook’s secret, a 32-character hex string, exactly once. Store it: every later read masks it, and it is what you verify signatures with.
Webhooks are capped per plan. Creating one past the cap answers 402 with webhook_limit_reached and a body carrying used, limit and a suggested action. The free Sandbox plan’s cap is 0, so webhooks need a paid plan. See billing and plans.

Filter what you receive

Two optional filters narrow deliveries at the source, so your endpoint is not woken by events it would discard anyway:
  • filters.account_sid restricts deliveries to one account (ln_ac_... for LinkedIn, em_ac_... for email). The sid is matched verbatim, so channels added later work here without a change.
  • filters.where takes a small filter expression over the event payload. It is validated strictly at write time: nesting depth is capped at 5 and the whole expression at 20 leaf conditions, answering filter_grammar_invalid or filter_leaves_limit_exceeded when it exceeds either.

2. Verify deliveries

Every real delivery is an HTTPS POST with a JSON body and these headers: The body is a fixed envelope, with the event’s own data nested under payload:
The signature is HMAC-SHA256(secret, "{t}.{raw_body}") over the raw request body, with the timestamp mixed in to block replays. Verify before parsing, and reject signatures older than about 5 minutes:
Compute the HMAC over the raw bytes you received, before any JSON re-serialization: parsing and re-encoding the body can reorder keys and break the signature.
Redirects are not followed, deliberately. A 302 from your endpoint counts as a failed delivery, not a hop. Deliveries also time out at 30 seconds total (5 seconds to connect), so acknowledge fast and do the work asynchronously.

3. Test before relying on it

POST /api/webhooks/{sid}/test fires a synthetic delivery at your endpoint, so you can confirm reachability, signature handling and parsing without waiting for a real event. Three things to know about it: it is rate-limited to 10 calls per minute per caller (the budget is shared across all your webhooks), it is rejected with invalid_transition while the webhook’s status is off, and it writes no row to the delivery log. It also sets a subset of the headers above, so build your verifier on the signature, timestamp, event and id headers rather than requiring all six.

4. Retries and auto-disable

A delivery is attempted up to 5 times, with waits of 1 minute, 5 minutes, 30 minutes and 2 hours between attempts. The fifth failure is terminal, so a dead endpoint settles about 2.5 hours after the first attempt. After 20 consecutive failed attempts, or a single 410 Gone, the subscription flips to failed and stops delivering. That transition itself emits webhooks.failed, so a second webhook can page you when the first one dies.

5. Use the delivery log

The log holds one row per event per subscription, not one per attempt: a retry updates that row in place, bumping retry_count and overwriting the response code. The row is the delivery, and its history is the counter.
  • POST /api/webhook-logs/search lists deliveries with status, response code and timing; filter by webhook or event type.
  • POST /api/webhook-logs/{sid}/retry re-sends. Allowed from pending, retrying, failed and success (a manual re-send of a delivered event is legitimate), and only while the parent subscription is still live: a deleted or off webhook rejects the retry.
  • POST /api/webhook-logs/{sid}/cancel stops a delivery that has not settled. Allowed from pending, retrying and in_progress.
  • POST /api/webhook-logs/metrics aggregates outcomes. period with from and to is required and may span at most 90 days.
Both retry and cancel answer 409 with invalid_transition when the row is not in a state that allows the verb, and the error names the state it found.

Rotating the secret

There is no self-service rotation endpoint. If a secret is exposed, delete the webhook and register a new one, then point your verifier at the new secret. Deleting and re-registering also gives you a clean delivery log boundary, which is easier to reason about than a rotation with no overlap window.