Docs
BlogHomeStart building

Webhooks

Get told when things happen.


Two kinds of webhooks:

  • Account webhooks — Totalum calls your server when something happens to your projects (a prompt finished, a publish ended, a limit was reached…). One subscription per event, optionally filtered to some projects.
  • The project webhook — an optional address where the app's own integration events (for example payments) are also sent, signed with the project's webhook secret, verified with totalum.webhooks.verify (SDK). Payment events need no address: they reach the app's own /api/webhooks/stripe by themselves (the preview for test payments, the published site for real ones).

Events

EventSent whendata
agent.prompt.finishedA prompt finished{ projectId, status, prompt, … }
deployment.finishedA publish ended{ projectId, deploymentId, status: "success" | "error", fixAttempts, creditRefunded?, errorDetails?, at }
project.credit_limit.reachedA project reached a monthly credit limit{ projectId, category, spent, limit }
project.feature_limit.warningA feature's monthly budget reached 80 % (once per feature and month){ projectId, feature, scope, refusing, cap, capKind, used, period, at }
project.feature_limit.reachedA feature's monthly budget is spentsame as above
project.feature.disabled / project.feature.enabledA feature was switched off / on{ projectId, feature, scope, refusing, at }
project.paused / project.resumedThe project was paused / resumed{ projectId, state, pausedAt, resumable, at }
project.lifecycle.state_changedThe project went to sleep after inactivity, or woke up{ projectId, to, at, reason, nextStep }
project.kill_switch.firedThe app's usage spiked far above normal and it was paused to protect your credits{ projectId, … }
project.credits.exhaustedThe account ran out of credits; the app's paid features stop{ projectId, … }
project.mobile_build.finishedA mobile build ended{ projectId, buildId, status, … }
project.preview.url_rotatedThe preview address changed{ projectId, … }
project.payments.disconnectedThe app's Stripe account was disconnected{ projectId, … }
cron.job.pausedA scheduled job failed too many runs in a row and was paused{ projectId, jobId, name, env, failures, lastStatus, at }
cron.job.failingA job set never to pause failed 10 runs in a row (once per streak)same as above

Payload — every delivery is a POST with this JSON body:

json
{ "event": "deployment.finished", "timestamp": "2026-10-08T11:23:10.000Z", "data": { "projectId": "barber-bookings", "deploymentId": "dep_01k6xf3a9b", "status": "success", "fixAttempts": 0, "at": "2026-10-08T11:23:10.000Z" } }

Verify a delivery. Each delivery carries X-Totalum-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" keyed with the subscription's secret (Get Webhook Secret): drop the whsec_ prefix and hex-decode the rest. X-Totalum-Event-Id identifies the event; a retry carries the same id. A delivery that does not answer 2xx within 10 s is retried after 30 s and after 2 minutes, then marked failed (the owner gets at most one e-mail a day about it).

js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyTotalumWebhook(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'hex');
  const expected = createHmac('sha256', key).update(`${t}.${rawBody}`).digest();
  return Math.abs(Date.now() / 1000 - Number(t)) < 300 && timingSafeEqual(expected, Buffer.from(v1, 'hex'));
}

#Register Webhook

PUT/api/v1/vcaas/webhooksapi-keyFree

Subscribe an HTTPS URL to one event. One unfiltered subscription per event; filtered ones (by project or feature) can sit beside it.

Body parameters

FieldTypeRequiredDescription
urlstringYeshttps:// URL that receives the POST
eventstringYesOne event from the table above
headersobjectNoExtra headers sent with every delivery (stored encrypted)
projectIdsstring[]NoOnly these projects (up to 50)
featuresstring[]NoOnly these features (ai, email…) — for the project.feature* events

Response fields

FieldTypeDescription
data.idstringSubscription id
data.urlstringURL
data.headersobject | undefinedExtra headers
data.eventstringEvent
data.createdAtstringISO 8601
data.projectIdsstring[] | undefinedProject filter
data.featuresstring[] | undefinedFeature filter

