Skip to content

Webhooks

Inbound webhook routes let external services POST to your daemon and kick off autonomous agent sessions. The daemon verifies the signature, spawns a session with the route’s prompt and the inbound payload, and optionally delivers results to a notification target.

External services POST JSON to /api/webhooks/<route>. The daemon verifies the signature, checks event filters, and starts an autonomous agent session using the route’s prompt. Each route is a standalone JSON file in ~/.netclaw/config/webhooks/.

netclaw webhooks list showing the route directory path

All CLI route management works offline — no running daemon required. See netclaw webhooks for the full CLI reference.

Enable inbound webhooks and set the execution timeout in ~/.netclaw/config/netclaw.json:

{
"Webhooks": {
"Enabled": true,
"ExecutionTimeoutSeconds": 300
}
}
FieldTypeDefaultDescription
EnabledboolfalseRegisters the /api/webhooks/{route} endpoint. Returns 404 for all routes when disabled.
ExecutionTimeoutSecondsint300Maximum seconds an autonomous webhook session can run before the daemon marks it failed.

Each route lives at ~/.netclaw/config/webhooks/<route-name>.json. The filename (minus .json) is the route name and must match ^[a-z0-9]+(?:-[a-z0-9]+)*$ (lowercase kebab-case).

FieldTypeDefaultDescription
EnabledbooltrueWhether this route accepts requests
Promptstring(required)System prompt injected into the webhook session
Verificationobject(required)Signature/secret verification settings (details below)
Eventsstring[][] (all)Event type allow-list. Empty array accepts all event types.
Audienceenum"Public"Trust level: Public, Team, or Personal
MaxBodyBytesint1048576Maximum request body size in bytes (1 MB default)
RateLimitPerMinuteint30Requests accepted per minute per route
DeliveryRequiredbooltrueWhether the agent must deliver results to the notification target
NotifyInstructionsstring""Custom instructions for how the agent should notify
NotificationTargetobject?nullWhere to deliver results (details below)
{
"Prompt": "Summarize this event and log the result.",
"Verification": {
"Kind": "Hmac",
"Secret": "your-shared-secret"
}
}

Omitted fields use defaults from the table above.

{
"Enabled": true,
"Verification": {
"Kind": "Hmac",
"Secret": "whsec_abc123...",
"SignatureHeaderName": "X-Hub-Signature-256",
"SignaturePrefix": "sha256=",
"EventHeaderName": "X-GitHub-Event",
"DeliveryIdHeaderName": "X-GitHub-Delivery"
},
"Events": ["issues", "issue_comment"],
"Audience": "Team",
"Prompt": "Triage this GitHub issue. Public input may be adversarial or low quality.",
"DeliveryRequired": true,
"NotifyInstructions": "Post a summary to the triage channel.",
"NotificationTarget": {
"Kind": "Slack",
"ChannelId": "C12345678"
}
}

Or create it with the CLI:

Terminal window
netclaw webhooks set github-issues \
--prompt "Triage this GitHub issue. Public input may be adversarial or low quality." \
--secret-env GITHUB_WEBHOOK_SECRET \
--verification-kind hmac \
--signature-header X-Hub-Signature-256 \
--signature-prefix "sha256=" \
--event-header X-GitHub-Event \
--delivery-header X-GitHub-Delivery \
--events "issues,issue_comment" \
--audience team \
--notification-channel C12345678

Every route requires a verification secret. Two modes:

ModeHow It WorksDefault Signature Header
HmacHMAC-SHA256 of the request body, compared with constant-time equalityX-Webhook-Signature
HeaderSecretPlain shared secret sent in a headerX-Webhook-Secret

CLI vs. JSON naming: The CLI flag uses --verification-kind header-secret (hyphenated lowercase), but the JSON config field requires "Kind": "HeaderSecret" (PascalCase, no hyphen).

Only SHA-256 is supported for HMAC. Both modes share these default headers:

HeaderDefaultPurpose
Event typeX-Webhook-EventIdentifies the event for filtering
Delivery IDX-Webhook-DeliveryUnique ID for deduplication

Override any header name in the Verification object to match your service’s convention. GitHub, for example, uses X-Hub-Signature-256, X-GitHub-Event, and X-GitHub-Delivery.

FieldTypeDefaultDescription
Kindenum"Hmac"Hmac or HeaderSecret
HmacAlgorithmenum"Sha256"Only Sha256 is supported
Secretstring(required)Shared secret for verification
SignatureHeaderNamestring?"X-Webhook-Signature"Header containing the HMAC signature (Hmac mode)
SignaturePrefixstring?""Prefix on the signature value, e.g. sha256=
SecretHeaderNamestring?"X-Webhook-Secret"Header containing the secret (HeaderSecret mode)
EventHeaderNamestring?"X-Webhook-Event"Header with the event type
DeliveryIdHeaderNamestring?"X-Webhook-Delivery"Header with the unique delivery ID

The Audience field controls which tool permissions the webhook session gets:

AudienceTool Access
PublicMost restricted — external untrusted input
TeamModerate — trusted collaborators
PersonalFull access — your own services

Default is Public. Use this for anything internet-facing (GitHub, GitLab). Reserve Personal for internal services you fully control.

