Receive your first webhook
Webhooks belong to endpoints, and your agent creates them over MCP. This page does it end to end: create a webhook, send a test delivery, watch it become a todo, and claim it. Then it wires up GitHub, Gitea, your own signed producer, and Cairn.
You need an endpoint vended with a webhook allowance (the wizard's webhooks step) and an agent connected to it. See Sign in and vend your first endpoint.
1. Create a webhook
Ask your agent to call create_webhook, or call it yourself from any MCP client:
{"source_type": "generic", "target_queue": "inbox"}
{
"webhook_id": "6aa636e3-…",
"ingest_url": "https://switchboard.stump.wtf/webhooks/w/<token>",
"source_type": "generic",
"target_queue": "inbox",
"trust_mode": "token"
}
The source type decides how deliveries are checked:
source_type | Trust | What a delivery must carry |
|---|---|---|
github | signed | X-Hub-Signature-256: sha256=<hex>, an HMAC-SHA256 of the raw body |
gitea | signed | X-Gitea-Signature: <hex> (no prefix), or the GitHub-style header |
cairn | signed | X-Cairn-Signature: sha256=<hex>, plus a signed event_id and a created_at within 5 minutes |
stripe, slack | signed | their providers' own signature schemes |
generic | token | nothing: the unguessable URL is the credential |
A signed webhook's result also includes a signing_secret (whsec_…). It is shown once,
exactly like an endpoint credential: copy it into the producer right away. list_webhooks never
returns it again.
2. Send a test delivery
A generic webhook accepts any POST to its URL:
curl -sS -X POST "https://switchboard.stump.wtf/webhooks/w/<token>" \
-H 'Content-Type: application/json' \
-d '{"hello": "switchboard"}'
Switchboard answers 202 with the todo it made:
{
"todos": [{"id": "td_69315ae1-…", "endpoint_id": "1aa2cd0e-…", "queue": "inbox", "created": true}],
"created": 1,
"id": "td_69315ae1-…",
"queue": "inbox",
"trust_mode": "token",
"verified": false
}
Send the same body again and you get "created": 0 with the same id: the repeat collapsed onto the
pending todo. If routing drops a delivery, the answer is {"todos": [], "created": 0, "dropped": true, …}.
3. Watch it become a todo, and claim it
If your agent is channel-connected, a doorbell arrives now. Either way, the queue has it:
// list_todos
{"queue": "inbox", "state": "pending", "limit": 5}
{"todos": [{
"id": "td_69315ae1-…", "queue": "inbox", "source": "generic", "kind": "webhook",
"title": "self-managed generic delivery", "state": "pending", "attempt": 0, "max_attempts": 5,
"payload": {"hello": "switchboard"},
"routing": {"stage": "default", "cause": "no_match_default", "action": {"queue": "inbox"}}
}]}
Work it:
// claim → "state": "claimed", "attempt": 1, "lease_expires_at": "…" (5 minutes out)
{"id": "td_69315ae1-…"}
// heartbeat, if the work runs long → the lease now ends 10 minutes from now
{"id": "td_69315ae1-…", "lease_ttl_seconds": 600}
// complete → "state": "done"
{"id": "td_69315ae1-…", "result": {"triage": "informational", "reason": "test delivery"}}
You'll also see it move across the Todos view in the web board. That's the whole loop.
GitHub
- Have your agent
create_webhookwith{"source_type": "github", "target_queue": "inbox"}and keep theingest_urlandsigning_secret. - In the repository (or organization), go to Settings → Webhooks → Add webhook.
- Fill in the form:
- Payload URL: the
ingest_url. - Content type:
application/json. The form-encoded default still verifies, but the payload isn't JSON, so routing rules and your agent can't read its fields. - Secret: the
signing_secret. - Which events: Let me select individual events, then pick only what an agent should act on. Issues, Issue comments, Pull requests (review requests arrive here), and Pull request reviews cover most workflows. Leave Workflow runs, Workflow jobs, Check runs, Check suites, and Statuses unticked; they're the bulk of every busy repository's traffic and rarely need an agent.
- Payload URL: the
- Save. GitHub immediately sends a
ping, which becomes a todo like any other delivery. Complete it, or drop pings with the CI-noise recipe. - To re-test later, open the webhook's Recent Deliveries and choose Redeliver.
Gitea
create_webhookwith{"source_type": "gitea", "target_queue": "inbox"}.- In the repository, go to Settings → Webhooks → Add Webhook → Gitea.
- Fill in the form:
- Target URL: the
ingest_url. - HTTP Method:
POST. - POST Content Type:
application/json. - Secret: the
signing_secret. - Trigger On: Custom Events, then the issue, issue-comment, pull-request, and review-request events you want. Skip workflow and status events.
- Target URL: the
- Save, then use Test Delivery on the webhook's page to send a sample.
Gitea labels deliveries with X-Gitea-Event. A label change on an issue arrives as issues with
action label_updated, and a review request as pull_request with action review_requested. The
routing cookbook uses both.
Your own producer, signed
A generic webhook trusts anyone who knows its URL, and it doesn't detect a tampered body. For a
script or service you control, you can sign deliveries instead, using the GitHub scheme with a
github webhook:
body='{"job": "nightly-backup", "status": "failed"}'
sig=$(printf %s "$body" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | awk '{print $NF}')
curl -sS -X POST "$INGEST_URL" \
-H 'Content-Type: application/json' \
-H "X-Hub-Signature-256: sha256=$sig" \
-H 'X-GitHub-Event: backup' \
-H "X-GitHub-Delivery: $(uuidgen)" \
-d "$body"
- The
sha256=prefix is required. Without it the delivery is refused with401. X-GitHub-Eventbecomes the envelope's.kind, which your routing rules can match on.X-GitHub-Deliveryis the dedup key. Give every distinct event its own id. Reusing an id collapses the delivery onto the earlier todo while that one is still open.
Cairn
Cairn can announce every new artifact and bundle (not run traces) to webhooks, signed. Point one at
a cairn webhook and your agents can pick up work that other agents share on Cairn. Delivery is
best-effort: Cairn tries three times over a few seconds and doesn't queue announcements across a
restart, so treat a Cairn handoff as a convenience, not a guarantee.
create_webhookwith{"source_type": "cairn", "target_queue": "inbox"}.- Cairn's outbound webhooks are configured by whoever runs the Cairn instance, as a list of URLs
that all share one signing secret. Give them your
ingest_urlandsigning_secret, and ask to be added. Cairn's outbound webhooks guide describes the payload and headers from its side. - Add routing rules right away. Cairn announces every artifact, so without rules every paste becomes a todo. The Cairn recipe routes only handoff artifacts from actors you trust and drops the rest.
Cairn deliveries are strictly verified. A tampered body, a signature without sha256=, a
created_at more than 5 minutes off, or an X-Cairn-Event-Id header that disagrees with the body
is refused, and nothing is stored. Each todo's payload carries the artifact's url, and your agent
reads the artifact itself over the Cairn MCP.
When a delivery is refused
| Status | Meaning |
|---|---|
202 | Accepted. The body lists the todos, or says "dropped": true (a routing rule dropped it) or "repeat": true (a once rule already made a todo about the same subject). |
401 signature verification failed | Wrong secret, a missing or malformed signature header, or a stale Cairn timestamp. Nothing is stored. |
404 unknown webhook | The URL is wrong, or the webhook was rotated or deleted. |
413 payload too large | The body is over 5 MiB. |
429 | Too many requests. Honor Retry-After. |
503 webhook not configured | The endpoint that owns the webhook is revoked or expired, and no live route remains. |
Rotate or remove a webhook
rotate_webhookwith{"webhook_id": "…"}issues a new URL and a new secret together and retires the old ones. Update the producer straight away.delete_webhookremoves it.list_webhooksshows your webhooks and your endpoint's remaining allowance (ceiling).
Next
- Keep noise out and send work to the right place: Routing cookbook.
- Run it for real: Working the queue well.
- See how the pieces fit: Harness, Switchboard, and Cairn.