Appearance
Webhooks & Triggers
Agentcy's webhook receiver is an HMAC-verified endpoint that turns inbound HTTP requests into agent runs. You can wire webhook-capable tasks and see a full "runs" timeline in the UI.
Receiver URL
POST /api/v1/hooks/<trigger_id>
Header: X-Agentcy-Signature: sha256=<hex>
Body: any JSON (or form/multipart — you decide what your task expects)Signing:
X-Agentcy-Signature = "sha256=" + hex(hmac_sha256(secret, raw_request_body))Verification happens before any JSON parsing, so an attacker can't tamper with spaces, encoding, or unicode normalization.
Create a webhook-backed task
bash
curl -X POST http://localhost:8080/api/v1/tasks \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{
"name":"alertmanager-bridge",
"agent":"default",
"realm":"infrastructure",
"trigger":{"kind":"webhook"},
"input_template":{
"instruction":"A new alert arrived: {{trigger.body.alerts[0].labels.alertname}}. Look it up in the runbook memory and summarize actions.",
"severity":"{{trigger.body.alerts[0].labels.severity}}"
}
}'Response contains the URL + secret:
json
{
"id":"task_01…",
"trigger":{
"kind":"webhook",
"url":"https://your-agentcy/api/v1/hooks/task_01…",
"secret":"whsec_01HABC…"
}
}Store the secret in your sender. Agentcy masks it on subsequent reads (GET /tasks/:id).
Test the trigger — sample payloads
You don't have to find a real caller to test. The API ships sample payloads for the common senders, and you can save your own:
bash
# List built-in samples
curl http://…/webhook-samples -H "authorization: Bearer $TOKEN" | jq
# Fire a sample into this task
curl -X POST "http://…/webhook-samples/alertmanager-single/run" \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"task_id":"task_01…"}'Saving a custom sample:
bash
curl -X POST http://…/webhook-samples \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{
"name":"my-paged-alert",
"description":"sample payload from our paging system",
"body":{"alert":"cpu_high","service":"checkout","severity":"warn"}
}'Samples live forever until you delete them; they're scoped per org.
Routing multiple senders
Many senders (GitHub, Stripe, Linear, etc.) have many event kinds. Either:
- One task per kind. Create a separate webhook URL for each, let the sender pick.
- One task with a smart template. Include a branch in
input_templatethat inspects the payload.
Example 2:
json
"input_template": {
"instruction":
"{{#if trigger.body.pull_request}}Review PR #{{trigger.body.pull_request.number}}.{{/if}}\
{{#if trigger.body.issue}}Triage issue #{{trigger.body.issue.number}}.{{/if}}"
}Template engine: Handlebars subset (if / each / lookup / raw helpers).
Replays
Every webhook hit is stored for 30 days (configurable via WEBHOOK_RETENTION_DAYS). Replay from the UI or API:
bash
curl -X POST "http://…/hooks/deliveries/$DELIVERY_ID/replay" \
-H "authorization: Bearer $TOKEN"Useful when the agent run errored and you've fixed the bug.
Signatures for outbound webhooks
Agentcy can also send webhooks (agent → your URL). Use the webhook skill / tool to declare an outbound endpoint:
bash
curl -X POST http://…/webhooks \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{
"name":"pager",
"url":"https://pager.internal/alert",
"secret":"whsec_outbound_…",
"retries":3
}'Every delivery carries X-Agentcy-Signature in the same sha256=hex form.
Verifying in your code
javascript
import crypto from "node:crypto";
export function verify(req, secret) {
const sig = req.headers["x-agentcy-signature"] || "";
const match = sig.match(/^sha256=([0-9a-f]+)$/);
if (!match) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(req.rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(match[1], "hex"),
);
}python
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
prefix = "sha256="
if not header.startswith(prefix): return False
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header[len(prefix):])Rate limiting
Default: 120 requests/min per trigger. Configure:
json
curl -X PATCH http://…/tasks/$TASK_ID \
-d '{"webhook":{"rate_limit_per_minute":600}}'Over-limit requests return 429 with Retry-After.
Gotchas
- Raw body matters for HMAC. Any framework middleware that parses JSON before you verify will break signatures. Capture the raw bytes first.
- Secrets are returned once. Immediately after creation. After that, rotate.
- Disabled tasks return 404. Not 503. Senders with retry logic will keep hammering — disable the endpoint upstream instead.
- Bodies over 1 MiB are rejected. Configurable via
WEBHOOK_MAX_BODY_BYTES.
Next
- How-To: Scheduled Tasks — the cron-triggered counterpart.
- Concept: Channels & Triggers — the overall trigger model.
- Reference: REST API —
/hooks,/webhooks,/webhook-samples.