Skip to content

Webhooks

AgentManager fires a signed HTTP POST to registered webhook URLs when a human makes an approval decision (approve or deny). Use webhooks to react to decisions without polling.

How it works

human approves/denies in portal or via API
  → AgentManager POSTs approval.decided to all registered webhook URLs
  → your server verifies HMAC signature and processes the payload

Webhooks fire only on approval decisions — not on preflight requests or policy changes.

Registering a webhook

An agent supports two webhook registration paths:

Single URL (on the agent record)

Set webhook_url directly on the agent via PUT /api/v1/agents/{agent_id}:

bash
curl -X PUT https://api.gavrun.ai/api/v1/agents/{agent_id} \
  -H "Authorization: Bearer <management_token>" \
  -H "X-Tenant-Id: <tenant_id>" \
  -H "Content-Type: application/json" \
  -d '{"webhook_url": "https://your-server.com/webhooks/gavrun"}'

Multiple subscribers (webhook_subscriptions)

Register independent receivers via the subscriptions API. Each fires independently — adding a new subscriber doesn't affect existing ones.

bash
# Register a subscriber
curl -X POST https://api.gavrun.ai/api/v1/agents/{agent_id}/webhooks \
  -H "Authorization: Bearer <management_token>" \
  -H "X-Tenant-Id: <tenant_id>" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-server.com/webhooks/gavrun"}'

# List subscribers
curl https://api.gavrun.ai/api/v1/agents/{agent_id}/webhooks \
  -H "Authorization: Bearer <management_token>" \
  -H "X-Tenant-Id: <tenant_id>"

# Remove a subscriber
curl -X DELETE https://api.gavrun.ai/api/v1/agents/{agent_id}/webhooks/{subscription_id} \
  -H "Authorization: Bearer <management_token>" \
  -H "X-Tenant-Id: <tenant_id>"

URLs must start with http:// or https://. Both paths fire on the same decision — if both webhook_url and subscriptions are registered, all of them receive the payload.

Payload

json
{
  "event": "approval.decided",
  "approval_id": "apr_abc123def456",
  "decision_id": "dec_abc123def456",
  "request_id": "req_abc123def456",
  "resume_decision_id": "dec_abc123def456",
  "agent_id": "agent_abc123def456",
  "tenant_id": "tenant_abc123",
  "status": "approved",
  "decided_at": "2026-07-30T12:00:00+00:00",
  "reviewer_id": "alice",
  "reviewer_note": "looks good"
}

status is either "approved" or "denied".

resume_decision_id is the ID to pass to GET /api/v1/approvals/status or use when resuming the agent — it is always equal to decision_id in the current version.

Verifying signatures

Every delivery includes an X-Gavrun-Signature header. The value is sha256=<hex> where the hex is an HMAC-SHA256 of the raw request body using the agent's client_secret as the key.

Verify before processing:

python
import hashlib
import hmac

def verify_gavrun_signature(body: bytes, header: str, client_secret: str) -> bool:
    if not header.startswith("sha256="):
        return False
    expected = "sha256=" + hmac.new(
        client_secret.encode("utf-8"),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(header, expected)
typescript
import { createHmac, timingSafeEqual } from "crypto";

function verifyGavrunSignature(
  body: Buffer,
  header: string,
  clientSecret: string
): boolean {
  if (!header.startsWith("sha256=")) return false;
  const expected =
    "sha256=" +
    createHmac("sha256", clientSecret).update(body).digest("hex");
  return timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}

The client_secret is returned at agent registration and stored by your agent process. Use hmac.compare_digest / timingSafeEqual (not ==) to prevent timing attacks.

Always return HTTP 200 — AgentManager does not retry on non-200 responses, but delivery failures are logged as approval.webhook_failed audit events.

Why webhooks instead of polling

PollingWebhooks
Agent threads per pending approval1 per approval0
API calls per approvalN × poll interval1
Latencyup to poll interval~1s
Scale concernself-inflicted load at high concurrencynone

At scale (100s concurrent approvals), polling spins 100s of threads each hitting GET /approvals/status every few seconds. Webhooks eliminate this entirely — AgentManager pushes the decision once, your server unblocks the right request by approval_id.

Audit trail

Every webhook delivery generates an audit event:

  • approval.webhook_sent — delivery succeeded (HTTP 2xx)
  • approval.webhook_failed — delivery failed; metadata.error contains the exception

Both events include approval_id, decision_id, and url in metadata. Subscription deliveries also include subscription_id.

Apache 2.0 License