Webhook invocation
Any Agent can expose a unique HTTPS webhook URL. When an external system POSTs to that URL, Pai creates a new task agent run automatically — no Pai access token required.
Common use cases:
- Trigger a planning agent when a new Linear project is created
- Run a code review agent on every GitHub pull request
- Start a data pipeline agent from a Zapier or Make automation
- Invoke an agent from any HTTP-capable tool (curl, n8n, Retool, etc.)
Enable the webhook
Add a webhook entry to spec.triggers on your Agent:
apiVersion: pai.io/v1
kind: Agent
metadata:
name: my-agent
spec:
models:
- anthropic/claude-sonnet-4-6
triggers:
- webhook: {}
system: |
You are a helpful agent. ...
Apply it:
pai apply -f agent-definition.yaml
The controller provisions a Secret with a 256-bit random token and sets status.webhookUrl within seconds:
pai get agent-def my-agent
# Webhook URL https://api.pairun.dev/webhooks/<namespace>/my-agent/<token>
Invoke the webhook
The webhook has two modes — fire-and-forget (default) and synchronous / "lambda" mode (wait set in the body). Pick whichever fits the caller.
Fire-and-forget
curl -X POST "https://api.pairun.dev/webhooks/<namespace>/my-agent/<token>" \
-H "Content-Type: application/json" \
-d '{"title": "Analyse Q1 results and send summary to Slack"}'
Response (HTTP 200, returns immediately):
{"session": "my-agent-wh-a1b2c3d4", "agentDefinition": "my-agent"}
The agent runs in the background. To read its reply, follow Reading the agent's response below.
The title becomes the agent's initial prompt. If omitted, the platform uses "Webhook trigger — <agent-name>".
Synchronous (lambda) mode
Add "wait": <seconds> (or "wait": true for the 300s default) and the apiserver blocks until the agent finishes, returning its reply directly. Useful for HTTP-callable agents — no second round-trip needed.
curl -X POST "https://api.pairun.dev/webhooks/<namespace>/my-agent/<token>" \
-H "Content-Type: application/json" \
-d '{"title": "what sound does a cow make? answer in one sentence", "wait": 300}'
Response on success:
{
"session": "my-agent-wh-a1b2c3d4",
"agentDefinition": "my-agent",
"response": "A cow says moo.",
"phase": "Complete"
}
response carries the agent's last agent.message text. phase is one of:
phase | Meaning |
|---|---|
"Complete" | Agent finished cleanly. response is the final reply (or null if the agent emitted no text). |
"Error" | Agent terminated with an error. Inspect logs/events for details. |
"Running" | The wait budget elapsed before the agent finished. The run is still progressing in the background. The response also includes "timedOut": true and a pollUrl you can follow with a real PaiAccessToken to retrieve the final reply later. |
{
"session": "my-agent-wh-a1b2c3d4",
"agentDefinition": "my-agent",
"response": null,
"phase": "Running",
"pollUrl": "/agents/my-agent-wh-a1b2c3d4/events?event_type=agent.message&namespace=<ns>",
"timedOut": true
}
wait is capped at 600 seconds — anything longer than that should use fire-and-forget plus polling, since GCP / cluster load-balancer idle timeouts will eventually drop the connection regardless. wait requires title; without a prompt the agent has nothing to do.
Request body
| Field | Type | Description |
|---|---|---|
title | string | Initial prompt / task description for the agent. Required when wait is set. |
env | object[] | [{name, value}] — session-specific env var overrides |
wait | int | boolean | null | Synchronous mode. Block on the spawned session and return its reply. true = 300s default, integer = explicit seconds (max 600). Omit / false / 0 = fire-and-forget. |
{
"title": "Process the overnight batch",
"env": [
{"name": "BATCH_DATE", "value": "2026-04-12"},
{"name": "TARGET_ENV", "value": "production"}
]
}
Reading the agent's response
In fire-and-forget mode the agent runs after the POST returns. Three ways to retrieve the reply later:
- Wait for it inline next time — re-call the webhook with
"wait": 300. (Simplest if the caller is willing to block.) - Poll the events endpoint — authenticated with a
PaiAccessTokenfor the same namespace:ReturnsGET https://api.pairun.dev/agents/<session>/events?event_type=agent.message&namespace=<ns>
Authorization: Bearer <PaiAccessToken>{events: [{type, content, timestamp}, ...], count}. The lastagent.messageis the final reply. The session'sphaseflips toComplete(viaGET /agents/<session>?namespace=<ns>) when the run is done. - SSE stream —
GET /agents/<session>/stream?namespace=<ns>with the same auth. Closes when the harness emitssession.status_closed.
Restrict by source IP
Use allowCIDRs inside the webhook trigger to whitelist specific IP ranges. Requests from other IPs receive 403 Forbidden:
triggers:
- webhook:
allowCIDRs:
- "35.231.147.226/32"
- "35.243.134.228/32"
The check honours X-Forwarded-For, so it works correctly behind a load balancer or proxy.
URL verification pings
Some platforms (Linear, GitHub, others) send a GET request to the webhook URL before activating it, to confirm the endpoint is reachable. Pai handles these automatically: a GET to /webhooks/<namespace>/<name>/<token> validates the token against the Agent's Secret and returns 200 {"ok": true} without creating a session. Invalid tokens receive 401; unknown or Agents without a webhook enabled receive 404.
No configuration is needed — just paste the webhook URL into the external platform and the verification ping will succeed.
Rotating the token
Delete the controller-managed Secret — a new token is generated within seconds:
kubectl delete secret pai-webhook-my-agent -n <namespace>
# status.webhookUrl updates automatically with the new token
Update any external systems with the new URL from pai get agent-def my-agent.
Linear integration
Trigger an agent automatically whenever a project is created or updated in Linear.
1. Enable the webhook on your Agent
triggers:
- webhook:
linearSignatureSecret: "<paste-from-linear>" # verify payloads are genuine
2. Create the webhook in Linear
- Go to Linear → Settings → API → Webhooks → New webhook
- URL: paste the value from
pai get agent-def <name>→Webhook URL - Data change events: check Projects and Project updates
- Team: choose your team or "All public teams"
- Copy the Signing secret and set it on
spec.triggers[].webhook.linearSignatureSecret(see above) - Click Save webhook
3. Allow Linear's IPs
Linear sends webhooks from a fixed set of IP addresses. Add them to your platform's LoadBalancer allowlist:
kubectl patch service pai-apiserver -n pai-system --type=json -p '[
{"op": "add", "path": "/spec/loadBalancerSourceRanges/-", "value": "35.231.147.226/32"},
{"op": "add", "path": "/spec/loadBalancerSourceRanges/-", "value": "35.243.134.228/32"},
{"op": "add", "path": "/spec/loadBalancerSourceRanges/-", "value": "34.140.253.14/32"},
{"op": "add", "path": "/spec/loadBalancerSourceRanges/-", "value": "34.38.87.206/32"},
{"op": "add", "path": "/spec/loadBalancerSourceRanges/-", "value": "34.134.222.122/32"},
{"op": "add", "path": "/spec/loadBalancerSourceRanges/-", "value": "35.222.25.142/32"}
]'
4. How it works
When Linear fires the webhook, the payload looks like:
{
"action": "create",
"type": "Project",
"data": {"name": "WhatsApp Provider", "description": "..."},
"webhookTimestamp": 1744123456789
}
Pai automatically composes the session title: "Project created: WhatsApp Provider". The agent starts with that as its initial prompt, with full access to its configured Providers, tools, and files.
Store linearSignatureSecret using a Kubernetes Secret reference rather than inline in the YAML to avoid committing it to version control. Direct string support exists for convenience; a secretRef option is planned.