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:
- Compute
sha256=<hex>="sha256="+ HMAC-SHA256(rawBody, secret). - Compare against the
X-Jaina-Signatureheader using a constant-time comparison (timing-safe), returningfalseon any length mismatch. - 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.
