> ## 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.

# Gateway Inbound Hooks

> Trigger agent runs from external HTTP events with POST /hooks/{path}

<Note>
  The gateway now ships in the `praisonai-bot` package. `praisonai serve gateway` still works exactly as documented here; for a standalone install see [praisonai-bot Migration](/docs/guides/praisonai-bot-migration).
</Note>

Trigger an agent run from any external HTTP event — Gmail, GitHub, Stripe, CI, forms, IoT — by POSTing JSON to `/hooks/<path>`.

<Note>
  See also: [Gateway Schedules](/docs/features/gateway-schedules) — the outbound declarative counterpart that runs an agent on a cron / interval / one-shot and posts the reply to a channel.
</Note>

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(name="assistant", instructions="Summarise inbound hook payloads for the team.")
agent.start("Process the Gmail hook payload and post a short summary.")
```

The user POSTs JSON to `/hooks/<path>`; the gateway verifies auth, runs the mapped agent, and delivers the reply on the configured channel.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    ES[🌐 External Service] --> GW

    subgraph "Gateway"
        GW[POST /hooks/path] --> AU[🔐 Auth]
        AU --> ID[🔁 Idempotency]
        ID --> SE[📋 Session]
        SE --> AG[🤖 Agent / Wake]
    end

    AG --> CB[📱 Channel Bot]
    CB --> US[👤 User]

    classDef ext fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef gw fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef agent fill:#10B981,stroke:#7C90A0,color:#fff
    classDef delivery fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef user fill:#6366F1,stroke:#7C90A0,color:#fff

    class ES ext
    class GW,AU,ID,SE gw
    class AG agent
    class CB delivery
    class US user

```

## Quick Start

<Steps>
  <Step title="YAML — simplest form">
    Create `gateway.yaml`:

    ```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    agents:
      assistant:
        instructions: "You are a helpful email assistant."

    hooks:
      - path: gmail                                      # POST /hooks/gmail
        agent: assistant
        auth: "${GATEWAY_HOOK_TOKEN}"
        session_key: "hook:gmail:{{ payload.message_id }}"
        idempotency_key: "{{ payload.message_id }}"
        deliver_to: "telegram:123456789"
        message: "New email from {{ payload.from }}: {{ payload.subject }}"
    ```

    Start the gateway:

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

    Fire a test event:

    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    curl -X POST http://localhost:8765/hooks/gmail \
      -H "Authorization: Bearer $GATEWAY_HOOK_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"message_id":"abc","from":"alice@example.com","subject":"Hello"}'
    ```

    Response:

    ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    {"ok": true, "session_key": "hook:gmail:abc"}
    ```
  </Step>

  <Step title="Python — register programmatically">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonai.gateway import WebSocketGateway

    gateway = WebSocketGateway()
    gateway.register_agent("assistant", Agent(
        name="Assistant",
        instructions="You are a helpful email assistant.",
    ))
    gateway.register_hook(
        path="gmail",
        agent="assistant",
        session_key="hook:gmail:{message_id}",
        idempotency_key="{message_id}",
        deliver_to="telegram:123456789",
        message_template="New email from {from}: {subject}",
    )
    ```
  </Step>

  <Step title="CLI — manage hooks at runtime">
    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    praisonai gateway hooks add gmail \
      --agent assistant \
      --session-key "hook:gmail:{message_id}" \
      --deliver-to telegram:123456789 \
      --message "New email from {from}: {subject}"

    praisonai gateway hooks list
    praisonai gateway hooks remove gmail
    ```
  </Step>
</Steps>

***

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant ES as External Service
    participant GW as Gateway
    participant AG as Agent
    participant CB as Channel Bot
    participant US as User

    ES->>GW: POST /hooks/gmail (raw body + signature header)
    GW->>GW: 1. Authenticate bearer token
    GW->>GW: 2. Verify signature (401 if invalid, before any agent)
    GW->>GW: 3. Event filter (200 skipped if not allowed)
    GW->>GW: 4. Reserve idempotency key (atomic — memory | sqlite backend)
    GW->>GW: 5. Resolve session_key template
    alt deliver_only
        GW->>CB: Deliver rendered message (no LLM turn)
    else action: agent
        GW->>AG: Run agent on templated message
        AG-->>GW: Agent reply
        GW->>CB: Deliver reply to telegram:123456789
    end
    CB-->>US: Message delivered
    GW->>GW: 6. Record idempotency key (success)
    GW-->>ES: 200 {"ok": true}
