AI agents: The changelog is available as markdown at https://mynth.io/changelog.md, JSON at https://mynth.io/changelog.json and plain text at https://mynth.io/changelog.txt. Each takes q (search words), product (api, sdk, cli, tanstack-ai-adapter), type (feature, improvement, fix, models), breaking=true, page and limit (up to 100); lists are comma-separated. Every entry is also at https://mynth.io/changelog/<slug>.md.
Use the adapter without the SDK, configure a gateway or custom fetch, and build model selectors with display names and capabilities.
The TanStack AI adapter now calls the Mynth API
directly and no longer needs @mynthio/sdk.
Configure baseURL, defaultHeaders, or a custom fetch for a gateway.
MYNTH_IMAGE_MODEL_META provides model display names and capabilities, and
isMynthImageModel() validates model IDs. The catalog includes Qwen Image
2.1 Pro and Gemini Nano Banana 2.1.
Error classes now come from @mynthio/tanstack-ai-adapter. Update imports
that previously came from the SDK. Failed tasks throw MynthTaskFailedError
with the task's errors. See the adapter documentation
for the available error classes.
Manage destinations on their own pages, inspect task outputs at full size, and expand wallet transactions to see their costs.
The dashboard has a new layout, with navigation in a sidebar
on desktop and a menu on mobile.
Destinations now have separate list, create, and detail pages. Open a
destination to edit its settings, test an upload, or delete it.
Task details show outputs at their original aspect ratio. Wallet transactions
expand to show the cost breakdown and a link to the task. The overview's daily
spending chart supports keyboard navigation, and dates use your time zone.
The tier is still strict, because it sets the price. A tier the model does not
serve answers 400 validation_error. With a first frame, the frame sets the
shape and the aspect ratio is ignored.
Each video in the result has aspect_ratio, the ratio it was rendered at, or
null when a first frame set it. The SDK returns it as aspectRatio.
Generate video has the full list.
Image editing accepts input URLs that omit the Content-Type header.
Image editing through Google, DeepInfra, and Cloudflare AI now detects the
image format when an input URL omits its Content-Type header. You can use
these URLs without adding the header to your image server.
When a task is created but its public access token cannot be generated, the create now answers 500 public_access_token_failed with the task's id, instead of 201 without the token.
A create that asks for generate_public_access_token: true used to answer
201 without public_access_token when the token could not be generated.
The task was created either way, and nothing said the token was missing on
purpose.
It now answers 500 public_access_token_failed, with the created task's id in
error.task_id:
{ "error": { "code": "public_access_token_failed", "message": "Task tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP was created and will run, but its public access token could not be generated. ...", "task_id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP" }}
The task runs. Follow it from your server, by polling or a webhook, rather
than resending the request, which would create and charge a second task. In
the SDK, MynthAPIError carries the id as taskId.
Creating, listing and updating an API key return the same fields, and updating a webhook returns the whole webhook.
Every API key endpoint now returns the same object: id, name,
key_preview, user_id, scopes, spending_limit, spending_limit_period,
created_at and updated_at. An unset name or spending_limit is null.
Creating a key used to leave both out, and only creating one returned user_id.
PUT /webhooks/{id} now returns the whole webhook, including user_id,
created_at and updated_at, like POST /webhooks does. The signing
secret is still returned only when the webhook is created.
Three request errors now use the same code as the rest of the API, a 500 no longer carries internal detail, and two not-found errors gain a message.
Three request error codes did not match the rest of the API:
Status
Before
Now
400
destinations_invalid_provider
destination_invalid_provider
413
validation_error
payload_too_large
500
unknown_error
internal_server_error
The 413 is an upload to POST /image/upload over 100 MB, and the 500 is a
failure to store or read a destination secret. If you branch on any of the old
codes, switch to the new ones.
Every 500 now has the message Internal server error. Some used to carry a
description of the internal step that failed.
task_not_found and api_key_not_found now include a message, like the
other not-found errors. See request errors.
One consistent public contract. snake_case everywhere, strict request bodies, an error envelope with lowercase codes, plural paths, and webhooks that follow Standard Webhooks with new whsec_ secrets.
The public API now has the contract we intend to keep. Every change below
lands at once, and none of them is backward compatible. Update your
integration before relying on it again.
Requests and responses
Before
Now
taskId, estimatedCost, createdAt, ...
task_id, estimated_cost, created_at, ... in every body
size.aspectRatio
size.aspect_ratio
Unknown request fields dropped
400 validation_error, naming each field
count: 1.5 accepted
count is a whole number from 1 to 20
A token by default, in access.publicAccessToken
generate_public_access_token: true, returned as public_access_token
webhook.dashboard: false
webhook.registered: false
/webhook, /api-key
/webhooks, /api-keys
oauthEnabled
include_session_tasks
apiKeyIds: [] meant every key
api_key_ids: null or omitted is every key, [] is none
url can be null, like an image's. Read url ?? mynth_url
webhook.custom URLs shown with 12 path characters
Shown as scheme://host only
request.metadata is yours and comes back unchanged.
Errors
Every error is { "error": { "code", "message", "issues", "scopes" } },
including 404, 415 and 500. Codes are lowercase snake_case, in request
errors and in task failures alike: insufficient_balance,
restricted_content, provider_error. A call to /webhook or /api-key
answers 404 not_found with the new path in message. See
errors.
Headers are webhook-id, webhook-timestamp and webhook-signature
(v1,<base64>). X-Mynth-Event, X-Mynth-Delivery and X-Mynth-Signature
are gone.
Every registered webhook has a new whsec_ secret. Copy it from the
endpoint's page in the dashboard into
MYNTH_WEBHOOK_SECRET. The old wbs_ secrets no longer verify anything.
The body is an event, { id, type, timestamp, data }. data is the task
exactly as GET /tasks/{id} returns it, and id equals webhook-id, the
same on every retry. Deduplicate on it.
Failed deliveries retry for about 72 hours instead of 2.5, with jitter.
Webhook URLs must be https on a public host. Per-request webhook.custom
URLs stay unsigned: put a token in the query string and check it.
Any Standard Webhooks library verifies a delivery. In TypeScript, update to
the new SDK, which adds verifyWebhook(); see
the SDK migration.
Webhook payloads has the full format.
CLI
webhook create --oauth-events is --include-session-tasks, and
image generate --no-dashboard-webhooks is --no-registered-webhooks.
The SDK follows the stable API. snake_case request fields, opt-in browser tokens, lowercase error codes, and webhook handlers built on Standard Webhooks events.
@mynthio/sdk@0.0.50 follows the stable API.
Earlier versions stop working against it, so upgrade and update your code:
Before
Now
size: { type: "aspect_ratio", aspectRatio }
size: { type: "aspect_ratio", aspect_ratio }
access: { pat: { enabled: false } }
Leave it out. Tokens are off by default
taskAsync.access.publicAccessToken always set
Set only with generate_public_access_token: true on the request
webhook: { dashboard: false }
webhook: { registered: false }
error.code === "INSUFFICIENT_BALANCE"
error.code === "insufficient_balance"
MynthSDKTypes.WebhookPayload
MynthSDKTypes.WebhookEvent
(result, { deliveryId }) in webhook callbacks
(result, { event }). Deduplicate on event.id
wbs_... in MYNTH_WEBHOOK_SECRET
The endpoint's new whsec_... secret, from the dashboard
Results from polling and webhook callbacks keep their shape: result.taskId,
result.images, image.mynthUrl. Raw API data the SDK passes through is
snake_case now: raw.request, the model catalog (display_name,
per_image), and video.estimate() (estimated_cost, estimate_kind). A
video's url can be null, and result.urls skips it.
verifyWebhook() verifies a delivery outside the framework helpers and
returns the typed event:
The helpers take onEvent for every event, including types newer than the
SDK. They refuse unsigned deliveries to a webhook.custom URL unless you pass
{ unsigned: { verify } }, which checks a token you put in the URL instead.
This is an excerpt of the payload. The webhook still includes request and
result. Failed task webhooks keep only task.id in the task object.
See webhook payloads.
Rejected requests name each invalid field in the message, including fields inside nested objects.
The message on a 400 VALIDATION_ERROR now prefixes each schema issue with
its field path. A count above 20, for example, returns
count: Too big: expected number to be <=20. Nested fields use paths such as
inputs[0].source.url.
The response still includes issues with separate path and message
values. Use code and issues[].path when handling errors in code, since the
message wording can change. Unknown model IDs still suggest the closest ID.
See validation errors.
The TanStack AI adapter now reads the plain result objects returned by SDK 0.0.49.
Upgrade to @mynthio/tanstack-ai-adapter@0.0.33 to use SDK 0.0.49.
The adapter maps successful images, failed-image errors, rewritten prompts,
and task cost from the SDK's plain result objects.
SDK results are serializable objects with task cost, successful media, and failures; webhook callbacks receive the same result shape.
Completed SDK calls now return plain objects. You can pass them to client
components, server functions, and Convex mutations, or serialize them with
JSON.stringify.
Update result access when upgrading to @mynthio/sdk@0.0.49:
Before
Now
result.id
result.taskId
result.getImages()
result.images
result.getVideos()
result.videos
result.getMetadata()
result.metadata
result.result.model
result.model
result.result.magic_prompt
result.magicPrompt
result.getImages({ includeFailed: true })
Read successful images and errors in failures separately
image.mynth_url
image.mynthUrl
Images from generation, upscaling, and background removal share width,
height, format, mimeType, and delivery fields. url stays null when
delivery fails, so choose image.url ?? image.mynthUrl when either URL works.
raw.request and raw.result keep the API fields before mapping.
Framework webhook helpers now pass the same results as polling. Replace
payload.task.id and payload.result.images with result.taskId and
result.images. Failed callbacks receive a plain object with taskId and
errors, plus request metadata when supported.
Result names such as ImageGenerationResult are type-only exports. Remove
constructor calls and instanceof checks. Pending TaskAsync handles still
use id and wait().
Set magic_prompt to rewrite text-to-image prompts for FLUX.3 while preserving your text, subject counts, and layout.
FLUX.3 now has model-specific prompt
rewriting. Set model: "black-forest-labs/flux-3" and magic_prompt: true on
POST /image/generate to use it.
The rewrite uses natural language and preserves supplied text, subject
counts, and layout. Your original prompt stays in request.prompt, and
the rewritten prompt is returned in result.magic_prompt.positive.
See enhance prompts.
Every rejected request now returns a code and a message you can act on, schema failures list each invalid field, and malformed requests get their own codes.
A request that failed validation used to come back in the validator's own shape: { data, error, success } on /image/* and /video/*, { success, errors } elsewhere, with no code and your request echoed back. Now every request error has the same body:
{ "code": "VALIDATION_ERROR", "message": "model \"flux.2-pro\" is not supported. Did you mean \"black-forest-labs/flux.2-pro\"? See https://mynth.io/models, or GET /models, for the model IDs.", "issues": [{ "path": ["model"], "message": "model \"flux.2-pro\" is not supported. …" }]}
message states every problem, so it is safe to show as it is. issues has one { path, message } per field that failed the schema. An unknown model suggests the closest id, and an invalid size lists the forms it takes.
Two new codes cover requests that never reach validation:
INVALID_JSON (400) for a body that is not valid JSON, with the parser's position. It used to be plain text.
UNSUPPORTED_MEDIA_TYPE (415) for a JSON endpoint called without Content-Type: application/json, or POST /image/upload without multipart/form-data. Such a body used to be read as empty, so the error blamed a missing prompt.
A size a pinned model cannot produce, such as 16:9_4k on a model without 4k, is now rejected at create with 400 VALIDATION_ERROR and the size to use instead. It used to be accepted and then fail the task with CAPABILITY_NOT_SUPPORTED. With model: "auto" nothing changes.
Other messages got more specific too: input, resolution, duration, and audio errors list what the model accepts, UNAUTHORIZED says what is wrong with the credential, balance and spending-limit errors state the amounts, upload errors name the file, and a 404 names the route.
If your code reads error, errors, or data from a 400, read code, message, and issues instead. MynthAPIError in the SDK now carries issues and a useful message, and the CLI exits with 2 for both new codes. See errors.
Running out of balance or hitting a key's spending limit now returns 402 Payment Required, so 429 only ever means slow down.
INSUFFICIENT_BALANCE used to return 422 and SPENDING_LIMIT_EXCEEDED used to return 429. Both now return 402 Payment Required. The code and message in the body are the same.
The old 429 looked like a rate limit, so HTTP clients that retry 429 kept resending a request that could only succeed once the key's period rolled over. Now a 429 means back off and retry, and a 402 means top up, raise the cap, or wait for the period to reset.
If your code checks code, nothing changes. If it checks the status, look for 402 where you looked for 422 or 429:
if (error instanceof MynthAPIError && error.status === 402) { // error.code is INSUFFICIENT_BALANCE or SPENDING_LIMIT_EXCEEDED}
The CLI still exits with 4 for both codes, and now also for any other 402.