Example request

curl -X PUT \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://yourserver.com/totalum-webhook","event":"deployment.finished"}' \
  https://builder-api.totalum.app/api/v1/vcaas/webhooks
Success · 201 Created
json
{ "errors": null, "data": { "id": "6701f2a3c4d5e6f7a8b9c0d1", "url": "https://yourserver.com/totalum-webhook", "event": "deployment.finished", "createdAt": "2026-10-08T10:00:00.000Z" } }

Error codes

CodeHTTPMeaning
MISSING_WEBHOOK_FIELDS400url and event are required
INVALID_WEBHOOK_URL400The URL is not HTTPS
INVALID_WEBHOOK_EVENT400Unknown event; the message lists every event you can register
VALIDATION_ERROR400projectIds/features is not a list of strings, or more than 50 projects
WEBHOOK_FILTER_NOT_APPLICABLE400features on an event that is not a feature event
WEBHOOK_PROJECT_NOT_OWNED403A projectIds entry is not your project
WEBHOOK_EVENT_ALREADY_EXISTS409A subscription for this event (and filter) already exists — delete it first
End of Register WebhookNext endpointGETList Webhooks

#List Webhooks

GET/api/v1/vcaas/webhooksapi-keyFree

Your subscriptions, newest first.

Response fields

FieldTypeDescription
data.webhooks[].idstringSubscription id
data.webhooks[].urlstringURL
data.webhooks[].headersobject | undefinedExtra headers
data.webhooks[].eventstringEvent
data.webhooks[].createdAtstringISO 8601
data.webhooks[].updatedAtstringISO 8601
data.webhooks[].projectIdsstring[] | undefinedProject filter
data.webhooks[].featuresstring[] | undefinedFeature filter

Example request

curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/webhooks
Success · 200 OK
json
{ "errors": null, "data": { "webhooks": [{ "id": "6701f2a3c4d5e6f7a8b9c0d1", "url": "https://yourserver.com/totalum-webhook", "event": "deployment.finished", "createdAt": "2026-10-08T10:00:00.000Z", "updatedAt": "2026-10-08T10:00:00.000Z" }] } }

#Delete Webhook

DELETE/api/v1/vcaas/webhooks/:webhookIdapi-keyFree

Remove a subscription.

Path parameters

ParameterTypeDescription
webhookIdstringSubscription id

Response fields

FieldTypeDescription
data.successbooleantrue

Example request

curl -X DELETE -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/webhooks/6701f2a3c4d5e6f7a8b9c0d1
Success · 200 OK
json
{ "errors": null, "data": { "success": true } }

Error codes

CodeHTTPMeaning
WEBHOOK_NOT_FOUND404No such subscription

#Get Webhook Secret

GET/api/v1/vcaas/webhooks/:webhookId/secretapi-keyFree

The subscription's signing secret, to verify X-Totalum-Signature.

Path parameters

ParameterTypeDescription
webhookIdstringSubscription id

Response fields

FieldTypeDescription
data.webhookIdstringSubscription id
data.secretstringwhsec_…

Example request

curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/webhooks/6701f2a3c4d5e6f7a8b9c0d1/secret
Success · 200 OK
json
{ "errors": null, "data": { "webhookId": "6701f2a3c4d5e6f7a8b9c0d1", "secret": "whsec_3f9a…" } }

Error codes

CodeHTTPMeaning
WEBHOOK_NOT_FOUND404No such subscription

#List Webhook Deliveries

GET/api/v1/vcaas/webhooks/:webhookId/deliveriesapi-keyFree

The subscription's deliveries of the last 30 days, newest first.

Path parameters

ParameterTypeDescription
webhookIdstringSubscription id

Query parameters

FieldTypeRequiredDescription
limitnumberNo1–200 (default 50)
cursornumberNonextCursor of the previous page

Response fields

