Webhooks
Webhooks push events to your server as they happen — async job completions, agent lifecycle transitions and billing alerts — so you never have to poll.
Create an endpoint
POST
/v1/webhooksRegister an HTTPS URL and the events you care about. Each endpoint gets its own signing secret, shown once at creation.
curl https://api.tapotik.ai/v1/webhooks \
-H "Authorization: Bearer $TAPOTIK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/api/tapotik/webhook",
"events": ["image.batch.completed", "agent.run.*"]
}'
# -> { "id": "wh_31xk", "secret": "whsec_Zk83jf...", ... }Events
| Event | Fires when |
|---|---|
image.batch.completed | An async image batch finishes; payload includes signed URLs. |
video.render.completed | A video render finishes (or fails, with a reason). |
voice.render.completed | A long-form speech job finishes. |
agent.run.awaiting_approval | An agent run pauses on a gated action. |
agent.run.completed | An agent run completes with its verified result. |
agent.run.failed | An agent run fails or exhausts max_steps. |
billing.credits.low | Remaining monthly credits drop below your alert threshold. |
key.auto_revoked | A leaked key is detected and revoked by our scanners. |
Wildcards are supported per resource, e.g. agent.run.* subscribes to every agent run transition.
Verify signatures
Every delivery is signed with HMAC-SHA256. Verify the Tapotik-Signature header before trusting a payload — and always compare with a timing-safe function.
| Header | Description |
|---|---|
Tapotik-Signature | Hex HMAC of {timestamp}.{body} using your endpoint secret. |
Tapotik-Timestamp | Unix seconds when the delivery was signed. |
Tapotik-Event-Id | Unique delivery ID — use it to deduplicate retries. |
import { createHmac, timingSafeEqual } from "node:crypto";
export async function POST(req: Request) {
const body = await req.text();
const timestamp = req.headers.get("tapotik-timestamp") ?? "";
const signature = req.headers.get("tapotik-signature") ?? "";
// Reject stale deliveries (> 5 minutes) to prevent replay.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return new Response("stale", { status: 400 });
}
const expected = createHmac("sha256", process.env.TAPOTIK_WEBHOOK_SECRET!)
.update(`${timestamp}.${body}`)
.digest("hex");
const valid =
signature.length === expected.length &&
timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) return new Response("invalid signature", { status: 401 });
const event = JSON.parse(body);
// handle event.type, event.data ...
return new Response("ok", { status: 200 });
}Delivery and retries
- Respond with a
2xxwithin 10 seconds — do heavy work async after acknowledging. - Failed deliveries retry with exponential backoff: 1m, 5m, 30m, 2h, 12h, then the endpoint is paused and admins are emailed.
- Deliveries can arrive out of order and, rarely, more than once — deduplicate on
Tapotik-Event-Id. - Replay any event from the last 30 days in Dashboard → Webhooks → Deliveries.
Developing locally?
tapotik listen --forward localhost:3000/api/tapotik/webhook tunnels events to your machine with the same signatures as production.