# Tensorlake Webhooks

[Tensorlake](https://www.tensorlake.ai/) signs every webhook delivery with Svix, following
Standard Webhooks with the `svix-*` header names. Each delivery
is an HTTP `POST` with a signed JSON body and three headers: `svix-id`, `svix-timestamp`, and
`svix-signature`. Webhooks are scoped to one project and report sandbox, application and
application-request lifecycle changes. The body is a versioned envelope:
`{ "event_id": "evt_...", "event_type": "tensorlake.sandbox.failed", "event_version": 1, "occurred_at": "...", "project_id": "...", "source_id": "...", "source_revision": "81247", "source_ordinal": 0, "data": { ... } }`.
`event_id` is stable across retries (the deduplication key). Order related events from the same
`source_id` by `source_revision` (a numeric string) and then `source_ordinal`.

## When to Use This Skill

* How do I receive Tensorlake webhooks?
* How do I verify Tensorlake webhook signatures (`svix-signature`)?
* How do I handle `tensorlake.sandbox.failed`, `tensorlake.sandbox.running` or `tensorlake.application.request.completed` events?
* How do I react to a failed sandbox or a failed application request?
* Why is my Tensorlake webhook signature verification failing, or rejecting Hookdeck retries?

## Verification (core)

Signed content is `{svix-id}.{svix-timestamp}.{raw body}`. Svix signs it with HMAC-SHA256 using
the base64-decoded secret (the part after `whsec_`), base64-encodes the result, and sends one or
more space-separated `v1,<signature>` entries. Use the official `svix` library, which compares in
constant time. Read the `svix-*` headers, and when `svix-id` is absent fall back to the `webhook-*`
headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`), as Hookdeck does.

```javascript
import { Webhook } from 'svix'; // svix 2.x is ESM-only

const prefix = req.headers['svix-id'] ? 'svix' : 'webhook';
const id = req.headers[`${prefix}-id`];
const timestamp = req.headers[`${prefix}-timestamp`];
const signature = req.headers[`${prefix}-signature`];
// rawBody: the raw Buffer/string, never re-serialized JSON
new Webhook(process.env.TENSORLAKE_WEBHOOK_SECRET).verify(rawBody, {
  'svix-id': id, 'svix-timestamp': timestamp, 'svix-signature': signature,
  'webhook-id': id, 'webhook-timestamp': timestamp, 'webhook-signature': signature,
}); // throws on a bad signature or a timestamp outside the window
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes

```

```python
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["TENSORLAKE_WEBHOOK_SECRET"])  # "whsec_..."
wh.verify(raw_body, {"svix-id": id, "svix-timestamp": ts, "svix-signature": sig,
                     "webhook-id": id, "webhook-timestamp": ts, "webhook-signature": sig})

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/tensorlake-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/tensorlake-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/tensorlake-webhooks/examples/fastapi/).

Each destination has its own signing secret: open the project's **Webhooks** page in
[Tensorlake Cloud](https://cloud.tensorlake.ai), open the destination, and copy the value under
**Signing secret**. It begins with `whsec_`. Keep the full value in `TENSORLAKE_WEBHOOK_SECRET`.
Handlers fail closed: with the variable unset (or not a `whsec_` value) they return 500 instead
of accepting anything. Tensorlake points at Svix's libraries and
manual procedure; the `svix`
library used here reads either header set, so no mapping is needed beyond the fallback shown.

### Hookdeck retries and replays hit the 5-minute window

The `svix` and `standardwebhooks` libraries always enforce a fixed 5-minute timestamp window, and
it can't be configured. Hookdeck retries and replays resend the **original** `svix-timestamp`, so
the library rejects any retry or replay that happens more than 5 minutes after the original
delivery. If you verify behind Hookdeck (or replay old events), use the manual verifier in
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tensorlake-webhooks/references/verification.md), where the window can be dropped or
widened. Deduplicate on `event_id` instead of relying on the timestamp.

## Common Event Types

12 events are documented (the list may not be exhaustive). The name is `event_type` in the JSON
body, and the examples handle all of them:

| Event | Emitted When |
| --- | --- |
| `tensorlake.sandbox.created` | A sandbox is persisted for the first time |
| `tensorlake.sandbox.running` | A sandbox enters the running state |
| `tensorlake.sandbox.suspended` | A sandbox enters the suspended state |
| `tensorlake.sandbox.resumed` | A suspended sandbox begins a new generation (status starts as `pending`) |
| `tensorlake.sandbox.terminated` | A sandbox terminates without a failure outcome |
| `tensorlake.sandbox.failed` | A sandbox terminates with a failure outcome |
| `tensorlake.application.created` | An application is persisted for the first time |
| `tensorlake.application.updated` | A deployment, metadata, capability or state change is persisted |
| `tensorlake.application.deleted` | An existing application is deleted |
| `tensorlake.application.request.created` | An application request is created |
| `tensorlake.application.request.completed` | An application request completes |
| `tensorlake.application.request.failed` | An application request fails (`data.request.failure_reason`) |

See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tensorlake-webhooks/references/overview.md) for the full catalog and payload shapes.

## Environment Variables

```bash
# Signing secret of the webhook endpoint (Svix format, starts with whsec_)
TENSORLAKE_WEBHOOK_SECRET=whsec_xxxxx

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 tensorlake --path /webhooks/tensorlake

```

Register the tunnel URL as a destination on the project's **Webhooks** page in Tensorlake Cloud,
then send a synthetic event with **Test webhook** on the destination's page.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tensorlake-webhooks/references/overview.md) - Tensorlake webhook concepts, events, payload shapes, delivery
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tensorlake-webhooks/references/setup.md) - Create a destination, get the secret, send a test event
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tensorlake-webhooks/references/verification.md) - Svix verification details, header fallback, manual verifier, timestamp window