FieldTypeDescription
data.deliveries[].idnumberDelivery id
data.deliveries[].webhookIdstringSubscription
data.deliveries[].eventstringEvent
data.deliveries[].eventIdstringThe event's id (X-Totalum-Event-Id)
data.deliveries[].projectIdstring | nullProject
data.deliveries[].featurestring | nullFeature, for feature events
data.deliveries[].statusstringpending, delivered or failed
data.deliveries[].attemptsnumberAttempts made
data.deliveries[].httpStatusnumber | nullYour server's last answer
data.deliveries[].durationMsnumber | nullIts duration
data.deliveries[].errorstring | nullWhy it failed
data.deliveries[].atstringWhen the event happened
data.deliveries[].updatedAtstringLast attempt
data.nextCursornumber | nullNext page

Example request

curl -H "api-key: tlm_sk_your_key" \
  "https://builder-api.totalum.app/api/v1/vcaas/webhooks/6701f2a3c4d5e6f7a8b9c0d1/deliveries?limit=20"
Success · 200 OK
json
{ "errors": null, "data": { "deliveries": [{ "id": 1042, "webhookId": "6701f2a3c4d5e6f7a8b9c0d1", "event": "deployment.finished", "eventId": "deploy:dep_01k6xf3a9b", "projectId": "barber-bookings", "feature": null, "status": "delivered", "attempts": 1, "httpStatus": 200, "durationMs": 182, "error": null, "at": "2026-10-08T11:23:10.000Z", "updatedAt": "2026-10-08T11:23:10.000Z" }], "nextCursor": null } }
End of List Webhook DeliveriesNext endpointGETGet Project Webhook

#Get Project Webhook

GET/api/v1/vcaas/projects/:projectId/webhookapi-keyFree

The project's optional extra webhook address (for the app's integration events; payment events reach the app's /api/webhooks/stripe without it).

Path parameters

ParameterTypeDescription
projectIdstringThe project id

Response fields

FieldTypeDescription
data.urlstring | nullThe address
data.enabledbooleanA URL and a secret are set
data.secretPrefixstring | nullThe start of the current secret

Example request

curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/webhook
Success · 200 OK
json
{ "errors": null, "data": { "url": "https://hooks.zapier.com/hooks/catch/123456/abcdef/", "enabled": true, "secretPrefix": "whsec_3f9a" } }
End of Get Project WebhookNext endpointPUTSet Project Webhook

#Set Project Webhook

PUT/api/v1/vcaas/projects/:projectId/webhookapi-keyFree

Set the project's optional extra webhook address (your own server, Zapier…). Account owner only.

Path parameters

ParameterTypeDescription
projectIdstringThe project id

Body parameters

FieldTypeRequiredDescription
urlstringYeshttps:// address

Response fields

FieldTypeDescription
data.urlstringThe address
data.enabledbooleanA URL and a secret are set
data.secretPrefixstring | nullThe start of the secret

Example request

curl -X PUT \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.zapier.com/hooks/catch/123456/abcdef/"}' \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/webhook
Success · 200 OK
json
{ "errors": null, "data": { "url": "https://hooks.zapier.com/hooks/catch/123456/abcdef/", "enabled": true, "secretPrefix": "whsec_3f9a" } }

Error codes

CodeHTTPMeaning
VALIDATION_ERROR400Not an https:// URL
OWNER_ONLY403Account owner only

#Rotate Project Webhook Secret

POST/api/v1/vcaas/projects/:projectId/webhook/rotate-secretapi-keyFree

Create a new signing secret (shown once). The app receives it at once; the old secret keeps signing for 24 hours. Account owner only.

Path parameters

ParameterTypeDescription
projectIdstringThe project id

Response fields

FieldTypeDescription
data.secretstringThe new whsec_… secret

Example request

curl -X POST -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/webhook/rotate-secret
Success · 200 OK
json
{ "errors": null, "data": { "secret": "whsec_0b1c…" } }

Error codes

CodeHTTPMeaning
OWNER_ONLY403Account owner only
End of Rotate Project Webhook Secret Back to top