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

EventWhendata
generation.completedA generation finished successfully.The generation.
generation.failedA generation failed or was cancelled (credits refunded).The generation, with error and timeline.
social_post.publishedA post published now to at least one platform.The social post.
social_post.failedAn immediate post failed on every platform.The social post.
credits.lowBalance 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
  }
}
HeaderValue
Orior-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
Orior-EventThe event type.
Orior-DeliveryDelivery ID (dlv_…), stable across retries.
User-AgentOrior-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
})

Delivery and retries

  • A delivery succeeds on any 2xx within 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 200 immediately 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.