Webhooks

Events

EventTrigger
meal.createdNew meal logged
meal.updatedMeal edited
gut_score.createdNew daily gut score
overnight_score.createdNew overnight score
digestion.state_changedState transition
windows.updatedWindows recalculated

Subscribe

curl -X POST "https://suna.health/api/v1/webhooks" \
  -H "Authorization: Bearer suna_sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://yourapp.com/webhook","events":["meal.created"],"secret":"your_secret_16chars"}'

Verify Signatures

Every webhook carries X-Suna-Signature with timestamp and HMAC:

X-Suna-Signature: t=1710590460,v1=abc123def456...

Signed payload = timestamp + "." + body. Reject timestamps older than 5 minutes to prevent replay attacks.

Python

import hmac, hashlib, time

def verify(payload, sig_header, secret, tolerance=300):
    parts = dict(p.split("=", 1) for p in sig_header.split(","))
    ts = int(parts["t"])
    if abs(time.time() - ts) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{ts}.{payload}".encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

TypeScript

import { createHmac, timingSafeEqual } from "crypto";

function verify(payload: string, sigHeader: string, secret: string): boolean {
  const parts: Record<string, string> = {};
  sigHeader.split(",").forEach(p => { const [k, ...v] = p.split("="); parts[k] = v.join("="); });
  const ts = parseInt(parts.t);
  if (Math.abs(Date.now() / 1000 - ts) > 300) return false;
  const expected = createHmac("sha256", secret).update(ts + "." + payload).digest("hex");
  return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(parts.v1, "hex"));
}

Test Your Webhook

curl -X POST "https://suna.health/api/v1/webhooks/SUBSCRIPTION_ID/test" \
  -H "Authorization: Bearer suna_sk_live_xxx"

Sends a test.ping event. Returns delivered: true if your endpoint responded with 2xx.

Retry Policy

After the initial delivery attempt, up to 5 retries with exponential backoff (1s, 2s, 4s, 8s, 16s) — 6 total attempts over ~31 seconds. 10s timeout per attempt. After all retries fail, delivery stops. Deduplicate via webhook_delivery_id. Reject signatures with timestamps older than 5 minutes.