BETAWe are currently in beta. Should you encounter any issues, please do not hesitate to contact us.
Discount on all models + if you follow us on twitter and hit us on dm you will get free credit to your email hurry up 🔥🔥🔥 click here
API · OPERATIONS

Get notified the moment a render finishes.

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.

Why use a webhook

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.

Setting up

  1. Open Settings → API & Webhooks.
  2. Click Add a webhook, paste your HTTPS URL, save. You can register up to 5 endpoints per account — set up per-environment URLs (production, staging, development).
  3. Copy the signing secret once — it disappears after 30 seconds. Each endpoint has its own secret; rotating one never affects the others.
  4. Store the secret in your server's env (e.g. VIVIX_WEBHOOK_SECRET).
The signing secret is shown exactly once. Lost it? Click Rotate secret to mint a new one — but your existing handler will start rejecting deliveries until you redeploy with the new value.

The payload

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"
}

Headers

HeaderPurpose
X-Vivix-SignatureHMAC-SHA256 of the raw request body, hex-encoded, prefixed with sha256=. Verify with constant-time compare.
X-Vivix-EventEither generation.completed or generation.failed. More event types coming as we add features.
X-Vivix-Delivery-IdUUID unique to this delivery attempt. Use it for idempotency (see below).
User-AgentAlways Vivix-Webhook/1.0. Allowlist on your CDN / WAF if you have one.

Verifying the signature

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.

Node.js

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()
  },
)

Python (Flask)

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 "", 200

Timeouts

We 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.

Retries

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.

AttemptWait before this attemptTotal elapsed
1none — fired immediately0s
21s~1s after attempt 1
35s~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.

We do not retry on 4xx. If your handler is rejecting the payload (bad signature verification, malformed parsing, auth mismatch), we record the single failure and move on. Fix the bug, redeploy, and rely on GET /api/v1/generations/:idas a fallback for any generation that didn't arrive.

Idempotency

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.

Debugging

The Recent deliveries panel in Settings shows the last 10 attempts — HTTP status, response time, and any transport error. Common failure modes:

SymptomLikely causeFix
Status 401 from your endpointSignature 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 errorURL is unreachable or has a bad TLS cert.Verify the URL resolves over HTTPS publicly. Self-signed certs are not accepted.
Status 404Path no longer exists on your server.Update the URL in Settings → API & Webhooks.
Status 5xxYour handler crashed.Check your server logs — the request was well-formed by the time we sent it.

Where to next