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./hooks/<path>.
See also: Gateway Schedules — the outbound declarative counterpart that runs an agent on a cron / interval / one-shot and posts the reply to a channel.
/hooks/<path>; the gateway verifies auth, runs the mapped agent, and delivers the reply on the configured channel.
Quick Start
1
YAML — simplest form
Create Start the gateway:Fire a test event:Response:
gateway.yaml:2
Python — register programmatically
3
CLI — manage hooks at runtime
How It Works
Two Actions: agent vs wake
Choose based on whether the external event carries new content for the agent.
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:
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.
session_key→"hook:gmail:abc123"idempotency_key→"abc123"message→"New email from alice@example.com: Hello"
Idempotency & Retries
idempotency_keyis a template; the rendered value is hashed (sha256) and scoped bypathso the same id on different hooks never collide.- If omitted, the entire payload is hashed deterministically (canonical JSON).
- Store is bounded (
10,000recorded entries) with a24hTTL — 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 withreplicas > 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:
hooks: is a top-level list (the common shape), use the sibling key instead:
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=NORMALfor durable but low-latency writes. reserveis a crash-safeUNIQUEinsert — a redelivery and a concurrent duplicate both fail on the primary-key constraint (this is the inbound analogue of the outbound queue’sUNIQUE idempotency_key).- Bounded: max
10,000recorded entries +24hTTL, pruned lazily onreserve. - Crash recovery: a durable
inflightreservation whose run neither recorded nor released it (only cause: a process crash during the hook) is reclaimable after a15 mininflight_lease_secondslease, so the provider’s post-restart retry re-runs instead of being deduplicated for the full TTL.recordedkeys 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).
For a custom store, implement
IdempotencyStoreProtocol and inject it — the memory and SQLite backends are the two built-ins.store_backend via reload_config rebuilds the store lazily on the next reserve. See 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_tokenwhen 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). 401if no token provided,403if token is wrong.
Verifying Provider Signatures (HMAC)
Setsecret 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).
Sign the exact body you POST to test locally:
Event Filtering
Setevents 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").
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:
Deliver-Only Mode (no LLM turn)
Setdeliver_only: true to route the rendered message straight to deliver_to — no agent, no LLM cost, sub-second forwarding. Requires deliver_to.
Because
deliver_only bypasses the agent, no untrusted-request fence is added — the recipient never sees literal <external_request_payload> markup.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_toto 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):
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.
Real User-Interaction Flow
Gmail received a new email. Zapier POSTs the parsed message to/hooks/gmail. The gateway deduplicates bymessage_id, runs theassistantagent on a templated summary, and Telegram chat123456789gets 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
#triage.
Stripe Payment Event
CI Failure Ping
Best Practices
Pick a stable idempotency_key from the payload
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.Use one per-hook auth secret per provider
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.Keep session_key templates payload-derived
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.Prefer action: wake when the event has no new content
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.Related
Gateway Overview
Gateway architecture and how channels, agents, and routing connect.
Gateway CLI
Full CLI reference including the
praisonai gateway hooks subcommand.Gateway Schedules
Declarative recurring agent → channel deliveries — the outbound counterpart to inbound HTTP triggers.
Bot Lifecycle Hooks
In-process outbound hooks (GATEWAY_START, SESSION_START, SCHEDULE_TRIGGER) — the counterpart to inbound HTTP triggers.
Gateway Hot-Reload
How hook changes and rotated secrets take effect without a process restart.
Untrusted Request Fencing
How inbound payloads are fenced as data before the agent sees them.

