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 furtherquota.*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).