Changelog

40 entries

Improvement

TanStack AIBreaking

TanStack AI adapter calls Mynth directly

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.

Fix

API

Weekly spending limits follow ISO weeks

Weekly API key spending limits reset on Monday in UTC, including across year boundaries.

Weekly spending limits now use ISO weeks, which start on Monday at midnight UTC. The week stays together when it spans December and January.

A key with no usage in the current period reports zero spent. A rejected reservation also keeps the period's usage counter until the period ends.

Improvement

Updated dashboard

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.

Feature

API, SDKBreaking

Aspect ratio for video

Video takes a size, with the same shapes as images. The resolution tier moves into it.

POST /video/generate takes size, with the same shapes as images. Mynth picks the closest aspect ratio the model serves.

{
  "model": "bytedance/seedance-2.0-mini",
  "prompt": "A lighthouse beam sweeping over waves",
  "size": "9:16_720p"
}

The resolution tier moves into size, so the top-level resolution field is gone:

BeforeNow
resolution: "1080p"size: "auto_1080p"
no aspect ratiosize: "9:16", or size: "9:16_1080p" with a tier
no structured sizesize: { type: "aspect_ratio", aspect_ratio: "9:16", resolution: "1080p" }

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.

Fix

API

Image inputs without a content type

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.

Improvement

API, SDK

A failed public access token names its task

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.

See request errors.

Improvement

API

API keys and webhooks return the full object

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.

Fix

APIBreaking

Consistent request error codes

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:

StatusBeforeNow
400destinations_invalid_providerdestination_invalid_provider
413validation_errorpayload_too_large
500unknown_errorinternal_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.

Improvement

API, SDK, CLIBreaking

Stable API and Standard Webhooks

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

BeforeNow
taskId, estimatedCost, createdAt, ...task_id, estimated_cost, created_at, ... in every body
size.aspectRatiosize.aspect_ratio
Unknown request fields dropped400 validation_error, naming each field
count: 1.5 acceptedcount is a whole number from 1 to 20
A token by default, in access.publicAccessTokengenerate_public_access_token: true, returned as public_access_token
webhook.dashboard: falsewebhook.registered: false
/webhook, /api-key/webhooks, /api-keys
oauthEnabledinclude_session_tasks
apiKeyIds: [] meant every keyapi_key_ids: null or omitted is every key, [] is none
spendingLimit number, period defaulted to monthlyspending_limit decimal string, spending_limit_period required
A video's url always seturl can be null, like an image's. Read url ?? mynth_url
webhook.custom URLs shown with 12 path charactersShown 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.

Webhooks

Deliveries follow Standard Webhooks:

  • 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.

Improvement

SDKsdk 0.0.50Breaking

SDK for the stable API, with verifyWebhook

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:

BeforeNow
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 setSet 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.WebhookPayloadMynthSDKTypes.WebhookEvent
(result, { deliveryId }) in webhook callbacks(result, { event }). Deduplicate on event.id
wbs_... in MYNTH_WEBHOOK_SECRETThe 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:

import { verifyWebhook } from "@mynthio/sdk";

const event = await verifyWebhook({ body: await request.text(), headers: request.headers });

if (event.type === "task.image.generate.completed") {
  console.log(event.id, event.data.result.images);
}

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.

See webhooks and webhook payloads.

Improvement

API

Completed webhooks include task cost

Read the final billed cost from task.cost on any completed task webhook.

Every task.*.completed webhook now includes task.cost, the final USD charge as a decimal string with eight decimal places:

{
  "event": "task.image.generate.completed",
  "task": {
    "id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP",
    "cost": "0.03000000"
  }
}

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.

Improvement

API

Validation messages include field paths

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.

Fix

TanStack AItanstack-ai-adapter 0.0.33

TanStack AI adapter supports SDK 0.0.49

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.

See the TanStack AI integration.

Improvement

SDKsdk 0.0.49Breaking

Plain SDK results for polling and webhooks

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:

BeforeNow
result.idresult.taskId
result.getImages()result.images
result.getVideos()result.videos
result.getMetadata()result.metadata
result.result.modelresult.model
result.result.magic_promptresult.magicPrompt
result.getImages({ includeFailed: true })Read successful images and errors in failures separately
image.mynth_urlimage.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().

See generating media and webhooks.

Improvement

API

Magic Prompt for FLUX.3

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.

Improvement

API, SDK, CLIBreaking

Request errors say what to fix

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.

Improvement

API, CLIBreaking

Balance and spending limit errors now return 402

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.