Skip to content

Command Palette

Search for a command to run...

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/webhooks

Register an HTTPS URL and the events you care about. Each endpoint gets its own signing secret, shown once at creation.

terminalbash
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

EventFires when
image.batch.completedAn async image batch finishes; payload includes signed URLs.
video.render.completedA video render finishes (or fails, with a reason).
voice.render.completedA long-form speech job finishes.
agent.run.awaiting_approvalAn agent run pauses on a gated action.
agent.run.completedAn agent run completes with its verified result.
agent.run.failedAn agent run fails or exhausts max_steps.
billing.credits.lowRemaining monthly credits drop below your alert threshold.
key.auto_revokedA 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.

HeaderDescription
Tapotik-SignatureHex HMAC of {timestamp}.{body} using your endpoint secret.
Tapotik-TimestampUnix seconds when the delivery was signed.
Tapotik-Event-IdUnique delivery ID — use it to deduplicate retries.
app/api/tapotik/webhook/route.tstypescript
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 2xx within 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.