Skip to main content
Every webhook trigger declared in TRIGGER.md gets a unique, stable URL keyed by source:
https://api.usezombie.com/v1/webhooks/{zombie_id}/{source}
External systems POST to this URL. The platform verifies the signature and delivers the event to the agent. No tunneling, no ngrok, no custom servers on your side. zombiectl install --from prints the full set of URLs (one per declared webhook trigger) at install time.
Most users don’t wire webhooks by hand. The platform-ops install skill registers each declared webhook automatically via your existing gh CLI — no paste-into-GitHub step. Use Quickstart for the full guided flow. The reference below is for custom webhooks — your own provider, an internal service, etc.

Declaring a webhook trigger

An agent declares its webhooks in TRIGGER.md under the triggers: array (1–8 entries). One agent can wake on multiple webhook sources plus a cron:
---
name: my-agent

x-usezombie:
  triggers:
    - type: webhook
      source: github
      events: [workflow_run]
      signature:
        secret_ref: github_secret
        header: x-hub-signature-256
        prefix: "sha256="
---
source is a label that appears in the activity stream (github, slack, linear, etc.) and selects the per-trigger inbound URL — https://api.usezombie.com/v1/webhooks/{zombie_id}/{source}. events is an optional whitelist of upstream event names (1–16 entries, ≤64 chars each); omit it to accept every event the source delivers. The signature block tells the platform how to verify the request. The singular trigger: shape is rejected at install (ERR_ZOMBIE_INVALID_CONFIG: use "triggers:" (array)); always declare the array form, even for a single webhook.

Authentication

Every inbound webhook is authenticated before the agent is woken. The platform supports HMAC signature verification — Hash-based Message Authentication Code, a standard scheme where the sender signs the payload with a shared secret and the receiver re-computes the signature to verify it. GitHub, Slack, Linear, Jira, Grafana, and most SaaS providers use HMAC.

Natively normalized providers

Three sources have built-in defaults — set triggers[].source to one of them and the platform fills in the signature header and prefix automatically. You only supply secret_ref:
triggers[].sourceSig header (auto)Prefix (auto)Timestamp header (auto)
slackx-slack-signaturev0=x-slack-request-timestamp (replay-protected)
githubx-hub-signature-256sha256=
linearlinear-signature(empty)
# Minimal — registry fills the rest
triggers:
  - type: webhook
    source: github
    signature:
      secret_ref: github

Other providers (generic HMAC)

For any other source — Jira, AgentMail, an internal service of your own — declare the signature header and prefix yourself (or omit the signature block entirely if the upstream doesn’t sign payloads, as some lightweight providers don’t):
triggers:
  - type: webhook
    source: my_provider
    signature:
      secret_ref: my_provider          # vault key holding the shared secret
      header: x-provider-signature     # the header your provider sets
      prefix: "sha256="                # leave "" if no prefix
      ts_header: x-provider-timestamp  # optional — enables replay protection
The parser rejects an unknown source that supplies only secret_ref with no header — explicit configuration is required outside the registry. secret_ref always names a credential in the workspace vault carrying the upstream’s signing secret. See Credentials.
Payload normalization is automatic for named sources. When source is github, slack, or linear, POST /v1/webhooks/{zombie_id}/{source} runs the provider-specific normalizer (event-type extraction, payload shape coercion) before the agent sees the event — no flag, no separate URL. Pick a different source label (e.g. source: github_raw) with an explicit signature block to bypass normalization and receive the raw payload. Clerk-shaped (Svix) webhooks are the one exception: they use a distinct route, POST /v1/webhooks/svix/{zombie_id}, to accommodate Svix’s multi-signature rotation scheme.

Sending an event

Once the agent is installed, your provider POSTs to its URL with a signed body:
curl -X POST https://api.usezombie.com/v1/webhooks/zmb_2041/github \
  -H "x-hub-signature-256: sha256=<computed-hmac>" \
  -H "Content-Type: application/json" \
  -d '{
    "event_id": "evt-001",
    "type": "email.received",
    "data": { "from": "[email protected]", "subject": "Demo request" }
  }'
The platform verifies the signature, enqueues the event, and returns 202 Accepted:
{ "accepted": true, "request_id": "req_01JQ7K..." }
Delivery is at-least-once — your provider may retry on transient failure, so the same event can arrive twice. Include a unique event_id in the body so the agent can dedupe.

Failure responses

StatusCause
401Missing, malformed, or mis-signed signature header.
404Agent ID does not exist.
410Agent was killed; re-install with zombiectl install --from <path>.
429Rate limit hit; back off per Retry-After.
503Budget breached, or platform applying backpressure; safe to retry after the period resets.

Approval gate webhooks

Some agent actions — sending money, merging a PR, posting to a public channel — should not happen without a human in the loop. The approval gate lets an agent pause mid-run, emit an approval request, and wait for a human to click approve or deny in the dashboard. For programmatic decisions (e.g. an internal tool calling back into usezombie), POST the decision to:
curl -X POST https://api.usezombie.com/v1/webhooks/zmb_2041/approval \
  -H "Authorization: Bearer $APPROVAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "approval_id": "apr_01JQ...", "decision": "approve" }'
The approval token is auto-provisioned for the agent at install time.

Observing webhook traffic

Every accepted and rejected webhook appears in the activity stream:
zombiectl logs zmb_2041
Rejected requests carry a reason code (webhook_rejected: signature_mismatch, etc.) so you can tell a noisy upstream from a real auth bug.

Advanced: URL-embedded secret

Some upstreams (a few SaaS form-postbacks, a few legacy systems) can’t attach a signature header. For those, the platform accepts a path-embedded secret:
https://api.usezombie.com/v1/webhooks/{zombie_id}/{url_secret}
The url_secret is matched in constant time. Reserved segments (approval, grant-approval, svix) cannot be used as secret values. Prefer HMAC where the upstream supports it — the URL-embedded form is a fallback. Path resolution order. The trailing segment is resolved against declared triggers[].source values first, then falls back to a url_secret lookup. So POST /v1/webhooks/zmb_2041/github routes to the github source when the agent declares one, and only searches url_secret values for a constant-time match on the literal string github when no such source is declared. Practical consequence: avoid choosing a url_secret value that collides with any triggers[].source on the same agent — the source wins and the secret will never match.