Guides
Webhooks
Instead of polling, let Orior call your server when something happens. Deliveries are signed, retried for about a day and visible in a delivery log.
Set up an endpoint
cURL
curl https://api.oriorai.com/v1/webhooks \
-H "Authorization: Bearer $ORIOR_API_KEY" -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" \
-d '{"url":"https://example.com/orior/webhooks","events":["generation.completed","generation.failed"]}'The response includes secret (whsec_…) — store it; it is not shown again. Endpoints must be public https URLs. Omit events to receive everything. See the reference.
Events
| Event | When | data |
|---|---|---|
generation.completed | A generation finished successfully. | The generation. |
generation.failed | A generation failed or was cancelled (credits refunded). | The generation, with error and timeline. |
social_post.published | A post published now to at least one platform. | The social post. |
social_post.failed | An immediate post failed on every platform. | The social post. |
credits.low | Balance dropped below 20 after an API charge (at most daily). | { balance, threshold } |
Payload
POST https://example.com/orior/webhooks
{
"id": "evt_gen_7f3c9a1e2b4d4c8f9a0b1c2d3e4f5a6b_completed",
"type": "generation.completed",
"created_at": 1758620031000,
"data": {
"id": "gen_7f3c9a1e2b4d4c8f9a0b1c2d3e4f5a6b",
"object": "generation",
"type": "image",
"model": "orior-image",
"status": "completed",
"credits": 2,
"refunded_credits": 0,
"result": {
"url": "https://storage.googleapis.com/…/generated-images/…/3kf9.webp",
"thumbnail_url": "https://storage.googleapis.com/…/3kf9_thumb.webp"
},
"error": null,
"request": {
"prompt": "A barista smiling behind a latte, morning light",
"character_id": null,
"product_id": null,
"aspect_ratio": "9:16",
"resolution": "2K",
"edit": false,
"has_scene_image": false,
"reference_images": 0
},
"timeline": [],
"created_at": 1758620000000,
"completed_at": 1758620031000
}
}| Header | Value |
|---|---|
Orior-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
Orior-Event | The event type. |
Orior-Delivery | Delivery ID (dlv_…), stable across retries. |
User-Agent | Orior-Webhooks/1 |
Verify signatures
Compute HMAC-SHA256(secret, t + "." + raw_body) and compare it to v1 in constant time. Reject timestamps older than 5 minutes to prevent replays. Always use the raw request body — re-serialised JSON will not match.
import express from 'express'
import { createHmac, timingSafeEqual } from 'node:crypto'
const app = express()
const secret = process.env.ORIOR_WEBHOOK_SECRET // whsec_…
// Verify against the raw body, before JSON parsing.
app.post('/orior/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('Orior-Signature') ?? ''
const parts = Object.fromEntries(header.split(',').map(part => part.split('=')))
const timestamp = Number(parts.t)
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return res.sendStatus(400)
const expected = createHmac('sha256', secret).update(`${parts.t}.${req.body}`).digest('hex')
const valid = parts.v1?.length === expected.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))
if (!valid) return res.sendStatus(400)
const event = JSON.parse(req.body)
res.sendStatus(200) // acknowledge fast, then process
void handle(event) // dedupe on event.id
})import hmac, hashlib, json, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["ORIOR_WEBHOOK_SECRET"].encode()
@app.post("/orior/webhooks")
def orior_webhook():
parts = dict(p.split("=", 1) for p in request.headers.get("Orior-Signature", "").split(",") if "=" in p)
if abs(time.time() - int(parts.get("t", 0))) > 300:
abort(400)
payload = request.get_data() # raw bytes
expected = hmac.new(SECRET, f"{parts['t']}.".encode() + payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")):
abort(400)
event = json.loads(payload)
# dedupe on event["id"], then process asynchronously
return "", 200Delivery and retries
- A delivery succeeds on any
2xxwithin 10 seconds. Redirects are not followed. - Failed deliveries are retried after 1, 5, 30, 120, 360 and 720 minutes (about 21 hours in total), then marked failed.
- Delivery history (30 days) is available at
GET /webhooks/{id}/deliveries. - Events for one generation are sent once per state; ordering between different events is not guaranteed.
Best practices
- Return
200immediately and process in the background. - Deduplicate on
id— a delivery can arrive more than once. - Treat the webhook as a signal; for critical flows re-read the resource with
GET /generations/{id}. - Keep polling as a fallback for generations you have not heard about after 30 minutes.
Never trust an unsigned or badly signed request — anyone can POST to a public URL.
Testing
POST /webhooks/{id}/test sends a signed event with data.test: true. For local development, expose your server with a tunnel (e.g. ngrok or Cloudflare Tunnel) and register the tunnel URL.