Webhooks
Get a signed POST when a job finishes, instead of polling for it.
Set up in three steps
Step 1: Add your endpoint
In the console’s Webhooks page, add a public
https://URL on your server. To hear about one job only, passwebhook_urlon its create call instead.Step 2: Check each message is from mnml
Each endpoint has its own signing secret: open Secret on its row, copy it, and verify every delivery with it. A
webhook_urlpassed on a create call is signed with the account secret on the same page. See Verifying the signature.Step 3: Answer 2xx within 15 seconds
Reply first, then do the work. Use Send test in the console to try it.
Events
| Event | When |
|---|---|
job.succeeded | A job finished and has outputs. |
job.failed | A job did not finish, and is refunded. |
job.canceled | A job was cancelled before it delivered, and is refunded. |
credits.low | A job settled with your balance under $4.95. At most once a day; data is { balance, threshold }, counted as GET /v2/account counts your balance. We email you too. |
webhook.test | You pressed Send test in the console. |
An endpoint gets every event unless you pick some. The console playground’s own runs send nothing.
The message
data is exactly what GET /v2/jobs/{id} answers, so one parser serves polling and webhooks. Output links are signed and expire; download what you keep.
{ "type": "job.succeeded", "timestamp": "2026-10-04T12:00:07.000Z", "data": { "id": "10482", "status": "succeeded", "kind": "render", "engine": "v4.6", "outputs": [ { "url": "https://api.mnml.ai/v2/files/10482?exp=1759575600&sig=Q2h4…", "media": "image", "expires_at": "2026-10-04T11:00:00.000Z" } ], "charged_usd": 0.2475, "refunded_usd": 0, "credits_charged": 25, "credits_refunded": 0, "error": null, "created_at": "2026-10-04T09:00:00.000Z", "completed_at": "2026-10-04T09:00:41.000Z" }}Verifying the signature
Each SDK verifies a delivery in one call, and rejects a bad signature or a stale timestamp. Deliveries follow the Standard Webhooks scheme, so its libraries work too, in any language. Verify against the raw body, before you parse it.
webhook-id, webhook-timestamp and webhook-signature, an HMAC-SHA256 of id.timestamp.body under the endpoint’s signing secret. Reject a timestamp more than five minutes old. Resetting a secret in the console signs every later delivery with the new one at once.import { verifyWebhook } from '@mnml-ai/sdk/webhooks'; // Next.js App Routerexport async function POST(request: Request) { const event = verifyWebhook( await request.text(), request.headers, process.env.MNML_WEBHOOK_SECRET!, ); if (event.type === 'job.succeeded') { // event.data is the job, as jobs.get returns it } return new Response(null, { status: 204 });}Retries
Answer with any 2xx within 15 seconds, then do the work. Anything else is retried after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours, about a day in all. A message can arrive more than once: use webhook-id to ignore a repeat.
Every attempt is in the console’s delivery log, with the status your endpoint answered, for 30 days. A delivery that ran out of retries can be sent again with Replay.
An endpoint you switch off or delete gets nothing more, its waiting retries included; a test event from the console still reaches a switched-off one. An endpoint that has answered nothing but errors for three days is switched off for you, and the console says so: switch it back on once it answers, and deliveries start again.
Endpoint URLs
- A public
https://address. Private and local addresses are refused when you save, and again when we connect. - Redirects are not followed: point at the final URL.
- Up to 10 endpoints per account.