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_token — the 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 fromallow_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:
{
"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"
}
}
| Field | Type | Description |
|---|---|---|
method | string | HTTP method of the inbound request. |
query | object | Query string parsed into a flat record; repeated keys are comma-joined. |
headers | object | Every inbound header, names lowercased. Includes Authorization and any signature headers. |
body | any | JSON.parse of the body, or null when the body is empty or not JSON. |
rawBody | string | The 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. |
receivedAt | string | ISO 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
{
"runId": 1234,
"status": "queued"
}
| Field | Type | Description |
|---|---|---|
runId | number | Platform-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
| Status | Reason |
|---|---|
401 | Unknown agent, or the token in the URL does not match. The two are deliberately indistinguishable so agent ids cannot be enumerated. |
403 | allow_webhook_triggers is off for this agent. |
413 | Request body exceeds 1 MiB. |
422 | Agent is not platform-managed, or is not active. |
429 | Rate limit exceeded — 60 requests/min per agent. |
500 | Runtime invocation failed. Callers that retry on 5xx (including Pub/Sub) will redeliver. |
Setting up a Google Pub/Sub push subscription
-
On the agent's Webhooks settings page, enable Allow webhook triggers and copy the webhook URL.
-
Create the push subscription, pointing at that URL and attaching a service account so Pub/Sub signs each delivery with an OIDC token:
bashgcloud 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" -
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
authorizationheader against Google's public keys, checking the signature, expiry, the audience you configured, and that the email claim is the service account you expect:tsconst 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' -
The Pub/Sub message is at
payload.webhook.body.message;datais 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.