Configure up to 5 HTTPS endpoints in Settings → API & Webhooks — one per environment (production, staging, development) and beyond. Every time a generation kicked off through /api/v1/generate completes or fails, we POST a signed JSON payload to each enabled endpoint — no polling required.
Image generations finish in a few seconds, but video can take 60–180 s. Polling GET /api/v1/generations/:idin a tight loop wastes your rate-limit budget and adds latency between “ready” and “your user sees it”. With a webhook, we push the result the instant the generation finishes — typical end-to-end latency is under 200 ms from generation completion to your endpoint receiving the POST.
VIVIX_WEBHOOK_SECRET).Every delivery is a POST with a JSON body. The same shape applies to both generation.completed and generation.failedevents — failed events simply have null output URLs and a populatederror_code / error_message.
POST https://api.your-app.com/vivix/webhook
Content-Type: application/json
User-Agent: Vivix-Webhook/1.0
X-Vivix-Signature: sha256=4f8b…
X-Vivix-Event: generation.completed
X-Vivix-Delivery-Id: 019038b7-3c44-7a92-b9f2-d4e8a91c6f08
{
"generation_id": "019038b7-1234-7a92-b9f2-d4e8a91c6f08",
"status": "completed",
"model": "runware:101@1",
"output_url": "https://im.runware.ai/image/abc.png",
"output_urls": ["https://im.runware.ai/image/abc.png"],
"credits_charged": 12,
"duration_ms": 4_321,
"completed_at": "2026-05-12T18:23:45.123Z"
}| Header | Purpose |
|---|---|
X-Vivix-Signature | HMAC-SHA256 of the raw request body, hex-encoded, prefixed with sha256=. Verify with constant-time compare. |
X-Vivix-Event | Either generation.completed or generation.failed. More event types coming as we add features. |
X-Vivix-Delivery-Id | UUID unique to this delivery attempt. Use it for idempotency (see below). |
User-Agent | Always Vivix-Webhook/1.0. Allowlist on your CDN / WAF if you have one. |
Always verify before trusting the payload. We sign the raw request body with HMAC-SHA256. Use a constant-time compare to defeat timing-attack-based secret discovery.
import crypto from 'node:crypto'
import express from 'express'
const app = express()
// IMPORTANT: pass the RAW body buffer to the verifier — JSON-parsing first
// then re-stringifying will change byte order and break the signature.
app.post(
'/vivix/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const header = req.header('X-Vivix-Signature') ?? ''
const sent = header.replace(/^sha256=/, '')
const mac = crypto
.createHmac('sha256', process.env.VIVIX_WEBHOOK_SECRET!)
.update(req.body)
.digest('hex')
const a = Buffer.from(sent, 'hex')
const b = Buffer.from(mac, 'hex')
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('bad signature')
}
const event = JSON.parse(req.body.toString('utf8'))
// …handle event.status, event.output_url, etc.
res.status(200).end()
},
)import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["VIVIX_WEBHOOK_SECRET"].encode()
@app.post("/vivix/webhook")
def vivix_webhook():
sent = request.headers.get("X-Vivix-Signature", "").removeprefix("sha256=")
mac = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
# constant-time compare — protects against timing-attack secret discovery
if not hmac.compare_digest(sent, mac):
abort(401)
event = request.get_json(force=True)
# …handle event["status"], event["output_url"], etc.
return "", 200We give your endpoint 5 seconds to respond. If you time out, return a 5xx, or hang up, we record the failure in your delivery log but do not retry in v1. Make your handler fast — enqueue the work, then return 200 immediately.
We retry up to 3 attempts with exponential backoff when delivery hits a transport error (timeout, DNS failure, TCP reset, etc.) or your endpoint returns a 5xx status code. A 2xx or 3xx stops the chain as success. A 4xxstops the chain as a terminal failure — we assume the customer's endpoint is rejecting the payload on purpose and retrying won't change that.
| Attempt | Wait before this attempt | Total elapsed |
|---|---|---|
| 1 | none — fired immediately | 0s |
| 2 | 1s | ~1s after attempt 1 |
| 3 | 5s | ~6s after attempt 1 |
| (give up) | 30s additional | ~36s after attempt 1 |
The full chain has a 90s wall-clock cap: if attempt 3 is still in flight after that, we abort and log error_message = “retry budget exhausted”.
Each attempt is its own row in the recent-deliveries log (attempt = 1 | 2 | 3) with its own unique X-Vivix-Delivery-Id. That gives you a complete audit trail of every try we made, plus a stable idempotency key per attempt — see the Idempotency section for how to dedupe.
GET /api/v1/generations/:idas a fallback for any generation that didn't arrive.Even without retries, we may occasionally re-deliver the same event if our log row write succeeds but the response timing is ambiguous. Dedupe on X-Vivix-Delivery-Id:
const seen = new Set<string>()
app.post('/vivix/webhook', (req, res) => {
const deliveryId = req.header('X-Vivix-Delivery-Id')!
if (seen.has(deliveryId)) return res.status(200).end() // already handled
seen.add(deliveryId)
// …handle
res.status(200).end()
})In production, back seen with Redis or a database table with a unique index on the delivery id.
The Recent deliveries panel in Settings shows the last 10 attempts — HTTP status, response time, and any transport error. Common failure modes:
| Symptom | Likely cause | Fix |
|---|---|---|
Status 401 from your endpoint | Signature verification failed. | Make sure you're hashing the RAW body bytes, not the JSON-reparsed form. |
Status — + “Timeout after 5000ms” | Your handler is doing too much work synchronously. | Queue the work (BullMQ, SQS, etc.) and return 200 immediately. |
Status — + DNS / certificate error | URL is unreachable or has a bad TLS cert. | Verify the URL resolves over HTTPS publicly. Self-signed certs are not accepted. |
Status 404 | Path no longer exists on your server. | Update the URL in Settings → API & Webhooks. |
Status 5xx | Your handler crashed. | Check your server logs — the request was well-formed by the time we sent it. |
error_code