Webhooks
Renders take minutes. Subscribe to an event and we will call you when it is done, instead of you polling until it is.
Create a subscription
The URL must be public and https.
curl -X POST https://app.makeaivideo.ai/api/v1/webhooks \
-H "Authorization: Bearer $MAV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/makeaivideo",
"events": ["video.ready"]
}'The signing secret is returned once, in the create response. Store it then; it is not retrievable afterwards.
What a delivery looks like
Every delivery is signed and carries these headers.
X-MakeAIVideo-Signature: sha256=<HMAC-SHA256(raw body, secret)>
X-MakeAIVideo-Event: video.ready
X-MakeAIVideo-Event-Id: <unique per event, use it to deduplicate>
X-MakeAIVideo-Timestamp: <unix seconds>
X-MakeAIVideo-Attempt: <1 on first delivery, higher on a retry>Deliveries can repeat. Treat X-MakeAIVideo-Event-Id as a deduplication key and make your handler idempotent: a retry after your server accepted but failed to respond will arrive with the same id.
Verify the signature
The signature is an HMAC-SHA256 of the raw request body, keyed with your webhook secret. Compare it in constant time, and reject anything that does not match.
import crypto from "node:crypto"
// The raw body matters. Verify BEFORE parsing JSON — re-serialising a
// parsed object changes the bytes and the signature will never match.
export function verify(rawBody, header, secret) {
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex")
const a = Buffer.from(header ?? "")
const b = Buffer.from(expected)
// Length check first: timingSafeEqual throws on a length mismatch.
return a.length === b.length && crypto.timingSafeEqual(a, b)
}import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(header or "", expected)Verify against the raw bytes. Most frameworks parse JSON before your handler runs, and re-serialising that object produces different bytes and a signature that never matches. In Express, that means express.raw() on the webhook route.
Retries
Respond 2xx to acknowledge. Anything else is a failure and we retry on this schedule, then stop.
- 1. 30s
- 2. 2m
- 3. 10m
- 4. 1h
- 5. 6h
Twenty consecutive failures disable the webhook. It stays disabled until you re-enable it explicitly, so a dead endpoint does not silently absorb events forever.
curl -X PATCH https://app.makeaivideo.ai/api/v1/webhooks/wh_... \
-H "Authorization: Bearer $MAV_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "is_active": true }'GET /webhooks/{id}/deliveries returns the last 50 attempts, which is usually enough to see why an endpoint started failing.
Test it
Send a signed ping from our servers to check your endpoint and your signature check without waiting for a real render. The payload carries data.test = true.
curl -X POST https://app.makeaivideo.ai/api/v1/webhooks/wh_.../test \
-H "Authorization: Bearer $MAV_API_KEY"Full endpoint list in the Webhooks reference.
Webhook questions
- Should I poll or use a webhook?
- Use a webhook for anything running unattended. Polling is fine for a script you are watching, but a render takes minutes and a webhook removes the loop entirely.
- My signature check never matches. Why?
- Almost always because the body was parsed before verification. The HMAC is over the raw bytes, and re-serialising a parsed JSON object produces different bytes. Capture the raw body first, verify, then parse.
- What happens if my endpoint is down?
- We retry five times over roughly six hours, then stop. Twenty consecutive failures disable the subscription until you re-enable it, so a dead endpoint does not silently swallow events.