Webhooks
Receive a signed POST when a task settles instead of polling. Register an endpoint, verify the signature, choose events and sources, and handle retries.
A webhook is an HTTP endpoint of yours that Mynth POSTs to when a task
completes or fails. Use one when the code that creates the task cannot wait
for it: serverless handlers, request/response paths where a user is waiting,
and long video renders. If your code can hold a connection, await the SDK
call or poll instead.
Add a Mynth webhook receiver to this project. Read npx @mynthio/cli docs get concepts/webhooks
first. Use the SDK helper for this stack if there is one, and fit the handler into the repo's
existing routes. Read the secret from MYNTH_WEBHOOK_SECRET. Verify the signature against the raw
request body, never re-serialized JSON. Return 2xx quickly and deduplicate on X-Mynth-Delivery.
Ask me which events to subscribe to. The CLI (npx @mynthio/cli webhook create) can register the
endpoint.
How delivery works#
task settles
│
▼
POST https://your-app.com/api/mynth-webhook
headers: X-Mynth-Event, X-Mynth-Delivery, X-Mynth-Signature
body: { event, task: { id }, request, result | errors }
│
├─ 2xx within 30 s ──▶ done
└─ anything else ────▶ retried: 5 s, 10 s, 20 s … up to 1 h apart, 12 attempts
same X-Mynth-Delivery on every attemptCreating the task needs nothing extra: every registered endpoint that subscribed to the event receives it. Each endpoint gets its own delivery, so one failing endpoint never holds back another.
Register an endpoint#
npx @mynthio/cli webhook create \
--url https://your-app.com/api/mynth-webhook \
-e task.image.generate.completed \
-e task.image.generate.failedThe response carries a wbs_... signing secret. Store it where your app reads
environment variables:
MYNTH_WEBHOOK_SECRET=wbs_...The API and the CLI return the secret only on create. The endpoint's page in
the dashboard keeps showing it. Webhook endpoints need
a key with the manage scope. The key from npx @mynthio/cli auth login has
it.
PUT /webhook/{id} replaces the whole configuration, so send url, events,
and enabled every time. An omitted oauthEnabled becomes false, and an
omitted apiKeyIds becomes every key. npx @mynthio/cli webhook update
behaves the same way. DELETE /webhook/{id} removes the endpoint and its
secret.
Receive events#
The SDK helpers verify the signature and call the handler for the event. They
answer 200 for an event with no handler, so subscribe only to what you
handle. Every helper takes the handlers below, image and video alike.
| Handler | Event |
|---|---|
imageTaskCompleted | task.image.generate.completed |
imageTaskFailed | task.image.generate.failed |
imageRateTaskCompleted | task.image.rate.completed |
imageRateTaskFailed | task.image.rate.failed |
imageAltTaskCompleted | task.image.alt.completed |
imageAltTaskFailed | task.image.alt.failed |
imageReviewTaskCompleted | task.image.review.completed |
imageReviewTaskFailed | task.image.review.failed |
imageRemoveBackgroundTaskCompleted | task.image.remove_background.completed |
imageRemoveBackgroundTaskFailed | task.image.remove_background.failed |
videoTaskCompleted | task.video.generate.completed |
videoTaskFailed | task.video.generate.failed |
Next.js#
The handler reads the secret from MYNTH_WEBHOOK_SECRET on each request, or
from { webhookSecret } as the second argument. It rejects a bad or missing
signature, a timestamp more than five minutes off, a missing
X-Mynth-Delivery, and an X-Mynth-Event header that does not match the
body, all with 400.
TanStack Start#
Same checks as the Next.js helper. TanStack Start covers the callback context.
Convex#
Same checks as the Next.js helper. Convex covers the action context.
Any other stack#
X-Mynth-Signature is t=<unix seconds>,v1=<hex>. v1 is the hex
HMAC-SHA256 of <t>.<raw body>, keyed with the whole wbs_... secret.
raw_body = the request body as bytes, before any JSON parsing
t, v1 = parse X-Mynth-Signature
reject unless |now_unix − t| <= 300
expected = hex(HMAC-SHA256(key = secret, message = t + "." + raw_body))
reject unless expected equals v1, compared in constant time
payload = JSON.parse(raw_body)
handle payload.event, return 2xxIn Node.js:
Verify the raw body exactly as received. Parsing the JSON and serializing it again changes the
bytes, and the signature no longer matches. On Express, mount express.raw({ type: "application/json" }) on the webhook route instead of express.json().
Events#
Subscribe an endpoint to exact events, to a status across all task types, or to everything:
| Subscription | Receives |
|---|---|
task.<type>.completed, task.<type>.failed | That task type settling. <type> is image.generate, image.rate, image.alt, image.review, image.remove_background, or video.generate |
task.completed, task.failed | Any task type settling that way |
all | Every event, including future task types |
The delivered event is always the exact name, such as
task.image.alt.completed, never all. An endpoint that matches through
several of its subscriptions still gets one delivery. The body and an example
for every task type are on webhook payloads.
Sources#
An endpoint receives only tasks from the sources it allows:
| Source | Default | Setting |
|---|---|---|
| Tasks from API keys | on, every key | apiKeyIds (CLI: repeat --api-key-id) limits it to those keys |
| Tasks with no API key, such as playground runs | off | oauthEnabled: true (CLI: --oauth-events) |
Tasks from the CLI carry the API key that auth login stored, so they deliver
on the default. You can also set sources in the endpoint's Sources section in
the dashboard.
Delivery behavior#
| Behavior | Value |
|---|---|
| Success | Any 2xx within 30 seconds |
| Failure | Any other status, a timeout, or a connection error |
| Redirects | Not followed. A 3xx counts as a failure |
| Retries | 12 attempts, 5 s apart at first, doubling up to 1 h apart. About 2.5 h in total |
| Duplicates | Possible. Every attempt repeats the same X-Mynth-Delivery, so deduplicate on it |
| Ordering | Not guaranteed. Two tasks can arrive in either order |
| Config changes | Endpoint edits can take up to 5 minutes to reach deliveries |
Deliveries are at least once. Make the handler idempotent: store
X-Mynth-Delivery and skip a value you have already handled. The SDK helpers
pass it to every handler as deliveryId. Answer first and
do slow work afterwards, because a handler that runs past 30 seconds counts as
a failure and is retried.
Per-request webhooks#
POST /image/generate, POST /image/remove-background, and
POST /video/generate take a webhook field for one-off URLs that are not
registered:
{
"model": "black-forest-labs/flux.2-pro",
"prompt": "A lighthouse at dusk, film grain",
"webhook": {
"dashboard": false,
"custom": [{ "url": "https://your-app.com/hooks/one-off/9f2c1e" }]
}
}custom takes up to five URLs. dashboard: false skips your registered
endpoints for this task. Rate, alt text, and review requests do not take
webhook.
Custom URLs get no X-Mynth-Signature, and the SDK helpers reject unsigned deliveries. Anyone who
learns the URL can POST to it, so treat the payload as unverified, or put a hard-to-guess token in
the URL path. The task record and other payloads show only the first 12 characters of that path.
Testing locally#
There is no test-event generator. A real task is cheap enough. Expose your local server with a tunnel, register the tunnel URL, and create a task:
ngrok http 3000
npx @mynthio/cli webhook create --url https://your-tunnel.ngrok.app/api/mynth-webhook -e all
npx @mynthio/cli image generate -m black-forest-labs/flux.2-klein-4b -p "A lighthouse at dusk"Subscribing to all while testing means a refused or failed task still sends
you something.
Next steps#
- Webhook payloads: headers, signature, and the body for each task type.
- Tasks: what
completedandfailedmean when the POST arrives. - Poll for results: the alternative when something can wait.