# Mynth Changelog

> Product updates across the Mynth API, SDK, CLI, and open-source packages.

Entries 1–20 of 40, newest first. Page 1 of 2.
Next page: https://mynth.io/changelog.md?page=2

Filter and page with query parameters, the same on /changelog.md, /changelog.txt and /changelog.json:

- `q` — words that must all appear (title, summary, body, version, model ids): `q=output.format`
- `product` — any of: api, sdk, cli, tanstack-ai-adapter (comma-separated)
- `type` — any of: feature, improvement, fix, models (comma-separated)
- `breaking=true` — breaking changes only
- `page` — from 1; `limit` — entries per page, 1–100, default 20

Example: https://mynth.io/changelog.md?product=sdk&breaking=true
Each entry is also at `https://mynth.io/changelog/<slug>.md`.

## 2026-10-13 — TanStack AI adapter calls Mynth directly (improvement · TanStack AI · BREAKING)

> 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](/docs/sdks/integrations/tanstack-start) 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](/docs/sdks/integrations/tanstack-start)
for the available error classes.

Permalink: https://mynth.io/changelog/2026-10-13-tanstack-ai-adapter · Markdown: https://mynth.io/changelog/2026-10-13-tanstack-ai-adapter.md

## 2026-10-13 — Weekly spending limits follow ISO weeks (fix · API)

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

