Webhooks
Signed event deliveries: registration, the event catalog, signature verification, and retries.
Registering
Register HTTPS endpoints in the Developers hub or via the API (up to 10 per account). The signing secret is returned once, on creation; store it immediately.
/webhooksList webhooks.POST/webhooksRegister a webhook (secret returned once).PATCH/webhooks/{id}Update url, events, or isActive.DELETE/webhooks/{id}Delete a webhook.curl https://www.opencharts.com/api/v1/webhooks \
-H "Authorization: Bearer $OPENCHARTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Production events",
"url": "https://example.com/hooks/opencharts",
"events": ["video.completed", "video.failed", "song.completed", "research.completed"]
}'The event catalog
The catalog below is generated from the same registry the API validates subscriptions against, so these names can never drift. Two naming notes: the code job type (job ids code_…) emits code_build.completed / code_build.failed, and Theo Agent runs emit evi.run_completed / evi.run_failed. Subscribing to an unknown or upcoming (not-yet-emitted) event returns a 400 naming it.
project.createdprojectsproject.updatedprojectsproject.sharedprojectsproject.deletedprojectscomment.createdcollaboration (upcoming)team.member_addedteamsteam.member_removedteamsexport.completedexportstemplate.publishedtemplates (upcoming)meeting.completedmeetingscode_canvas.iteration_completecode_canvascode_canvas.iteration_failedcode_canvascode_canvas.publishedcode_canvaspodcast.completedpodcastspodcast.failedpodcastsnotes_movie.completedprojectsnotes_movie.failedprojectsskill.installedskillsevi.run_completedflowsevi.run_failedflowsvideo.completedapi_jobsvideo.failedapi_jobssong.completedapi_jobssong.failedapi_jobscode_build.completedapi_jobscode_build.failedapi_jobsresearch.completedapi_jobsresearch.failedapi_jobsThe delivery payload
{
"eventId": "0d5b2c9e-...",
"eventVersion": "2026-02-24",
"event": "video.completed",
"occurredAt": "2026-07-22T14:03:11.000Z",
"timestamp": "2026-07-22T14:03:11.000Z",
"data": {
"jobId": "vid_6870f3...",
"status": "complete",
"pollUrl": "https://www.opencharts.com/api/v1/jobs/vid_6870f3..."
}
}For job events, data always carries jobId, status, and pollUrl; pipelines append extras such as an asset URL, a project id, or an error message on failures.
X-OpenCharts-EventstringX-OpenCharts-Event-IduuidX-OpenCharts-Event-Versiondate stringX-OpenCharts-Signaturesha256=<hex>Verifying signatures
Always verify before trusting
Compute the HMAC over the raw request body (before any JSON parsing) and compare with a constant-time check.
import crypto from "node:crypto";
export function verifyOpenChartsSignature(rawBody, signatureHeader, secret) {
// Header format: "sha256=<hex>"
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
return (
signatureHeader?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected))
);
}
// Express example: use the RAW body, not the parsed JSON.
app.post("/hooks/opencharts", express.raw({ type: "*/*" }), (req, res) => {
const ok = verifyOpenChartsSignature(
req.body.toString("utf8"),
req.header("X-OpenCharts-Signature"),
process.env.OPENCHARTS_WEBHOOK_SECRET,
);
if (!ok) return res.status(401).end();
const payload = JSON.parse(req.body.toString("utf8"));
// handle payload.event / payload.data ...
res.status(200).end();
});Retries & health
Each delivery attempt times out after 5 seconds and retries twice (after 1s, then 3s) on failure. Any 2xx from your endpoint counts as delivered; respond fast and do heavy work asynchronously. After 5 consecutive failed deliveries a webhook is automatically disabled (re-enable it with PATCH /webhooks/{id} once your endpoint recovers).
Inspecting deliveries
Every attempt is logged with status, duration, and response info: