Webhooks push events to your server the moment they happen — no polling. They're the recommended way to track generations and deployments in production integrations.
Looking to connect your app to ClickFunnels? You don't need a webhook for that — the built-in ClickFunnels integration sends opt-ins, leads, and tags directly, with no setup in your app's code.
Registering a webhook
POST /api/v1/webhooks
X-API-Key: os_…
Content-Type: application/json
{
"url": "https://yourapp.example.com/overskill",
"name": "Production listener",
"events": ["app.generation.completed", "app.generation.failed", "app.generation.blocked"],
"secret": "a-strong-shared-secret-you-generate"
}
Always set a secret — it's what makes signature verification possible. Manage endpoints with GET /api/v1/webhooks, PATCH /api/v1/webhooks/{id}, and DELETE /api/v1/webhooks/{id}.
The event catalog
Generation (the ones most integrations need):
app.generation.started/app.generation.progress/app.generation.completed/app.generation.failedapp.generation.blocked— generation halted by a business rule (for example, the team is out of credits). The payload includesblock_type,balance, andrequired_credits. Don't auto-retry a blocked generation — surface the block to your user (e.g. a top-up prompt) instead.
App lifecycle: app.created, app.updated, app.deleted
Deployments: app.deployment.started, app.deployment.completed, app.deployment.failed, app.rollback
App users: app.user.created, app.user.access_granted, app.user.access_revoked, app.user.access_restored, app.user.tier_upgraded, app.user.tier_downgraded
Team/billing: team.subscription.created, team.subscription.updated, team.subscription.cancelled
Knowledge base: rag.document.created, rag.document.indexed, rag.enabled
Domains: domain.verified
Testing: webhook.test
Verifying signatures
Every delivery carries an X-Overskill-Signature header:
X-Overskill-Signature: t=1753031000,v1=5f8a2c…
tis a Unix timestampv1isHMAC-SHA256(secret, "{t}.{raw_request_body}")in hex
To verify: recompute the HMAC over the timestamp + a dot + the raw request body (before any JSON parsing), compare with a constant-time comparison, and reject deliveries whose timestamp is older than ~5 minutes (replay protection).
Node example:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(header, rawBody, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected); const b = Buffer.from(v1);
return a.length === b.length && timingSafeEqual(a, b);
}
A complete runnable webhook server (Node, zero dependencies) ships in the Overskill repo at docs/api-examples/node/webhook-server.mjs.
Testing and monitoring deliveries
POST /api/v1/webhooks/{id}/test— sends awebhook.testevent to your endpoint right nowGET /api/v1/webhooks/{id}/deliveries— recent delivery attempts with status codes
Delivery behavior
- Respond with a 2xx quickly (do your processing async). Non-2xx responses and timeouts are retried; 5xx responses trigger retry with backoff.
- Deliveries can occasionally arrive more than once — make your handlers idempotent.
Prefer per-job callbacks?
If you only care about one generation at a time, you can skip endpoint registration entirely: pass callback_url (plus optional metadata for correlation) when you POST /api/v1/generation_queue, and status updates for that job POST straight to your URL.