> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getspine.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Verify inbound Spine webhook signatures from the TypeScript SDK.

`client.webhooks` verifies the HMAC signature on an inbound webhook and returns a typed `WebhookEvent`. See [Webhooks](/api-reference/concepts/webhooks) for the conceptual model and signing algorithm.

No network call is made — verification is local crypto against the `whsec_...` secret you saved in the developer portal. Works on Node 18+, browsers, Deno, Bun, and Cloudflare Workers via WebCrypto.

## Verifying a delivery

```ts theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
import { Spine, SpineWebhookSignatureError } from 'spine-sdk';

const client = new Spine({ apiKey: process.env.SPINE_API_KEY! });

export async function handleSpineWebhook(
  rawBody: Uint8Array | string,
  sigHeader: string,
) {
  try {
    const event = await client.webhooks.constructEvent(
      rawBody,
      sigHeader,
      process.env.SPINE_WEBHOOK_SECRET!,
    );
    if (event.type === 'run.completed') {
      // persist event.data.object.run_id etc.
    }
    return { ok: true };
  } catch (err) {
    if (err instanceof SpineWebhookSignatureError) {
      return { ok: false, status: 400 };
    }
    throw err;
  }
}
```

Pass the **raw request body bytes**, not the parsed JSON — frameworks may reorder keys during re-serialization and the signature will no longer verify.

| Argument                   | Type                                  | Default                         | Description                                       |
| -------------------------- | ------------------------------------- | ------------------------------- | ------------------------------------------------- |
| `payload`                  | `Uint8Array \| ArrayBuffer \| string` | required                        | Raw request body.                                 |
| `sigHeader`                | `string`                              | required                        | Value of the `Spine-Signature` request header.    |
| `secret`                   | `string`                              | required                        | The `whsec_...` secret from the developer portal. |
| `options.toleranceSeconds` | `number`                              | `300`                           | Replay window in seconds.                         |
| `options.now`              | `number`                              | `Math.floor(Date.now() / 1000)` | Inject for deterministic tests.                   |

Returns a `Promise<WebhookEvent>`. Throws [`SpineWebhookSignatureError`](/api-reference/sdks/typescript/errors) on any verification failure.

## Express example

Express parses JSON by default and discards the raw bytes the signature was computed over. Use `express.raw()` on the webhook route so the SDK receives untouched bytes.

```ts theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
import express from 'express';
import { Spine, SpineWebhookSignatureError } from 'spine-sdk';

const app = express();
const client = new Spine({ apiKey: process.env.SPINE_API_KEY! });

app.post(
  '/webhooks/spine',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    try {
      const event = await client.webhooks.constructEvent(
        req.body,                                          // Buffer → Uint8Array-compatible
        req.header('Spine-Signature') ?? '',
        process.env.SPINE_WEBHOOK_SECRET!,
      );
      // Dedupe on event.id, then dispatch event.type
      res.status(200).json({ ok: true });
    } catch (err) {
      if (err instanceof SpineWebhookSignatureError) {
        return res.status(400).json({ error: 'invalid signature' });
      }
      throw err;
    }
  },
);
```

## Next.js route handler

```ts theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// app/api/webhooks/spine/route.ts
import { NextRequest } from 'next/server';
import { Spine, SpineWebhookSignatureError } from 'spine-sdk';

const client = new Spine({ apiKey: process.env.SPINE_API_KEY! });

export async function POST(req: NextRequest) {
  const raw = new Uint8Array(await req.arrayBuffer());
  try {
    const event = await client.webhooks.constructEvent(
      raw,
      req.headers.get('Spine-Signature') ?? '',
      process.env.SPINE_WEBHOOK_SECRET!,
    );
    // dispatch event.type
    return Response.json({ ok: true });
  } catch (err) {
    if (err instanceof SpineWebhookSignatureError) {
      return Response.json({ error: 'invalid signature' }, { status: 400 });
    }
    throw err;
  }
}
```

## `WebhookEvent`

| Field         | Type                                  | Notes                                                     |
| ------------- | ------------------------------------- | --------------------------------------------------------- |
| `id`          | `string`                              | `evt_<uuid>`. Use for idempotency.                        |
| `type`        | `string`                              | e.g. `"run.completed"`, `"run.failed"`, `"webhook.ping"`. |
| `created`     | `number`                              | Unix seconds.                                             |
| `livemode`    | `boolean`                             | `true` in production.                                     |
| `api_version` | `string \| undefined`                 | Envelope schema version.                                  |
| `data`        | `{ object: Record<string, unknown> }` | Event payload. `data.object` carries the primary entity.  |

## Notes

* Clock skew matters. If your server drifts more than 5 minutes from Spine's clock, deliveries will be rejected as stale. Run NTP.
* Rotation invalidates the previous secret immediately. Update your env var before rotating if you care about strict uptime.
* The SDK accepts multiple `v1=` values in one header, so a future dual-signing rotation strategy will be forward-compatible.
