Webhooks (totalum-sdk/webhooks)
import … from 'totalum-sdk/webhooks' — or the same namespace on totalum from 'totalum-sdk'. Install, configuration and errors: Totalum SDK.
Receive Totalum's signed events (payments, e-mail delivery, browser programs, connected-account events, credits
exhausted) and check that they really come from Totalum. verifyWebhook() checks the Totalum-Signature header
with TOTALUM_WEBHOOK_SECRET, which Totalum sets for you; totalum.webhooks reads and changes the optional extra URL
events are also sent to, and rotates the secret. Free.
import { verifyWebhook } from 'totalum-sdk/webhooks';
const event = await verifyWebhook(await request.text(), request.headers);| Method or type | What it does |
|---|---|
verifyWebhook | Checks that a webhook delivery really comes from Totalum and returns its event. |
totalumWebhooks | Creates the webhooks namespace (the same object as totalum.webhooks; a key with scope *): where the project receives its signed events (payments, e-mail, browser programs, project.credits.exhausted) and the secret they are signed with. |
webhooks.verify | The same function as verifyWebhook: checks Totalum-Signature and returns the parsed event. |
webhooks.get | The project's extra webhook URL and the first characters of its signing secret (the secret itself is only shown by rotateSecret). |
webhooks.setUrl | Sets the extra https:// URL the project's events are also delivered to (for example a separate backend or an automation tool). |
webhooks.rotateSecret | Replaces the signing secret, for example after it leaked. |
TotalumWebhookError | What verifyWebhook (and totalum.webhooks.verify, connections.verifyEvent) throws when a delivery cannot be trusted: answer it with 401 and do nothing else. |
WebhookConfig | The project's extra webhook address, from webhooks.get() and webhooks.setUrl(). |
WebhookEvent | A signed event Totalum delivers to the app, returned by verifyWebhook(). |
WebhookSecretOutput | The answer of webhooks.rotateSecret(). |
#verifyWebhook
verifyWebhook(rawBody: string, headers: Headers | Record<string, string>, secret?: string, now?: number): Promise<WebhookEvent>Checks that a webhook delivery really comes from Totalum and returns its event. It verifies the
Totalum-Signature: t=…,v1=… header — HMAC-SHA256 keyed with the secret over ${t}.${rawBody} — and refuses a
timestamp more than 300 s away. Pass the body exactly as received (await request.text(), never re-serialised
JSON). secret defaults to TOTALUM_WEBHOOK_SECRET, which Totalum sets on the preview, the published site and the
sandbox (a rotation reaches them at once): never ask the owner to set it. Totalum retries a delivery that does not
get a 2xx, so handle each event id once.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
rawBody | string | yes | The request body as text, untouched. |
headers | Headers | Record<string, string> | yes | The request's headers (a Headers or a plain object). |
secret | string | no | The signing secret. Default: TOTALUM_WEBHOOK_SECRET. |
now | number | no | The current time in milliseconds, for tests. Default: Date.now(). |
Returns Promise<WebhookEvent> — The event: its type, id and data.
| Field | Type | Always present | Description |
|---|---|---|---|
id | string | yes | The event id; the same event can be delivered more than once (retries), so handle each id once. |
type | string | yes | What happened: payments.<Stripe event type> (payments.checkout.session.completed, payments.invoice.paid, …), email.delivered, email.bounced, email.complained, email.failed, email.delivery_delayed, browser.program.completed, connections.event, project.credits.exhausted, project.payments.disconnected. |
createdAt | string | yes | When it happened. |
projectId | string | yes | Your project id. |
env | 'dev' | 'live' | yes | dev for the preview (Stripe test mode), live for the published site. |
data | Record<string, unknown> | yes | The event's content: for a payment event { object } (the Stripe object), for an e-mail event { emailId, to, providerId, at }. |
livemode | boolean | no | For payment events: whether it is a real-money (live) Stripe event. |
Errors — besides the errors every call can throw:
| Code | HTTP | When |
|---|---|---|
WEBHOOK_SECRET_MISSING | — | No secret: TOTALUM_WEBHOOK_SECRET is unset and none was passed. |
WEBHOOK_SIGNATURE_INVALID | 401 | The header is missing or no signature matches: not from Totalum, or the body was changed. |
WEBHOOK_TIMESTAMP_EXPIRED | 401 | The signature is older (or newer) than 300 s: a replayed delivery. |
Example
// src/app/api/webhooks/stripe/route.ts
import { isTotalumError } from 'totalum-sdk';
import { verifyWebhook } from 'totalum-sdk/webhooks';
export async function POST(request: Request) {
let event;
try {
event = await verifyWebhook(await request.text(), request.headers);
} catch (e) {
if (isTotalumError(e)) return new Response('invalid signature', { status: 401 });
throw e;
}
if (event.type === 'payments.checkout.session.completed') {
const session = event.data.object as { metadata: { orderId: string } };
await markOrderPaid(session.metadata.orderId);
}
return new Response('ok');
}Result:
{
"id": "evt_1QZ7cDPQm4n8Xa2LoP5qR6sT",
"type": "payments.checkout.session.completed",
"createdAt": "2026-10-11T18:01:12.000Z",
"projectId": "acme-crm",
"env": "live",
"livemode": true,
"data": { "object": { "id": "cs_live_a1B2c3D4e5F6g7H8", "payment_status": "paid", "metadata": { "orderId": "ord_42" } } }
}#totalumWebhooks
totalumWebhooks(options?: TotalumClientOptions): TotalumWebhooksCreates the webhooks namespace (the same object as totalum.webhooks; a key with scope *): where the project
receives its signed events (payments, e-mail, browser programs, project.credits.exhausted) and the secret they are
signed with. Payment events also reach the app's own /api/webhooks/stripe by themselves; this URL is an optional
extra address.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
options | TotalumClientOptions | no | The project key, API URL and environment; each falls back to its environment variable. |
options.key | string | no | Project key tlm_pk_…; default TOTALUM_PROJECT_KEY. Server-side only. |
options.apiUrl | string | no | The Totalum API's URL: default TOTALUM_SDK_API_URL, else https://sdk-api.totalum.app. |
options.env | 'dev' | 'live' | no | Sent as x-totalum-env on every call (the database it targets, and dev vs live usage); default TOTALUM_ENV, else live. |
Returns TotalumWebhooks — The webhooks namespace: the methods below.
Example
import { totalumWebhooks } from 'totalum-sdk/webhooks';
const webhooks = totalumWebhooks();
const config = await webhooks.get();The namespace's type is exported as TotalumWebhooks (import type { TotalumWebhooks } from 'totalum-sdk/webhooks').
#webhooks.verify
webhooks.verify(rawBody: string, headers: Headers | Record<string, string>, secret?: string, now?: number): Promise<WebhookEvent>The same function as verifyWebhook: checks Totalum-Signature and returns the parsed event.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
rawBody | string | yes | The request body as text, untouched. |
headers | Headers | Record<string, string> | yes | The request's headers. |
secret | string | no | The signing secret. Default: TOTALUM_WEBHOOK_SECRET. |
now | number | no | The current time in milliseconds, for tests. Default: Date.now(). |
Returns Promise<WebhookEvent> — The verified event.
| Field | Type | Always present | Description |
|---|---|---|---|
id | string | yes | The event id; the same event can be delivered more than once (retries), so handle each id once. |
type | string | yes | What happened: payments.<Stripe event type> (payments.checkout.session.completed, payments.invoice.paid, …), email.delivered, email.bounced, email.complained, email.failed, email.delivery_delayed, browser.program.completed, connections.event, project.credits.exhausted, project.payments.disconnected. |
createdAt | string | yes | When it happened. |
projectId | string | yes | Your project id. |
env | 'dev' | 'live' | yes | dev for the preview (Stripe test mode), live for the published site. |
data | Record<string, unknown> | yes | The event's content: for a payment event { object } (the Stripe object), for an e-mail event { emailId, to, providerId, at }. |
livemode | boolean | no | For payment events: whether it is a real-money (live) Stripe event. |
Errors — besides the errors every call can throw:
| Code | HTTP | When |
|---|---|---|
WEBHOOK_SECRET_MISSING | — | No secret is set or passed. |
WEBHOOK_SIGNATURE_INVALID | 401 | No signature matches. |
WEBHOOK_TIMESTAMP_EXPIRED | 401 | The signature is more than 300 s old. |
Example
const event = await totalum.webhooks.verify(await request.text(), request.headers);Result:
{
"id": "msg_2nQx8kZf0aYc3Vb1",
"type": "email.delivered",
"createdAt": "2026-10-11T18:01:12.000Z",
"projectId": "acme-crm",
"env": "live",
"data": { "emailId": "em_01M4P0S77M6XCM69SYZWP1XME9", "to": ["ana@example.com"], "providerId": "01a12c0c-9e5f-74ea-a128-a72d3fdd47ae", "at": "2026-10-11T18:01:12.000Z" }
}#webhooks.get
webhooks.get(): Promise<WebhookConfig>The project's extra webhook URL and the first characters of its signing secret (the secret itself is only shown
by rotateSecret). Free.
Returns Promise<WebhookConfig> — The configured URL (or null), whether delivery is on, and the secret's prefix.
| Field | Type | Always present | Description |
|---|---|---|---|
url | string | null | yes | The https:// URL events are also sent to, or null. |
enabled | boolean | yes | Whether events are being delivered there (a URL and a secret are both set). |
secretPrefix | string | null | yes | The first 10 characters of the signing secret (whsec_…), to tell secrets apart; null before the first one. |
Errors — besides the errors every call can throw:
| Code | HTTP | When |
|---|---|---|
FORBIDDEN_SCOPE | 403 | The key does not have scope *. |
Example
const config = await totalum.webhooks.get();Result:
{ "url": "https://hooks.acme.com/totalum", "enabled": true, "secretPrefix": "whsec_4f9a" }#webhooks.setUrl
webhooks.setUrl(url: string): Promise<WebhookConfig>Sets the extra https:// URL the project's events are also delivered to (for example a separate backend or an
automation tool). Free.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
url | string | yes | The https:// URL that receives the events. |
Returns Promise<WebhookConfig> — The new configuration.
| Field | Type | Always present | Description |
|---|---|---|---|
url | string | null | yes | The https:// URL events are also sent to, or null. |
enabled | boolean | yes | Whether events are being delivered there (a URL and a secret are both set). |
secretPrefix | string | null | yes | The first 10 characters of the signing secret (whsec_…), to tell secrets apart; null before the first one. |
Errors — besides the errors every call can throw:
| Code | HTTP | When |
|---|---|---|
VALIDATION_ERROR | 400 | Not an https:// URL. |
FORBIDDEN_SCOPE | 403 | The key does not have scope *. |
Example
const config = await totalum.webhooks.setUrl('https://hooks.acme.com/totalum');Result:
{ "url": "https://hooks.acme.com/totalum", "enabled": true, "secretPrefix": "whsec_4f9a" }#webhooks.rotateSecret
webhooks.rotateSecret(): Promise<WebhookSecretOutput>Replaces the signing secret, for example after it leaked. The new secret is shown once; for 24 hours every delivery
is signed with both, so receivers keep working while they update. Totalum sets the new one as
TOTALUM_WEBHOOK_SECRET on the preview, the published site and the sandbox by itself.
Returns Promise<WebhookSecretOutput> — The new secret (store it only if another system verifies the events).
| Field | Type | Always present | Description |
|---|---|---|---|
secret | string | yes | The new signing secret (whsec_…), shown only this once. |
Errors — besides the errors every call can throw:
| Code | HTTP | When |
|---|---|---|
FORBIDDEN_SCOPE | 403 | The key does not have scope *. |
Example
const { secret } = await totalum.webhooks.rotateSecret();Result:
{ "secret": "whsec_7b1e0c9f…" }#TotalumWebhookError
What verifyWebhook (and totalum.webhooks.verify, connections.verifyEvent) throws when a delivery cannot be
trusted: answer it with 401 and do nothing else. errorCode says why.
| Member | Type | Description |
|---|---|---|
name | string | Always 'TotalumWebhookError'. |
errorCode | 'WEBHOOK_SECRET_MISSING' | 'WEBHOOK_SIGNATURE_INVALID' | 'WEBHOOK_TIMESTAMP_EXPIRED' | Why the delivery was refused: no secret, a signature that does not match, or a timestamp outside ±300 s. |
#WebhookConfig
The project's extra webhook address, from webhooks.get() and webhooks.setUrl().
| Field | Type | Always present | Description |
|---|---|---|---|
url | string | null | yes | The https:// URL events are also sent to, or null. |
enabled | boolean | yes | Whether events are being delivered there (a URL and a secret are both set). |
secretPrefix | string | null | yes | The first 10 characters of the signing secret (whsec_…), to tell secrets apart; null before the first one. |
#WebhookEvent
A signed event Totalum delivers to the app, returned by verifyWebhook().
| Field | Type | Always present | Description |
|---|---|---|---|
id | string | yes | The event id; the same event can be delivered more than once (retries), so handle each id once. |
type | string | yes | What happened: payments.<Stripe event type> (payments.checkout.session.completed, payments.invoice.paid, …), email.delivered, email.bounced, email.complained, email.failed, email.delivery_delayed, browser.program.completed, connections.event, project.credits.exhausted, project.payments.disconnected. |
createdAt | string | yes | When it happened. |
projectId | string | yes | Your project id. |
env | 'dev' | 'live' | yes | dev for the preview (Stripe test mode), live for the published site. |
data | Record<string, unknown> | yes | The event's content: for a payment event { object } (the Stripe object), for an e-mail event { emailId, to, providerId, at }. |
livemode | boolean | no | For payment events: whether it is a real-money (live) Stripe event. |
#WebhookSecretOutput
The answer of webhooks.rotateSecret().
| Field | Type | Always present | Description |
|---|---|---|---|
secret | string | yes | The new signing secret (whsec_…), shown only this once. |
