Comis-side webhooks vs. channel webhooks. This page describes the daemon’s own HTTP endpoints under
/hooks/*. Some chat platforms (Telegram, Discord, Slack) also expose their own webhook callbacks. The Comis adapters for those platforms typically connect via long-poll or socket APIs by default; see Channels if you specifically need to expose your bot to a platform’s webhook callback.Configuration
Top-Level Webhooks Config
Configure the webhook subsystem under thewebhooks key in your config file.
| Setting | Type | Default | Description |
|---|---|---|---|
webhooks.enabled | boolean | false | Enable the webhook subsystem |
webhooks.path | string | "/hooks" | Base path for webhook endpoints |
webhooks.token | string | SecretRef | — | Bearer token for webhook authentication (min 32 chars) |
webhooks.maxBodyBytes | number | 262144 (256 KB) | Maximum request body size in bytes |
webhooks.presets | string[] | [] | Preset mapping names to load (e.g., ["gmail", "github"]) |
webhooks.mappings | WebhookMapping[] | [] | Custom webhook mapping configurations |
Source:
WebhooksConfigSchema in packages/core/src/config/schema-webhooks.tsWebhook Mapping Config
Each entry inwebhooks.mappings[] defines a routing rule.
| Setting | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique identifier for this mapping |
match | MatchConfig | — | Match conditions (path and/or source) |
action | "wake" | "agent" | "agent" | Action to take when matched |
wakeMode | "now" | "next-heartbeat" | "now" | Wake timing (only for action: "wake") |
name | string | — | Human-readable name |
agentId | string | — | Target agent ID for agent actions |
sessionKey | string | — | Session key template (supports {{expr}} placeholders) |
messageTemplate | string | — | Message template for agent actions (supports {{expr}} placeholders) |
textTemplate | string | — | Alternative plain text template |
deliver | boolean | — | Whether to deliver the message to a channel |
channel | string | — | Target channel for delivery |
to | string | — | Target recipient for delivery |
model | string | — | Model override for agent execution |
timeoutSeconds | number | — | Timeout in seconds for agent execution (positive integer) |
Source:
WebhookMappingConfigSchema in packages/core/src/config/schema-webhooks.tsMatch Conditions
Thematch object controls which incoming requests hit this mapping.
| Setting | Type | Description |
|---|---|---|
match.path | string | URL path segment to match (normalized: leading/trailing slashes stripped, case-insensitive) |
match.source | string | Source identifier to match (from payload or header) |
path and source are provided, both must match (AND logic). If
neither is provided, the mapping matches all requests (catch-all).
Source:
WebhookMappingMatchSchema in packages/core/src/config/schema-webhooks.tsStrict Webhook Endpoint
Route
POST /hooks/webhook
The strict endpoint requires HMAC signature verification and validates the
request body against a fixed schema. Use this when you control the sending
service and can format payloads to the expected structure.
Request
The request body must conform toWebhookPayloadSchema:
| Field | Type | Required | Description |
|---|---|---|---|
event | string | Yes | Event type (e.g., "deployment.completed", "alert.fired") |
source | string | Yes | Source system identifier |
data | Record<string, unknown> | Yes | Arbitrary event data object |
timestamp | string | No | ISO 8601 timestamp |
POST /hooks/webhook
Responses
| Status | Body | Cause |
|---|---|---|
200 | { "received": true } | Webhook processed successfully |
401 | { "error": "Missing webhook signature" } | Missing or invalid HMAC signature |
400 | { "error": "Invalid JSON body" } | Request body is not valid JSON |
422 | { "error": "Validation failed", "issues": [...] } | Payload does not match schema |
500 | { "error": "Internal error" } | Handler threw an error |
Source:
createWebhookEndpoint() in packages/gateway/src/webhook/webhook-endpoint.tsMapped Webhook Endpoint
Route
POST /hooks/:path
The mapped endpoint accepts any JSON payload and routes it to the first matching
webhook mapping’s action handler. HMAC verification is optional (applied only
when webhooks.token is configured).
Path Routing
Incoming request paths are matched against thematch.path field of each
mapping in order. The first matching mapping wins.
Normalization: Both the request path and the mapping path are normalized by
stripping leading/trailing slashes and converting to lowercase.
- If a mapping has no
matchconditions, it matches all requests (catch-all) - If
match.pathis set, the normalized request path must equal it - If
match.sourceis set, thesourcefield from the payload must equal it - If both are set, both must match (AND logic)
- First match wins — mappings are evaluated in array order
Actions
"agent" (default): Renders the messageTemplate and sessionKey
templates using the template engine, then invokes the agent
with the rendered message and session key.
"wake": Triggers a daemon heartbeat. The wakeMode controls timing:
"now"(default): Fire the heartbeat immediately"next-heartbeat": Wait for the next scheduled heartbeat cycle
Responses
| Status | Body | Cause |
|---|---|---|
200 | { "received": true, "mapping": "mapping-id" } | Mapping matched and action executed |
401 | { "error": "..." } | Invalid HMAC signature (when token configured) |
400 | { "error": "Invalid JSON body" } | Request body is not valid JSON |
400 | { "error": "Request body exceeds maximum size" } | Body exceeds maxBodyBytes |
404 | { "error": "No matching webhook mapping" } | No mapping matched the request path |
500 | { "error": "Internal error" } | Handler threw an error |
Source:
createMappedWebhookEndpoint() in packages/gateway/src/webhook/webhook-endpoint.tsHMAC Verification
Webhook signatures are verified using HMAC with constant-time comparison to prevent timing attacks.Configuration
The HMAC middleware accepts the following configuration:| Setting | Type | Default | Description |
|---|---|---|---|
secret | string | (required) | Shared secret for HMAC computation |
headerName | string | "x-webhook-signature" | HTTP header containing the signature |
algorithm | "sha256" | "sha384" | "sha512" | "sha256" | HMAC hash algorithm |
timestampHeaderName | string | "x-webhook-timestamp" | HTTP header containing the Unix timestamp |
maxAgeSec | number | 300 (5 minutes) | Maximum age in seconds for timestamp freshness |
requireTimestamp | boolean | false | Reject requests without a timestamp header |
Source:
HmacMiddlewareConfig in packages/gateway/src/webhook/hmac-verifier.tsSignature Computation
The sender computes the signature as a hex-encoded HMAC of the raw request body:Signature generation (Python example)
Signature generation (bash)
Verification Process
- Read the signature from the configured header (default:
x-webhook-signature) - If no signature header is present, return
401 - Compute the expected HMAC of the raw request body using the shared secret
- Compare using
crypto.timingSafeEqual(constant-time to prevent timing attacks) - If lengths differ, reject immediately (lengths cannot match for valid signatures)
- If signatures do not match, return
401
Timestamp Freshness
If a timestamp header is present (default:x-webhook-timestamp):
- Parse the header value as a Unix timestamp (seconds)
- Compare against the current time
- If the absolute difference exceeds
maxAgeSec(default: 300 seconds), return401
requireTimestamp is true, requests without a timestamp header are rejected
with 401. When false (default), missing timestamps are allowed — the HMAC
signature alone provides tamper protection.
Source:
verifyHmacSignature() and createHmacMiddleware() in packages/gateway/src/webhook/hmac-verifier.tsTemplate Engine
Mapped webhook endpoints use a template engine to transform incoming payloads into agent messages and session keys. Templates use{{expression}} syntax.
Expression Types
| Expression | Resolves To | Example |
|---|---|---|
{{payload.field}} | Dot-path into the JSON payload | {{payload.action}} |
{{payload.field.nested}} | Nested object traversal | {{payload.repository.full_name}} |
{{payload.items[0].name}} | Array index notation | {{payload.messages[0].from}} |
{{headers.x-header}} | HTTP request header (lowercased keys) | {{headers.x-github-event}} |
{{query.key}} | URL query parameter | {{query.format}} |
{{path}} | URL path segment (after webhook base path) | github |
{{now}} | Current ISO 8601 timestamp | 2026-03-12T10:30:00.000Z |
payload., headers., query.) are
resolved against the payload object. This means {{repository.full_name}} is
equivalent to {{payload.repository.full_name}}.
Context Object
The template engine receives a context object with the following properties:| Property | Type | Description |
|---|---|---|
payload | unknown | Parsed JSON body from the webhook request |
headers | Record<string, string> | HTTP request headers (lowercased keys) |
query | Record<string, string> | URL query parameters |
path | string | URL path segment after the webhook base path |
now | string | Current ISO 8601 timestamp |
Unresolved Expressions
Expressions that cannot be resolved (missing fields, null values) are replaced with an empty string. No error is thrown for unresolved expressions.Source:
resolveTemplateExpr() and renderTemplate() in packages/gateway/src/webhook/webhook-mapping.tsPresets
Comis includes built-in webhook mapping presets for common services. Enable presets in your config:Gmail Preset
| Field | Value |
|---|---|
id | "gmail" |
match.path | "gmail" |
action | "agent" |
wakeMode | "now" |
sessionKey | "hook:gmail:{{payload.messages[0].id}}" |
messageTemplate | "New email from {{payload.messages[0].from}}\nSubject: {{payload.messages[0].subject}}\n{{payload.messages[0].snippet}}\n{{payload.messages[0].body}}" |
GitHub Preset
| Field | Value |
|---|---|
id | "github" |
match.path | "github" |
action | "agent" |
wakeMode | "now" |
sessionKey | "hook:github:{{headers.x-github-delivery}}" |
messageTemplate | "GitHub {{headers.x-github-event}}: {{payload.repository.full_name}}\n{{payload.action}} by {{payload.sender.login}}" |
x-github-delivery header for deduplication.
Source:
GMAIL_PRESET and GITHUB_PRESET in packages/gateway/src/webhook/webhook-presets.tsExample Configuration
Full webhook config example
End-to-end: a GitHub webhook
A complete walkthrough that takes a real GitHubpush event and turns it into an agent message.
Enable the webhook subsystem
Add the Reload config (
github preset and enable HMAC verification.comis config patch ... or restart) — the daemon now mounts POST /hooks/github.Generate the shared secret
Use a 32+ character random string. Store it both in Comis (via
comis secrets set WEBHOOK_SHARED_SECRET) and in GitHub’s webhook UI as the Secret field.Configure the GitHub webhook
Repository → Settings → Webhooks → Add webhook:
- Payload URL:
https://your-host.example.com/hooks/github - Content type:
application/json - Secret: the value from the previous step
- SSL verification: enabled
- Select the events you want (e.g. just
push)
Verify the signature flow
Trigger a test event from GitHub’s webhook page. Comis will compute HMAC-SHA256 over the raw body using your secret and compare against the
x-hub-signature-256 header. A successful 200 response carries {"received": true, "mapping": "github"}.Tail logs while testing:hook:github:<delivery-id> so retries are deduplicated automatically by GitHub’s redelivery machinery.
Related
Security Model
HMAC verification and security architecture
HTTP Gateway
Full gateway endpoint reference
Hot Reload
Config reloading and skill discovery
Defense in Depth
User-friendly security overview
