> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Channel

> Turn any HTTP webhook into an agent trigger with a YAML route — no adapter code

Route any third-party HTTP webhook (GitHub, Stripe, CI, alerting) to an agent through configuration alone.

> [!NOTE]
>
> Webhook-produced messages should carry `allow_control=False` so a webhook body of `/stop` or `/new` is treated as plain text, never a privileged command. See [Control-Trust Provenance](https://praison.ai/docs/features/bot-control-trust.md).

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonai_bot.bots import WebhookBot, WebhookRoute

agent = Agent(
    name="issue-triage",
    instructions="Triage new GitHub issues into priority buckets.",
)

bot = WebhookBot(
    agent=agent,
    path="/hooks/github",
    routes=[
        WebhookRoute(
            when={"field": "headers.X-GitHub-Event", "equals": "issues"},
            prompt="New issue #{{ payload.issue.number }}: {{ payload.issue.title }}",
        ),
    ],
)

await bot.start()
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Webhook → Agent"
        A[🌐 HTTP POST] --> V[🔐 HMAC Verify]
        V --> F[🧭 Route Filter]
        F --> P[📝 Prompt Template]
        P --> AG[🤖 Agent]
    end

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef process fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff

    class A input
    class V,F,P process
    class AG agent
```

## Quick Start

**1. One command (CLI)**

The quickest way to start a generic webhook receiver — no Python needed:

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai bot run --platform webhook --port 8080
```

Any field a channel declares in its descriptor is also accepted as a `--field-name` flag; the webhook channel declares none, so use `--port` and YAML for its extras. `bot run` starts any registered platform by name. See [`praisonai bot run`](https://praison.ai/docs/features/messaging-bots#registered-but-verb-less-platforms-praisonai-bot-run) for the full flag list.

**2. YAML gateway channel (recommended, no code)**

Declare a `webhook` channel in `gateway.yaml`. The gateway recognises `webhook` as a built-in channel type.

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# gateway.yaml
channels:
  github:
    type: webhook
    path: /hooks/github
    verify:
      hmac:
        header: X-Hub-Signature-256
        secret: ${GITHUB_WEBHOOK_SECRET}
        prefix: "sha256="
    routes:
      - when:
          all:
            - { field: headers.X-GitHub-Event, equals: issues }
            - { field: payload.action, in: [opened, reopened] }
        agent: triage
        prompt: "New issue #{{ payload.issue.number }}: {{ payload.issue.title }}"
      - when: { field: payload.action, equals: closed }
        silent: true
```

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai gateway start --config gateway.yaml
```

> [!NOTE]
>
> Multiple `webhook` channels? Leave `webhook_port` unset and they all serve behind one gateway URL at `/webhooks/<channel>` — no port collisions. See [Shared Webhook Ingress](https://praison.ai/docs/features/shared-webhook-ingress.md).

**3. Python (single bot)**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonai_bot.bots import WebhookBot, WebhookRoute

agent = Agent(
    name="issue-triage",
    instructions="Triage new GitHub issues into priority buckets.",
)

bot = WebhookBot(
    agent=agent,
    path="/hooks/github",
    routes=[
        WebhookRoute(
            when={"field": "headers.X-GitHub-Event", "equals": "issues"},
            prompt="New issue #{{ payload.issue.number }}: {{ payload.issue.title }}",
        ),
    ],
)

await bot.start()
```

***

## How It Works

Every POST is verified, filtered, templated, then dispatched to the agent through the durable session manager.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Sender as 🌐 Sender
    participant Bot as WebhookBot
    participant Verifier as HmacWebhookVerifier
    participant Filter as WebhookFilter
    participant Session as BotSessionManager
    participant Agent

    Sender->>Bot: POST /hooks/github (headers + JSON body)
    Bot->>Verifier: verify(headers, raw_body)
    alt Signature invalid
        Verifier-->>Bot: False
        Bot-->>Sender: 401 Invalid signature
    else Signature valid
        Verifier-->>Bot: True
        Bot->>Filter: matches({payload, headers, query})
        alt No route matches
            Filter-->>Bot: False
            Bot-->>Sender: 200 No matching route
        else Route matches (silent)
            Filter-->>Bot: True (silent=true)
            Bot-->>Sender: 200 OK (ack only)
        else Route matches (dispatch)
            Filter-->>Bot: True
            Bot->>Bot: render_prompt(template, event)
            Bot->>Session: chat(agent, user_id, prompt, message_id)
            Session->>Agent: run
            Agent-->>Session: reply
            Bot-->>Sender: 200 OK
        end
    end
```

### User interaction flow

A developer wires the route once; the external service does the rest.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    A[👤 Developer defines route in YAML] --> B[🚀 Deploy gateway]
    B --> C[🌐 External service POSTs webhook]
    C --> D[🤖 Agent runs]
    D --> E[✅ Operator sees output in delivery surface]

    classDef dev fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef proc fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef done fill:#10B981,stroke:#7C90A0,color:#fff

    class A,B dev
    class C,D proc
    class E done
```

***

## Configuration Reference

`WebhookBot` serves one HTTP path and dispatches matching events to its bound agent.

| Parameter | Type | Default | Description |
| - | - | - | - |
| `token` | `str` | `""` | Unused for webhook; kept for adapter uniformity |
| `agent` | `Agent \| None` | `None` | Agent to dispatch matching events to |
| `config` | `BotConfig \| None` | `None` | Optional bot config; defaults to `BotConfig(token=token, mode="webhook")` |
| `path` | `str` | `"/webhook"` | HTTP path served (leading `/` auto-added) |
| `webhook_port` | `Optional[int]` | `None` | Port for a standalone aiohttp listener. `None` (default) → the channel is served through the gateway's shared listener at `/webhooks/<channel>` (see [Shared Webhook Ingress](https://praison.ai/docs/features/shared-webhook-ingress.md)). Set an int to bind a private server (opt-out; backward-compatible) |
| `verify` | `Mapping \| verifier \| None` | `None` | A verifier object exposing `.verify(...)` (returned as-is) or a `{"hmac": {...}}` mapping (constructs an `HmacWebhookVerifier`). `None` disables signature verification |
| `routes` | `list[WebhookRoute \| dict]` | `[]` | Route list. Empty list → single catch-all route matching every event |

`WebhookRoute` decides which events fire and how the prompt is built.

| Field | Type | Default | Description |
| - | - | - | - |
| `when` | filter tree \| `None` | `None` | Declarative predicate; `None`/`{}` matches everything |
| `agent` | `str \| None` | `None` | Informational — the adapter uses its bound `agent` |
| `prompt` | `str \| None` | `None` | `{{ dotted.path }}` template. `None` → raw JSON payload |
| `silent` | `bool` | `False` | Ack (HTTP 200) without running the agent |

The YAML `verify.hmac` block is parsed by `_build_verifier_from_config`.

| Key | Type | Default | Description |
| - | - | - | - |
| `header` | `str` | — | Single signature header name. Alias: `signature_header` |
| `signature_headers` | `list[str]` | `[header]` | Multiple accepted headers; falls back to `[header]` or `["X-Signature"]` |
| `secret` | `str` | `""` | Shared secret |
| `digest` | `str` | `"sha256"` | Digest name passed to `HmacWebhookVerifier` |
| `prefix` | `str \| None` | `None` | e.g. `"sha256="` for GitHub |

***

## Filter DSL

A filter is a small predicate tree that round-trips through YAML. Every node is either a combinator or a leaf.

| Combinator | Meaning | Empty behaviour |
| - | - | - |
| `{"all": [...]}` | AND — every child must match | `True` |
| `{"any": [...]}` | OR — at least one child matches | `False` |
| `{"not": <node>}` | Negation | — |

A leaf tests one field: `{"field": "<dotted.path>", "<op>": <value>}`.

| Operator | Meaning |
| - | - |
| `exists` | `True` = field present, `False` = field absent |
| `equals` | Deep equality with the resolved value |
| `contains` | Substring / membership (`value in resolved`) |
| `in` | Resolved value is one of a list |
| `regex` | `re.search(pattern, str(resolved))` |
| *(bare `field`)* | A leaf with only `field` is treated as `exists: True` |

Field paths are dotted — `payload.issue.number`, `headers.X-GitHub-Event`, `query.debug`. Header lookup is **case-insensitive**.

> [!NOTE]
>
> **Fail-safe by design.** Missing paths resolve to `None`; unknown nodes, invalid regex, and type mismatches evaluate to `False` and never raise. A `None` or `{}` filter matches every event, so a route with no `when` is unconditional.

### Which operator?

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q[What are you matching?] --> E{Exact value?}
    E -->|One value| EQ[equals]
    E -->|Several values| IN[in]
    Q --> S{Substring or membership?}
    S -->|Text contains X| CT[contains]
    S -->|Pattern| RE[regex]
    Q --> P{Just presence?}
    P -->|Field must exist| EX["exists: true"]
    P -->|Field must be absent| NX["exists: false"]

    classDef q fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef op fill:#10B981,stroke:#7C90A0,color:#fff
    class Q,E,S,P q
    class EQ,IN,CT,RE,EX,NX op
```

***

## Routing and prompts

**First-match wins.** Routes are evaluated in list order; the first whose filter matches is used. A filter that raises is skipped (fail-safe) so a bad route can't kill ingress.

**Prompt templating.** `{{ dotted.path }}` placeholders resolve against `{payload, headers, query}`. Missing paths render as empty strings. When `prompt` is omitted, the raw JSON payload becomes the agent's prompt.

**Silent routes.** `silent: true` acknowledges with HTTP 200 without running the agent — useful for dropping noisy events at the edge.

**Delivery IDs.** The bot derives a stable `message_id` for the ingress journal, in priority order:

1. `X-GitHub-Delivery` header, or
2. `X-Request-Id` header, or
3. `X-Delivery-Id` header, or
4. `Stripe-Signature` header, otherwise
5. deterministic `webhook-<sha256(path + "\0" + raw_body)>` fallback, so redeliveries collapse to one journaled run even for generic senders.

***

## Response contract

| Status | When |
| - | - |
| `200 OK` | Route matched and dispatched, or route matched and silent, or no route matched (ack) |
| `200 Webhook endpoint` | `GET` health check on the path |
| `400 Bad request` | Body read failed |
| `401 Invalid signature` | HMAC verification failed |
| `500 Dispatch failed` | Agent dispatch raised — sender should retry |

***

## Untrusted Payload Fencing

Payload-derived interpolations are fenced as untrusted data before the agent sees them.

* Values from `{{ dotted.path }}` land inside `<external_request_payload>...</external_request_payload>`; the operator's static template text stays outside the fence.
* When a payload value is fenced, a one-line inline notice is prepended once (also outside the fence).
* A route with **no** `prompt` still gets a fixed operator prefix — `"An external event was received on route {name}; the payload follows."` — so attacker-controlled text is never the sole instruction.
* Attempts to smuggle a fence closer inside a payload field are delimiter-escaped, so the fence's own single closer is the only real one.

See [Untrusted Request Fencing](https://praison.ai/docs/features/untrusted-request-fencing.md) for the full picture and rendered examples.

***

## Common Patterns

### GitHub issue triage

Verify the signature, match the event header and body, then template the prompt.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonai_bot.bots import WebhookBot, WebhookRoute

agent = Agent(name="triage", instructions="Bucket issues by priority.")

bot = WebhookBot(
    agent=agent,
    path="/hooks/github",
    verify={"hmac": {"header": "X-Hub-Signature-256", "secret": "gh-secret", "prefix": "sha256="}},
    routes=[
        WebhookRoute(
            when={"all": [
                {"field": "headers.X-GitHub-Event", "equals": "issues"},
                {"field": "payload.action", "in": ["opened", "reopened"]},
            ]},
            prompt="New issue #{{ payload.issue.number }}: {{ payload.issue.title }}",
        ),
    ],
)

await bot.start()
```

### Stripe payment fan-in

Match several event types with one `in` leaf.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonai_bot.bots import WebhookBot, WebhookRoute

agent = Agent(name="payments", instructions="Reconcile Stripe payment events.")

bot = WebhookBot(
    agent=agent,
    path="/hooks/stripe",
    routes=[
        WebhookRoute(
            when={"field": "payload.type", "in": [
                "payment_intent.succeeded",
                "payment_intent.payment_failed",
            ]},
            prompt="Stripe event {{ payload.type }} for {{ payload.data.object.id }}",
        ),
    ],
)

await bot.start()
```

### Silent-drop noisy events

Put a `silent: true` route before a broad catch-all to drop noise at the edge.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonai_bot.bots import WebhookBot, WebhookRoute

agent = Agent(name="ci", instructions="Report CI failures.")

bot = WebhookBot(
    agent=agent,
    path="/hooks/ci",
    routes=[
        WebhookRoute(when={"field": "payload.status", "equals": "in_progress"}, silent=True),
        WebhookRoute(prompt="CI status: {{ payload.status }} on {{ payload.branch }}"),
    ],
)

await bot.start()
```

***

## Best Practices

> [accordion] Always set an HMAC secret in production
>
> Signature verification is opt-in — a `webhook` channel with no `verify` block accepts unsigned traffic. Set `verify.hmac.secret` from an env var before exposing the endpoint.
>
> > [!WARNING]
> >
> > The `verify` extra declared in a `bot.yaml` is now forwarded correctly through `praisonai bot start` (and `praisonai bot run --platform webhook`). Previously the `start` path silently dropped it, replacing your configured verifier with the adapter's unverified default — accepting unsigned traffic. Re-check your exit code and logs after upgrading to confirm verification is active.

> [accordion] Put narrow routes before broad ones
>
> First-match wins, so a specific route placed after a catch-all never runs. Order routes from most to least specific.

> [accordion] Use silent routes for high-volume noise
>
> `silent: true` acknowledges without running the agent, keeping the ingress journal clean for events you never want to act on.

> [accordion] Prefer YAML gateway config over Python
>
> For anything shared across an environment, declare the channel in `gateway.yaml` rather than constructing `WebhookBot(...)` in code — config is easier to review and redeploy.

***

## Related

> [card] [Shared Webhook Ingress](https://praison.ai/docs/features/shared-webhook-ingress.md)
>
> Serve every webhook channel behind one gateway port at `/webhooks/<channel>`

> [card] [Webhook Verification](https://praison.ai/docs/features/webhook-verification.md)
>
> The HMAC signature primitive this channel reuses

> [card] [Bot Platform Plugins](https://praison.ai/docs/features/bot-platform-plugins.md)
>
> The registry seam that `webhook` slots into

> [card] [Channels Gateway](https://praison.ai/docs/features/channels-gateway.md)
>
> The gateway that hosts channel adapters

> [card] [Inbound Journal](https://praison.ai/docs/features/inbound-journal.md)
>
> Dedup and retry behaviour that `message_id` feeds


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.