Webhook payloads

The HTTP request Mynth sends when a task settles. Headers, signature, event names, and the body for each task type.

When a task settles, Mynth sends one POST to each endpoint that subscribed. This page describes that request exactly. Registering endpoints, receiver code for each framework, and retry behavior are on webhooks.

POST https://your-app.com/api/mynth-webhook
Content-Type: application/json
User-Agent: Mynth-Webhook/1.0 (+https://mynth.io)
X-Mynth-Event: task.image.generate.completed
X-Mynth-Delivery: tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP:dashboard:wbh_01KE7Y0C3D7XH2M9QK4VZ8TBN5:task.image.generate.completed
X-Mynth-Signature: t=1783159212,v1=5f0c6e...

{"event":"task.image.generate.completed","task":{"id":"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"},"request":{...},"result":{...}}

Headers#

HeaderValue
Content-Typeapplication/json
User-AgentMynth-Webhook/1.0 (+https://mynth.io)
X-Mynth-EventThe event name. Always equal to event in the body
X-Mynth-DeliveryA stable ID for this task, endpoint and event. Retries repeat it
X-Mynth-Signaturet=<unix seconds>,v1=<hex>. Only on endpoints registered in the dashboard or with POST /webhook

X-Mynth-Delivery#

The value has four parts separated by ::

<task id>:<dashboard|custom>:<endpoint key>:<event>

The endpoint key is the wbh_ id for a registered endpoint, or the first 16 hex characters of the URL's SHA-256 for a per-request URL. Every retry of one delivery sends the same value. Store it and skip any value you have already handled. Treat it as an opaque string and do not parse it.

Signature#

A registered endpoint gets a wbs_ secret when it is created. Every delivery to it is signed with that secret:

X-Mynth-Signature: t=<unix seconds>,v1=<hex>

v1 = hex( HMAC-SHA256( key = your full wbs_... secret,
                       message = "<t>." + raw request body ) )
  • The key is the whole secret string, wbs_ prefix included.
  • The message is t, a literal ., then the body bytes exactly as received. Parsing and re-serializing the JSON changes the bytes and breaks the match.
  • Each retry is signed again with a fresh t. A five-minute tolerance on t rejects replays and still accepts late retries.
  • Compare in constant time. Mynth sends one v1 today. If the header ever carries several, accept the delivery when any of them matches, as the SDK does.

Per-request webhook.custom URLs get no signature. Anyone who knows the URL can post to it.

The SDK helpers verify all of this for you. Node.js code and a language-neutral version are on webhooks.

Events#

The delivered event is always the exact name, task.<type>.<status>:

Task typeCompletedFailed
image.generatetask.image.generate.completedtask.image.generate.failed
image.remove_backgroundtask.image.remove_background.completedtask.image.remove_background.failed
image.ratetask.image.rate.completedtask.image.rate.failed
image.alttask.image.alt.completedtask.image.alt.failed
image.reviewtask.image.review.completedtask.image.review.failed
video.generatetask.video.generate.completedtask.video.generate.failed

An endpoint can also subscribe to task.completed, task.failed or all. Those decide what reaches the endpoint, not what it is called. An endpoint on all still receives task.image.alt.completed, never all. An endpoint that matches through more than one of its events still gets one delivery.

Branch on event, or on X-Mynth-Event if you want to route before reading the body. all also covers task types added later, so handle unknown events by answering 2xx and ignoring them.

Body#

{
  "event": "task.image.alt.completed",
  "task": { "id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP" },
  "request": { "url": "https://assets.example.com/lighthouse.webp" },
  "result": { "alt": "A white lighthouse on dark rocks, lit by an orange sunset sky" }
}
FieldTypePresentNotes
eventstringAlwaysSee events
task.idstringAlwaysThe tsk_ ID. The only field under task
requestobjectAlwaysThe task's request, defaults filled in, metadata unchanged when the type takes it
resultobject.completedSame shape as result on the task object
errors[{ code, message? }].failedAt least one entry. Codes are on errors

A completed payload has no errors key and a failed one has no result key. The key is missing, not null.

The body has no status, cost, apiKeyId, or timestamps. If you need them, call GET /tasks/{id} with task.id. To match the task to your own records, read request.metadata. You set it when you created the task, and it comes back unchanged. Only image generate, remove background, and video generate take metadata. For rate, alt, and review, store the taskId from the create response instead.

URLs in request.webhook.custom are shortened to scheme, host and the first 12 characters of the path, the same as on GET /tasks/{id}. A token in a per-request URL never reaches your other endpoints.

Examples#

{
  "event": "task.image.generate.completed",
  "task": { "id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP" },
  "request": {
    "model": "black-forest-labs/flux.2-pro",
    "prompt": "A lighthouse at dusk, film grain",
    "count": 2
  },
  "result": {
    "model": "black-forest-labs/flux.2-pro",
    "images": [
      {
        "status": "success",
        "id": "img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk",
        "url": "https://cdn.mynth.io/images/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp",
        "mynth_url": "https://cdn.mynth.io/images/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp",
        "size": "1536x1024",
        "format": "webp"
      },
      {
        "status": "failed",
        "error": {
          "code": "PROVIDER_ERROR",
          "message": "The provider failed to process the request. Please try again."
        }
      }
    ]
  }
}

task.image.generate.completed and task.video.generate.completed do not mean the media exists. Walk result.images or result.videos and check each status. A completed task can hold only failed items.

TypeScript types#

The SDK exports the whole payload as a union discriminated on event:

import type { MynthSDKTypes } from "@mynthio/sdk";

function handle(payload: MynthSDKTypes.WebhookPayload) {
  switch (payload.event) {
    case "task.image.generate.completed":
      for (const image of payload.result.images) {
        if (image.status === "success") console.log(image.url ?? image.mynth_url);
      }
      break;
    case "task.image.generate.failed":
      console.error(payload.task.id, payload.errors);
      break;
  }
}

Each variant also has its own name, such as MynthSDKTypes.WebhookTaskImageAltCompletedPayload. The framework helpers in @mynthio/sdk/next, /tanstack-start and /convex pass these types to your handlers already.

Responding#

Answer any 2xx within 30 seconds, then do the slow work. Mynth does not follow redirects and ignores the response body. Anything other than a 2xx gets retried with the same X-Mynth-Delivery. The full schedule and ordering rules are in delivery behavior.

Next steps#