← Sonny

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.