Weekly [spending limits](/docs/authentication#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.

Permalink: https://mynth.io/changelog/2026-10-13-spending-limits · Markdown: https://mynth.io/changelog/2026-10-13-spending-limits.md

## 2026-10-13 — Updated dashboard (improvement · )

> Manage destinations on their own pages, inspect task outputs at full size, and expand wallet transactions to see their costs.

The [dashboard](/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.

Permalink: https://mynth.io/changelog/2026-10-13-dashboard · Markdown: https://mynth.io/changelog/2026-10-13-dashboard.md

## 2026-10-12 — Aspect ratio for video (feature · API, SDK · BREAKING)

> 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](/docs/guides/generate-images#sizes). Mynth picks the closest aspect
ratio the model serves.

```json
{
  "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:

| Before                | Now                                                                         |
| --------------------- | --------------------------------------------------------------------------- |
| `resolution: "1080p"` | `size: "auto_1080p"`                                                        |
| no aspect ratio       | `size: "9:16"`, or `size: "9:16_1080p"` with a tier                         |
| no structured size    | `size: { 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](/docs/guides/generate-video#sizes) has the full list.

Permalink: https://mynth.io/changelog/2026-10-12-video-size · Markdown: https://mynth.io/changelog/2026-10-12-video-size.md

## 2026-10-12 — Image inputs without a content type (fix · API)

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

Permalink: https://mynth.io/changelog/2026-10-12-image-input-content-type · Markdown: https://mynth.io/changelog/2026-10-12-image-input-content-type.md

## 2026-10-08 — A failed public access token names its task (improvement · API, SDK)

> 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`:

```json
{
  "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](/docs/api-reference/errors#request-errors).

Permalink: https://mynth.io/changelog/2026-10-08-public-access-token-failure · Markdown: https://mynth.io/changelog/2026-10-08-public-access-token-failure.md

## 2026-10-07 — API keys and webhooks return the full object (improvement · API)

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

Permalink: https://mynth.io/changelog/2026-10-07-full-api-key-and-webhook-objects · Markdown: https://mynth.io/changelog/2026-10-07-full-api-key-and-webhook-objects.md

## 2026-10-07 — Consistent request error codes (fix · API · BREAKING)

> 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](/docs/api-reference/errors#request-errors).

Permalink: https://mynth.io/changelog/2026-10-07-error-code-consistency · Markdown: https://mynth.io/changelog/2026-10-07-error-code-consistency.md

## 2026-10-06 — Qwen Image 2.1 Pro (models · API, SDK)

> Qwen Image 2.1 Pro is live for text-to-image and image edits.

[Qwen Image 2.1 Pro](/models/alibaba%2Fqwen-image-2.1-pro) is available now.

Permalink: https://mynth.io/changelog/2026-10-06-qwen-image-2-1-pro · Markdown: https://mynth.io/changelog/2026-10-06-qwen-image-2-1-pro.md

## 2026-10-05 — Stable API and Standard Webhooks (improvement · API, SDK, CLI · BREAKING)

> 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             |
| `spendingLimit` number, period defaulted to monthly | `spending_limit` decimal string, `spending_limit_period` required       |
| A video's `url` always set                          | `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](/docs/api-reference/errors).

## Webhooks

Deliveries follow [Standard Webhooks](https://www.standardwebhooks.com):

- 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](/dashboard/webhooks) 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](/changelog/2026-10-05-sdk-stable-api).
[Webhook payloads](/docs/api-reference/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`.

Permalink: https://mynth.io/changelog/2026-10-05-stable-api · Markdown: https://mynth.io/changelog/2026-10-05-stable-api.md

## 2026-10-05 — SDK for the stable API, with verifyWebhook (improvement · SDK · sdk 0.0.50 · BREAKING)

> 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](/changelog/2026-10-05-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:

```ts
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](/docs/concepts/webhooks) and
[webhook payloads](/docs/api-reference/webhook-payloads).

Permalink: https://mynth.io/changelog/2026-10-05-sdk-stable-api · Markdown: https://mynth.io/changelog/2026-10-05-sdk-stable-api.md

## 2026-10-04 — Completed webhooks include task cost (improvement · API)

> 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:

```json
{
  "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](/docs/api-reference/webhook-payloads#body).

Permalink: https://mynth.io/changelog/2026-10-04-webhook-task-cost · Markdown: https://mynth.io/changelog/2026-10-04-webhook-task-cost.md

## 2026-10-04 — Validation messages include field paths (improvement · API)

> 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](/docs/api-reference/errors#validation-errors).

Permalink: https://mynth.io/changelog/2026-10-04-validation-field-paths · Markdown: https://mynth.io/changelog/2026-10-04-validation-field-paths.md

## 2026-10-04 — TanStack AI adapter supports SDK 0.0.49 (fix · TanStack AI · tanstack-ai-adapter 0.0.33)

> 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](/docs/sdks/integrations/tanstack-start#tanstack-ai).

Permalink: https://mynth.io/changelog/2026-10-04-tanstack-ai-sdk-plain-results · Markdown: https://mynth.io/changelog/2026-10-04-tanstack-ai-sdk-plain-results.md

## 2026-10-04 — Plain SDK results for polling and webhooks (improvement · SDK · sdk 0.0.49 · BREAKING)

> 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()`.

See [generating media](/docs/sdks/typescript/generating-media) and
[webhooks](/docs/concepts/webhooks).

Permalink: https://mynth.io/changelog/2026-10-04-sdk-plain-results · Markdown: https://mynth.io/changelog/2026-10-04-sdk-plain-results.md

## 2026-10-04 — Magic Prompt for FLUX.3 (improvement · API)

> Set magic_prompt to rewrite text-to-image prompts for FLUX.3 while preserving your text, subject counts, and layout.

[FLUX.3](/models/black-forest-labs%2Fflux-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](/docs/guides/enhance-prompts).

Permalink: https://mynth.io/changelog/2026-10-04-flux-3-magic-prompt · Markdown: https://mynth.io/changelog/2026-10-04-flux-3-magic-prompt.md

## 2026-10-02 — FLUX.3 (models · API, SDK)

> FLUX.3 is live for text-to-image and image edits.

[FLUX.3](/models/black-forest-labs%2Fflux-3) is available now.

Permalink: https://mynth.io/changelog/2026-10-02-flux-3 · Markdown: https://mynth.io/changelog/2026-10-02-flux-3.md

## 2026-10-01 — Ideogram 4.5 (models · API, SDK)

> Ideogram 4.5 is live for text-to-image and image edits.

[Ideogram 4.5](/models/ideogram%2Fideogram-4.5) is available now.

Permalink: https://mynth.io/changelog/2026-10-01-ideogram-4-5 · Markdown: https://mynth.io/changelog/2026-10-01-ideogram-4-5.md

## 2026-09-26 — Request errors say what to fix (improvement · API, SDK, CLI · BREAKING)

> 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:

```json
{
  "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](/docs/api-reference/errors#validation-errors).

Permalink: https://mynth.io/changelog/2026-09-26-clearer-request-errors · Markdown: https://mynth.io/changelog/2026-09-26-clearer-request-errors.md

## 2026-09-25 — Balance and spending limit errors now return 402 (improvement · API, CLI · BREAKING)

> 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`:

```ts
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`.

Permalink: https://mynth.io/changelog/2026-09-25-billing-errors-402 · Markdown: https://mynth.io/changelog/2026-09-25-billing-errors-402.md

---

Next page: https://mynth.io/changelog.md?page=2
