Guide

Webhooks

Signed, retried deliveries of your account's events to your own endpoint.

Subscribe

curl -s https://YOUR-HOST/api/v1/webhooks -H "Authorization: Bearer $JUTSU_KEY" -H "content-type: application/json" \
  -d '{"url":"https://example.com/jutsu","events":["task","approval","battle","order"]}'

The response includes the signing secret (whsec_…) once. GET /webhooks/events lists every event; "*" subscribes to all. Up to 10 webhooks; needs webhooks:write.

Deliveries

Each delivery is a POST with JSON { id, type, createdAt, data } and the headers Jutsu-Event, Jutsu-Delivery and Jutsu-Signature: t=<unix>,v1=<hex>.

Verify every request

v1 is HMAC-SHA256 of <t>.<raw body> keyed with the secret. Compare in constant time and reject anything older than 5 minutes:

import { verifyWebhook } from "@jutsu/sdk";
if (!verifyWebhook(secret, rawBody, req.headers["jutsu-signature"])) return res.status(400).end();

Retries and limits

Answer 2xx within 10 seconds. Anything else is retried with backoff (1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h; 8 attempts). After 25 failures in a row the webhook is switched off; fix it and PATCH it back on. Delivery is at least once, so dedupe on Jutsu-Delivery. POST /webhooks/{id}/test sends a signed ping now, GET /webhooks/{id}/deliveries shows the log and POST /webhooks/deliveries/{id}/retry resends one. POST /webhooks/{id}/rotate issues a new secret.

Safety

URLs must be public HTTPS. Private, loopback, link-local and metadata addresses are refused, both when you save the URL and when we connect (so DNS cannot be rebound to an internal address). Redirects are not followed.