Skip to main content

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:

phaseMeaning
"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

FieldTypeDescription
titlestringInitial prompt / task description for the agent. Required when wait is set.
envobject[][{name, value}] — session-specific env var overrides
waitint | boolean | nullSynchronous 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:

  1. Wait for it inline next time — re-call the webhook with "wait": 300. (Simplest if the caller is willing to block.)
  2. Poll the events endpoint — authenticated with a PaiAccessToken for the same namespace:
    GET https://api.pairun.dev/agents/<session>/events?event_type=agent.message&namespace=<ns>
    Authorization: Bearer <PaiAccessToken>
    Returns {events: [{type, content, timestamp}, ...], count}. The last agent.message is the final reply. The session's phase flips to Complete (via GET /agents/<session>?namespace=<ns>) when the run is done.
  3. SSE streamGET /agents/<session>/stream?namespace=<ns> with the same auth. Closes when the harness emits session.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

  1. Go to Linear → Settings → API → Webhooks → New webhook
  2. URL: paste the value from pai get agent-def <name>Webhook URL
  3. Data change events: check Projects and Project updates
  4. Team: choose your team or "All public teams"
  5. Copy the Signing secret and set it on spec.triggers[].webhook.linearSignatureSecret (see above)
  6. 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.

Signing secret security

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.