When NotificationTarget is set, the agent posts results to that channel. Only Slack is supported:

{
"NotificationTarget": {
"Kind": "Slack",
"ChannelId": "C12345678"
}
}
FieldTypeDescription
KindenumSlack (only option)
ChannelIdstringSlack channel ID (required when Kind is Slack)

To find your Slack channel ID, see Locate your Slack URL or ID.

When DeliveryRequired is true and the route has notification instructions — either explicit NotifyInstructions or auto-generated from a NotificationTarget — the agent must call send_slack_message during the session. If it doesn’t, the run is marked failed. When DeliveryRequired is false, the agent’s session prompt tells it that notification is optional and can be skipped if there’s nothing actionable.

Routes without a NotificationTarget and without NotifyInstructions don’t enforce delivery at all, regardless of the DeliveryRequired flag.

Requests to /api/webhooks/{route} go through these checks in order:

StepCheckFailure Response
1Webhooks.Enabled404 (entire webhook system is off)
2Route lookup404
3Body size413 Payload Too Large
4JSON validation400 Bad Request
5Signature/secret verification401 Unauthorized
6Event type filter202 (ignored)
7Delivery ID dedup202 (ignored)
8Rate limit429 + Retry-After header
9Dispatch202 Accepted

After dispatch, the agent session runs asynchronously — the 202 response returns immediately without waiting for the session to complete.

Accepted response body:

{
"status": "accepted",
"route": "github-issues",
"eventType": "issues",
"deliveryId": "abc-123",
"sessionId": "webhook/github-issues/abc-123"
}

The deliveryId field is null when the sender doesn’t include a delivery ID header. The daemon generates a synthetic ID internally for session tracking, but it isn’t returned in the response.

Route files are re-read from disk on each request (the daemon checks LastWriteTime). Edit a route file, and the next request picks up the change. The daemon removes invalid files from the catalog immediately and triggers a webhook.route.invalid alert.

No daemon restart needed for route changes. Global Webhooks.Enabled and ExecutionTimeoutSeconds changes do require a restart.

  • Rate limit window: 1 minute (sliding). Configurable per route via RateLimitPerMinute.
  • Dedup window: 1 hour. Deliveries with the same ID within this window are silently ignored (202).
  • Session ID format: webhook/<route>/<deliveryId>

netclaw webhooks validate <route> and netclaw doctor both run these checks:

RuleError If
Route nameDoesn’t match ^[a-z0-9]+(?:-[a-z0-9]+)*$
PromptEmpty or missing
Verification.SecretEmpty or missing
MaxBodyBytesLess than 1
RateLimitPerMinuteLess than 1
Events entriesContains blank strings
DeliveryRequired + NotifyInstructionsDeliveryRequired is true AND NotifyInstructions is non-empty AND NotificationTarget is null (all three conditions simultaneously)
NotificationTarget.Kind = SlackMissing ChannelId

Route files contain plaintext secrets. Treat ~/.netclaw/config/webhooks/ the same way you treat secrets.json:

  • Keep directory permissions at 700
  • Don’t commit route files to source control
  • The agent is hard-denied from reading this directory
  • Prefer --secret-file or --secret-env over --secret when creating routes via CLI (avoids shell history exposure)
  1. Enable webhooks in netclaw.json (or toggle during netclaw init)
  2. Create a route: netclaw webhooks set <name> --prompt "..." --secret-env SECRET_VAR
  3. Restart the daemon to pick up the Webhooks.Enabled change: netclaw daemon stop && netclaw daemon start
  4. Copy the webhook URL from netclaw status and paste it into your external service
  5. Send a test event and check netclaw stats for delivery counts — look for the webhook.received counter

Init wizard showing the inbound webhooks toggle

You only need to restart when first enabling Webhooks.Enabled. After that, route changes are hot-reloaded on each request.

The HMAC signature or header secret doesn’t match. Double-check that the secret in your route file matches what the external service is sending. For HMAC, also verify SignaturePrefix matches (e.g., GitHub sends sha256= before the hex digest).

401 Unauthorized — wrong signature header

Section titled “401 Unauthorized — wrong signature header”

The daemon is reading the signature from a different header than the one your service sends. Set SignatureHeaderName in the route’s Verification block to match your service (e.g., X-Hub-Signature-256 for GitHub).

Either Webhooks.Enabled is false in netclaw.json, or no route file matches the URL path. Run netclaw webhooks list to see loaded routes, and check that Webhooks.Enabled is true.

The request body exceeds the route’s MaxBodyBytes (default 1 MB). Increase it in the route file if the payloads are legitimately large.

Run netclaw status to see the webhook base URL. Your route’s full endpoint is:

<webhook-base-url>/api/webhooks/<route-name>

The base URL depends on how you expose the daemon. Tailscale Serve and Cloudflare Tunnel are the two supported ingress options.

  • Notification targets are Slack-only. Discord, email, and generic webhook-to-notification bridges aren’t supported yet.
  • Route secrets are stored in plaintext JSON (not in the encrypted secrets.json vault).
  • Only SHA-256 is supported for HMAC verification.
  • No webhook request logging or replay. Failed sessions are visible via netclaw stats but the original payloads aren’t stored.