Skip to main content
What this is for: turn arbitrary HTTP events from external services (Gmail, GitHub, CI, custom) into agent messages or wake signals. Who it’s for: anyone wiring Comis into a broader automation pipeline. The webhook subsystem receives events from external services and routes them to agents or triggers daemon heartbeats. Comis provides two webhook endpoint types: a strict endpoint with required HMAC verification and a fixed payload schema, and mapped endpoints with path-based routing and a template engine for transforming arbitrary payloads into agent messages.
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 the webhooks key in your config file.
SettingTypeDefaultDescription
webhooks.enabledbooleanfalseEnable the webhook subsystem
webhooks.pathstring"/hooks"Base path for webhook endpoints
webhooks.tokenstring | SecretRefBearer token for webhook authentication (min 32 chars)
webhooks.maxBodyBytesnumber262144 (256 KB)Maximum request body size in bytes
webhooks.presetsstring[][]Preset mapping names to load (e.g., ["gmail", "github"])
webhooks.mappingsWebhookMapping[][]Custom webhook mapping configurations
Source: WebhooksConfigSchema in packages/core/src/config/schema-webhooks.ts

Webhook Mapping Config

Each entry in webhooks.mappings[] defines a routing rule.
SettingTypeDefaultDescription
idstringUnique identifier for this mapping
matchMatchConfigMatch 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")
namestringHuman-readable name
agentIdstringTarget agent ID for agent actions
sessionKeystringSession key template (supports {{expr}} placeholders)
messageTemplatestringMessage template for agent actions (supports {{expr}} placeholders)
textTemplatestringAlternative plain text template
deliverbooleanWhether to deliver the message to a channel
channelstringTarget channel for delivery
tostringTarget recipient for delivery
modelstringModel override for agent execution
timeoutSecondsnumberTimeout in seconds for agent execution (positive integer)
Source: WebhookMappingConfigSchema in packages/core/src/config/schema-webhooks.ts

Match Conditions

The match object controls which incoming requests hit this mapping.
SettingTypeDescription
match.pathstringURL path segment to match (normalized: leading/trailing slashes stripped, case-insensitive)
match.sourcestringSource identifier to match (from payload or header)
If both 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.ts

Strict 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 to WebhookPayloadSchema:
FieldTypeRequiredDescription
eventstringYesEvent type (e.g., "deployment.completed", "alert.fired")
sourcestringYesSource system identifier
dataRecord<string, unknown>YesArbitrary event data object
timestampstringNoISO 8601 timestamp
POST /hooks/webhook
{
  "event": "deployment.completed",
  "source": "ci-system",
  "data": {
    "environment": "production",
    "version": "1.2.3"
  },
  "timestamp": "2026-03-12T10:30:00Z"
}
HMAC verification is required. The request must include a signature header (see HMAC Verification below).

Responses

StatusBodyCause
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.ts

Mapped 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 the match.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.
Request: POST /hooks/Gmail/   -> normalized: "gmail"
Mapping: match.path: "gmail"  -> normalized: "gmail"
Result:  MATCH
Match resolution logic:
  1. If a mapping has no match conditions, it matches all requests (catch-all)
  2. If match.path is set, the normalized request path must equal it
  3. If match.source is set, the source field from the payload must equal it
  4. If both are set, both must match (AND logic)
  5. 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

StatusBodyCause
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.ts

HMAC Verification

Webhook signatures are verified using HMAC with constant-time comparison to prevent timing attacks.

Configuration

The HMAC middleware accepts the following configuration:
SettingTypeDefaultDescription
secretstring(required)Shared secret for HMAC computation
headerNamestring"x-webhook-signature"HTTP header containing the signature
algorithm"sha256" | "sha384" | "sha512""sha256"HMAC hash algorithm
timestampHeaderNamestring"x-webhook-timestamp"HTTP header containing the Unix timestamp
maxAgeSecnumber300 (5 minutes)Maximum age in seconds for timestamp freshness
requireTimestampbooleanfalseReject requests without a timestamp header
Source: HmacMiddlewareConfig in packages/gateway/src/webhook/hmac-verifier.ts

Signature Computation