```

| Concept                | YAML key           | Python kwarg        | CLI flag            |
| ---------------------- | ------------------ | ------------------- | ------------------- |
| URL segment            | `path`             | `path=`             | positional          |
| Agent to run           | `agent`            | `agent=`            | `--agent`           |
| Action mode            | `action`           | `action=`           | `--action`          |
| Bearer secret          | `auth`             | `auth=`             | `--auth`            |
| Session id template    | `session_key`      | `session_key=`      | `--session-key`     |
| Dedup key template     | `idempotency_key`  | `idempotency_key=`  | `--idempotency-key` |
| Delivery target        | `deliver_to`       | `deliver_to=`       | `--deliver-to`      |
| Agent message template | `message`          | `message_template=` | `--message`         |
| Active?                | `enabled`          | `enabled=`          | —                   |
| HMAC signing secret    | `secret`           | `secret=`           | —                   |
| Signature header       | `signature_header` | `signature_header=` | —                   |
| Signature algorithm    | `signature_algo`   | `signature_algo=`   | —                   |
| Signature prefix       | `signature_prefix` | `signature_prefix=` | —                   |
| Event allow-list       | `events`           | `events=`           | —                   |
| Event type source      | `event_header`     | `event_header=`     | —                   |
| Deliver without a turn | `deliver_only`     | `deliver_only=`     | —                   |

***

## Two Actions: `agent` vs `wake`

Choose based on whether the external event carries new content for the agent.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    EV[📨 External Event] --> Q{Has new content?}
    Q -->|Yes| AG[action: agent]
    Q -->|No| WK[action: wake]

    AG --> RUN[🤖 Full agent turn on templated message]
    WK --> NUDGE[💤 Nudge session last_activity — no new message]

    classDef ev fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef q fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef agent fill:#10B981,stroke:#7C90A0,color:#fff
    classDef wake fill:#6366F1,stroke:#7C90A0,color:#fff

    class EV ev
    class Q q
    class AG,RUN agent
    class WK,NUDGE wake
```

**`action: agent`** (default) — runs a full agent turn on the templated `message`. Use when the external event carries new content the agent should process.

**`action: wake`** — nudges an existing session's `_last_activity` without a new user message. Use when the external event means "this session is still alive / re-deliver any pending work".

***

## Templating

Both placeholder styles work and render the same payload fields:

| Style             | Example                    | Used in               |
| ----------------- | -------------------------- | --------------------- |
| `{{ payload.x }}` | `{{ payload.message_id }}` | YAML examples         |
| `{x}`             | `{message_id}`             | Python / CLI examples |

Rules:

* A leading `payload.` is optional — `{{ payload.from }}` and `{{ from }}` both work.
* Dotted paths resolve nested keys: `{{ payload.user.email }}` → `{user: {email: "x"}}`.
* **Missing keys render as empty strings** — templates never raise.
* Substitution is **single-pass** — payload values containing `{...}` are not re-expanded (prevents key-corruption via payload injection).
* Interpolated payload values are automatically fenced as untrusted request data on agent-turn hooks. Operator template text stays outside the fence. See [Untrusted Request Fencing](/docs/features/untrusted-request-fencing).

**Worked example:**

Payload:

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{"message_id": "abc123", "from": "alice@example.com", "subject": "Hello"}
```

Template config:

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
session_key: "hook:gmail:{{ payload.message_id }}"
idempotency_key: "{{ payload.message_id }}"
message: "New email from {{ payload.from }}: {{ payload.subject }}"
```

Resolved values:

* `session_key` → `"hook:gmail:abc123"`
* `idempotency_key` → `"abc123"`
* `message` → `"New email from alice@example.com: Hello"`

***

## Idempotency & Retries

* `idempotency_key` is a template; the rendered value is hashed (`sha256`) and scoped by `path` so the same id on different hooks never collide.
* If omitted, the **entire payload** is hashed deterministically (canonical JSON).
* Store is bounded (`10,000` recorded entries) with a `24h` TTL — pruned lazily on each delivery.
* Key is **only recorded on success** — transient failures stay retryable.
* Concurrent identical deliveries are deduplicated atomically (in-flight reservation prevents TOCTOU between seen-check and record).
* Duplicate response: `200 {"ok": true, "deduplicated": true}`.

### Dedup Store — Memory vs Durable (SQLite)

By default the dedup store is in-memory (per process). A webhook provider that retries **after a gateway restart within its retry window** — or a deployment with `replicas > 1` — needs the dedup key to survive that, otherwise the retry starts a **duplicate agent run** (duplicate reply, duplicate tool action).

