Developer Libraries.
Drop-in code for verifying Sonny webhook signatures and calling the API. Copy, paste, ship.
Verify a webhook signature — TypeScript / Node
// sonny-verify.ts — verify a Sonny webhook signature
import crypto from "node:crypto";
export class SonnySignatureError extends Error {}
export function verifySonnyWebhook(args: {
secret: string;
rawBody: string;
signatureHeader: string;
toleranceSeconds?: number;
}): void {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(args.signatureHeader);
if (!m) throw new SonnySignatureError("malformed signature");
const [, ts, v1] = m;
const tol = args.toleranceSeconds ?? 300;
const drift = Math.abs(Math.floor(Date.now() / 1000) - Number(ts));
if (drift > tol) throw new SonnySignatureError(`timestamp drift ${drift}s > ${tol}s`);
const expected = crypto
.createHmac("sha256", args.secret)
.update(`${ts}.${args.rawBody}`)
.digest("hex");
if (v1.length !== expected.length) throw new SonnySignatureError("length mismatch");
if (!crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) {
throw new SonnySignatureError("signature mismatch");
}
}
// Express usage:
import express from "express";
const app = express();
app.post("/sonny-webhooks", express.text({ type: "*/*" }), (req, res) => {
try {
verifySonnyWebhook({
secret: process.env.SONNY_WEBHOOK_SECRET!,
rawBody: req.body as string,
signatureHeader: req.header("x-sonny-signature") ?? "",
});
} catch {
return res.status(401).end("invalid signature");
}
const event = JSON.parse(req.body as string);
// event.event is the event name; event.data is the payload.
// Use req.header("x-sonny-delivery-id") as your idempotency key.
res.status(200).end("ok");
});
Verify a webhook signature — Python
# sonny_verify.py — verify a Sonny webhook signature
import hmac, hashlib, re, time
from typing import Optional
SIG_RE = re.compile(r"^t=(\d+),v1=([0-9a-f]{64})$")
class SonnySignatureError(Exception):
pass
def verify_sonny_webhook(
secret: str,
raw_body: str,
signature_header: str,
tolerance_seconds: int = 300,
) -> None:
m = SIG_RE.match(signature_header)
if not m:
raise SonnySignatureError("malformed signature")
ts, v1 = m.group(1), m.group(2)
drift = abs(int(time.time()) - int(ts))
if drift > tolerance_seconds:
raise SonnySignatureError(f"timestamp drift {drift}s > {tolerance_seconds}s")
expected = hmac.new(secret.encode(), f"{ts}.{raw_body}".encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(v1, expected):
raise SonnySignatureError("signature mismatch")
# Flask usage:
from flask import Flask, request
import os
app = Flask(__name__)
@app.post("/sonny-webhooks")
def sonny_webhooks():
try:
verify_sonny_webhook(
os.environ["SONNY_WEBHOOK_SECRET"],
request.get_data(as_text=True),
request.headers.get("x-sonny-signature", ""),
)
except SonnySignatureError:
return ("invalid signature", 401)
# ... your handler ...
return ("ok", 200)
API client — TypeScript
See /sonny/docs/reference for the full SonnyError + Idempotency-Key + Retry-After client. Sign-in required.
No-code? Use a workflow automation tool.
Wiring Sonny into a workflow automation tool (n8n, Zapier, Make.com) instead of a server? /sonny/integrations/automation-cookbook has paste-ready HTTP Request configs and code snippets that verify webhook signatures end-to-end.
Official npm SDK — @9mil/sonny
Single dependency-free TypeScript file. Drops into Node 18+, Bun, or Deno. Typed methods for every public endpoint plus a Web-Crypto-API webhook verifier that works on edge runtimes.
npm install @9mil/sonny
import { SonnyClient } from "@9mil/sonny";
const sonny = new SonnyClient({ apiKey: process.env.SONNY_API_KEY! });
const screen = await sonny.screen({
candidate_id: "cand_abc123",
role_brief: "Senior Go engineer, payments domain",
});Source lives in packages/sonny-sdk on GitHub — see the README for the full method surface, error model, and webhook verifier example.
Official Python SDK — sonny
Built on httpx, Python 3.10+. Symmetric to the TypeScript SDK — same method surface, same error model, same webhook verifier.
pip install sonny
from sonny import SonnyClient
sonny = SonnyClient(api_key="sk_live_...")
screen = sonny.screen(
candidate_id="cand_abc123",
role_brief="Senior Go engineer, payments domain",
)Source lives in packages/sonny-sdk-py on GitHub.
Test your endpoint
Once your subscription is registered, fire a synthetic event at it with the same envelope as a real delivery:
curl https://9mil.io/api/v1/sonny/webhooks/<id>/test \
-H "Authorization: Bearer sk_live_…" \
-H "content-type: application/json" \
-X POST \
-d '{"event": "sonny.webhook.test", "payload": {"hello": "world"}}'The response includes the receiver’s status code and (truncated) response body, plus the signature we sent so you can replay verification locally.