Webhooks

Subscribe to Agent Handler events from your own pipeline.

Webhooks let your systems react to events happening inside Agent Handler - a Registered User is created, a tool call fails, a security rule fires, an OAuth token expires. Agent Handler signs every outbound webhook and retries on failure, so your endpoint can stay simple.

Outbound webhooks are events from Agent Handler to you - the common case. Inbound webhooks are events from a third party (Slack, Jira) to Agent Handler, then forwarded to your outbound webhook so you get one unified event stream.

Outbound webhooks

Setting up a subscription

  1. Open Connectors → Webhooks.
  2. Click Add webhook.
  3. Enter your HTTPS callback URL.
  4. Select the events to subscribe to (see catalog below).
  5. Save.

Your endpoint should accept POST requests, parse JSON, and return 2xx on success. Anything non-2xx triggers a retry.

For local development, webhook.site gives you a temporary URL that captures requests in a browser. Once you’ve confirmed payloads look right, point at your real endpoint.

Event catalog

EventFires when
registered_user.createdA Registered User is created via the API or dashboard
registered_user.updatedProfile, groupings, or activation state changes
registered_user.deletedA Registered User is hard-deleted
credential.createdAn end user authenticates a Connector through Link or Magic Link
credential.expiredA stored OAuth token’s refresh fails (revoked or expired upstream)
credential.deletedA credential is removed via the dashboard or API
tool_call.completedA tool call returns successfully
tool_call.failedA tool call errors (auth, rate limit, Connector error, etc.)
tool_call.blockedThe Security Gateway blocked a tool call
rule_violationThe Security Gateway matched on tool arguments (any action - allow, redact, block)
tool_pack.updatedTool Pack edited (Connectors added/removed, tools toggled, overrides changed)
inbound_webhook.receivedUsed by inbound-webhook forwarding (see below)

Payload shape

All payloads share the same envelope. The data field’s shape varies by event type.

1{
2 "id": "evt_01H8K2X9Y3Z4...",
3 "type": "tool_call.completed",
4 "created_at": "2026-05-04T16:42:11Z",
5 "organization_id": "org_•••••",
6 "data": {
7 "tool_call_id": "tc_•••••",
8 "tool_name": "slack__post_message",
9 "registered_user_id": "f9813dd5-e70b-484c-91d8-00acd6065b07",
10 "tool_pack_id": "tp_•••••",
11 "arguments": { "channel": "general", "text": "[REDACTED:EMAIL] confirmed." },
12 "result": { "ok": true, "ts": "1714838531.001" },
13 "latency_ms": 412
14 }
15}

Sensitive values follow the same redaction rules as the Tool Call Logs - anything the Security Gateway redacted on the original call is also redacted in the webhook payload.

Verifying the signature

Every outbound webhook is signed with HMAC-SHA256 using a secret you control. The signature is in the X-Merge-Signature header. Verify it before trusting the payload - anyone can POST to a public URL.

Get your signing secret at Webhooks → Verification key. Rotate it from the same page; rotation invalidates all signatures generated with the old key.

webhook_handler.py
1import hmac
2import hashlib
3import os
4
5SIGNING_SECRET = os.environ["MERGE_WEBHOOK_SIGNING_SECRET"]
6
7def verify(raw_body: bytes, signature_header: str) -> bool:
8 expected = hmac.new(
9 SIGNING_SECRET.encode(),
10 raw_body,
11 hashlib.sha256,
12 ).hexdigest()
13 return hmac.compare_digest(expected, signature_header)
webhook-handler.ts
1import { createHmac, timingSafeEqual } from "node:crypto";
2
3const SIGNING_SECRET = process.env.MERGE_WEBHOOK_SIGNING_SECRET!;
4
5export function verify(rawBody: Buffer, signatureHeader: string): boolean {
6 const expected = createHmac("sha256", SIGNING_SECRET).update(rawBody).digest("hex");
7 return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
8}

Verify against the raw request body. If your framework parses JSON before you see the bytes, configure it to give you the raw body - re-stringifying parsed JSON will produce a different signature.

Retries and timeouts

Agent Handler waits up to 10 seconds for a 2xx response. Slower than that or non-2xx triggers a retry. Retry schedule is exponential backoff with jitter:

AttemptDelay before retry
1st retry~30 seconds
2nd retry~2 minutes
3rd retry~10 minutes
4th retry~1 hour
5th retry~6 hours

After five failures, the event is dropped. You can replay individual events from the dashboard’s webhook delivery log.

For long-running work, return 2xx immediately and process asynchronously. Holding the connection open past 10 seconds will burn retries even if your work eventually succeeds.

Idempotency

The id field on the envelope is unique per event. Use it as your idempotency key - if you’ve already processed an event with that ID, drop the duplicate. Retries reuse the same ID, so dedupe is mechanical.

Inbound webhooks

Some Connectors (Slack, Jira, GitHub) emit webhooks to subscribers. Agent Handler can be that subscriber, then forward the event to your outbound webhook. You get a single, unified event stream regardless of how many third parties you’re reacting to.

Inbound webhooks are passthrough only - every inbound subscription must be paired with an outbound subscription that includes inbound_webhook.received.

Setup

  1. Subscribe an outbound webhook to inbound_webhook.received. Add the event to an existing outbound subscription (or create a new one) at Connectors → Webhooks.
  2. Enable inbound webhooks for the Connector. Open the Connector’s detail page; if it supports inbound webhooks, you’ll see a Webhook section. Click Add webhook.
  3. Connector-specific configuration. Each third party requires different credentials. Slack wants the app’s signing secret; GitHub wants a webhook secret you generate; Jira wants admin access to register the webhook with the third party.

Each Connector’s webhook setup is documented inline in the dashboard. The Slack flow, as a representative example:

  1. In the Slack app dashboard, copy the signing secret from Basic Information → App Credentials.
  2. Paste it into the Slack Connector’s webhook field in Agent Handler.
  3. Agent Handler returns a webhook URL - copy it.
  4. In Slack, go to Event Subscriptions → Request URL, paste the URL, and select the events you want forwarded.
  5. Slack verifies the URL automatically. Once verified, events flow Slack → Agent Handler → your outbound endpoint as inbound_webhook.received payloads.

Inbound payload shape

The forwarded payload preserves the third party’s original event under a data.original_event field, plus metadata about which Connector and which Registered User the event belongs to:

1{
2 "id": "evt_•••••",
3 "type": "inbound_webhook.received",
4 "data": {
5 "Connector": "slack",
6 "registered_user_id": "f9813dd5-e70b-484c-91d8-00acd6065b07",
7 "original_event_type": "channel_created",
8 "original_event": { "channel": { "id": "C01...", "name": "new-channel" } }
9 }
10}

Next

Tag tool calls with session or workflow metadata using Custom headers for MCP.