Docs
Open console
Core concepts

Webhooks

Get a signed POST when a job finishes, instead of polling for it.

v2 preview2 min read

Set up in three steps

  1. 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, pass webhook_url on its create call instead.

  2. 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_url passed on a create call is signed with the account secret on the same page. See Verifying the signature.

  3. Step 3: Answer 2xx within 15 seconds

    Reply first, then do the work. Use Send test in the console to try it.

Events

EventWhen
job.succeededA job finished and has outputs.
job.failedA job did not finish, and is refunded.
job.canceledA job was cancelled before it delivered, and is refunded.
credits.lowA 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.testYou 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.

POST to your endpoint
{  "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.

Under the hood: each delivery carries 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.