# Guide to Tensorlake Webhooks: Features and Best Practices

[Tensorlake](https://www.tensorlake.ai/) 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](https://docs.tensorlake.ai/platform/webhooks/overview) 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](https://github.com/hookdeck/webhook-skills/tree/main/skills/tensorlake-webhooks/examples).

## 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` (`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](https://github.com/hookdeck/webhook-samples/blob/main/providers/tensorlake/latest/tensorlake.application.request.completed.json). Note that `source_revision` is a string:

```json
{
  "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](https://cloud.tensorlake.ai), 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:

```bash
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](/docs/cli) to receive it locally:

```bash
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](https://docs.tensorlake.ai/platform/webhooks/signature-verification) 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:

```javascript
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:

```javascript
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](/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](/docs/retries) 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](/docs/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](/docs/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](/webhooks/guides/implement-webhook-idempotency) 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](https://hookdeck.com) stores, filters and replays Tensorlake's lifecycle events. [Try it free](https://dashboard.hookdeck.com/signup).