The sender computes the signature as a hex-encoded HMAC of the raw request body:
Signature generation (Python example)
import hmac, hashlib

signature = hmac.new(
    key=shared_secret.encode(),
    msg=request_body.encode(),
    digestmod=hashlib.sha256
).hexdigest()
Signature generation (bash)
echo -n "$REQUEST_BODY" | openssl dgst -sha256 -hmac "$SHARED_SECRET" | awk '{print $2}'

Verification Process

  1. Read the signature from the configured header (default: x-webhook-signature)
  2. If no signature header is present, return 401
  3. Compute the expected HMAC of the raw request body using the shared secret
  4. Compare using crypto.timingSafeEqual (constant-time to prevent timing attacks)
  5. If lengths differ, reject immediately (lengths cannot match for valid signatures)
  6. If signatures do not match, return 401

Timestamp Freshness

If a timestamp header is present (default: x-webhook-timestamp):
  1. Parse the header value as a Unix timestamp (seconds)
  2. Compare against the current time
  3. If the absolute difference exceeds maxAgeSec (default: 300 seconds), return 401
If 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.ts

Template Engine

Mapped webhook endpoints use a template engine to transform incoming payloads into agent messages and session keys. Templates use {{expression}} syntax.

Expression Types

ExpressionResolves ToExample
{{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 timestamp2026-03-12T10:30:00.000Z
Expressions without a recognized prefix (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:
PropertyTypeDescription
payloadunknownParsed JSON body from the webhook request
headersRecord<string, string>HTTP request headers (lowercased keys)
queryRecord<string, string>URL query parameters
pathstringURL path segment after the webhook base path
nowstringCurrent 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.
Template: "Event from {{payload.sender.name}}"
Payload:  { "sender": {} }
Result:   "Event from "
Source: resolveTemplateExpr() and renderTemplate() in packages/gateway/src/webhook/webhook-mapping.ts

Presets

Comis includes built-in webhook mapping presets for common services. Enable presets in your config:
webhooks:
  enabled: true
  presets: ["gmail", "github"]
Unknown preset names are silently ignored.

Gmail Preset

FieldValue
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}}"
Receives Gmail push notifications and routes them as agent messages. The session key uses the first message ID for deduplication.

GitHub Preset

FieldValue
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}}"
Receives GitHub webhook events and routes them as agent messages. The session key uses the delivery ID from the x-github-delivery header for deduplication.
Source: GMAIL_PRESET and GITHUB_PRESET in packages/gateway/src/webhook/webhook-presets.ts

Example Configuration

Full webhook config example
webhooks:
  enabled: true
  path: "/hooks"
  token: "a-shared-secret-token-at-least-32-characters"
  maxBodyBytes: 262144
  presets: ["gmail", "github"]
  mappings:
    - id: "ci-deploy"
      match:
        path: "deploy"
        source: "jenkins"
      action: "agent"
      sessionKey: "hook:ci:{{payload.build_id}}"
      messageTemplate: "Deployment {{payload.status}}: {{payload.project}} v{{payload.version}}"
    - id: "monitoring-wake"
      match:
        path: "alert"
      action: "wake"
      wakeMode: "now"

End-to-end: a GitHub webhook

A complete walkthrough that takes a real GitHub push event and turns it into an agent message.
1

Enable the webhook subsystem

Add the github preset and enable HMAC verification.
webhooks:
  enabled: true
  path: "/hooks"
  token: "${WEBHOOK_SHARED_SECRET}"
  presets: ["github"]
Reload config (comis config patch ... or restart) — the daemon now mounts POST /hooks/github.
2

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.
openssl rand -hex 32 | xargs -I {} comis secrets set WEBHOOK_SHARED_SECRET --value {}
3

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)
4

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:
comis daemon logs -f | grep webhook
5

Troubleshoot 401s

If GitHub shows a 401: re-check the secret matches on both sides, that the header name is x-hub-signature-256 (configurable via headerName), and that no proxy strips signature headers in front of the daemon.
The result: every matching GitHub event becomes an agent message keyed by hook:github:<delivery-id> so retries are deduplicated automatically by GitHub’s redelivery machinery.

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