Docs / Jobs

Webhooks

Get the result pushed to you when a job ends, instead of polling its status.

Two ways to get them

  • Per request. Add ?webhook_url=https://your.app/hooks to the submit. That request's events go there.
  • Per account. Add an endpoint in the console under Webhooks. Every request's events go there. You can pick which events it gets.

A request with a webhook_url on an account with endpoints sends to all of them, with the same webhook-id.

Events

EventWhen
request.completedThe job finished. data.payload has the result, as the result endpoint gives it.
request.failedThe job failed. You are not charged for it. data.error.code says why.
request.cancelledYou cancelled it. You are not charged for it.
request.expiredIt did not finish within 24 hours. You are not charged for it.
request.completed
{
  "type": "request.completed",
  "timestamp": "2026-10-12T18:02:11.482Z",
  "data": {
    "request_id": "0192f3a4-5b6c-7d8e-9f01-23456789abcd",
    "model_id": "seedance-2.5",
    "status": "COMPLETED",
    "state": "completed",
    "payload": { "video": { "url": "https://...", "content_type": "video/mp4", "file_size": 4183920, "width": 1280, "height": 720, "fps": 24, "duration": 5.04 }, "seed": null },
    "error": null,
    "billing": { "price": "1.750000", "currency": "USD" },
    "created_at": "2026-10-12T18:01:02.001Z",
    "completed_at": "2026-10-12T18:02:11.002Z"
  }
}

File links in a webhook work for 24 hours from that delivery. After that, read the result endpoint for fresh ones.

Verify the signature

Every delivery follows Standard Webhooks. Three headers come with it:

HeaderWhat
webhook-idmsg_ and an id, the same on every retry of one event. Use it to drop repeats.
webhook-timestampUnix seconds when this attempt was sent.
webhook-signaturev1, and a base64 HMAC-SHA256 of id.timestamp.body. Two of them, space-separated, for 24 hours after you rotate your secret.

Get your secret from GET /v1/webhooks/default/secret or from the console, then check every delivery with the Standard Webhooks library before you trust it.

import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.MINIAPI_WEBHOOK_SECRET); // whsec_...

app.post("/hooks", express.raw({ type: "application/json" }), (req, res) => {
  const event = wh.verify(req.body.toString(), req.headers); // throws if it does not match
  res.sendStatus(200);
  // then handle event.type and event.data
});

Use the raw body exactly as it arrived. Parsing and re-encoding the JSON first changes the bytes and the check fails.

Answer fast, and retries

Answer with any 2xx within 15 seconds, then do the work. Anything else counts as a failure: a 3xx is not followed, and a slower answer is a timeout. A failed delivery is tried again on this schedule, up to 10 attempts in about 3 days:

Attempts 1 to 5Attempts 6 to 10
1: now6: 5 hours later
2: 5 seconds later7: 10 hours later
3: 5 minutes later8: 10 hours later
4: 30 minutes later9: 24 hours later
5: 2 hours later10: 20 hours later

Every attempt shows in the console under Webhooks with its status code and how long it took. Press Resend on any of them to send that event again now, with the same webhook-id.

An endpoint that fails every delivery for 5 days is turned off, and we email the account owner. Turn it back on in the console once it is fixed.

Rotate the secret

Press Rotate in the console. The new secret signs every delivery from then on, and the old one signs too for 24 hours, so you can update your receivers without missing an event.

Addresses we refuse

The URL must be https, with no user name or password in it. We resolve the name and refuse it if any answer is a private or internal address: loopback, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, link-local (including 169.254.169.254), 100.64.0.0/10, 0.0.0.0/8, and their IPv6 forms. A submit with such a webhook_url answers 400 invalid_webhook_url.