Webhooks
A webhook tells your server when something happens, such as a video finishing. It saves you
polling GET /v1/job/{id}. Webhooks are managed with the same API key as the rest of the API,
under https://reignitor.com/api/webhooks.
Events
Section titled “Events”| Event | Sent when |
|---|---|
video.completed |
A render finished. The payload has the video URL. |
video.failed |
A render failed. A render you cancel does not send this. |
credits.low |
The account’s credit balance is running low. |
credits.purchased |
Credits were added to the account. |
subscription.created |
A plan subscription started. |
subscription.canceled |
A plan subscription was cancelled. |
user.signup |
The account was created. |
user.onboarded |
The account finished onboarding. |
GET /api/webhooks/events returns this list.
Set up a webhook
Section titled “Set up a webhook”-
Build an endpoint on your server that accepts
POSTrequests with a JSON body and answers2xxquickly. It must be reachable on the public internet. Private and local network addresses are refused. -
Subscribe it to an event. One webhook listens to one event; create one per event you need.
Terminal window curl https://reignitor.com/api/webhooks/hooks \-H "Authorization: Bearer $REIGNITOR_API_KEY" \-H "Content-Type: application/json" \-d '{"event": "video.completed","webhook_url": "https://hooks.example.com/reignitor","description": "Post finished videos to our CMS"}'const res = await fetch('https://reignitor.com/api/webhooks/hooks', {method: 'POST',headers: {Authorization: `Bearer ${process.env.REIGNITOR_API_KEY}`,'Content-Type': 'application/json',},body: JSON.stringify({event: 'video.completed',webhook_url: 'https://hooks.example.com/reignitor',description: 'Post finished videos to our CMS',}),});const { hook, secret } = await res.json();res = requests.post("https://reignitor.com/api/webhooks/hooks",headers={"Authorization": f"Bearer {os.environ['REIGNITOR_API_KEY']}"},json={"event": "video.completed","webhook_url": "https://hooks.example.com/reignitor","description": "Post finished videos to our CMS",},)data = res.json()hook, secret = data["hook"], data["secret"] -
Store the
secretfrom the201response. You need it to check signatures. -
Send a test.
POST /api/webhooks/hooks/{id}/testsends a signedpingevent to your URL and tells you what your endpoint answered:{ "delivered": true, "status": 200 }
Subscribing the same event and URL again does not create a duplicate. It returns the existing
webhook with reused: true and switches it back on.
What a delivery looks like
Section titled “What a delivery looks like”Every delivery is a POST with a JSON body:
{ "event": "video.completed", "payload": { "job_id": "7d2e4b10-5c3a-4f8e-a1b2-9c0d8e7f6a54", "user_id": "3f1c2a9e-8b7d-4c1e-9f0a-2d6b5e4c3a21", "output_url": "https://cdn.example.com/exports/7d2e4b10.mp4", "canvas_id": null, "scene_count": 4, "scenes_failed": 0, "credits_charged": 12 }, "emitted_at": "2026-09-25T10:15:00.000Z", "delivery_id": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed"}video.failed carries job_id, user_id, canvas_id and a reason.
Match payload.job_id to the job_id you got from POST /v1/render.
And these headers:
| Header | Value |
|---|---|
X-Reignitor-Event |
The event name. |
X-Reignitor-Delivery |
A unique ID for this delivery. It stays the same when a delivery is retried. |
X-Reignitor-Timestamp |
Unix time, in seconds, when the delivery was signed. |
X-Reignitor-Signature |
sha256= and the hex HMAC-SHA256 of {timestamp}.{raw body}, keyed with your secret. |
Verify the signature
Section titled “Verify the signature”Check every delivery before you trust it:
- Read the raw request body, before any JSON parsing.
- Build the string
{X-Reignitor-Timestamp}.{raw body}. - Compute HMAC-SHA256 of it with your secret, as hex, and add
sha256=in front. - Compare it with
X-Reignitor-Signatureusing a constant-time comparison. - Reject the delivery if the timestamp is more than 5 minutes old.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyReignitorWebhook(rawBody, headers, secret) { const timestamp = headers['x-banshea-timestamp']; const received = headers['x-banshea-signature'] ?? ''; if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = 'sha256=' + createHmac('sha256', secret) .update(`${timestamp}.${rawBody}`) .digest('hex');
const a = Buffer.from(expected); const b = Buffer.from(received); return a.length === b.length && timingSafeEqual(a, b);}import hashlib, hmac, time
def verify_reignitor_webhook(raw_body: bytes, headers, secret: str) -> bool: timestamp = headers.get("X-Reignitor-Timestamp") received = headers.get("X-Reignitor-Signature", "") if not timestamp or abs(time.time() - int(timestamp)) > 300: return False
signed = f"{timestamp}.".encode() + raw_body expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, received)Delivery and retries
Section titled “Delivery and retries”- Each delivery waits up to 10 seconds for your endpoint to answer.
- A
5xx, a429, a timeout or a network error is retried, up to 3 attempts in total, about a second apart. - Any other
4xxis not retried. - Retries keep the same
X-Reignitor-Delivery. Use it to ignore a delivery you have already handled. - After 20 failed deliveries in a row, the webhook is switched off.
GET /api/webhooks/hooksshowsactive: falseand the last error inlast_status. Fix your endpoint, then turn it back on withPATCH /api/webhooks/hooks/{id}and{ "active": true }.
Answer 2xx as soon as you have stored the delivery, and do slow work afterwards.
Manage webhooks
Section titled “Manage webhooks”| Method | Path | What it does |
|---|---|---|
GET |
/api/webhooks/events |
List event names and the signature scheme |
GET |
/api/webhooks/hooks |
List your webhooks |
POST |
/api/webhooks/hooks |
Create a webhook |
PATCH |
/api/webhooks/hooks/{id} |
Pause, resume, or change the URL or description |
DELETE |
/api/webhooks/hooks/{id} |
Delete a webhook |
POST |
/api/webhooks/hooks/{id}/test |
Send a signed ping |
An account can have up to 25 webhooks. Full request and response shapes are in the endpoint reference.