# Mynth Changelog

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

Filtered by product=sdk · breaking=true.
Entries 1–6 of 6, newest first. Page 1 of 1.

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-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-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 — 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-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-08-02 — Rate and alt take a single image (improvement · SDK, CLI · sdk 0.0.34 · BREAKING)

> The rate and alt APIs are simplified to one image per call.

`mynth.image.rate` and `mynth.image.alt` now take a single image instead of a batch. Calls that passed an array must make one call per image. The batched form is gone from the SDK and the CLI.

Permalink: https://mynth.io/changelog/2026-08-02-single-image-rate-and-alt · Markdown: https://mynth.io/changelog/2026-08-02-single-image-rate-and-alt.md
