Trigger a run from a webhook

Start a run from a caller that cannot send a CXP API key — a Google Pub/Sub push subscription, a GitHub or Stripe webhook, any provider that signs its own payload under its own scheme.

This endpoint is different from every other one in this reference: the platform does not authenticate the caller. The request body, headers, and query string are forwarded to your agent exactly as they arrived, and your agent is responsible for deciding whether the caller is legitimate. If you do not verify the caller inside your task, anyone who learns the URL can start runs.

Endpoint

POST /api/agents/{agentId}/runs/webhook/{webhookToken}

Auth: none. The webhookToken path segment is the agent's webhook_tokenthe URL is the credential. Treat the whole URL as a secret: share it only with the caller that needs it, and rotate it if it leaks. Reveal, copy, and rotate it on the agent's Webhooks settings page.

The Authorization header is never read or consumed by the platform. For Pub/Sub it carries Google's OIDC token, and it reaches your task intact so you can verify it.

Eligibility

The agent must satisfy all of:

  • managed_by = 'platform'
  • status = 'active'
  • allow_webhook_triggers = true — a per-agent toggle, separate from allow_external_triggers, that platform admins flip on the agent's Webhooks settings page. Enabling it for the first time mints the webhook token.

Request

Any body. JSON is parsed as a convenience but is not required, and the body may be empty. Bodies over 1 MiB are rejected with 413.

There is no input field to fill in and no merge with the agent's saved input configuration — a webhook run's input is the request that arrived, and nothing else.

POST /api/agents/42/runs/webhook/whk_9f3c1a…
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJSUzI1NiIs…

{
  "message": {
    "data": "aGVsbG8gd29ybGQ=",
    "messageId": "11923004001",
    "attributes": { "source": "orders" }
  },
  "subscription": "projects/my-project/subscriptions/my-sub"
}

What your task receives

The task payload's input is the inbound request, under a single webhook key:

json
{
  "webhook": {
    "method": "POST",
    "query": {},
    "headers": {
      "content-type": "application/json",
      "authorization": "Bearer eyJhbGciOiJSUzI1NiIs…",
      "user-agent": "APIs-Google; (+https://developers.google.com/webmasters/APIs-Google.html)"
    },
    "body": {
      "message": { "data": "aGVsbG8gd29ybGQ=", "messageId": "11923004001" },
      "subscription": "projects/my-project/subscriptions/my-sub"
    },
    "rawBody": "{\"message\":{\"data\":\"aGVsbG8gd29ybGQ=\"…",
    "rawBodyEncoding": "utf8",
    "receivedAt": "2026-08-04T13:05:11.482Z"
  }
}
FieldTypeDescription
methodstringHTTP method of the inbound request.
queryobjectQuery string parsed into a flat record; repeated keys are comma-joined.
headersobjectEvery inbound header, names lowercased. Includes Authorization and any signature headers.
bodyanyJSON.parse of the body, or null when the body is empty or not JSON.
rawBodystringThe body's exact bytes. Use this, not body, when verifying a signature — re-serialising parsed JSON does not reproduce the sender's key order or whitespace, so the digest would not match.
rawBodyEncoding"utf8" | "base64"How rawBody is encoded. Binary payloads arrive base64-encoded.
receivedAtstringISO 8601 timestamp of when the platform accepted the request.

Headers are redacted in run history

Your task receives every header verbatim, but the copy stored on the platform — the input snapshot shown on the run detail page — masks the values of authorization, proxy-authorization, cookie, set-cookie, x-api-key, and any header whose name ends in -signature, -secret, or -token. Header names are preserved, so you can still see what a rejected caller sent without a live token being readable by everyone in your organization. rawBody is not stored at all.

Response — 202 Accepted

json
{
  "runId": 1234,
  "status": "queued"
}
FieldTypeDescription
runIdnumberPlatform-side run identity. Use this in callbacks and reads.
status"queued"Initial state; transitions to running once the runtime picks it up.

202, not 200: the platform has accepted the event for processing, not finished it. If you need the agent's output in the HTTP response, use webhook & wait instead.

Errors

StatusReason
401Unknown agent, or the token in the URL does not match. The two are deliberately indistinguishable so agent ids cannot be enumerated.
403allow_webhook_triggers is off for this agent.
413Request body exceeds 1 MiB.
422Agent is not platform-managed, or is not active.
429Rate limit exceeded — 60 requests/min per agent.
500Runtime invocation failed. Callers that retry on 5xx (including Pub/Sub) will redeliver.

Setting up a Google Pub/Sub push subscription

  1. On the agent's Webhooks settings page, enable Allow webhook triggers and copy the webhook URL.

  2. Create the push subscription, pointing at that URL and attaching a service account so Pub/Sub signs each delivery with an OIDC token:

    bash
    gcloud pubsub subscriptions create my-sub \
      --topic=my-topic \
      --push-endpoint="https://app.example.com/api/agents/42/runs/webhook/whk_9f3c1a…" \
      --push-auth-service-account=pubsub-pusher@my-project.iam.gserviceaccount.com \
      --push-auth-token-audience="https://app.example.com"
    
  3. Verify the token in your task. This is the step that makes the endpoint safe — the platform does not do it for you. Validate the JWT from the authorization header against Google's public keys, checking the signature, expiry, the audience you configured, and that the email claim is the service account you expect:

    ts
    const bearer = payload.webhook.headers['authorization']?.replace(/^Bearer /, '')
    if (!bearer) throw new Error('unauthenticated')
    
    // Verify against Google's JWKS (https://www.googleapis.com/oauth2/v3/certs):
    // check signature, exp, aud === your configured audience, and
    // claims.email === 'pubsub-pusher@my-project.iam.gserviceaccount.com'
    
  4. The Pub/Sub message is at payload.webhook.body.message; data is base64-encoded, so decode it before use.

Pub/Sub redelivers on any non-2xx response and can deliver the same message more than once even on success. If duplicate runs are a problem for your agent, dedupe on body.message.messageId inside your task — the platform stays payload-agnostic and does not dedupe for you.