Webhooks

Subscribe to Agent Handler events from your own pipeline

Webhooks let your systems react to events happening inside Agent Handler. Today Agent Handler emits three events: a Registered User is created, a Registered User is deleted, and a third party sends an inbound webhook that Agent Handler forwards to you. Every outbound webhook is signed, so your endpoint can confirm it came from Agent Handler before trusting the payload.

Outbound webhooks are events from Agent Handler to you, the common case. Inbound webhooks are events from a third party (Slack, Jira, GitHub) 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. A non-2xx response, or a response slower than the timeout, is recorded as a failed delivery (see Delivery and failures).

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

Agent Handler emits three event types. This table is the complete set.

EventFires when
REGISTERED_USER_CREATEDA Registered User is created through the API or dashboard
REGISTERED_USER_DELETEDA Registered User is deleted
INBOUND_WEBHOOK_RECEIVEDA subscribed third-party Connector (Slack, Jira, GitHub) sent an inbound webhook that Agent Handler forwards to you (see Inbound webhooks)

There are no tool-call, credential, or security-rule webhook events today. Tool-call activity and security violations are visible in the Tool Call Logs and Violations and alerts dashboards, and can be pulled via the logs API.

Payload shape

Every payload shares the same envelope: a hook object with delivery metadata, a data object whose shape depends on the event, and an origin object that is populated only for inbound events.

1{
2 "hook": {
3 "id": "a1c4f0d2-9b3e-4a77-8f21-6d5e2b9c0a14",
4 "event": "REGISTERED_USER_CREATED",
5 "target": "https://your-app.example.com/webhooks/merge",
6 "event_timestamp": "2026-05-04T16:42:11Z",
7 "data_type": "registered_user",
8 "third_party_data_event_type": null
9 },
10 "data": {
11 "id": "f9813dd5-e70b-484c-91d8-00acd6065b07",
12 "origin_id": "user_a3f9b2",
13 "name": "Alice Chen",
14 "registered_company": "Acme Inc.",
15 "is_test_user": false,
16 "user_type": "external"
17 },
18 "origin": null
19}
FieldDescription
hook.idThe ID of the subscription this delivery came from. Not unique per delivery.
hook.eventOne of the three event types in the catalog.
hook.targetThe callback URL the event was sent to.
hook.event_timestampWhen the event occurred, ISO 8601.
hook.data_typeregistered_user for the Registered User events, third_party_data for inbound events.
hook.third_party_data_event_typeFor inbound events, the third party’s own event name (channel_created, for example). null otherwise.
dataThe event payload. For Registered User events this is the Registered User, with credentials and groups omitted. For inbound events it’s the forwarded third-party body.
originThe inbound request that triggered the forward. Populated for inbound events only; null for the Registered User events.

Verifying the signature

Every outbound webhook is signed with HMAC-SHA256 using a secret you control. The signature is in the X-Webhook-Signature header, hex-encoded. Verify it before trusting the payload, since 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}

The signature is computed over the raw request body exactly as sent. 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.

Delivery and failures

Agent Handler sends each event with a 2-second timeout. A response slower than 2 seconds, a non-2xx status, or a connection error is recorded as a failed delivery.

Failed deliveries are not retried, and there is no replay from the dashboard today. A delivery that fails is not sent again. Build for this:

  • Return 2xx as fast as you can and do the real work asynchronously. Holding the connection open to finish processing will blow past the 2-second window and the event is lost.
  • If you need a guarantee that you’ve seen every Registered User, reconcile against the Registered Users API on a schedule rather than relying on the webhook alone. Treat webhooks as a low-latency signal, not the system of record.

Idempotency

The envelope has no per-delivery unique ID today. hook.id identifies the subscription, not the individual event, so you can’t dedupe on the envelope alone. Because deliveries aren’t retried, duplicates are unlikely, but if you need strict idempotency, derive a key from the event contents, for example hook.event plus data.id plus hook.event_timestamp.

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 as an INBOUND_WEBHOOK_RECEIVED event. 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

For an inbound event, data holds the forwarded third-party body and origin holds the original request Agent Handler received from the third party (method, URL, headers, and body), along with the Connector and Registered User it belongs to. hook.third_party_data_event_type carries the third party’s own event name when the Connector can determine it.

1{
2 "hook": {
3 "id": "a1c4f0d2-9b3e-4a77-8f21-6d5e2b9c0a14",
4 "event": "INBOUND_WEBHOOK_RECEIVED",
5 "target": "https://your-app.example.com/webhooks/merge",
6 "event_timestamp": "2026-05-04T16:42:11Z",
7 "data_type": "third_party_data",
8 "third_party_data_event_type": "channel_created"
9 },
10 "data": {
11 "channel": { "id": "C01ABC", "name": "new-channel" }
12 },
13 "origin": {
14 "connector_slug": "slack",
15 "organization_id": "org_8f21a0",
16 "registered_user_id": "f9813dd5-e70b-484c-91d8-00acd6065b07",
17 "method": "POST",
18 "url": "https://ah-api.merge.dev/api/webhooks/inbound/listener/...",
19 "headers": { "x-slack-signature": "v0=..." },
20 "body": { "type": "event_callback", "event": { "type": "channel_created" } }
21 }
22}

Next

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