MakeAIVideo

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

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.

Node.js
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)
}
Python
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. 1. 30s
  2. 2. 2m
  3. 3. 10m
  4. 4. 1h
  5. 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.

Re-enable
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
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.