Opt in to the durable SQLite backend:

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
hooks:
  idempotency:
    store_backend: sqlite      # "sqlite" (default) | "memory" | "redis"
  hooks:
    - path: gmail
      agent: assistant
      # ...
```

When `hooks:` is a top-level **list** (the common shape), use the sibling key instead:

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
hooks_idempotency:
  store_backend: sqlite

hooks:
  - path: gmail
    agent: assistant
    # ...
```

| Backend            | Survives restart?                | Shared across replicas?                                                                                                 | Dependencies          | When to use                                                                                                                              |
| ------------------ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `sqlite` (default) | ✅                                | ✅ (when the DB file is shared)                                                                                          | stdlib `sqlite3` only | Production, any provider with retries, multi-replica                                                                                     |
| `memory`           | ❌                                | ❌                                                                                                                       | none                  | Tests / ephemeral runs (explicit opt-out)                                                                                                |
| `redis`            | ✅ (falls back to durable SQLite) | ⚠️ Only when the SQLite state file is shared — **not** a genuine cross-replica Redis dedup; a degraded fact is recorded | none (fallback path)  | Signals multi-replica intent; today it downgrades to SQLite with a `durability:idempotency` degraded fact until the Redis backend ships. |

<Warning>
  Setting `store_backend: redis` is honoured as **intent**, not as a working cross-replica backend — no Redis idempotency backend is implemented yet. The wrapper falls back to the durable SQLite store, logs a path-redacted `warning`, and records a `durability:idempotency` degraded fact with reason `"redis idempotency backend not available (running per-replica)"`. If the SQLite fallback *also* fails, the more severe `"...unavailable (running in-memory)"` fact wins. See [Gateway Hooks → Multi-replica gateways](/docs/features/gateway-hooks#multi-replica-gateways-and-store_backend-redis) for the full behaviour and workaround.
</Warning>

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    subgraph "Dedup Store Backend"
        M[💾 In-Memory<br/>per-process<br/>lost on restart]
        S[🗄️ SQLite<br/>~/.praisonai/state/hook_idempotency.sqlite<br/>survives restart · shareable across replicas]
    end
    Choose{Restart or multi-replica?}
    Choose -->|No| M
    Choose -->|Yes| S

    classDef choice fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef mem fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef sql fill:#10B981,stroke:#7C90A0,color:#fff

    class Choose choice
    class M mem
    class S sql
```

**Storage details (SQLite):**

* DB file: `~/.praisonai/state/hook_idempotency.sqlite` (auto-created; same state dir as the ingress journal and outbound queue).
* WAL mode with `synchronous=NORMAL` for durable but low-latency writes.
* `reserve` is a crash-safe `UNIQUE` insert — a redelivery **and** a concurrent duplicate both fail on the primary-key constraint (this is the inbound analogue of the outbound queue's `UNIQUE idempotency_key`).
* Bounded: max `10,000` recorded entries + `24h` TTL, pruned lazily on `reserve`.
* **Crash recovery:** a durable `inflight` reservation whose run neither recorded nor released it (only cause: a process crash *during* the hook) is reclaimable after a `15 min` `inflight_lease_seconds` lease, so the provider's post-restart retry re-runs instead of being deduplicated for the full TTL. `recorded` keys are never touched by the lease.
* On any failure to build the SQLite store, the gateway falls back to the in-memory default so inbound delivery keeps working (a warning is logged).

<Note>
  For a custom store, implement `IdempotencyStoreProtocol` and inject it — the memory and SQLite backends are the two built-ins.
</Note>

**Hot-reload:** flipping `store_backend` via `reload_config` rebuilds the store lazily on the next `reserve`. See [Gateway Hot-Reload](/docs/features/gateway-hot-reload).

***

## Authentication

* Per-hook bearer: set `auth:` to a literal token or `${ENV_VAR}` in YAML.
* Falls back to the gateway's `auth_token` when no hook-specific secret is set.
* **Bearer header only** — `?token=` query params are rejected by design (prevents secret leakage into access logs).
* Compared in constant time (`secrets.compare_digest`).
* `401` if no token provided, `403` if token is wrong.

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
hooks:
  - path: github
    auth: "${GITHUB_WEBHOOK_SECRET}"    # per-hook secret
    agent: triage
```

***

## Verifying Provider Signatures (HMAC)

Set `secret` to verify the provider's HMAC signature over the raw request body — a missing or invalid signature returns `401 {"error": "invalid signature"}` before any agent runs.

Verification is **fail-closed** and **opt-in**: without `secret`, nothing changes. When `secret` is set with no explicit `signature_header`, the header defaults to `X-Hub-Signature-256` (the GitHub convention).

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
hooks:
  - path: github
    agent: triage
    secret: "${GITHUB_WEBHOOK_SECRET}"
    signature_prefix: "sha256="          # signature_header defaults to X-Hub-Signature-256
    deliver_to: "slack:#triage"
    message: "New {{ payload.action }} on #{{ payload.issue.number }}: {{ payload.issue.title }}"
```

| Field              | Purpose                                                                 |
| ------------------ | ----------------------------------------------------------------------- |
| `secret`           | Signing secret; enables verification.                                   |
| `signature_header` | Header carrying the signature (auto-defaults to `X-Hub-Signature-256`). |
| `signature_algo`   | HMAC digest, defaults to `sha256`.                                      |
| `signature_prefix` | Optional prefix on the signature, e.g. `sha256=`.                       |

Sign the exact body you POST to test locally:

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
SECRET="my-signing-secret"
BODY='{"action":"opened","issue":{"number":42,"title":"Bug"}}'
SIG="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')"

curl -X POST http://localhost:8765/hooks/github \
  -H "Content-Type: application/json" \
  -H "X-Hub-Signature-256: $SIG" \
  -H "X-GitHub-Event: issues" \
  -d "$BODY"
```

***

## Event Filtering

Set `events` to an allow-list so only matching deliveries run a turn — everything else is a cheap `200 {"ok": true, "skipped": "event"}` with no LLM cost.

The event type is read from `event_header` (a request header) or, when absent, from the payload as a dotted path (defaulting to `"event"`).

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
hooks:
  - path: github
    agent: triage
    event_header: "X-GitHub-Event"
    events: ["issues.opened", "pull_request.opened"]
    deliver_to: "slack:#triage"
    message: "New {{ payload.action }} on #{{ payload.issue.number }}"
```

A namespaced filter like `issues.opened` matches only when the payload's `action == "opened"` — **fail-closed**: a delivery that omits `action` is never admitted, so a bare `issues` event cannot pass a filter that only allows `issues.opened`.

Read the event from a payload field by pointing `event_header` at a dotted path:

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
hooks:
  - path: stripe
    event_header: "type"                 # read payload["type"]
    events: ["invoice.payment_failed"]
    deliver_to: "slack:#billing"
    message: "⚠️ Payment failed for {{ payload.data.object.customer }}"
```

***

## Deliver-Only Mode (no LLM turn)

Set `deliver_only: true` to route the rendered `message` straight to `deliver_to` — no agent, no LLM cost, sub-second forwarding. Requires `deliver_to`.

<Note>
  Because `deliver_only` bypasses the agent, no untrusted-request fence is added — the recipient never sees literal `<external_request_payload>` markup.
</Note>

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
hooks:
  - path: sentry
    deliver_only: true
    deliver_to: "telegram:${OPS_CHAT_ID}"
    secret: "${SENTRY_WEBHOOK_SECRET}"
    signature_header: "Sentry-Signature"
    message: "🚨 {{ payload.project }}: {{ payload.event.title }}\n{{ payload.url }}"
```

| Outcome                | Response                                                                            |
| ---------------------- | ----------------------------------------------------------------------------------- |
| Delivered              | `{"ok": true, "action": "deliver", "delivered": true}`                              |
| Empty rendered message | `{"ok": true, "action": "deliver", "delivered": false, "skipped": "empty message"}` |
| Missing `deliver_to`   | `{"ok": false, "error": "deliver_only hook requires 'deliver_to'"}`                 |
| Delivery failed        | `{"ok": false, "error": "hook delivery failed", "delivered": false}`                |

`deliver_only` composes with signature verification and event filtering — forward alerts (Sentry → Telegram, CI → Slack) at near-instant latency with zero LLM cost.

***

## Delivery

* `deliver_to: "channel:target"` — e.g. `"telegram:123456789"`, `"discord:987654321"`, `"slack:U12345"`.
* Reuses the same channel-bot send path as scheduled delivery (hooks and scheduler route outbound identically).
* If the channel bot is not registered, delivery is logged as failed and the hook returns `{"ok": false}` so the sender retries.
* Omit `deliver_to` to skip outbound delivery entirely — the agent still runs.

***

## Config Locations & Hot-Reload

YAML hooks can live at the **top level** (`hooks:`) or nested under `gateway:` (for grouping with other gateway settings):

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Top-level (recommended)
hooks:
  - path: gmail
    agent: assistant

# Or nested under gateway:
gateway:
  host: "0.0.0.0"
hooks:
  - path: gmail
    agent: assistant
```

Both locations are picked up at startup and at `reload_config` — a config reload **clears and re-registers** the entire hook table, so removed hooks and rotated secrets take effect without a process restart. See [Gateway Hot-Reload](/docs/features/gateway-hot-reload).

***

## Real User-Interaction Flow

> Gmail received a new email. Zapier POSTs the parsed message to `/hooks/gmail`. The gateway deduplicates by `message_id`, runs the `assistant` agent on a templated summary, and Telegram chat `123456789` gets a notification with the agent's reply. If Zapier retries the same delivery, the gateway returns `{"ok": true, "deduplicated": true}` instantly without re-running the agent.

***

## Common Patterns

### GitHub Issue Triage

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
hooks:
  - path: github/issue
    agent: triage
    auth: "${GITHUB_WEBHOOK_SECRET}"
    session_key: "gh:issue:{{ payload.issue.number }}"
    idempotency_key: "{{ payload.delivery }}"
    deliver_to: "slack:#triage"
    message: "New issue #{{ payload.issue.number }}: {{ payload.issue.title }}"
```

Webhook POSTs arrive, the triage agent classifies the issue, and a Slack notification lands in `#triage`.

### Stripe Payment Event

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
hooks:
  - path: stripe/payment
    agent: billing-support
    action: wake                          # event has no new content — just re-activate
    auth: "${STRIPE_WEBHOOK_SECRET}"
    session_key: "billing:{{ payload.data.object.customer }}"
    idempotency_key: "{{ payload.id }}"
```

Payment events nudge the billing-support session for that customer without generating a new agent turn.

### CI Failure Ping

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
hooks:
  - path: ci/failure
    agent: ops
    auth: "${CI_WEBHOOK_TOKEN}"
    session_key: "ci:{{ payload.repo }}:{{ payload.branch }}"
    idempotency_key: "{{ payload.run_id }}"
    deliver_to: "telegram:${OPS_CHAT_ID}"
    message: "CI failed on {{ payload.repo }}/{{ payload.branch }} — {{ payload.url }}"
```

The ops agent investigates and the reply goes directly to the on-call Telegram chat.

***

## Best Practices

<AccordionGroup>
  <Accordion title="Pick a stable idempotency_key from the payload">
    Use the provider's native delivery id (Gmail `message_id`, GitHub `X-GitHub-Delivery`, Stripe event `id`). Never rely on time of receipt — providers retry with identical ids.
  </Accordion>

  <Accordion title="Use one per-hook auth secret per provider">
    A separate `auth:` token per integration means a leaked secret isolates to one source, not your entire webhook surface.
  </Accordion>

  <Accordion title="Keep session_key templates payload-derived">
    Derive the session key from a stable entity id (`customer`, `repo`, `user`) so related events thread through the same conversation and the agent has full context.
  </Accordion>

  <Accordion title="Prefer action: wake when the event has no new content">
    If the webhook just signals "still alive" or "payment completed" without carrying content the agent should read, `action: wake` saves an LLM call.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Gateway Overview" icon="broadcast-tower" href="/docs/features/gateway-overview">
    Gateway architecture and how channels, agents, and routing connect.
  </Card>

  <Card title="Gateway CLI" icon="terminal" href="/docs/features/gateway-cli">
    Full CLI reference including the `praisonai gateway hooks` subcommand.
  </Card>

  <Card title="Gateway Schedules" icon="calendar-clock" href="/docs/features/gateway-schedules">
    Declarative recurring agent → channel deliveries — the outbound counterpart to inbound HTTP triggers.
  </Card>

  <Card title="Bot Lifecycle Hooks" icon="webhook" href="/docs/features/bot-lifecycle-hooks">
    In-process outbound hooks (GATEWAY\_START, SESSION\_START, SCHEDULE\_TRIGGER) — the counterpart to inbound HTTP triggers.
  </Card>

  <Card title="Gateway Hot-Reload" icon="rotate" href="/docs/features/gateway-hot-reload">
    How hook changes and rotated secrets take effect without a process restart.
  </Card>

  <Card title="Untrusted Request Fencing" icon="shield-check" href="/docs/features/untrusted-request-fencing">
    How inbound payloads are fenced as data before the agent sees them.
  </Card>
</CardGroup>
