Skip to content

Docs / Outbound webhooks

Outbound webhooks

Signed event deliveries to your endpoint: envelope, signature, retries, and which events fire today.

developerz.ai can POST signed events to an HTTPS endpoint you own. Use them to mirror work into a tracker, post into a chat channel, or trigger a workflow in n8n — everything the first-party integrations would do, and everything they do not do yet.

Create one

curl -X POST https://api.developerz.ai/v1/webhooks \
  -H "Authorization: Bearer dev_pat_…" \
  -H "content-type: application/json" \
  -d '{"name":"ops","url":"https://example.com/hooks/developerz"}'

Scope: write:webhooks. Names are unique per account.

The response contains the signing secret, once. It is sealed server-side and never returned again — store it when you create the hook, or delete the hook and create a new one.

Method Path Scope
GET /v1/webhooks read:webhooks
POST /v1/webhooks write:webhooks
DELETE /v1/webhooks/:id write:webhooks
POST /v1/webhooks/:id/test write:webhooks
GET /v1/webhooks/deliveries read:webhooks

GET /v1/webhooks returns the URL masked. The delivery ledger keeps the last 100 attempts, newest first, with the status and a response excerpt.

POST /v1/webhooks/:id/test sends a synthetic test.ping through the real delivery path, and the attempt lands in the ledger like any other — so a test proves the whole pipe, not a mock of it.

The envelope

{
  "id": "evt_01J8Z6M4Q9T7X2C5N0R3B1D8FK",
  "type": "pr.merged",
  "occurred_at": "2026-07-27T09:14:03.117Z",
  "data": { }
}

id is your idempotency key. It stays the same across every retry of the same event, so the correct receiver behaviour is: look up id, and if you have seen it, return 2xx and do nothing.

data carries the event-specific payload. Everything lives under data — there are no event fields at the top level beside the four above.

Verifying the signature

Two headers ride with every delivery:

X-Developerz-Signature: sha256=<hex>
X-Developerz-Event-Version: 1

The signature is HMAC-SHA256(secret, raw_request_body). Verify against the raw bytes, before any JSON parse or re-serialization.

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody: string, header: string, secret: string): boolean {
  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(header);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

The body we sign is canonicalized (stable key order) before it is sent, so a retry re-sends byte-identical bytes and therefore the same signature. You never need to re-canonicalize anything on your side.

Events that fire today

Event When
pr.merged A pull request the agent was watching merged.
issue.escalated The agent escalated an issue to a human instead of acting.
issue.handoff A handoff fired via the webhook_ref in your .maintainer.yml.
test.ping You pressed the test button. Never fires on its own.

Other event types exist in the schema and are not yet produced. We list only what actually fires; if you subscribe to something else today, nothing will arrive. When more events go live they appear here and in the changelog.

Retries

A delivery succeeds on any 2xx. Anything else — including a timeout or a network error — is a failure and is retried on this ladder:

1s → 5s → 30s → 5m → 30m → 2h → 12h → 24h

That is 9 attempts in total. After the ninth failure the webhook is marked unhealthy and stops being retried; fix the endpoint and create a new hook.

Each attempt has a 10-second timeout, and we store the first 2 KB of your response body in the delivery ledger so you can debug from our side.

Endpoint requirements

  • HTTPS or HTTP only. Any other scheme is refused.
  • Public hosts only. URLs that resolve to loopback, link-local or RFC 1918 addresses are refused, and the refusal reason is generic on purpose.

That second rule exists because your response body is echoed back to you in the delivery ledger. Without it, a webhook pointed at 169.254.169.254 would be a read primitive against internal metadata services. If your endpoint is internal, put a public reverse proxy in front of it.

Inbound webhooks are a different thing

developerz.ai also receives webhooks — from GitHub, and optionally from Sentry and Linear — to learn that something happened in your systems. That side is configured by connecting the source, not by implementing a receiver, and it is covered in the quickstart.