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.

Verifying a delivery

import os
from spine import SpineClient, WebhookSignatureError

client = SpineClient()  # SPINE_API_KEY picked up from env

def handle_spine_webhook(raw_body: bytes, sig_header: str) -> dict:
    try:
        event = client.webhooks.construct_event(
            payload=raw_body,
            sig_header=sig_header,
            secret=os.environ["SPINE_WEBHOOK_SECRET"],
        )
    except WebhookSignatureError:
        return {"ok": False}, 400

    if event.type == "run.completed":
        run = event.data["object"]
        # persist run["run_id"] etc.
    return {"ok": True}, 200
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
payloadbytes | strrequiredRaw request body. bytes is preferred.
sig_headerstrrequiredValue of the Spine-Signature request header.
secretstrrequiredThe whsec_... secret from the developer portal.
toleranceint300Replay window in seconds.
Returns a WebhookEvent. Raises WebhookSignatureError on any verification failure.

FastAPI example

from fastapi import FastAPI, Header, HTTPException, Request
from spine import SpineClient, WebhookSignatureError

app = FastAPI()
client = SpineClient()

@app.post("/webhooks/spine")
async def spine_webhook(
    request: Request,
    spine_signature: str = Header(default=""),
):
    raw = await request.body()
    try:
        event = client.webhooks.construct_event(
            payload=raw,
            sig_header=spine_signature,
            secret=os.environ["SPINE_WEBHOOK_SECRET"],
        )
    except WebhookSignatureError as exc:
        raise HTTPException(status_code=400, detail=str(exc))

    # Dedupe on event.id, then dispatch
    return {"ok": True}

Flask example

from flask import Flask, request
from spine import SpineClient, WebhookSignatureError

app = Flask(__name__)
client = SpineClient()

@app.post("/webhooks/spine")
def spine_webhook():
    try:
        event = client.webhooks.construct_event(
            payload=request.get_data(),                      # raw bytes
            sig_header=request.headers.get("Spine-Signature", ""),
            secret=os.environ["SPINE_WEBHOOK_SECRET"],
        )
    except WebhookSignatureError:
        return {"ok": False}, 400

    # Dedupe on event.id, then dispatch
    return {"ok": True}, 200

Async

AsyncSpineClient.webhooks.construct_event(...) has the same signature. Verification is CPU-only, so the async variant exists for API-parity with the rest of the client rather than for performance.
from spine import AsyncSpineClient

async with AsyncSpineClient() as client:
    event = client.webhooks.construct_event(...)

WebhookEvent

FieldTypeNotes
idstrevt_<uuid>. Use for idempotency.
typestre.g. "run.completed", "run.failed", "webhook.ping".
createdintUnix seconds.
livemodeboolTrue in production.
api_versionstr | NoneEnvelope schema version.
datadictEvent 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.