Webhook payloads
The HTTP request Mynth sends when a task settles. Standard Webhooks headers and signature, event names, and the event body.
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.
Deliveries follow Standard Webhooks, so any of its libraries verifies them.
POST https://your-app.com/api/mynth-webhook
Content-Type: application/json
User-Agent: Mynth-Webhook/1.0 (+https://mynth.io)
webhook-id: evt_01KE7Y9M3R8T2QXN5BVC4WJ6HD
webhook-timestamp: 1791108012
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{"id":"evt_01KE7Y9M3R8T2QXN5BVC4WJ6HD","type":"task.image.generate.completed","timestamp":"2026-10-04T10:00:12.000Z","data":{...}}| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Mynth-Webhook/1.0 (+https://mynth.io) |
webhook-id | The event ID, evt_.... The same for every delivery of the event, and equal to the body id |
webhook-timestamp | When this attempt was sent, in Unix seconds. Each retry sends a new one |
webhook-signature | v1,<base64>. Only on endpoints registered in the dashboard or with POST /webhooks |
webhook-idEvery delivery of one event carries the same ID: to each of your endpoints, on every retry, and after Mynth recovers from an outage. Store it and skip any ID you have already handled. Treat it as an opaque string: do not parse it.
An endpoint registered twice with the same URL gets the event twice, with the same ID. Deduplicating drops the second one, which is correct.
A registered endpoint gets a whsec_ secret when it is created. Every delivery
to it is signed with that secret:
webhook-signature: v1,<base64>
signature = base64( HMAC-SHA256( key = base64-decode( secret without "whsec_" ),
message = "<webhook-id>.<webhook-timestamp>.<raw body>" ) )., the timestamp, a ., then the body bytes
exactly as received. Parsing and re-serializing the JSON changes the bytes
and breaks the match.webhook-timestamp. Reject a
timestamp more than five minutes from your clock: that stops replays and
still accepts late retries.webhook-id to get past your
deduplication fails verification.v1 today; accept the
delivery when any v1 entry matches. Compare in constant time.Per-request webhook.custom URLs get no signature. They carry webhook-id
and webhook-timestamp but anyone who knows the URL can post to them: put a
secret token in the URL and check it. See
per-request webhooks.
verifyWebhook() in the SDK, the SDK's framework helpers, and every
Standard Webhooks library
verify all of this for you.
The event type is always the exact name, task.<type>.<status>:
| Task type | Completed | Failed |
|---|---|---|
image.generate | task.image.generate.completed | task.image.generate.failed |
image.remove_background | task.image.remove_background.completed | task.image.remove_background.failed |
image.upscale | task.image.upscale.completed | task.image.upscale.failed |
image.rate | task.image.rate.completed | task.image.rate.failed |
image.alt | task.image.alt.completed | task.image.alt.failed |
image.review | task.image.review.completed | task.image.review.failed |
video.generate | task.video.generate.completed | task.video.generate.failed |
completed means the task finished with results. Items in the result can
still have failed: check each one's status. failed means the task failed
as a whole and has no results; data.errors says why.
An endpoint can also subscribe to task.completed, task.failed or all.
Those decide what reaches the endpoint, not what an event 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 gets one delivery.
all also covers task types added later, so answer an unknown type with a
2xx and ignore it.
{
"id": "evt_01KE7Y9M3R8T2QXN5BVC4WJ6HD",
"type": "task.image.alt.completed",
"timestamp": "2026-10-04T10:00:12.000Z",
"data": {
"id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP",
"type": "image.alt",
"status": "completed",
"request": { "url": "https://assets.example.com/lighthouse.webp" },
"result": { "alt": "A white lighthouse on dark rocks, lit by an orange sunset sky" },
"errors": null,
"cost": "0.00040000",
"user_id": "user_01KE7XS3J0V5N8QF2PZ6W4MBRC",
"api_key_id": "ak_01KE7XT9K2C6M4RX8NBV1QZJ5W",
"created_at": "2026-10-04T10:00:00.000Z",
"updated_at": "2026-10-04T10:00:12.000Z"
}
}| Field | Type | Notes |
|---|---|---|
id | string | The event ID, equal to the webhook-id header |
type | string | See events |
timestamp | string | ISO 8601. When the event happened: the task's updated_at, not when this attempt was sent |
data | object | The task exactly as GET /tasks/{id} returns it: same fields, same nulls, redacted |
data.request comes back with defaults filled in and metadata unchanged, so
match the task to your own records with data.request.metadata. Only image
generate, remove background, upscale and video generate take metadata. For
rate, alt and review, store the task_id from the create response instead.
URLs in data.request.webhook.custom are cut down to scheme://host, the same
as on GET /tasks/{id}, so a token in a per-request URL never reaches your
other endpoints.
These show data only; the envelope around it is the same for every event.
{
"id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP",
"type": "image.generate",
"status": "completed",
"request": {
"model": "black-forest-labs/flux.2-pro",
"prompt": "A lighthouse at dusk, film grain",
"count": 2,
"metadata": { "order_id": "ord_42" }
},
"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."
}
}
]
},
"errors": null,
"cost": "0.03000000",
"user_id": "user_01KE7XS3J0V5N8QF2PZ6W4MBRC",
"api_key_id": "ak_01KE7XT9K2C6M4RX8NBV1QZJ5W",
"created_at": "2026-10-04T10:00:00.000Z",
"updated_at": "2026-10-04T10:00:12.000Z"
}task.image.generate.completed and task.video.generate.completed do not mean the media exists.
Walk data.result.images or data.result.videos and check each status. A completed task can
hold only failed items.
verifyWebhook() returns the event typed as a union discriminated on type:
import { verifyWebhook } from "@mynthio/sdk";
const event = await verifyWebhook({ body: await request.text(), headers: request.headers });
switch (event.type) {
case "task.image.generate.completed":
for (const image of event.data.result.images) {
if (image.status === "success") console.log(image.url ?? image.mynth_url);
}
break;
case "task.image.generate.failed":
console.error(event.data.id, event.data.errors);
break;
}The type is MynthSDKTypes.WebhookEvent, and one variant is
MynthSDKTypes.WebhookEventOf<"image.alt", "completed">. The framework helpers
in @mynthio/sdk/next, /tanstack-start and /convex map completed events to
the same results as polling. See
receive events.
Answer any 2xx within 30 seconds, then do the slow work. Mynth does not
follow redirects and ignores the response body. Anything else gets retried
with the same webhook-id. The full schedule is in
delivery behavior.
data.