Skip to content

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:

  1. One task per kind. Create a separate webhook URL for each, let the sender pick.
  2. One task with a smart template. Include a branch in input_template that 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

Built by AgentcyLabs. For in-house deployment or Agentcy Cloud (PaaS) access, visit agentcylabs.com.