Webhooks & Signatures

Verify Jaina webhook deliveries in one line with the SDK signature helpers.

How Jaina signs deliveries

Every webhook delivery is signed with HMAC-SHA256 over the raw JSON request body. The signature is sent in the X-Jaina-Signature header, formatted as:

X-Jaina-Signature: sha256=<hex digest>

Jaina also sends X-Jaina-Event (the event type), X-Jaina-Delivery (a unique delivery id), and X-Jaina-Timestamp (unix seconds). To trust a delivery you must recompute the signature with your webhook's signing secret (whsec_...) and compare it in constant time. Each SDK ships a one-line helper that does exactly this, so you never have to hand-roll HMAC verification.

Always verify against the raw request body. If you parse the JSON and re-serialize it, whitespace and key-ordering differences will change the bytes and the signature will not match.

TypeScript / Node

import { verifyWebhookSignature } from "@jaina/sdk";

// Express example — note express.raw() so we keep the exact bytes Jaina signed.
import express from "express";
const app = express();

app.post("/webhooks/jaina", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body.toString("utf8");
  const signature = req.header("X-Jaina-Signature");

  if (!verifyWebhookSignature(rawBody, signature, process.env.JAINA_WEBHOOK_SECRET!)) {
    return res.status(401).send("invalid signature");
  }

  const event = JSON.parse(rawBody);
  console.log("verified event:", event.type);
  res.sendStatus(200);
});

Python

from jaina.webhooks import verify_signature

# Flask example — request.get_data() returns the raw bytes Jaina signed.
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/webhooks/jaina")
def jaina_webhook():
    raw_body = request.get_data()  # bytes
    signature = request.headers.get("X-Jaina-Signature")

    if not verify_signature(raw_body, signature, JAINA_WEBHOOK_SECRET):
        abort(401)

    event = request.get_json()
    print("verified event:", event["type"])
    return "", 200

C# / .NET

using Jaina.Client;

// ASP.NET minimal API — read the raw request body before model binding.
app.MapPost("/webhooks/jaina", async (HttpRequest req) =>
{
    using var reader = new StreamReader(req.Body);
    var rawBody = await reader.ReadToEndAsync();
    var signature = req.Headers["X-Jaina-Signature"].ToString();

    if (!JainaWebhook.VerifySignature(rawBody, signature, webhookSecret))
    {
        return Results.Unauthorized();
    }

    // ... handle the verified event ...
    return Results.Ok();
});

What the helpers do

All three helpers implement the identical algorithm:

  1. Compute sha256=<hex> = "sha256=" + HMAC-SHA256(rawBody, secret).
  2. Compare against the X-Jaina-Signature header using a constant-time comparison (timing-safe), returning false on any length mismatch.
  3. Return false (never throw) when the signature header or secret is missing.

A false result means the delivery is not authentic. Reject it with 401.