Gareth Wilson Gareth Wilson

Guide to Tensorlake Webhooks: Features and Best Practices

Published


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.

AspectValue
MethodPOST, JSON body
Signature headerssvix-id, svix-timestamp, svix-signature (Standard Webhooks)
SuccessAny 2xx acknowledges the attempt; Svix retries failed attempts
DeliveryAt least once and unordered; event_id is stable across retries
Test deliveriesTest 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

FeatureDetails
SignatureStandard Webhooks HMAC-SHA256
Signature headersvix-signature
BodyJSON
Event routingBody 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:

FieldDescription
event_idStable identifier beginning with evt_; the deduplication key
event_typeOne of the event names below
event_versionPayload schema version (currently 1)
occurred_atRFC 3339 UTC time the transition was recorded
project_idPublic ID of the producing project
source_idOpaque identifier of the producer's ordering domain
source_revisionUnsigned decimal revision, kept as a string to avoid precision loss
source_ordinalZero-based order when one source revision produces several events
dataEvent-specific sandbox, application, or request data

Tensorlake documents twelve lifecycle events in three groups:

EventEmitted Whendata fields
tensorlake.sandbox.createdA sandbox is persisted for the first timeprevious_status, sandbox
tensorlake.sandbox.runningA sandbox enters the running statesame
tensorlake.sandbox.suspendedA sandbox enters the suspended statesame
tensorlake.sandbox.resumedA suspended sandbox begins a new generation (public status starts as pending)same
tensorlake.sandbox.terminatedA sandbox terminates without a failure outcomesame
tensorlake.sandbox.failedA sandbox terminates with a failure outcomesame
tensorlake.application.createdAn application is persisted for the first timeprevious_version and previous_state (both null), application
tensorlake.application.updatedA deployment, metadata, capability, or state change is persistedsame
tensorlake.application.deletedAn existing application is deletedsame
tensorlake.application.request.createdAn application request is createdapplication, request
tensorlake.application.request.completedAn application request completessame
tensorlake.application.request.failedAn application request failssame, with request.failure_reason

The resources inside data have these fields:

  • Sandbox: id, name, type (named or ephemeral), image, generation_id, status (pending, running, suspended, terminated), outcome (success, failure or null), outcome_reason, snapshot_id, created_at and resources.
  • Application: name and version.
  • Request: id, status, failure_reason, created_at and finished_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.

  1. In Tensorlake Cloud, select the organisation and project, then Webhooks in the project navigation.
  2. Select Create webhook and enter a Webhook name and an absolute HTTPS Endpoint URL without embedded credentials, such as https://example.com/webhooks/tensorlake.
  3. Select at least one event type. tensorlake.sandbox.failed, tensorlake.sandbox.running and tensorlake.application.request.completed make a useful first set.
  4. 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, compare source_revision as an integer, then source_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 2xx as 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.