Webhook ingestion
POST external events to a per-trigger webhook URL to start GTM Engine workflows.
Webhook triggers give you a unique inbound endpoint that any external system can POST JSON to. Each delivery is stored and can start one or more workflows. Because this is a code integration you drive from your own systems (marketing site, product backend, form provider, etc.), the contract is documented here alongside the REST API.
This endpoint is a receiver, not part of the CRM REST API. It is not authenticated with a Bearer API key and is intentionally not listed in the OpenAPI spec. It authenticates with the trigger's configured secret mode instead (see Authentication).
Endpoint
POST https://api.gtmengine.ai/webhooks/trigger/{triggerId}
Content-Type: application/json{triggerId}— the UUID of your webhook trigger. Create a webhook trigger in the app (Automation → Triggers) and copy its endpoint URL; thetriggerIdis the last path segment.- The endpoint is mounted at the host root (not under
/v1). - Only
POSTis accepted.
Authentication
Every webhook trigger requires a secret — there is no unauthenticated mode. Send it in the X-Webhook-Secret header:
X-Webhook-Secret: your-secret-or-webhook-key| Mode | Header required | How to obtain |
|---|---|---|
| Shared webhook key | X-Webhook-Secret | Create a key on the API Keys page (/automation/api) with the Webhook permission, then select that key on the trigger. Only the selected key is accepted; the same key can be selected for many triggers. |
| Unique secret for this trigger | X-Webhook-Secret | A dedicated secret generated for this trigger, shown once at creation. It's stored only as a hash — rotate the trigger to generate a new one if lost. |
Requests that omit or present an invalid secret are rejected with 401.
Request body
The body must be a JSON object (not an array or primitive). An empty body is treated as {}.
{
"event": "newsletter_signup",
"user": {
"email": "jordan@example.com",
"name": "Jordan Lee"
},
"form": {
"company": "Acme Inc."
}
}- Maximum body size: 512 KB. Larger requests are rejected with
413. - Structural limits: payloads with more than 1,000 total keys or nested deeper than 20 levels are rejected with
400. - The full body is stored and flattened into dot-notation fields (e.g.
payload.user.email,payload.form.company) that you map to workflow inputs. See Using the payload as variables.
Responses
| Status | Body | Meaning |
|---|---|---|
202 Accepted | { "received": true } | Delivery accepted, stored, and enqueued. |
400 Bad Request | { "error": "Request body must be valid JSON." } | Body was invalid, exceeded structural limits, or did not satisfy the required inputs for an assigned workflow. |
401 Unauthorized | { "error": "Missing or invalid webhook secret." } | A secret was required but missing/invalid. |
404 Not Found | { "error": "Webhook trigger not found." } | No active webhook trigger with that ID. |
413 Payload Too Large | { "error": "Payload too large." } | Body exceeded 512 KB. |
429 Too Many Requests | { "error": "Rate limit exceeded. Please try again later." } | Too many requests for this trigger; back off and retry. |
A 202 means the delivery was accepted and stored — the downstream workflow runs asynchronously. Don't retry on 202. Retrying on 5xx is safe.
Examples
cURL
curl -X POST "https://api.gtmengine.ai/webhooks/trigger/8d88fcdc-cdc6-4ca9-9c1c-acf4d183dd12" \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: your-secret-or-webhook-key" \
-d '{"event":"newsletter_signup","user":{"email":"jordan@example.com"}}'JavaScript (fetch)
await fetch(
"https://api.gtmengine.ai/webhooks/trigger/8d88fcdc-cdc6-4ca9-9c1c-acf4d183dd12",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Webhook-Secret": process.env.GTM_WEBHOOK_SECRET,
},
body: JSON.stringify({
event: "newsletter_signup",
user: { email: "jordan@example.com" },
}),
},
);