Skip to main content

Outbound webhooks

uniqOS can push events to your backend instead of making you poll. You register HTTPS endpoints, choose which event types they receive, and uniqOS delivers signed JSON payloads with automatic retries.

Registering an endpoint

Create an endpoint with POST /v1/webhooks: a URL, an optional description, and the list of event subscriptions. The response includes the signing secret exactly once — store it like a password; you can rotate it later with regenerate-secret.

Event catalogue

  • personality.review_approved / review_rejected / review_conditional — manual-review outcomes.
  • quota.threshold_reached — a quota alert you configured crossed its threshold (enable the webhook channel on the alert).
  • account.*, payment.* and further quota.* events are catalogued and will start emitting as their sources are wired.

Verifying deliveries

Every delivery is HMAC-SHA256 signed with your endpoint's secret over the raw request body, sent as x-uniqos-signature: sha256=<hex>. Both SDKs (0.6.0+) ship a verifier — use it instead of hand-rolling the HMAC:

// TypeScript — works with any framework that exposes the raw body
import { constructWebhookEvent, WebhookSignatureError } from '@uniq-os/sdk'

app.post('/hooks/uniqos', (req, res) => {
try {
const event = constructWebhookEvent({
rawBody: req.rawBody, // MUST be the raw bytes, not re-serialized JSON
signatureHeader: req.headers['x-uniqos-signature'],
secret: process.env.UNIQOS_WEBHOOK_SECRET!, // or [newSecret, oldSecret] during rotation
})
handle(event) // { id, type, created_at, data } — dedupe on event.id
res.sendStatus(200)
} catch (err) {
if (err instanceof WebhookSignatureError) return res.sendStatus(400)
throw err
}
})
# Python
from uniqos import construct_event, WebhookSignatureError

@app.post("/hooks/uniqos")
async def uniqos_hook(request: Request):
try:
event = construct_event(
await request.body(), # raw bytes
request.headers.get("x-uniqos-signature"),
os.environ["UNIQOS_WEBHOOK_SECRET"], # or [new, old] during rotation
)
except WebhookSignatureError:
raise HTTPException(400)
handle(event) # dedupe on event.id
return {"ok": True}

The helper compares in constant time, accepts a list of secrets (so rotating with regenerate-secret is zero-downtime), and rejects deliveries whose signed created_at is older than 5 minutes (replay protection — configurable via toleranceSeconds / tolerance). Two classic mistakes it protects you from: comparing signatures with ==, and verifying against parsed-then-re-serialized JSON instead of the raw bytes.

Delivery semantics

Failed deliveries retry with exponential backoff (1m, 5m, 30m, 2h, 12h, 24h — six attempts), then park in a dead-letter state you can inspect from the portal. Deliveries are at-least-once: make your handler idempotent (the delivery id is stable across retries).