Guide to Tensorlake Webhooks: Features and Best Practices
Tensorlake runs document ingestion and serverless workloads: sandboxes that start, suspend and resume, and applications that serve requests. Those resources change state on their own schedule. A sandbox runs out of memory, a deployment lands, a long application request finishes. Tensorlake's webhooks report each of those transitions so your code doesn't have to poll for them.
| Aspect | Value |
|---|---|
| Method | POST, JSON body |
| Signature headers | svix-id, svix-timestamp, svix-signature (Standard Webhooks) |
| Success | Any 2xx acknowledges the attempt; Svix retries failed attempts |
| Delivery | At least once and unordered; event_id is stable across retries |
| Test deliveries | Test webhook on the destination's page sends a signed synthetic event through the normal path |
What are Tensorlake webhooks?
Tensorlake webhooks are destinations that belong to a project. Each has a name, an HTTPS URL and a set of selected event types, and it only receives those types from its own project. Svix signs and delivers every message, so requests carry svix-id, svix-timestamp and svix-signature headers in the Standard Webhooks layout. Tensorlake's webhook documentation covers configuration, events and verification.
Tensorlake webhook features
| Feature | Details |
|---|---|
| Signature | Standard Webhooks HMAC-SHA256 |
| Signature header | svix-signature |
| Body | JSON |
| Event routing | Body field event_type |
Every one of the twelve event types has a handler in the skill's reference code.
Events and payloads
Every delivery is a versioned envelope:
| Field | Description |
|---|---|
event_id | Stable identifier beginning with evt_; the deduplication key |
event_type | One of the event names below |
event_version | Payload schema version (currently 1) |
occurred_at | RFC 3339 UTC time the transition was recorded |
project_id | Public ID of the producing project |
source_id | Opaque identifier of the producer's ordering domain |
source_revision | Unsigned decimal revision, kept as a string to avoid precision loss |
source_ordinal | Zero-based order when one source revision produces several events |
data | Event-specific sandbox, application, or request data |
Tensorlake documents twelve lifecycle events in three groups:
| Event | Emitted When | data fields |
|---|---|---|
tensorlake.sandbox.created | A sandbox is persisted for the first time | previous_status, sandbox |
tensorlake.sandbox.running | A sandbox enters the running state | same |
tensorlake.sandbox.suspended | A sandbox enters the suspended state | same |
tensorlake.sandbox.resumed | A suspended sandbox begins a new generation (public status starts as pending) | same |
tensorlake.sandbox.terminated | A sandbox terminates without a failure outcome | same |
tensorlake.sandbox.failed | A sandbox terminates with a failure outcome | same |
tensorlake.application.created | An application is persisted for the first time | previous_version and previous_state (both null), application |
tensorlake.application.updated | A deployment, metadata, capability, or state change is persisted | same |
tensorlake.application.deleted | An existing application is deleted | same |
tensorlake.application.request.created | An application request is created | application, request |
tensorlake.application.request.completed | An application request completes | same |
tensorlake.application.request.failed | An application request fails | same, with request.failure_reason |
The resources inside data have these fields:
- Sandbox:
id,name,type(namedorephemeral),image,generation_id,status(pending,running,suspended,terminated),outcome(success,failureornull),outcome_reason,snapshot_id,created_atandresources. - Application:
nameandversion. - Request:
id,status,failure_reason,created_atandfinished_at.
A failed sandbox's outcome_reason can be out_of_memory, timeout, internal_error, bad_image, image_not_found, container_startup_failed or process_crash. A failed request's failure_reason is one of unknown, internal_error, function_error, function_timeout, request_error, constraint_unsatisfiable, cancelled or out_of_memory.
This is the tensorlake.application.request.completed sample from the webhook samples library. Note that source_revision is a string:
{
"event_id": "evt_019f9604-13d0-7000-8000-00000000000b",
"event_type": "tensorlake.application.request.completed",
"event_version": 1,
"occurred_at": "2026-07-24T21:24:37.456Z",
"project_id": "project_123",
"source_id": "src_01k0example",
"source_revision": "81252",
"source_ordinal": 0,
"data": {
"application": {
"name": "invoice-parser",
"version": "2026-07-24"
},
"request": {
"id": "req_01k0example",
"status": "completed",
"failure_reason": null,
"created_at": "2026-07-24T21:24:35.101Z",
"finished_at": "2026-07-24T21:24:37.456Z"
}
}
}
Setting up Tensorlake webhooks
You need organisation administrator or project administrator access; project members can view destinations but not change them.
- In Tensorlake Cloud, select the organisation and project, then Webhooks in the project navigation.
- Select Create webhook and enter a Webhook name and an absolute HTTPS Endpoint URL without embedded credentials, such as
https://example.com/webhooks/tensorlake. - Select at least one event type.
tensorlake.sandbox.failed,tensorlake.sandbox.runningandtensorlake.application.request.completedmake a useful first set. - Create the webhook, open the new destination, and copy the value under Signing secret. It begins with
whsec_and belongs to this destination only:
WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
Edit webhook changes the name, URL, events or delivery status. Turning off Delivery enabled pauses attempts while keeping the configuration; deleting the destination removes it.
To receive Tensorlake through Hookdeck, create a source with the Tensorlake type, paste the destination's secret, and use the source URL as the endpoint. Then run the Hookdeck CLI to receive it locally:
npx hookdeck-cli listen 3000 tensorlake --path /webhooks/tensorlake
Then press Test webhook on the destination. It sends a signed synthetic event through the normal delivery path, which is the quickest way to see a payload for one of the event types the docs don't show.
Securing Tensorlake webhooks
Tensorlake's signature verification page defers to Svix: the signed message is svix-id, a dot, svix-timestamp, another dot and the raw body, and the key is the base64-decoded secret after whsec_. svix-signature can hold several space-separated v1, entries, and any match is enough. The function below also reads webhook-* headers, as the Hookdeck source does:
const crypto = require('node:crypto');
function decodeBase64(value) {
if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
const bytes = Buffer.from(value, 'base64');
return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
? bytes : null;
}
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const prefix = headers['svix-id'] ? 'svix' : 'webhook';
const id = headers[`${prefix}-id`];
const timestamp = headers[`${prefix}-timestamp`];
const signature = headers[`${prefix}-signature`];
if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
!/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
const key = decodeBase64(secret.replace(/^whsec_/, ''));
if (!key) return false;
const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
const received = decodeBase64(candidate);
if (!received) return false;
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
Tensorlake sets no timestamp tolerance, while the Svix and Standard Webhooks libraries reject anything older than five minutes. Hookdeck has no tolerance and keeps the original svix-timestamp on retries and replays, so those libraries would reject authentic deliveries behind it. The function above checks the signature alone.
Because delivery is unordered, the receiver stores each event under event_id along with its ordering keys:
const express = require('express');
// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
try {
const value = JSON.parse(rawBody.toString('utf8'));
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
} catch {
return null;
}
}
// lifecycleLog.append(event, order) stores the event once per event_id and resolves when durable.
function createTensorlakeReceiver(lifecycleLog) {
const app = express();
app.post('/webhooks/tensorlake', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.TENSORLAKE_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500);
if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
const event = parseJsonObject(req.body);
if (!event) return res.sendStatus(400); // signed, but not a JSON object
if (typeof event.source_revision !== 'string' || !/^[0-9]+$/.test(event.source_revision)) {
return res.sendStatus(400);
}
const order = {
sourceId: event.source_id,
revision: BigInt(event.source_revision), // a decimal string, too large for Number in general
ordinal: event.source_ordinal,
};
try {
await lifecycleLog.append(event, order);
} catch {
return res.sendStatus(503);
}
return res.sendStatus(200);
});
return app;
}
Requests to a Hookdeck Tensorlake source always get 200. If the signature doesn't match, Hookdeck logs the request as failed verification and doesn't pass it on.
Make Tensorlake webhooks production-ready. Hookdeck Event Gateway verifies every Tensorlake event, keeps a searchable history of sandbox and request transitions, and replays any of them on demand.
Tensorlake webhook limitations and pain points
Events arrive out of order
The Problem: A sandbox shows as running after it has already terminated.
Why It Happens: Tensorlake delivers at least once with no ordering guarantee, so a later transition can arrive first.
Workarounds:
- For events with the same
source_id, comparesource_revisionas an integer, thensource_ordinal, and ignore anything older than the state you hold. - When the exact current state matters, check the resource in Tensorlake rather than trusting the last event received.
How Hookdeck Can Help: Hookdeck keeps every transition with its arrival time, so you can see the true sequence when debugging and replay events after fixing a handler.
Retry schedule isn't documented
The Problem: You can't tell how long a failed sandbox event will keep being retried.
Why It Happens: Tensorlake only says that Svix retries failed endpoint attempts. It gives no schedule or response timeout.
Workarounds:
- Acknowledge with
2xxas soon as the event is stored. - After an outage, check the state of affected sandboxes and requests in Tensorlake.
How Hookdeck Can Help: Hookdeck takes each event as soon as Svix sends it and retries your service on a schedule you set.
Nine of twelve events have no example
The Problem: You're writing a handler for tensorlake.sandbox.suspended or tensorlake.application.deleted from field tables alone.
Why It Happens: Tensorlake publishes one example per group: sandbox.failed, application.updated and application.request.completed.
Workarounds:
- Subscribe to the type and press Test webhook to receive a signed sample.
How Hookdeck Can Help: Hookdeck stores the full body and headers of each test and real delivery, so you can inspect them before writing code.
Destinations are per project and admin-only
The Problem: A team with several projects has to repeat the setup, and engineers without admin rights can't add an endpoint.
Why It Happens: Each destination receives events from one project, and only organisation or project administrators can create or edit it.
Workarounds:
- Point every project's destination at the same receiver, and route on
project_id.
How Hookdeck Can Help: Send each project to its own Hookdeck source and use filters on project_id or event_type to reach the right service.
Best practices
Keep source_revision as a string or big integer
It's a decimal string to avoid precision loss. Parse it with BigInt in JavaScript or int in Python, never a float.
Alert on failure reasons, not only failures
outcome_reason and failure_reason tell you whether to fix an image, raise a memory limit or look at your own function. Route bad_image and function_error to the team that owns them.
Treat tensorlake.sandbox.resumed carefully
A resumed sandbox begins a new generation with a status of pending. Wait for tensorlake.sandbox.running before you send it work.
Deduplicate on event_id
event_id survives retries. Store it with the change it caused; Hookdeck's idempotency guide has worked patterns.
Conclusion
Tensorlake's webhooks turn sandbox and application lifecycles into a signed stream with an envelope built for deduplication and reordering. Sort by source_revision and source_ordinal, and confirm state when it matters.
Hookdeck Event Gateway stores, filters and replays Tensorlake's lifecycle events. Try it free.