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
Three headers ride with every delivery:
X-Developerz-Signature: sha256=<hex>
X-Developerz-Timestamp: 1700000000
X-Developerz-Event-Version: 1
The signature is HMAC-SHA256(secret, "<timestamp>.<raw_request_body>"): the
timestamp is part of what gets signed, not just a sibling header. Verify
against the raw bytes, before any JSON parse or re-serialization, and
reject any delivery whose timestamp is too old (we recommend 5 minutes):
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(
rawBody: string,
signatureHeader: string,
timestampHeader: string,
secret: string,
toleranceSeconds = 300,
): boolean {
const timestamp = Number(timestampHeader);
if (!Number.isFinite(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected =
'sha256=' +
createHmac('sha256', secret).update(`${timestampHeader}.${rawBody}`).digest('hex');
const a = Buffer.from(signatureHeader);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}
Binding the timestamp into the signature (rather than trusting it unsigned) means an attacker who replays a captured request cannot just strip the freshness check: the signature is over the timestamp too, so a forged newer timestamp fails the HMAC.
The body we sign is canonicalized (stable key order) before it is sent, so a
retry re-sends byte-identical BODY bytes, but the timestamp is minted fresh on
every attempt (including the last retry, about 38.6 hours after the first
attempt), so the full signature
differs attempt to attempt. Verify each delivery independently; do not cache a
signature by id and expect it to match on a later retry.
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
Each step is the minimum wait after the previous failure. Retries are picked up by a scan that runs every 15 seconds, so a retry lands up to 15 seconds after its step is due, and the 1s and 5s steps in practice arrive about 15 seconds apart. The steps add up to 38 hours 35 minutes 36 seconds, so the last retry comes about 38.6 hours after the first attempt (a couple of minutes later once scan delays are counted).
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,
Linear or any system that can sign a POST, to learn that something happened in your
systems. That side is configured by connecting an inbound source over MCP
(integration source_connect), not by implementing a receiver: each source gets
its own URL and signing secret, and an eligible delivery can file an ask. Not every
accepted delivery does: a repeat for an existing ask answers noop, and one with no
work signal or no repository to bind is dropped with a reason. Every answer is in
what each delivery answers, part of
the Sentry walkthrough.