Skip to main content
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.
Trigger an agent run from any external HTTP event — Gmail, GitHub, Stripe, CI, forms, IoT — by POSTing JSON to /hooks/<path>.
The user POSTs JSON to /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 gateway.yaml:
Start the gateway:
Fire a test event:
Response:
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.
Worked example: Payload:
Template config:
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 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}.

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.

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).
Sign the exact body you POST to test locally:

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").
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:

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.
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_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):
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.

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

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

Stripe Payment Event

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

CI Failure Ping

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

Best Practices

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.
A separate auth: token per integration means a leaked secret isolates to one source, not your entire webhook surface.
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.
If the webhook just signals “still alive” or “payment completed” without carrying content the agent should read, action: wake saves an LLM call.

Gateway Overview

Gateway architecture and how channels, agents, and routing connect.

Gateway CLI

Full CLI reference including the praisonai gateway hooks subcommand.

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.