Skip to main content
client.webhooks verifies the HMAC signature on an inbound webhook and returns a typed WebhookEvent. See 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

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.
ArgumentTypeDefaultDescription
payloadUint8Array | ArrayBuffer | stringrequiredRaw request body.
sigHeaderstringrequiredValue of the Spine-Signature request header.
secretstringrequiredThe whsec_... secret from the developer portal.
options.toleranceSecondsnumber300Replay window in seconds.
options.nownumberMath.floor(Date.now() / 1000)Inject for deterministic tests.
Returns a Promise<WebhookEvent>. Throws SpineWebhookSignatureError 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.
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

// 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

FieldTypeNotes
idstringevt_<uuid>. Use for idempotency.
typestringe.g. "run.completed", "run.failed", "webhook.ping".
creatednumberUnix seconds.
livemodebooleantrue in production.
api_versionstring | undefinedEnvelope 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.