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 have null output URLs, a populatederror_code / error_message, and aretryable boolean telling you whether resubmitting the same request would succeed.

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:  c17a4e6d-9b2f-4c8a-8d31-7f0e9a5c2b46

{
  "generation_id":   "5b91a581-ee19-4f86-9fea-bd29471d69d5",
  "status":          "completed",
  "model":           "grok-imagine-image-quality",
  "output_url":      "https://getvivix.com/api/v1/output/5b91a581-ee19-4f86-9fea-bd29471d69d5/1758734625/a1b2c3d4e5f60718293a4b5c6d7e8f9/0/getvivix-5b91a581.png",
  "output_urls":     ["https://getvivix.com/api/v1/output/5b91a581-ee19-4f86-9fea-bd29471d69d5/1758734625/a1b2c3d4e5f60718293a4b5c6d7e8f9/0/getvivix-5b91a581.png"],
  "credits_charged": 12,
  "duration_ms":     4321,
  "completed_at":    "2026-05-12T18:23:45.123Z"
}

output_url / output_urls are signed, getvivix-hosted links, not the raw provider file — and they expire 3 days after the generation completes. Save the bytes to your own storage before then; polling GET /api/v1/generations/:id mints a fresh link if you need one later.

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 — a fresh one every retry. Log it for correlation; use generation_id in the body for dedup (see Idempotency 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. A timeout, a hang-up, or a 5xx all count as a transport failure and are retried per the policy below — they are not a dead end. Make your handler fast — enqueue the work, then return 200 immediately, so a slow downstream job never turns into an avoidable retry.

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

Each attempt then gets its own 5-second response window (see Timeouts above), so the whole chain normally finishes in under 25 seconds. As a safety valve, if something delays the chain so long that 90 seconds have already passed before the next attempt would start — rare, well outside the schedule above — we abandon it early and log error_message = “retry budget exhausted” instead of trying again.

Each attempt is its own row in the recent-deliveries log (attempt = 1 | 2 | 3), each with its own fresh X-Vivix-Delivery-Id — useful for correlating one try with your own logs, but not a stable key across retries. See Idempotency below for the field that is.

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

Each retry (see above) is a genuinely new delivery with its own fresh X-Vivix-Delivery-Id, so that header can't tell you “I've seen this before.” Dedupe on generation_id instead — it stays identical across every attempt for the same event, so if your endpoint already processed one and we retried anyway (your 200 got lost in transit, for example), the retry is recognizably the same generation.

const seen = new Set<string>()

app.post('/vivix/webhook', (req, res) => {
  const event = JSON.parse(req.body.toString('utf8'))
  if (seen.has(event.generation_id)) return res.status(200).end()  // already handled
  seen.add(event.generation_id)
  // …handle
  res.status(200).end()
})

In production, back seen with Redis or a database table with a unique index on generation_id. Keep X-Vivix-Delivery-Id around too — log it alongside each attempt so a support request can point at the exact delivery row.

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