# Introduction > Mynth is one HTTP API in front of many image and video models. Send a model id and a prompt, get back a task, collect the file. Mynth is one API in front of many image and video models. You send a model id and a prompt. Mynth routes the request to a provider, retries on another one when it fails, sizes the output for that model, and hands you back a file URL. Switching models is a change to one field. Every model takes the same request body. There is no per-provider SDK, no per-model options bag, and no field that only one provider understands. ## How a generation runs ```text POST /image/generate or POST /video/generate │ ▼ 201 { task_id, estimated_cost } the estimate is held on your balance │ ├─ wait in the SDK, poll the task, or receive a webhook ▼ task completed result.images[].url a file URL, kept for 7 days or your own bucket, if you named a destination ``` There is no synchronous generate endpoint. `mynth.image.generate()` in the SDK looks synchronous because it creates the task and polls it for you. [Tasks](https://mynth.io/docs/concepts/tasks.md) covers statuses, partial failure, and how long files are kept. ## Rules worth knowing first - **Pass a model id.** Ids are `vendor/name`, copied from the [catalog](https://mynth.io/models), for example `black-forest-labs/flux.2-pro`. There are no aliases. Image generation accepts `auto` and uses it when `model` is omitted, but `auto` is experimental. See [the model field](https://mynth.io/docs/models.md#auto-is-experimental). - **Keep the API key on the server.** A browser polls one task with the short-lived `pat_` token the create call returns. See [authentication](https://mynth.io/docs/authentication.md). - **`completed` does not mean every image worked.** Each item in `result.images` or `result.videos` has its own `status`. - **Mynth is not a file host.** Generated files are served for 7 days. Name a [destination](https://mynth.io/docs/concepts/destinations.md) or copy the file when the task completes. - **Read prices, don't copy them.** Prices live in the catalog and in the estimate endpoints. See [pricing](https://mynth.io/docs/pricing.md). - **Mynth does not moderate prompts.** A refusal comes from the provider. See [prompts and images](https://mynth.io/docs/concepts/prompts.md). ## Where to start - [Getting started](https://mynth.io/docs/getting-started.md): a key and a first image, with the SDK, REST or the CLI. - [Authentication](https://mynth.io/docs/authentication.md): `mak_` keys, scopes, spending limits, and the `pat_` token. - [Tasks](https://mynth.io/docs/concepts/tasks.md): statuses, partial failure, and the ways to collect a result. - [API reference](https://mynth.io/docs/api-reference.md): every endpoint, generated from the OpenAPI document. ## For agents Every docs page is also plain markdown. Append `.md` to its URL, or send `Accept: text/markdown`. This page is [/docs.md](https://mynth.io/docs.md), the index is [/llms.txt](https://mynth.io/llms.txt), and every page in one file is [/llms-full.txt](https://mynth.io/llms-full.txt). None of it needs an account. ```bash npx @mynthio/cli docs list npx @mynthio/cli docs get getting-started ``` The model catalog is public too: [/models.json](https://mynth.io/models.json) and `GET https://api.mynth.io/models`. [llms.txt](https://mynth.io/docs/sdks/agents/llms-txt.md) has the details. --- # Getting started > Create an API key and generate a first image with the TypeScript SDK, the REST API, or the CLI. You need an API key and one request. Pick how you want to work below. > **Prompt to give a coding agent** > > Integrate Mynth media generation into this project. First read the docs: run `npx @mynthio/cli > docs list` and `npx @mynthio/cli docs get getting-started`. Docs need no account. Before you > change anything, ask me how to handle credentials: (1) you run `npx @mynthio/cli auth login` and > `npx @mynthio/cli api-key create my-app`, I approve the login in my browser, and you add the key > to the right env file; or (2) I add MYNTH_API_KEY myself. Then scan the codebase, tell me where > media generation would fit, and wait for my go-ahead. Use @mynthio/sdk in TypeScript or JavaScript > projects. Always pass an explicit model id, never `auto`. Keep the API key on the server. ## Using the SDK ### Get an API key Log in once with the CLI. It opens your browser to approve the login. Then create a key for your app: ```bash npx @mynthio/cli auth login npx @mynthio/cli api-key create my-app ``` `api-key create` prints a `mak_...` key once. Put it where your app reads environment variables: ```bash MYNTH_API_KEY=mak_... ``` You can also create a key in the [dashboard](https://mynth.io/dashboard/keys). ### Install the SDK **pnpm** ```bash pnpm add @mynthio/sdk ``` **npm** ```bash npm install @mynthio/sdk ``` **bun** ```bash bun add @mynthio/sdk ``` **yarn** ```bash yarn add @mynthio/sdk ``` ### Generate an image ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); // reads MYNTH_API_KEY const task = await mynth.image.generate({ model: "black-forest-labs/flux.2-pro", prompt: "A lighthouse at dusk, film grain", }); console.log(task.urls[0]); ``` `generate()` creates the task, waits for it, and returns the finished result. The URL is served for 7 days. [Tasks](https://mynth.io/docs/concepts/tasks.md#file-lifetimes) covers keeping the file longer. ## Using the REST API ### Get an API key Log in once with the CLI. It opens your browser to approve the login. Then create a key and export it: ```bash npx @mynthio/cli auth login npx @mynthio/cli api-key create my-app export MYNTH_API_KEY=mak_... ``` `api-key create` prints the key once. You can also create a key in the [dashboard](https://mynth.io/dashboard/keys). ### Create a task ```bash curl https://api.mynth.io/image/generate \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "black-forest-labs/flux.2-pro", "prompt": "A lighthouse at dusk, film grain" }' ``` ```json { "data": { "task_id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimated_cost": "0.03" } } ``` Generation is asynchronous. The response is a task id, not an image. ### Poll for the result Poll the status until it leaves `pending`, then read the result: ```bash curl https://api.mynth.io/tasks/tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP/status \ -H "Authorization: Bearer $MYNTH_API_KEY" curl https://api.mynth.io/tasks/tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP/result \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ```json { "data": { "id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "type": "image.generate", "status": "completed", "result": { "model": "black-forest-labs/flux.2-pro", "images": [ { "status": "success", "id": "img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk", "url": "https://cdn.mynth.io/images/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp", "mynth_url": "https://cdn.mynth.io/images/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp", "size": "1536x1024", "format": "webp" } ] } } } ``` Check `status` on each image, not only on the task. To skip polling, register a [webhook](https://mynth.io/docs/concepts/webhooks.md). [Poll for results](https://mynth.io/docs/guides/poll-for-results.md) has a polling loop to copy. ## Using the CLI ### Log in ```bash npx @mynthio/cli auth login ``` Approve the one-time code in the browser. The CLI creates an API key and stores it on this machine, so later commands need no `MYNTH_API_KEY`. ### Generate an image ```bash npx @mynthio/cli image generate \ -m black-forest-labs/flux.2-pro \ -p "A lighthouse at dusk, film grain" ``` The CLI waits for the task and prints the image URL. Add `-o ./images` to save the file, or `--json` for output a script or agent can parse. ```bash npx @mynthio/cli models list # model ids you can pass to -m npx @mynthio/cli task list # your recent tasks npx @mynthio/cli balance # balance, reserved, available ``` `npm install -g @mynthio/cli` installs the `mynth` binary, so you can drop the `npx @mynthio/cli` prefix. ## Next steps - [Tasks](https://mynth.io/docs/concepts/tasks.md): statuses, partial failure, and file lifetimes. - [The model field](https://mynth.io/docs/models.md): what `model` accepts and where to find ids. - [Webhooks](https://mynth.io/docs/concepts/webhooks.md): get called when a task settles. --- # Authentication > Create and rotate mak_ API keys, give each one the scopes it needs, cap its spending, and let a browser poll with a pat_ token. Every request sends one credential as `Authorization: Bearer ` to `https://api.mynth.io`. Which credential you send depends on where the code runs: | Where the code runs | Credential | Reaches | | ------------------- | ---------- | --------------------------------------------------------- | | Your server | `mak_...` | Every endpoint the key's scopes allow | | A browser | `pat_...` | `GET /tasks/{id}/status` and `/result` for one task | | mynth.io | OAuth | The dashboard and playground. Your code never sends this. | The API key is the only credential you create and store. The `pat_` token is issued with each task. [The API reference](https://mynth.io/docs/api-reference/authentication.md) has the header rules, the scope each endpoint needs, CORS, and every authentication error. ## Create a key **CLI** ```bash npx @mynthio/cli auth login npx @mynthio/cli api-key create my-app ``` **REST** ```bash curl https://api.mynth.io/api-keys \ -X POST \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "my-app" }' ``` `POST /api-keys` needs a key with the `keys` scope. In the [dashboard](https://mynth.io/dashboard/keys/new), the new key appears once in a dialog, next to a ready-made `MYNTH_API_KEY=mak_...` line. The key is shown once. The create response carries it at `data.raw`, and no endpoint returns it again. Mynth stores only an HMAC-SHA256 of the key, so a lost key cannot be recovered. Lists show `key_preview`, such as `mak_9d4...e3f`. A key is `mak_` followed by 48 hex characters. An account can hold 100 live keys. Deleted keys do not count toward that. The SDK and the CLI both read `MYNTH_API_KEY`. In the CLI it takes precedence over the key stored by `auth login`. ## Check a key `GET /me` accepts any key and describes it. `npx @mynthio/cli whoami` prints the same thing. ```bash curl https://api.mynth.io/me -H "Authorization: Bearer $MYNTH_API_KEY" ``` ```json { "data": { "user_id": "user_01JD8G3W1R5T6Y7U8I9O0P1Q2W", "auth": { "method": "api-key", "api_key": { "id": "ak_01KE7XWWEQ4MCGWKBQKJ1G47RP", "name": "my-app", "key_preview": "mak_9d4...e3f", "scopes": ["generate"], "spending": { "mode": "unlimited" } } } } } ``` Run it first when a request fails with `401` or `403`. ## Scopes A key carries one or more scopes. New keys get `generate` unless you ask for more. | Scope | Reaches | | ---------- | ------------------------------------------------------------------------------ | | `generate` | `/image/*`, `/video/*`, `/tasks/*`, `/me` | | `manage` | `/webhooks`, [`/destinations`](https://mynth.io/docs/concepts/destinations.md), `/balance`, `/me` | | `keys` | `/api-keys`, `/me` | Give a key only what its job needs. A leaked `generate` key can spend your balance. It cannot create keys, read your balance, or point a webhook somewhere else. You can change a key's scopes later, on its page in the [dashboard](https://mynth.io/dashboard/keys) or with `PUT /api-keys/{id}`. The change applies to the next request. **An API key cannot grant `manage` or `keys`.** When the caller authenticates with an API key, `POST /api-keys` and `PUT /api-keys/{id}` accept only `["generate"]` and answer anything wider with `403 scope_escalation`. Create or widen those keys in the dashboard. The key `auth login` creates is the exception: it is minted through the browser approval and gets all three scopes by default. ## Rotate a key Create a second key, deploy it, then delete the first. Both keys work during the switch. Deleting a key revokes it immediately. ## Spending limits A key can carry a USD cap that resets each day, week, or calendar month in UTC. Weekly caps reset on Monday and follow ISO weeks across year boundaries. Set it on the key's page in the dashboard, or send `spending_limit` and `spending_limit_period` to `PUT /api-keys/{id}`. The create call does not take one. `spending_limit` is a decimal string of US dollars with at most two decimal places, such as `"25.00"`, and `spending_limit_period` is `day`, `week`, or `month`. A limit needs a period, and `"spending_limit": null` with no period removes the cap. The update replaces the key's settings, so send `scopes` and `spending_limit` every time. The cap counts each task's estimate when the task is created. Once the next estimate would pass the cap, creating a task fails with `402 spending_limit_exceeded` until the period rolls over. A task that fails later does not give its estimate back to the cap. `GET /me` reports `used`, `limit`, and `remaining` for a capped key. ## Browser polling `POST /image/generate`, `POST /image/remove-background`, `POST /image/upscale`, and `POST /video/generate` return `data.public_access_token`, a `pat_` token for that one task, when the request sets `generate_public_access_token: true`. It works only on `GET /tasks/{id}/status` and `GET /tasks/{id}/result` for that task, and it expires one hour after it is issued. Create the task on your server, give the browser the task id and the token, and poll from the page. [Poll for results](https://mynth.io/docs/guides/poll-for-results.md#from-a-browser) has the code, and [the reference](https://mynth.io/docs/api-reference/authentication.md#public-access-tokens) has every rule. > **Warning** > > Never ship a `mak_` key to the browser. Only the two polling paths allow cross-origin requests, so > a key in a page cannot generate from that page, but anyone can read it from the bundle and spend > your balance from anywhere else. ## Next steps - [Authentication reference](https://mynth.io/docs/api-reference/authentication.md): the header, per-endpoint scopes, CORS, and error codes. - [Getting started](https://mynth.io/docs/getting-started.md): first key, first image. - [Webhooks](https://mynth.io/docs/concepts/webhooks.md): signed with their own `whsec_` secret, separate from your key. --- # Pricing and billing > How a price is computed, how to read it before you send, and how the prepaid balance holds and charges each task. Every price is a flat USD number you can total before a task starts. An image is priced per output, a video per second, and the analysis tools per call. Creating a task holds the estimate on your balance. Completing it charges only what succeeded and releases the rest. There is no subscription and no minimum spend. You prepay credits in the [wallet](https://mynth.io/dashboard/wallet). The minimum top-up is $5, with no Mynth fee on top. An idle account is never billed. > **Warning** > > Do not hardcode prices from these docs. They change when providers change theirs. Read them from > `GET https://api.mynth.io/models` or [/models.json](https://mynth.io/models.json), or price a real request with an > [estimate call](https://mynth.io/docs/guides/estimate-cost.md). ## Where prices come from Providers bill by megapixel, by token, or by GPU time. Mynth publishes one number per model, usually the provider's own price. Where it is higher, the difference covers routing, retries, and delivery, and it is already included. The catalog carries prices as decimal strings in USD: | Field | Meaning | | ------------------------- | --------------------------------------------------------- | | `pricing.per_image.base` | One image at the model's normal size | | `pricing.per_image["4k"]` | One image at a `_4k` size. Absent when the model has none | | `pricing.per_input` | Each input image, charged per output. Absent means $0 | | `pricing.per_second` | Video, keyed by resolution tier | | `pricing.audio` | Video audio per second, when billed separately | The catalog is at [mynth.io/models](https://mynth.io/models), the [pricing table](https://mynth.io/pricing), `GET https://api.mynth.io/models`, [/models.json](https://mynth.io/models.json), and `npx @mynthio/cli models list`. None of them need an API key. ## Image price ```text per image = per_image[scale] + per_input × number of inputs task = per image × successful images ``` `scale` is `4k` for a `_4k` size and `base` otherwise. Nothing else changes the price: not prompt length, pixel count within a scale, output format, webhooks, destinations, which provider ran the job, or how many retries it took. For example, with `per_image.base` at $0.03 and `per_input` at $0.03, a request with `count: 2` and two reference images holds $0.18 and charges $0.09 per image that succeeds. A `_4k` size on a model without 4k is rejected at create with `400 validation_error`, so nothing is held. With `model: "auto"` the model is not known yet, so the task fails with `capability_not_supported` instead, and a failed task costs nothing. With `model: "auto"`, the hold is a flat $0.20 per image because the model is not chosen yet. On completion Mynth charges the published price of the model in `result.model` and releases the rest. See [the model field](https://mynth.io/docs/models.md#auto-is-experimental). ## Video price ```text video = (per_second[tier] + audio.per_second) × duration ``` `tier` is the resolution in `size`. The aspect ratio does not change the price. An omitted tier, `duration`, or `audio` uses the model's default, and the estimate includes it. `audio.per_second` applies only when the catalog lists it. The video models available today include audio in `per_second`. ## Tool prices | Request | Price | | ------------------------------------------- | ------- | | `POST /image/rate` | $0.0002 | | `rating` on `POST /image/generate` | $0 | | `POST /image/alt` | $0.0004 | | `POST /image/review` with `effort: "low"` | $0.01 | | `POST /image/review` with `effort: "high"` | $0.30 | | `POST /image/remove-background` | $0.02 | | `POST /image/upscale` with `effort: "low"` | $0.03 | | `POST /image/upscale` with `effort: "high"` | $0.15 | | `magic_prompt` on `POST /image/generate` | $0 | | `POST /image/upload` | $0 | Magic Prompt is included in the generation estimate, so if it gets a price later, the estimate will show it. ## Before you send `POST /image/generate/estimate` and `POST /video/generate/estimate` take the same body as generate. They validate it and return the price without creating a task or holding anything. [Estimate cost](https://mynth.io/docs/guides/estimate-cost.md) has the request and response. ## Balance ```text available = balance − reserved balance credits on the account reserved held by tasks that are still pending available what the next task can hold ``` `GET /balance` returns all three as decimal strings. It needs a key with the `manage` scope. `npx @mynthio/cli balance` prints them. ```json { "data": { "balance": "12.50", "reserved": "0.18", "available": "12.32", "currency": "usd" } } ``` ## Holds and charges ```text create task ── hold estimated_cost against available │ ├─ completed ── charge each successful output, release the rest of the hold └─ failed ───── release the whole hold, charge $0 ``` - The create response returns the hold as `estimated_cost`. - If `available` is below the hold, the create fails with `402 insufficient_balance` and nothing is queued. - If the key's [spending limit](https://mynth.io/docs/authentication.md#spending-limits) would be passed, it fails with `402 spending_limit_exceeded`. - Only successful outputs are charged. If three of four images fail, you pay for one. - The amount charged is `cost` on the finished task. It is `null` while the task is pending and when it failed. A task still `pending` 24 hours after its last update is failed with `task_expired` by a sweep that runs daily at 02:00 UTC, and its hold is released. The sweep looks at tasks last updated within the past 7 days. If `reserved` stays high after that, email [mynth@mynth.io](mailto:mynth@mynth.io). ## Refunds Credits are for spending on Mynth and are not paid back as cash. The [terms](https://mynth.io/legal/terms) cover refunds when a charge was our fault. The wallet lists each top-up and each charge with what it paid for. ## Next steps - [Estimate cost](https://mynth.io/docs/guides/estimate-cost.md): price a request before you create it. - [Tasks](https://mynth.io/docs/concepts/tasks.md): where `cost` lands, and partial success. - [Choosing an image model](https://mynth.io/docs/models/choosing.md): comparing prices across models. --- # Beta > What Mynth's beta means for the API, Magic Prompt, testing credits, and running Mynth in production. Mynth is in beta. The early alpha period is over: the core platform has settled, and Mynth is ready for real use beyond demos and experiments. ## The base API is stable Models, image size, Magic Prompt, webhooks, and the rest of the base API are stable. They will not change in a breaking way without a migration period and advance notice. Mynth is suitable for non-critical production, or for production behind a fallback. > **Warning** > > Where image generation is on a critical path, design for graceful degradation. ## Magic Prompt is still in beta [Magic Prompt](https://mynth.io/docs/guides/enhance-prompts.md) stays in beta while it keeps changing. Unlike the base API, its output may still change. It is free during the beta, so you can test it without worrying about cost. ## Testing credits Automatic signup credits are paused because bots and other automated signups kept abusing them. Developers evaluating Mynth can still get up to $10 in testing credits, based on what they need. [DM @mynthio on X](https://x.com/mynthio) or [join Discord](https://discord.gg/ktHXuPVaqB) and say what you are building. ## Top-ups and production Top-ups are enabled in the [wallet](https://mynth.io/dashboard/wallet). For now, think of them mainly as a way to support Mynth while it grows. Running Mynth in production at higher load is welcome. We are happy to help anyone who uses Mynth seriously: solving issues, tuning for a workload, and working through whatever comes up. Get in touch on [Discord](https://discord.gg/ktHXuPVaqB) or at [mynth@mynth.io](mailto:mynth@mynth.io). --- # Tasks > Every generation and analysis call creates a task. Its three statuses, why a completed task can hold failed items, the ways to collect a result, and how long files last. Every generation and analysis request creates a task. Mynth validates the request, holds the estimated cost on your balance, and returns a task id within milliseconds. The work runs after that, and you collect the result separately. The SDK's `generate()`, `rate()`, and similar methods look synchronous because they create the task and poll it for you. The HTTP API has no synchronous generate call. ## What creates a task | Request | Task `type` | `result` holds | | ------------------------------- | ------------------------- | ---------------------------- | | `POST /image/generate` | `image.generate` | generated images | | `POST /image/remove-background` | `image.remove_background` | one cutout image | | `POST /image/upscale` | `image.upscale` | one upscaled image | | `POST /image/rate` | `image.rate` | a content rating level | | `POST /image/alt` | `image.alt` | alt text | | `POST /image/review` | `image.review` | a quality score and findings | | `POST /video/generate` | `video.generate` | a generated video | Each answers `201` with the task id and the held estimate: ```json { "data": { "task_id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimated_cost": "0.03" } } ``` Task ids are `tsk_` followed by a ULID, so they sort by creation time. ## Statuses There are three. `pending` covers both queued and running, so there is no `running` status to wait for. ```text 201 { task_id, estimated_cost } estimate held on your balance │ ▼ pending ──┬──▶ completed result set, successful outputs charged └──▶ failed errors set, hold released, nothing charged ``` A task settles once. It never leaves `completed` or `failed`, and Mynth never reruns a settled task. To try again, send a new request, which creates a new task and a new hold. ## Completed does not mean every item worked `image.generate` with `count: 4` runs four generations independently. When one fails, the task still completes, and each entry in `result.images` carries its own `status`: ```json { "status": "completed", "result": { "model": "black-forest-labs/flux.2-pro", "images": [ { "status": "success", "id": "img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk", "url": "https://cdn.mynth.io/images/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp" }, { "status": "failed", "error": { "code": "restricted_content", "message": "The request was blocked by content moderation." } } ] } } ``` `video.generate` works the same way under `result.videos`. A provider refusal (`restricted_content`) or a model with no capacity (`providers_busy`) fails the item, not the task, so a completed generation task can hold no usable output at all. Always check the task status, then every item. Only failed items are free: you pay for the images that succeeded. The other task types (`image.remove_background`, `image.upscale`, `image.rate`, `image.alt`, `image.review`) have no items. When their work fails, the task is `failed`. > **Warning** > > In the SDK, `urls` and `images` skip failed images without saying so. If a partial result is not > acceptable, compare `images.length` with the `count` you sent. ## Collecting the result Pick one of three: | Approach | Use it when | | ------------------------------------- | --------------------------------------------------------------- | | Wait in the SDK or CLI | The code can hold a connection: a script, a worker, a job queue | | [Poll](https://mynth.io/docs/guides/poll-for-results.md) | You want control over the loop, or a browser does the waiting | | [Webhook](https://mynth.io/docs/concepts/webhooks.md) | Nothing can wait: serverless handlers, long video renders | A [destination](https://mynth.io/docs/concepts/destinations.md) changes where the file is stored, not how you learn the task finished, so combine it with one of the three. The SDK polls on a fixed schedule and gives up after 30 minutes for images and an hour for video. [Tasks and polling](https://mynth.io/docs/sdks/typescript/tasks.md) has the exact intervals. `GET /tasks/{id}/status` and `GET /tasks/{id}/result` are served from a cache, so point a polling loop at them rather than at `GET /tasks/{id}`. ## Reading a task | Call | Returns | | ------------------------ | ---------------------------------------------------------------- | | `GET /tasks/{id}/status` | `status` only | | `GET /tasks/{id}/result` | `id`, `type`, `status`, `result` | | `GET /tasks/{id}` | The full record: `request`, `result`, `errors`, `cost`, and more | | `GET /tasks` | Recent tasks, newest first, without `request` or `result` | `GET /tasks` with an API key lists only the tasks that key created. Any key on your account with the `generate` scope can read any of your tasks by id. [The task object](https://mynth.io/docs/api-reference/task-object.md) documents every field. ## Retries and timeouts Mynth retries before a failure reaches you: | Work | Attempts | Time budget | Each attempt | | --------- | -------- | ----------- | ---------------------- | | One image | 4 | 35 minutes | picks a provider again | | One video | 3 | 32 minutes | up to 15 minutes | A failing provider therefore fails over rather than failing the image. `restricted_content`, `invalid_prompt`, `invalid_input`, and `capability_not_supported` are not retried, because another attempt would fail the same way. [Errors](https://mynth.io/docs/api-reference/errors.md#task-failures) lists every code and whether retrying helps. A task still `pending` 24 hours after its last update is failed with `task_expired` by a daily sweep, and its hold is released. [Pricing](https://mynth.io/docs/pricing.md#holds-and-charges) has the details. ## File lifetimes | File | Served for | | -------------------------------------- | ---------- | | A generated image or video | 7 days | | An image uploaded with `/image/upload` | 1 day | Mynth hands you a URL because that is the practical way to use the file. The id in it is long and random, but Mynth is not a file host. To keep a file, name a [destination](https://mynth.io/docs/concepts/destinations.md) so Mynth writes it to your own storage, or copy it when the webhook arrives or polling shows `completed`. When copying on a machine with little memory, stream the download into the upload instead of buffering the whole file. ## Next steps - [Poll for results](https://mynth.io/docs/guides/poll-for-results.md): a polling loop, from a server or a browser. - [Webhooks](https://mynth.io/docs/concepts/webhooks.md): get called when the task settles. - [The task object](https://mynth.io/docs/api-reference/task-object.md): every field of the record. - [Pricing](https://mynth.io/docs/pricing.md): the hold, the charge, and `cost`. --- # Prompts and images > Mynth does not moderate or train on your prompts and images. What the provider receives, and what a restricted_content refusal means. Mynth sits in front of other companies' models. You send a model and a prompt, Mynth routes and retries the request, and the model itself runs at a provider. ## Mynth does not review your content Mynth does not read your prompts or images, train on them, or use them for anything beyond the request you sent. There is no Mynth moderation pass on the way through. Any other use needs your written approval. The [terms](https://mynth.io/legal/terms) and the [privacy policy](https://mynth.io/legal/privacy) are the full rules, including the narrow case where the law requires us to act on something we have already become aware of. That is not a scan of your traffic. Features you turn on read what they need, inside the request you made: | Feature | Reads | | --------------------------- | ----------------------------------- | | `magic_prompt` | the prompt | | `size: "auto"` or no `size` | the prompt, to pick an aspect ratio | | `model: "auto"` | the prompt, to pick a model | | Rating, alt text, review | the image | ## What the provider receives The provider sees the call as coming from Mynth. It does not receive your user id, your email, your API key, or the task's `metadata`. It receives what the model needs: the prompt, any input images, the size, and the other generation fields that model accepts. When an attempt fails and Mynth retries on another provider, that provider receives the same fields. Providers apply their own rules. Logging, retention, and content filters are theirs and differ between providers. ## When the provider refuses `restricted_content` means the provider or the model refused the prompt or an input image. Mynth did not make that decision. Mynth does not retry a refusal, because another attempt usually gets the same answer. Nothing stops you from sending the request again yourself, and it sometimes goes through. On `image.generate` and `video.generate`, a refusal usually fails one output while the task still ends `completed`. The refused output is not charged. [Tasks](https://mynth.io/docs/concepts/tasks.md#completed-does-not-mean-every-item-worked) shows where the code sits on the result. The CLI exits with code `5` for it. ## Next steps - [Generate images](https://mynth.io/docs/guides/generate-images.md): the request body. - [Errors](https://mynth.io/docs/api-reference/errors.md#task-failures): `restricted_content` and the other task codes. - [Privacy policy](https://mynth.io/legal/privacy): prompts, images, and third-party providers. --- # Destinations > Have Mynth write finished images into your own S3, R2 or Bunny storage and return a URL on your domain. Setup, path templates, and how a failed upload shows up. A destination is storage of yours that Mynth writes finished files into. Name one on a request and Mynth uploads each image there before the task completes. `url` on the image is then your URL, and `mynth_url` is still Mynth's own copy. Without a destination, the file is served from `mynth_url` for 7 days. Add a destination when the file has to last longer or live on your own domain. ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); const task = await mynth.image.generate({ model: "black-forest-labs/flux.2-pro", prompt: "A lighthouse at dusk, film grain", destination: "prod-cdn", }); const [image] = task.images; console.log(image?.url); // your storage, or null if the upload failed console.log(image?.mynthUrl); // Mynth's copy, always set ``` ## Rules - `destination` takes the destination's **name**, not its `dst_` id. - Only `POST /image/generate`, `POST /image/remove-background`, and `POST /image/upscale` accept it. `POST /video/generate` rejects it with `400 validation_error`. - A name you don't own fails at create with `400 validation_error` and `Destination "prod-cdn" not found`. Nothing is queued or held. - The upload happens inside the task. A task that reports `completed` has already finished it. - A failed upload does not fail the image. The image succeeds with `url: null`, and `mynth_url` is set. See [what comes back](#what-comes-back). - Uploading does not change the price. - Every `/destinations` endpoint needs a key with the `manage` scope. ## How the upload works ```text image generated ──▶ Mynth CDN mynth_url (always set) │ └─▶ PUT into your storage url key: path_template + "." + format url: url_template with {path} filled in 5 attempts, then url: null ``` Mynth writes its own copy first and streams from there into your storage, so a destination is a second copy rather than a redirect. Each upload gets 5 attempts of up to 10 minutes each. Deleting a destination later leaves the files already uploaded in place. ## Providers | `provider.id` | Config | `secret` | | ------------- | -------------------------------------------------------------------- | ------------------------------------ | | `s3` | `bucket`, `region`, optional `endpoint`, optional `force_path_style` | `access_key_id`, `secret_access_key` | | `r2` | `account_id`, `bucket`, optional `jurisdiction` | `access_key_id`, `secret_access_key` | | `bunny` | `storage_zone`, optional `region` | `password` | Use `s3` for anything that speaks the S3 API. Point `endpoint` at Backblaze B2, MinIO, Wasabi, or DigitalOcean Spaces, and set `"force_path_style": true` when the endpoint needs path-style addressing. R2 `jurisdiction` accepts `default`, `eu`, or `fedramp`. Bunny `region` defaults to `de`. Mynth keeps `secret` in a vault separate from the destination record, and no endpoint returns it. `GET /destinations` returns `id`, `name`, `provider`, `config`, and timestamps. ## Create a destination **CLI** ```bash npx @mynthio/cli destination create prod-cdn \ --provider r2 \ --account-id 3b1c9f2e7a4d6b8c0e1f3a5d7b9c1e3f \ --bucket media \ --path-template 'images/{YYYY}/{MM}/{id}' \ --url-template 'https://cdn.example.com/{path}' \ --secret ./r2-secret.json ``` **REST** ```bash curl https://api.mynth.io/destinations \ -X POST \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "prod-cdn", "provider": { "id": "r2", "account_id": "3b1c9f2e7a4d6b8c0e1f3a5d7b9c1e3f", "bucket": "media" }, "secret": { "access_key_id": "...", "secret_access_key": "..." }, "config": { "path_template": "images/{YYYY}/{MM}/{id}", "url_template": "https://cdn.example.com/{path}" } }' ``` The CLI reads the secret from a file, or from stdin with `--secret -`, so it stays out of your shell history. For Bunny the file can hold the bare password instead of JSON. The [dashboard](https://mynth.io/dashboard/destinations) has a form per provider with a live preview of the resolved path. `name` is 1 to 64 characters of lowercase letters, digits, and dashes, and it cannot be changed. `PUT /destinations/{id}` replaces `provider` and `config`, and takes a new `secret` when you rotate credentials. Changing `provider.id` fails with `400 destination_invalid_provider`, so switching providers means a new destination. Mynth caches the destination for 5 minutes and its secret for 10. An edited template or rotated credentials can take up to 10 minutes to reach new tasks, so keep the old credentials valid until then. ## Path and URL templates `path_template` is the object key without the extension. Mynth appends a dot and the delivered format (`webp`, `png`, or `jpg`) and sets the matching `Content-Type`. Tokens resolve per file, at upload time: | Token | Becomes | | --------------------------------------- | -------------------------------------------------------------------------- | | `{id}` | the Mynth image id, `img_...` | | `{YYYY}` `{MM}` `{DD}` | the upload date | | `{ulid}` `{uuid}` `{uuidv4}` `{uuidv7}` | a fresh random id, different for every file | | `{meta.}` | that string from the request's `metadata`, or the literal text `undefined` | `url_template` is what Mynth reports as `url`. It must contain `{path}`. If you leave it out, the upload still runs, but `url` comes back `null`, which on `image.generate` looks the same as a failed upload. Set it unless the storage is private and you only ever read objects by key. > **Warning** > > A template with no per-file token overwrites. `images/{meta.slug}` writes the same key four times > for a `count: 4` request, and you keep one image. Include `{id}` or `{ulid}` unless overwriting is > what you want. ## Test the credentials `POST /destinations/{id}/test` uploads a small probe image to the `path` in the body and answers `204`. Bad credentials, a wrong region, or a missing bucket come back as `502 destination_test_failed` with the provider's own message. ```bash npx @mynthio/cli destination test dst_01KE7XWWEQ4MCGWKBQKJ1G47RP ``` The `path` is used exactly as given: no tokens resolve and no extension is added. The probe is a WEBP image whatever you name it. Delete it yourself afterwards. ## Use a destination on every request Set `MYNTH_DESTINATION`, or pass `new Mynth({ destination: "prod-cdn" })`. The SDK and the CLI both read the variable, and a `destination` on the request takes precedence over both. ## What comes back ```json { "status": "success", "id": "img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk", "url": "https://cdn.example.com/images/2026/07/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp", "mynth_url": "https://cdn.mynth.io/images/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp", "size": "1536x1024", "format": "webp" } ``` `url` is your storage when the upload worked and `null` when it did not. Store both fields. Render `url`, and fall back to `mynth_url`. How a failure shows up depends on the task type: | Task type | Upload failed | Destination deleted before the upload | | ------------------------- | --------------------------------------- | ---------------------------------------------------------------- | | `image.generate` | `url: null`, no reason given | `url` is the Mynth URL; nothing reaches your storage | | `image.remove_background` | `url: null`, plus a `destination` block | `url: null`, `destination.error.code` is `destination_not_found` | | `image.upscale` | `url: null`, plus a `destination` block | `url: null`, `destination.error.code` is `destination_not_found` | On `image.remove_background` and `image.upscale`, the image carries a `destination` block: ```json "destination": { "status": "failed", "name": "prod-cdn", "error": { "code": "unknown_error" } } ``` Rejected credentials or a wrong bucket currently report `unknown_error`, so check your provider's logs for the reason. > **Warning** > > On `image.generate` a failed upload leaves no error in `errors` and no `destination` block: only > `url: null` next to a populated `mynth_url`. In the SDK, `task.urls` drops those images. Read > `task.images` when a destination is in use. ## Errors | Status | Code | When | | ------ | ------------------------------ | ------------------------------------------------------------ | | 400 | `validation_error` | A generation request named a destination you don't own | | 400 | `destination_invalid_provider` | `PUT /destinations/{id}` changed `provider.id` | | 404 | `destination_not_found` | Unknown `dst_` id | | 409 | `destination_name_taken` | You already have a destination with that name | | 500 | `internal_server_error` | Storing or reading the secret failed | | 502 | `destination_test_failed` | Your storage rejected the probe. `message` is the provider's | ## Next steps - [Tasks](https://mynth.io/docs/concepts/tasks.md#file-lifetimes): how long `mynth_url` lasts. - [Webhooks](https://mynth.io/docs/concepts/webhooks.md): learn when the task, and its upload, is done. - [Create destination](https://mynth.io/docs/api-reference/endpoints/destinations/create.md): the full request schema. --- # Webhooks > Receive a signed POST when a task settles instead of polling. Register an endpoint, verify it with Standard Webhooks, 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](https://mynth.io/docs/guides/poll-for-results.md) instead. > **Prompt to give a coding agent** > > 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, or a Standard Webhooks library, 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 the webhook-id header. Ask me which events to subscribe to. The CLI (`npx > @mynthio/cli webhook create`) can register the endpoint. ## How delivery works ```text task settles │ ▼ POST https://your-app.com/api/mynth-webhook headers: webhook-id, webhook-timestamp, webhook-signature body: { id, type, timestamp, data: } │ ├─ 2xx within 30 s ──▶ done └─ anything else ────▶ retried: 5 s, 10 s, 20 s … up to 1 h apart, for about 72 h same webhook-id on every attempt ``` Deliveries follow [Standard Webhooks](https://www.standardwebhooks.com), and `data` is the task exactly as `GET /tasks/{id}` returns it. The headers and body are on [webhook payloads](https://mynth.io/docs/api-reference/webhook-payloads.md). A task reaches two kinds of endpoint: | | Registered | Custom | | ---------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------- | | Set up | Once, in the dashboard, the CLI or `POST /webhooks` | Per task, in the request's `webhook.custom` | | Receives | Every task that matches its events and sources, unless the task sets `webhook.registered: false` | Only the task it was set on, completed or failed | | Signed | Yes, with its `whsec_` secret | No. Check a token you put in the URL | | Shown back | In full, on the endpoint | As `scheme://host` only, on the task and in every event | Creating 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 **CLI** ```bash npx @mynthio/cli webhook create \ --url https://your-app.com/api/mynth-webhook \ -e task.image.generate.completed \ -e task.image.generate.failed ``` **REST** ```bash curl https://api.mynth.io/webhooks \ -X POST \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.com/api/mynth-webhook", "events": ["task.image.generate.completed", "task.image.generate.failed"] }' ``` The URL must be `https` on a public host. The response carries a `whsec_...` signing secret. Store it where your app reads environment variables: ```bash MYNTH_WEBHOOK_SECRET=whsec_... ``` The API and the CLI return the secret only on create. The endpoint's page in the [dashboard](https://mynth.io/dashboard/webhooks) keeps showing it. Webhook endpoints need a key with the `manage` scope. The key from `npx @mynthio/cli auth login` has it. `PUT /webhooks/{id}` replaces the whole configuration, so send `url`, `events`, and `enabled` every time. An omitted `include_session_tasks` becomes `false`, and an omitted `api_key_ids` becomes every key. `npx @mynthio/cli webhook update` behaves the same way. `DELETE /webhooks/{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, including event types newer than the SDK, so subscribe only to what you handle. Every helper takes the handlers below, image and video alike, plus `onEvent`, which receives every event before its handler runs. | 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` | | `imageUpscaleTaskCompleted` | `task.image.upscale.completed` | | `imageUpscaleTaskFailed` | `task.image.upscale.failed` | | `videoTaskCompleted` | `task.video.generate.completed` | | `videoTaskFailed` | `task.video.generate.failed` | ### Next.js ```ts // app/api/mynth-webhook/route.ts import { mynthWebhookHandler } from "@mynthio/sdk/next"; export const POST = mynthWebhookHandler({ imageTaskCompleted: async (result) => { for (const image of result.images) { console.log(result.taskId, image.url ?? image.mynthUrl); } }, imageTaskFailed: async (failure) => { console.error(failure.taskId, failure.errors); }, }); ``` The handler reads the secret from `MYNTH_WEBHOOK_SECRET` on each request, or from `{ webhookSecret }` as the second argument. It answers `400` for a missing or wrong signature, a `webhook-timestamp` more than five minutes off, a missing `webhook-id`, and a body whose `id` is not the `webhook-id`. Completed callbacks receive the same plain result objects as polling, including `taskId`, `cost`, and successful media. Failed callbacks receive `taskId`, `errors`, and `raw`, plus `metadata` when the request supports it. The callback objects can be passed directly to server functions and Convex mutations. The second argument carries the verified `event`; deduplicate on `event.id`. See [generating media](https://mynth.io/docs/sdks/typescript/generating-media.md). ### TanStack Start ```ts // src/routes/api/mynth-webhook.ts import { createFileRoute } from "@tanstack/react-router"; import { mynthWebhookHandler } from "@mynthio/sdk/tanstack-start"; export const Route = createFileRoute("/api/mynth-webhook")({ server: { handlers: { POST: mynthWebhookHandler({ imageTaskCompleted: async (result) => { console.log(result.taskId, result.images); }, }), }, }, }); ``` Same checks as the Next.js helper. [TanStack Start](https://mynth.io/docs/sdks/integrations/tanstack-start.md) covers the callback context. ### Convex ```ts // convex/http.ts import { httpRouter } from "convex/server"; import { mynthWebhookAction } from "@mynthio/sdk/convex"; import { internal } from "./_generated/api"; const http = httpRouter(); export const mynthWebhook = mynthWebhookAction({ imageTaskCompleted: async (result, { context }) => { await context.runMutation(internal.images.save, { taskId: result.taskId, images: result.images, }); }, }); http.route({ path: "/mynth-webhook", method: "POST", handler: mynthWebhook }); export default http; ``` Same checks as the Next.js helper. [Convex](https://mynth.io/docs/sdks/integrations/convex.md) covers the action context. ### Any other stack Use a [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries). There is one for most languages, and each takes the `whsec_` secret as it is. Without the SDK, `verifyWebhook()` does the same in TypeScript: ```js import http from "node:http"; import { Webhook } from "standardwebhooks"; const SECRET = process.env.MYNTH_WEBHOOK_SECRET; if (!SECRET) throw new Error("MYNTH_WEBHOOK_SECRET is required"); const webhook = new Webhook(SECRET); http .createServer((req, res) => { const chunks = []; req.on("data", (chunk) => chunks.push(chunk)); req.on("end", () => { let event; try { event = webhook.verify(Buffer.concat(chunks).toString("utf8"), req.headers); } catch { res.writeHead(400).end("Bad signature"); return; } res.writeHead(200).end("OK"); // do the slow work after answering, once per event.id console.log(event.id, event.type, event.data.id); }); }) .listen(3000); ``` To verify by hand, the signing scheme is on [webhook payloads](https://mynth.io/docs/api-reference/webhook-payloads.md#signature). > **Warning** > > 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..completed`, `task..failed` | That task type settling. `` is `image.generate`, `image.rate`, `image.alt`, `image.review`, `image.remove_background`, `image.upscale`, or `video.generate` | | `task.completed`, `task.failed` | Any task type settling that way | | `all` | Every event, including future task types | The event `type` 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](https://mynth.io/docs/api-reference/webhook-payloads.md). ## Sources An endpoint receives only tasks from the sources it allows: | Source | Default | Setting | | ---------------------------------------------- | ------------- | -------------------------------------------------------------------------------------------- | | Tasks from API keys | on, every key | `api_key_ids` (CLI: repeat `--api-key-id`) limits it to those keys. `[]` turns every key off | | Tasks with no API key, such as playground runs | off | `include_session_tasks: true` (CLI: `--include-session-tasks`) | 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](https://mynth.io/dashboard/webhooks). ## 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 | 5 s apart at first, doubling up to 1 h apart, with jitter. For about 72 h in total | | Duplicates | Possible. Every delivery of an event repeats its `webhook-id`, 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 the `webhook-id` and skip one you have already handled. The ID is the same on every retry and to every endpoint, and the SDK helpers pass it as `event.id`. Answer first and do slow work afterwards, because a handler that runs past 30 seconds counts as a failure and is retried. A URL that resolves to a private, loopback or link-local address is refused when the delivery connects, and is not retried. ## Per-request webhooks `POST /image/generate`, `POST /image/remove-background`, `POST /image/upscale`, and `POST /video/generate` take a `webhook` field for one-off URLs that are not registered: ```json { "model": "black-forest-labs/flux.2-pro", "prompt": "A lighthouse at dusk, film grain", "webhook": { "registered": false, "custom": [{ "url": "https://your-app.com/hooks/mynth?token=9f2c1e7b4a0d" }] } } ``` `custom` takes up to five `https` URLs on public hosts. Each one receives this task's event, completed or failed, whatever events your registered endpoints subscribe to. `registered: false` skips your registered endpoints for this task. Rate, alt text, and review requests do not take `webhook`. > **Warning** > > Custom URLs get no `webhook-signature`: Mynth has no secret of yours to sign them with, so > checking that a delivery is real is up to you. Put a long random token in the query string and > compare it in constant time. The task, `GET /tasks/{id}` and every event show the URL only as > `scheme://host`, so the token never reaches anyone else. The SDK helpers reject unsigned deliveries unless you pass `unsigned`, which replaces the signature check with yours: ```ts // app/api/mynth-webhook/one-off/route.ts import { timingSafeEqual } from "node:crypto"; import { mynthWebhookHandler } from "@mynthio/sdk/next"; const TOKEN = Buffer.from(process.env.MYNTH_CUSTOM_WEBHOOK_TOKEN ?? ""); export const POST = mynthWebhookHandler( { imageTaskCompleted: async (result) => { console.log(result.taskId, result.images); }, }, { unsigned: { verify: (request) => { const token = Buffer.from(new URL(request.url).searchParams.get("token") ?? ""); return TOKEN.length > 0 && token.length === TOKEN.length && timingSafeEqual(token, TOKEN); }, }, }, ); ``` `verify` returning `false` answers `401`. The event is still checked: its body must be a Mynth event whose `id` is the `webhook-id`. ## 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: ```bash 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](https://mynth.io/docs/api-reference/webhook-payloads.md): headers, signature, and the body for each task type. - [Tasks](https://mynth.io/docs/concepts/tasks.md): what `completed` and `failed` mean when the POST arrives. - [Poll for results](https://mynth.io/docs/guides/poll-for-results.md): the alternative when something can wait. --- # Rate limits > Provider rate limits are retried for you and surface on the task, not as an HTTP error. A key's spending limit is a 402, not a 429. Mynth is built for production traffic, and a normal app should not need a backoff loop. There are two kinds of limit to plan for, and they show up in different places. ## Limits on your requests Capacity limits can apply to protect the service. Do not build code that assumes a key can send without limit. If you get a `429`, slow down and retry with backoff. There is no `Retry-After` header. The CLI exits with code `6` for a `429`. A key's [spending limit](https://mynth.io/docs/authentication.md#spending-limits) is not a rate limit. It answers `402 spending_limit_exceeded`, and a retry does not clear it. The key's day, week, or month has to roll over, or you raise the cap. ## Limits at the provider Providers rate-limit Mynth. A refused attempt is retried on another provider, within the attempt budget on [tasks](https://mynth.io/docs/concepts/tasks.md#retries-and-timeouts), and you do not see those retries. When every attempt is refused, the item fails on a task that was already accepted: | Code | Means | Do | | ---------------- | ------------------------------------------------- | ---------------------------------- | | `rate_limited` | Providers kept rate-limiting the request | Send a new request shortly | | `providers_busy` | No provider has capacity for this model right now | Retry later, or pick another model | Neither is an HTTP `429` on the create call. The code sits on the failed image or video inside a `completed` task, and failed items are not charged. [Errors](https://mynth.io/docs/api-reference/errors.md#task-failures) lists every task code. ## Next steps - [Authentication](https://mynth.io/docs/authentication.md#spending-limits): set a spending limit on a key. - [Tasks](https://mynth.io/docs/concepts/tasks.md#retries-and-timeouts): attempts and time budgets. - [Errors](https://mynth.io/docs/api-reference/errors.md): every HTTP status and task code. --- # The model field > What the model field accepts, where to read a model's modes, input rules and price, why to pass an explicit id instead of auto, and what happens when a model cannot serve a field. `model` decides which generation model runs. Every model takes the same request body, so switching models is a change to this one field. Model ids are `vendor/name`, spelled exactly as the catalog prints them, for example `black-forest-labs/flux.2-pro` or `bytedance/seedance-2.0-mini`. There are no aliases. | Endpoint | `model` accepts | When omitted | | ---------------------- | ---------------------------- | --------------------- | | `POST /image/generate` | an image model id, or `auto` | `auto` (experimental) | | `POST /video/generate` | a video model id | required | An id that is not in the catalog answers `400 validation_error`. When the id is close to a real one, such as `flux.2-pro` for `black-forest-labs/flux.2-pro`, the message suggests it. See [validation errors](https://mynth.io/docs/api-reference/errors.md#validation-errors). `/image/rate`, `/image/alt`, `/image/review`, `/image/remove-background`, and `/image/upscale` take no `model`. Mynth picks the model behind them and replaces it when a better one is available, without changing the endpoint. ## Find a model The catalog is public, and none of these need an API key: | Surface | Good for | | -------------------------------------------------------- | ----------------------------------------------------- | | [mynth.io/models](https://mynth.io/models) | browsing and filtering, one page per model | | `GET https://api.mynth.io/models` | modes, input rules and pricing, as the SDK types them | | [/models.json](https://mynth.io/models.json), [/models.txt](https://mynth.io/models.txt) | agents: id, name, type, capabilities, price | | `npx @mynthio/cli models list` | the terminal, with filters | **CLI** ```bash npx @mynthio/cli models list --type image --capability img2img --max-price 0.05 ``` **SDK** ```ts import Mynth from "@mynthio/sdk"; const models = await new Mynth().models.list(); // no key needed const imageModels = models.filter((model) => model.type === "image"); ``` Read the catalog at runtime rather than hardcoding a model list or prices. Models are added, and prices change when providers change theirs. ## Reading a catalog entry `GET /models` returns every enabled model in `data`, sorted by id: ```json { "id": "black-forest-labs/flux.2-pro", "display_name": "FLUX.2 Pro", "type": "image", "modes": { "txt->img": {}, "img->img": { "inputs": { "rules": [{ "type": "image", "max": 3 }] } } }, "pricing": { "per_image": { "base": "0.03" }, "per_input": "0.03" } } ``` **`modes`** is what the model serves on Mynth today, which can be less than the vendor advertises. Images use `txt->img` and `img->img`. Video uses `txt->vid` and `img->vid`. A missing mode cannot be used, and a mode without `inputs` takes no images. **`inputs.rules`** is the contract for the request's `inputs` array. Each rule covers one kind of input with a `max`, plus a `min` when that kind is required. A rule's `kind` is the value you put in `inputs[].as`: `source` or `reference` for images, `first_frame` or `last_frame` for video. A rule without a `kind` accepts any image. `max_total` caps the whole array. **`pricing`** is decimal strings in USD, or `null` when the model has no price on file. [Pricing](https://mynth.io/docs/pricing.md#where-prices-come-from) explains each field. The catalog does not publish pixel sizes. You ask for an aspect ratio and a scale, and Mynth maps it to the nearest size the model serves. [/models.json](https://mynth.io/models.json) adds a `supports` list per model, where `4k` means the model has both 4k sizes and a 4k price. ## `auto` is experimental Pass a catalog id on every image request. When `model` is omitted, the API uses `auto`, which is experimental. It has not picked models well, it is not maintained as a model picker, and it is not something to build a product on. The API will keep accepting it. When a request does send `auto`: - It chooses a model by reading the prompt and nothing else. It does not look at `inputs` or `size`. - Creating the task holds a flat $0.20 per image. The estimate endpoint returns that hold with `estimate_kind: "upper_bound"`. - On completion Mynth charges the published price of the model in `result.model` and releases the rest of the hold. - An edit or a `_4k` size can land on a model that cannot serve it. The task then fails with `capability_not_supported`, the hold is released, and nothing is charged. With an explicit id, the price is exact before you send, and input and size problems are rejected at create instead of failing later. ## Fields a model cannot serve There is no per-model options bag, and no request field for steps, guidance, or a scheduler. What differs between models is how each reacts to a field it cannot serve: | You send | Result | | --------------------------------------------------------- | ------------------------------------------------------------- | | A `size` ratio the model has no preset for | Snaps to the closest ratio it serves | | A `_4k` size on a pinned model without 4k | `400 validation_error` at create, nothing queued | | A `_4k` size with `auto`, landing on a model without 4k | Accepted, then the task fails with `capability_not_supported` | | `inputs` a pinned model cannot take | `400 validation_error` at create, nothing queued | | `inputs` with `auto`, landing on a model that cannot edit | Accepted, then the task fails with `capability_not_supported` | | `negative_prompt` to a model without one | Dropped before the provider call, no error | | A video tier, `duration`, or `audio` the model rejects | `400 validation_error` at create | | A field that is not in the schema | `400 validation_error` at create, naming the field | The two `capability_not_supported` rows fail the whole task rather than one image, because the model, inputs, and size are resolved once before any image is generated. ## Next steps - [Choosing an image model](https://mynth.io/docs/models/choosing.md): what to check before you pin one. - [Video models](https://mynth.io/docs/models/video.md): the video models and their limits. - [Pricing](https://mynth.io/docs/pricing.md): how the catalog price becomes a charge. --- # Choosing an image model > Pick an image model id from the catalog. Check that it edits, that it serves 4k, and what it costs, then try your own prompts on a shortlist. Pick a model id from the [catalog](https://mynth.io/models) and pass it on every request. Three things in the catalog entry decide whether a model can serve your request at all. None of them is a matter of taste: | Check | Where to look | If the model lacks it | | -------------------- | ------------------------------------------------ | -------------------------------------------- | | Does it take images? | `img->img` in `modes`, or `inputs` in `supports` | `400 validation_error` when you send inputs | | Does it serve 4k? | `pricing.per_image["4k"]`, or `4k` in `supports` | The task fails on a `_4k` size | | What does it cost? | `pricing.per_image.base`, plus `per_input` | Nothing fails. You pay more than you planned | Image prices differ by more than two orders of magnitude across the catalog, so check the price before you pin a model. ```bash npx @mynthio/cli models list --type image --capability img2img --4k --max-price 0.05 ``` The same filters are on [mynth.io/models](https://mynth.io/models). `--max-price` compares the base price per image. > **Note** > > The catalog does not rank models, and neither do we. Which model looks best depends on your > prompts and your taste. Pick two or three in the right price band and run your own prompts through > them. Once you have an id, pass it: ```bash curl https://api.mynth.io/image/generate \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"black-forest-labs/flux.2-pro","prompt":"A lighthouse at dusk, 35mm film"}' ``` Do not leave `model` off to let Mynth choose. That falls back to `auto`, which is experimental. [The model field](https://mynth.io/docs/models.md#auto-is-experimental) explains why. ## Next steps - [The model field](https://mynth.io/docs/models.md): catalog fields, input rules, and fields a model cannot serve. - [Image to image](https://mynth.io/docs/guides/image-to-image.md): sending inputs to a model that edits. - [Estimate cost](https://mynth.io/docs/guides/estimate-cost.md): price a request before you run it. --- # Video models > The video models, the resolutions, durations and frame inputs each one accepts, and why video always needs an explicit model id. Video runs on `POST /video/generate`, and `model` is required. There is no `auto` for video. | Model | Id | Resolutions | Duration (s) | Frame inputs | | ---------------------- | ------------------------------ | --------------------- | --------------- | ----------------------- | | Seedance 2.0 Mini | `bytedance/seedance-2.0-mini` | 480p, **720p** | 4–15, default 5 | first frame, last frame | | Gemini Omni Flash 1.1 | `google/gemini-omni-flash-1.1` | **720p**, 1080p, 4k | 3–10, default 8 | first frame, last frame | | P-Video | `prunaai/p-video` | **720p**, 1080p | 1–10, default 5 | first frame | | Grok Imagine Video 1.5 | `xai/grok-imagine-video-1.5` | **480p**, 720p, 1080p | 1–15, default 8 | first frame | Bold marks the default resolution. Every model does text-to-video and image-to-video, and generates audio by default. Image-to-video needs exactly one `first_frame`. A `last_frame` is optional where the model takes one. No video model accepts a `reference` input today. This table is a snapshot. The SDK exports the same data as `AVAILABLE_VIDEO_MODELS`, and `modes` and `pricing.per_second` on `GET https://api.mynth.io/models` are the live source for inputs and resolution tiers. Every model takes every [size](https://mynth.io/docs/guides/generate-video.md#sizes) shape: the aspect ratio snaps to the closest one the model serves. > **Warning** > > The resolution tier, `duration`, `audio`, and `inputs` are checked against the model when the task > is created. A value the model does not serve answers `400 validation_error`, and nothing is > queued. ## Price Video is priced per second at the requested resolution, so the same prompt costs different amounts on different models and tiers. Video estimates are always exact, because the model is always explicit. ```bash curl https://api.mynth.io/video/generate/estimate \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"bytedance/seedance-2.0-mini","prompt":"A lighthouse beam sweeping over waves","duration":5}' ``` ## Next steps - [Generate video](https://mynth.io/docs/guides/generate-video.md): the full request and how to collect the file. - [The model field](https://mynth.io/docs/models.md): input rules, and the `first_frame` and `last_frame` kinds. - [Pricing](https://mynth.io/docs/pricing.md#video-price): the per-second formula. --- # Generate images > The image generation request, every field it takes, the sizes you can ask for, and what comes back. `POST /image/generate` creates an image task. Send a `model` id and a `prompt`. Everything else is optional. **SDK** ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); const task = await mynth.image.generate({ model: "black-forest-labs/flux.2-pro", prompt: "A lighthouse at dusk, film grain", size: "landscape", }); console.log(task.urls[0]); ``` **CLI** ```bash npx @mynthio/cli image generate \ -m black-forest-labs/flux.2-pro \ -p "A lighthouse at dusk, film grain" \ -s landscape ``` **REST** ```bash curl https://api.mynth.io/image/generate \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"black-forest-labs/flux.2-pro","prompt":"A lighthouse at dusk, film grain","size":"landscape"}' ``` The SDK and the CLI wait and return the finished task. The REST call returns at once with a task id: ```json { "data": { "task_id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimated_cost": "0.03" } } ``` Collect the result by [polling](https://mynth.io/docs/guides/poll-for-results.md) or with a [webhook](https://mynth.io/docs/concepts/webhooks.md). ## Fields Every image model takes this body. | Field | Default | Notes | | ------------------------------ | --------------- | --------------------------------------------------------------------------------------------------------------------------- | | `model` | `auto` | A catalog id. Always pass one. [`auto` is experimental](https://mynth.io/docs/models.md#auto-is-experimental) | | `prompt` | required | 1 to 8192 characters | | `negative_prompt` | none | Up to 8192 characters. Dropped without an error if the model has none | | `count` | `1` | A whole number, 1 to 20. Each image succeeds or fails on its own | | `size` | `auto` | See [sizes](#sizes) | | `inputs` | none | Up to 20 input images. See [image to image](https://mynth.io/docs/guides/image-to-image.md) | | `magic_prompt` | `false` | Rewrite the prompt first. See [enhance prompts](https://mynth.io/docs/guides/enhance-prompts.md) | | `rating` | none | `true`, or a rating object. See [rate images](https://mynth.io/docs/guides/rate-images.md#on-a-generation) | | `output.format` | the provider's | `png`, `jpg`, or `webp` | | `destination` | none | A destination name. See [destinations](https://mynth.io/docs/concepts/destinations.md) | | `webhook` | registered ones | Per-request URLs. See [webhooks](https://mynth.io/docs/concepts/webhooks.md#per-request-webhooks) | | `metadata` | none | A JSON object up to 2048 bytes, returned on the task and in webhooks | | `generate_public_access_token` | `false` | `true` returns a browser token as `public_access_token`. See [from a browser](https://mynth.io/docs/guides/poll-for-results.md#from-a-browser) | A field the schema does not define answers `400 validation_error`, naming it, so a typo never runs as an ignored setting. The task's `request` shows the body as it was accepted, with defaults filled in. ## Sizes You ask for a shape, not pixels, and Mynth picks the closest size the model serves. Omit `size`, or send `"auto"`, and Mynth picks a ratio that suits the prompt. | `size` | Result | | ------------------------------------------------------------------------------------- | ------------------------- | | `square`, `portrait`, `landscape`, `portrait_tall`, `landscape_wide` | 1:1, 2:3, 3:2, 9:16, 16:9 | | `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9`, `2:1`, `1:2` | that ratio | | Any ratio with `_4k`, such as `16:9_4k` | that ratio at 4k | | `{ "type": "aspect_ratio", "aspect_ratio": "16:9", "scale": "4k" }` | the same, as an object | A ratio the model has no preset for snaps to the closest one it serves. A 4k size costs `per_image["4k"]` and needs a model that has it. On a model without 4k the create answers `400 validation_error` with the size to use instead, and nothing is queued. With `model: "auto"` the model is picked later, so the task fails with `capability_not_supported` instead. ## What comes back A completed task has one entry in `result.images` per requested image: ```json { "model": "black-forest-labs/flux.2-pro", "images": [ { "status": "success", "id": "img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk", "url": "https://cdn.mynth.io/images/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp", "mynth_url": "https://cdn.mynth.io/images/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp", "size": "1536x1024", "format": "webp" } ] } ``` - Check `status` on every image. One image failing does not fail the task. See [tasks](https://mynth.io/docs/concepts/tasks.md#completed-does-not-mean-every-item-worked). - `url` is where the file ended up: your storage when you named a destination, otherwise the same as `mynth_url`. - `size` is the delivered file in `{width}x{height}` pixels. - `result.model` is the model that ran. - The file is served for 7 days. See [file lifetimes](https://mynth.io/docs/concepts/tasks.md#file-lifetimes). Every field of the result is on [the task object](https://mynth.io/docs/api-reference/task-object.md#imagegenerate). ## Price The create response holds `estimated_cost`, and you pay only for the images that succeed. [Pricing](https://mynth.io/docs/pricing.md#image-price) has the formula, and [estimate cost](https://mynth.io/docs/guides/estimate-cost.md) prices a body without generating. ## Next steps - [Choosing an image model](https://mynth.io/docs/models/choosing.md): what to check before you pin an id. - [Image to image](https://mynth.io/docs/guides/image-to-image.md): send input images. - [Poll for results](https://mynth.io/docs/guides/poll-for-results.md): collect the task over REST. --- # Generate video > Start a video render on an explicit model, with optional frame inputs, and collect the file. `POST /video/generate` creates a video task. `model` and `prompt` are required. One request renders one video, and there is no `auto` or `count`. **SDK** ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); const task = await mynth.video.generate({ model: "bytedance/seedance-2.0-mini", prompt: "A lighthouse beam sweeping over waves", duration: 5, size: "9:16_720p", }); console.log(task.urls[0]); ``` **REST** ```bash curl https://api.mynth.io/video/generate \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"bytedance/seedance-2.0-mini","prompt":"A lighthouse beam sweeping over waves","duration":5,"size":"9:16_720p"}' ``` The REST call returns a `task_id` and `estimated_cost` at once. The SDK waits up to an hour. A render takes minutes, so in a request handler, create the task with `generateAsync()` and finish in a [webhook](https://mynth.io/docs/concepts/webhooks.md) instead of waiting. The CLI has no video command. ## Fields | Field | Notes | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | | `model` | Required. An id from [video models](https://mynth.io/docs/models/video.md) | | `prompt` | Required. 1 to 8192 characters | | `negative_prompt` | Optional. Up to 8192 characters | | `size` | Aspect ratio and resolution tier. Defaults to `auto`. See [sizes](#sizes) | | `duration` | Seconds. Defaults to the model's default. Each model accepts its own range | | `audio` | Generated audio. Defaults to the model's default, which is on for every current model | | `inputs` | Frame images. See [frame inputs](#frame-inputs) | | `webhook` | Per-request URLs, same shape as on images. See [webhooks](https://mynth.io/docs/concepts/webhooks.md#per-request-webhooks) | | `metadata` | A JSON object up to 2048 bytes | | `generate_public_access_token` | `true` returns a browser token as `public_access_token`. See [from a browser](https://mynth.io/docs/guides/poll-for-results.md#from-a-browser) | Video does not take `destination`, `count`, `magic_prompt`, or `output`. Sending any of them answers `400 validation_error`. Every value is checked against the model when the task is created. A resolution tier, duration, audio setting, or input the model does not serve answers `400 validation_error`, and nothing is queued. ## Sizes You ask for a shape and a resolution tier, not pixels. The shapes are the same as for [images](https://mynth.io/docs/guides/generate-images.md#sizes), and Mynth picks the closest aspect ratio the model serves. The tier is not adjusted: it sets the price, so a tier the model does not serve answers `400 validation_error`. | `size` | Result | | ------------------------------------------------------------------------------------- | ------------------------------------------------ | | `auto`, or no `size` | the model's default shape and tier | | `square`, `portrait`, `landscape`, `portrait_tall`, `landscape_wide` | 1:1, 2:3, 3:2, 9:16, 16:9 | | `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9`, `2:1`, `1:2` | that ratio | | Any of the above with `_480p`, `_720p`, `_1080p`, or `_4k`, such as `9:16_1080p` | that shape at that tier | | `{ "type": "aspect_ratio", "aspect_ratio": "9:16", "resolution": "1080p" }` | the same, as an object. `resolution` is optional | | `{ "type": "auto", "resolution": "1080p" }` | the model's default shape at that tier | With a first frame, the frame sets the shape and the aspect ratio is ignored. The tier still applies. The result's `aspect_ratio` is the ratio the video was rendered at, or `null` when the frame set it. ## Frame inputs Send `inputs` to animate from an image. Each input is a URL string or an object with a role: ```json { "model": "bytedance/seedance-2.0-mini", "prompt": "The camera pulls back from the lighthouse", "inputs": [ { "type": "image", "as": "first_frame", "source": { "type": "url", "url": "https://cdn.example.com/start.png" } }, { "type": "image", "as": "last_frame", "source": { "type": "url", "url": "https://cdn.example.com/end.png" } } ] } ``` Every current model needs exactly one `first_frame` for image-to-video. Seedance and Gemini Omni Flash also take an optional `last_frame`. P-Video and Grok Imagine Video take the first frame only. [Video models](https://mynth.io/docs/models/video.md) has the table, and `modes` on the catalog entry is the live contract. ## What comes back ```json { "model": "bytedance/seedance-2.0-mini", "videos": [ { "status": "success", "id": "vid_3HkP9wQz7LmR2tXc8VbN5dFy0JsA4gUe", "url": "https://cdn.mynth.io/videos/vid_3HkP9wQz7LmR2tXc8VbN5dFy0JsA4gUe.mp4", "mynth_url": "https://cdn.mynth.io/videos/vid_3HkP9wQz7LmR2tXc8VbN5dFy0JsA4gUe.mp4", "cost": "0.40500000", "duration": 5, "resolution": "720p", "aspect_ratio": "9:16", "audio": true } ] } ``` Check `status` on the video. A refusal or a provider failure fails the item with an `error.code` while the task still completes. The file is served for 7 days. See [tasks](https://mynth.io/docs/concepts/tasks.md#file-lifetimes). ## Price Video is `per_second[tier] × duration`, where `tier` is the resolution in `size`, and the estimate is exact because the model is explicit. Price a body with `POST /video/generate/estimate`, or `mynth.video.estimate()` in the SDK. [Pricing](https://mynth.io/docs/pricing.md#video-price) has the formula. ## Next steps - [Video models](https://mynth.io/docs/models/video.md): resolutions, durations, and frame inputs per model. - [Webhooks](https://mynth.io/docs/concepts/webhooks.md): collect a long render without holding a connection. - [Poll for results](https://mynth.io/docs/guides/poll-for-results.md): the status and result calls. --- # Image to image > Send input images with a generation to edit or reference them. Input roles, which models accept them, uploading local files, and what inputs cost. An edit is the same `POST /image/generate` as a text prompt, plus `inputs`. Pass a model whose catalog entry lists `img->img`, and check its `inputs.rules` for how many images it takes. **SDK** ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); const task = await mynth.image.generate({ model: "black-forest-labs/flux.2-pro", prompt: "The same lighthouse, now in heavy fog", inputs: ["https://cdn.example.com/lighthouse.png"], }); ``` **CLI** ```bash npx @mynthio/cli image generate \ -m black-forest-labs/flux.2-pro \ -p "The same lighthouse, now in heavy fog" \ -i https://cdn.example.com/lighthouse.png ``` **REST** ```bash curl https://api.mynth.io/image/generate \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "black-forest-labs/flux.2-pro", "prompt": "The same lighthouse, now in heavy fog", "inputs": ["https://cdn.example.com/lighthouse.png"] }' ``` ## Inputs and roles `inputs` takes up to 20 entries. A string is a public image URL. The object form sets a role: ```json { "type": "image", "as": "reference", "source": { "type": "url", "url": "https://cdn.example.com/lighthouse.png" } } ``` | `as` | Meaning | | ----------- | ------------------------------------------------------ | | `source` | The image to edit | | `reference` | An image to take style or content from | | `auto` | Let Mynth assign a role the model accepts. The default | The catalog entry's `inputs.rules` says which roles the model has and how many images each allows. A rule with no `kind` accepts any image. In the CLI, set a role with a prefix: `-i source:./photo.png` or `-i reference:https://...`. An input URL must be reachable from the public internet. An image Mynth cannot fetch or read fails with `invalid_input`. ## Pass a model that edits With an explicit model, inputs are checked when the task is created. A model that cannot take them, or too many of them, answers `400 validation_error` and nothing is queued. `auto` does not look at `inputs`. An edit sent with `auto` can be accepted and then fail with `capability_not_supported` when it lands on a model that cannot edit. [The model field](https://mynth.io/docs/models.md#auto-is-experimental) has the details. ## Local files The SDK and the CLI upload local files for you. In the SDK, put a `File` or `Blob` in `inputs`. In the CLI, pass a path to `-i`. ```ts const task = await mynth.image.generate({ model: "black-forest-labs/flux.2-pro", prompt: "The same lighthouse, at noon", inputs: [file], }); ``` Over REST, upload first with `POST /image/upload` and pass the returned URLs: ```bash curl https://api.mynth.io/image/upload \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -F "images=@./lighthouse.png" ``` ```json { "data": { "urls": ["https://cdn.mynth.io/inputs/img_3f9a0c1d2e4b5a6978c0d1e2f3a4b5c6.png"] } } ``` An upload takes up to 10 JPEG, PNG, or WEBP files, each between 1 KB and 10 MB. It is free, and the URLs are served for 1 day. The SDK and the CLI send all local files of one request in a single upload, so the same 10-file limit applies to them. ## Price Models that charge for inputs list `per_input` in the catalog. It is charged once per input image for each successful output, so two inputs at `count: 4` add eight input charges to the four image charges. A model without `per_input` charges nothing for inputs. [Pricing](https://mynth.io/docs/pricing.md#image-price) has the formula. ## Next steps - [Generate images](https://mynth.io/docs/guides/generate-images.md): the rest of the request. - [Choosing an image model](https://mynth.io/docs/models/choosing.md): finding a model that edits. - [Remove background](https://mynth.io/docs/guides/remove-background.md): a cutout without a prompt. --- # Enhance prompts > Set magic_prompt to have Mynth rewrite the prompt for the chosen model before it runs, and read back the text that ran. Set `magic_prompt: true` on `POST /image/generate`. Mynth rewrites the prompt for the model that will run, then generates from the rewrite. Your original prompt stays on the task's `request`, and the text that ran is `result.magic_prompt`. **SDK** ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); const task = await mynth.image.generate({ model: "black-forest-labs/flux.2-pro", prompt: "please make a nice picture of a lighthouse", magic_prompt: true, }); console.log(task.magicPrompt?.positive); ``` **CLI** ```bash npx @mynthio/cli image generate \ -m black-forest-labs/flux.2-pro \ -p "please make a nice picture of a lighthouse" \ --magic-prompt ``` ```json { "magic_prompt": { "positive": "A lighthouse on a rocky point at dusk, film grain, cool blue haze", "negative": "blurry, watermark" } } ``` `negative` is present only when the request had a `negative_prompt`, which is rewritten too. ## What the rewrite does It keeps your intent. The subject, counts, names, and any text meant to appear in the image stay. Wrapper phrases such as "please generate" go. It does not add objects you did not ask for, and it adapts to the style of the model that will run. A more creative rewrite mode is in development. [FLUX.3](https://mynth.io/models/black-forest-labs%2Fflux-3) also has model-specific rewriting. Set `model: "black-forest-labs/flux-3"` and `magic_prompt: true` for a text-to-image request. The rewrite preserves your supplied text, subject counts, and layout, and describes the image in natural language. ## Price and limits Magic Prompt costs $0 today. It is included in the generation estimate, so a future price will show up before you send. Video requests do not take `magic_prompt`. ## Next steps - [Generate images](https://mynth.io/docs/guides/generate-images.md): the rest of the request. - [Prompts and images](https://mynth.io/docs/concepts/prompts.md): what the rewrite reads, and what the provider receives. --- # Remove background > Turn one image into a cutout with a transparent background. No model or prompt to choose. `POST /image/remove-background` takes one image URL and returns a cutout with a transparent background. There is no `model` field and no prompt. Mynth picks the model and keeps it current. **SDK** ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); const cutout = await mynth.image.removeBackground({ url: "https://cdn.example.com/product.jpg", output: { format: "png" }, }); console.log(cutout.image.url); ``` **CLI** ```bash npx @mynthio/cli image remove-background https://cdn.example.com/product.jpg -f png -o ./out ``` **REST** ```bash curl https://api.mynth.io/image/remove-background \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://cdn.example.com/product.jpg","output":{"format":"png"}}' ``` ## Fields | Field | Notes | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | | `url` | Required. A public http(s) image URL | | `output.format` | `png` or `webp`. Both keep transparency. Omit it for the provider's format | | `destination` | A destination name. See [destinations](https://mynth.io/docs/concepts/destinations.md) | | `webhook` | Per-request URLs. See [webhooks](https://mynth.io/docs/concepts/webhooks.md#per-request-webhooks) | | `metadata` | A JSON object up to 2048 bytes | | `generate_public_access_token` | `true` returns a browser token as `public_access_token`. See [from a browser](https://mynth.io/docs/guides/poll-for-results.md#from-a-browser) | For a local file, pass `file` instead of `url` in the SDK, or a path in the CLI. Both upload it first. ## What comes back ```json { "image": { "id": "img_Q8mZr2LkT0vWc5NhY7pD3xFa9GsJ1bEu", "url": "https://cdn.mynth.io/images/img_Q8mZr2LkT0vWc5NhY7pD3xFa9GsJ1bEu.png", "mynth_url": "https://cdn.mynth.io/images/img_Q8mZr2LkT0vWc5NhY7pD3xFa9GsJ1bEu.png", "size": "1024x1024", "format": "png" } } ``` There is one image and no per-item status. If the work fails, the task is `failed`. With a destination, `image` also carries a `destination` block that says whether the upload worked. See [destinations](https://mynth.io/docs/concepts/destinations.md#what-comes-back). The create response includes a `pat_` token, so a browser can poll this task the same way it polls a generation. See [poll for results](https://mynth.io/docs/guides/poll-for-results.md#from-a-browser). ## Price $0.02 per image, held at create and charged when the task succeeds. See [pricing](https://mynth.io/docs/pricing.md#tool-prices). ## Next steps - [Destinations](https://mynth.io/docs/concepts/destinations.md): write the cutout to your own storage. - [Image to image](https://mynth.io/docs/guides/image-to-image.md): edits that need a prompt. --- # Upscale images > Enlarge one image 2x or 4x, up to 4096x4096, for a flat price per effort. No model or prompt to choose. > **Note** > > Upscaling is in beta. The request shape is settled, but the models behind each effort may change > as we see real traffic. `POST /image/upscale` takes one image URL and returns it larger. There is no `model` field and no prompt. You pick a size and an effort, and Mynth picks the model. **SDK** ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); const upscaled = await mynth.image.upscale({ url: "https://cdn.example.com/product.jpg", size: "2x", effort: "low", }); console.log(upscaled.image.url); ``` **CLI** ```bash npx @mynthio/cli image upscale https://cdn.example.com/product.jpg --size 2x --effort low -o ./out ``` **REST** ```bash curl https://api.mynth.io/image/upscale \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://cdn.example.com/product.jpg","size":"2x","effort":"low"}' ``` ## Fields | Field | Notes | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | | `url` | Required. A public http(s) image URL | | `size` | Required. `"2x"` or `"4x"`, or `{ "type": "scale", "factor": 2 }` with 2 or 4 | | `effort` | Required. `"low"` or `"high"`. It sets the price, so there is no default | | `output.format` | `png`, `jpg`, or `webp`. Omit it for the provider's format | | `destination` | A destination name. See [destinations](https://mynth.io/docs/concepts/destinations.md) | | `webhook` | Per-request URLs. See [webhooks](https://mynth.io/docs/concepts/webhooks.md#per-request-webhooks) | | `metadata` | A JSON object up to 2048 bytes | | `generate_public_access_token` | `true` returns a browser token as `public_access_token`. See [from a browser](https://mynth.io/docs/guides/poll-for-results.md#from-a-browser) | For a local file, pass `file` instead of `url` in the SDK, or a path in the CLI. Both upload it first. The CLI takes `--size 2x` or `--size 4x` only. ## Pick an effort | `effort` | Good for | Price | | -------- | -------------------------------------------------------------------------- | ----- | | `low` | Fast and sharp. Product shots, generated images, anything already clean | $0.03 | | `high` | Rebuilds fine detail: small text, skin, fur, leaves, soft or damaged input | $0.15 | > **Warning** > > `high` can rebuild faces on old or damaged photos, which can change a person's likeness. Use `low` > when the face must stay exactly as it is. ## Size limit The upscaled image can be at most 4096x4096 pixels (16.8 MP), counted in pixels rather than per side. That means: - `2x` takes images up to about 4.2 MP, such as 2048x2048 or 1600x2600. - `4x` takes images up to about 1 MP, such as 1024x1024 or 800x1300. A larger request is accepted at create, then fails with `output_too_large` before any work starts. It is not charged. The result comes back at whatever size the provider returns, which can be a few pixels off the source times the factor. Read `size` for the real dimensions. ## What comes back ```json { "image": { "id": "img_Hn4cW8pLx2RsT6vYb0QjM9kE3dAf7ZuG", "url": "https://cdn.mynth.io/images/img_Hn4cW8pLx2RsT6vYb0QjM9kE3dAf7ZuG.png", "mynth_url": "https://cdn.mynth.io/images/img_Hn4cW8pLx2RsT6vYb0QjM9kE3dAf7ZuG.png", "size": "3072x4608", "format": "png" } } ``` There is one image and no per-item status. If the work fails, the task is `failed`. With a destination, `image` also carries a `destination` block that says whether the upload worked. See [destinations](https://mynth.io/docs/concepts/destinations.md#what-comes-back). The create response includes a `pat_` token, so a browser can poll this task the same way it polls a generation. See [poll for results](https://mynth.io/docs/guides/poll-for-results.md#from-a-browser). ## Price $0.03 for `low` and $0.15 for `high`, per image, whatever the `size`. The price is held at create and charged when the task succeeds. See [pricing](https://mynth.io/docs/pricing.md#tool-prices). ## Next steps - [Remove background](https://mynth.io/docs/guides/remove-background.md): cut the subject out first, then upscale. - [Destinations](https://mynth.io/docs/concepts/destinations.md): write the result to your own storage. --- # Alt text > Generate an alt text string, 1 to 160 characters, for an existing image. `POST /image/alt` writes alt text for one image. There is no `model` field. Mynth picks the model and keeps it current. The result is a string of 1 to 160 characters, written to be read aloud. **SDK** ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); const result = await mynth.image.alt({ url: "https://cdn.example.com/lighthouse.png" }); console.log(result.alt); ``` **CLI** ```bash npx @mynthio/cli image alt https://cdn.example.com/lighthouse.png ``` **REST** ```bash curl https://api.mynth.io/image/alt \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://cdn.example.com/lighthouse.png"}' ``` The body takes only `url`, a public http(s) image URL. The SDK also accepts a `file`, and the CLI a local path. Both upload it first. The request does not take `webhook` or `metadata`, and the response has no `pat_` token. The REST call returns a task. The task's `result` is: ```json { "alt": "A white lighthouse on dark rocks, lit by an orange sunset sky" } ``` A URL Mynth cannot fetch fails the task with `fetch_failed`. ## Price $0.0004 per image, charged only when the task succeeds. See [pricing](https://mynth.io/docs/pricing.md#tool-prices). ## Next steps - [Rate images](https://mynth.io/docs/guides/rate-images.md): classify the same image instead of describing it. - [Review images](https://mynth.io/docs/guides/review-images.md): score it and list defects. --- # Review images > Score an image from 1 to 4 and list what is wrong with it, using a panel of reviewers. Two effort levels trade thoroughness for price. `POST /image/review` scores one image and lists its defects and strengths. A panel of vision models reviews the image independently, and Mynth merges their verdicts. There is no `model` field. `effort` picks the panel. **SDK** ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); const review = await mynth.image.review({ url: "https://cdn.example.com/lighthouse.png", effort: "low", }); console.log(review.score, review.summary); for (const finding of review.findings) console.log(finding.severity, finding.finding); ``` **CLI** ```bash npx @mynthio/cli image review https://cdn.example.com/lighthouse.png --effort low ``` **REST** ```bash curl https://api.mynth.io/image/review \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://cdn.example.com/lighthouse.png","effort":"low"}' ``` | `effort` | Panel | Price | Use it for | | ---------------- | -------------------- | ----- | --------------------------- | | `high` (default) | five strong models | $0.30 | final checks on hero images | | `low` | three smaller models | $0.01 | triage at volume | You pay only when the task succeeds. The body takes `url` and `effort`, and no `webhook` or `metadata`. The SDK also accepts a `file`. ## The result ```json { "score": 3, "summary": "Clean composition with one visible defect in the left hand.", "findings": [ { "finding": "The left hand has six fingers", "category": "anatomy", "severity": "major", "where": "Bottom left, the hand resting on the railing", "confidence": "high" } ], "strengths": [{ "strength": "Warm, consistent lighting", "confidence": "medium" }] } ``` | Field | Meaning | | ------------ | ----------------------------------------------------------------------------------------- | | `score` | 1 to 4, higher is better. The median of the reviewers' scores | | `summary` | A short prose summary | | `finding` | What is wrong, in plain language | | `category` | Usually `anatomy`, `text`, `composition`, `artifact`, `color`, or `lighting`. An open set | | `severity` | `critical`, `major`, or `minor` | | `where` | Where in the image, in plain language | | `confidence` | `low`, `medium`, or `high`: how strongly the reviewers agreed | A finding or strength is reported only when at least two reviewers named it. Treat `category` as a label, not an enum: an unusual defect gets its own category. The rubric is revised from time to time, so compare scores taken around the same time rather than across months. ## Next steps - [Rate images](https://mynth.io/docs/guides/rate-images.md): sfw/nsfw, a different question from quality. - [Pricing](https://mynth.io/docs/pricing.md#tool-prices): the two effort prices. --- # Rate images > Classify an image as sfw or nsfw, or against rating levels you define, on its own or as part of a generation. `POST /image/rate` classifies one image. There is no `model` field. Mynth picks the model and keeps it current. By default the result `level` is `sfw` or `nsfw`. **SDK** ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); const rating = await mynth.image.rate({ url: "https://cdn.example.com/lighthouse.png" }); console.log(rating.level); // "sfw" or "nsfw" ``` **CLI** ```bash npx @mynthio/cli image rate https://cdn.example.com/lighthouse.png ``` **REST** ```bash curl https://api.mynth.io/image/rate \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://cdn.example.com/lighthouse.png"}' ``` A standalone rating costs $0.0002, charged only when the task succeeds. The body takes `url` and the rating mode, and no `webhook` or `metadata`. The SDK also accepts a `file`. ## Custom levels Send `mode: "custom"` with your own levels. The result `level` is one of the `value` strings you sent, and the SDK types it that way. ```json { "url": "https://cdn.example.com/lighthouse.png", "mode": "custom", "levels": [ { "value": "safe", "description": "No explicit content" }, { "value": "adult", "description": "Nudity or sexual content" } ] } ``` `levels` takes 2 to 7 entries. `value` is 1 to 24 characters and `description` 1 to 150. Omitting `mode` means `nsfw_sfw`. In the CLI, repeat `-l value=description`, or pass `--levels-file` or `--levels-json`. ## On a generation `rating` on `POST /image/generate` rates each successful image and puts the outcome on that image. `true` runs the default sfw/nsfw check. A `{ "mode": "custom", "levels": [...] }` object runs your levels. Rating on a generation costs $0. ```json { "model": "black-forest-labs/flux.2-pro", "prompt": "A lighthouse at dusk", "rating": true } ``` Each successful image then carries `"rating": { "status": "success", "level": "sfw" }`. A rating that fails does not fail the image: `rating.status` is `failed` with an `error.code`, and the image is still there. The CLI flag is `--content-rating`. ## Next steps - [Generate images](https://mynth.io/docs/guides/generate-images.md): where `rating` sits on the request. - [Review images](https://mynth.io/docs/guides/review-images.md): quality, not content. --- # Estimate cost > Price an image or video request before creating it. The estimate endpoints validate the same body as generate and hold nothing. `POST /image/generate/estimate` and `POST /video/generate/estimate` take the same body as generate. They validate it the same way and return the price. No task is created and nothing is held on your balance. **REST** ```bash curl https://api.mynth.io/image/generate/estimate \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"black-forest-labs/flux.2-pro","prompt":"A lighthouse at dusk","count":2}' ``` **CLI** ```bash npx @mynthio/cli image generate --dry-run \ -m black-forest-labs/flux.2-pro \ -p "A lighthouse at dusk" \ -c 2 ``` **SDK** ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); // video only; the SDK has no image.estimate() const quote = await mynth.video.estimate({ model: "bytedance/seedance-2.0-mini", prompt: "A lighthouse beam sweeping over waves", duration: 5, }); ``` ```json { "data": { "estimated_cost": "0.06", "currency": "usd", "estimate_kind": "exact" } } ``` | Field | Meaning | | ---------------- | ------------------------------------------------------------------------------------------ | | `estimated_cost` | USD as a decimal string. The amount a real request would hold | | `estimate_kind` | `exact` for an explicit model. `upper_bound` for image `auto`, which holds $0.20 per image | Keep `estimated_cost` as a decimal. Parse it with a decimal library if you add amounts up, because floats drift. `--dry-run` in the CLI prints the same number, and `--json` prints the object. Local input files are not uploaded on a dry run. ## Use it as a preflight A body the model cannot serve fails here the same way it would on generate, so the estimate also checks a request before you spend anything. Before a batch, compare the total with `available` from `GET /balance`, which needs a key with the `manage` scope. The formulas behind the number, and how the hold becomes a charge, are on [pricing](https://mynth.io/docs/pricing.md). ## Next steps - [Pricing and billing](https://mynth.io/docs/pricing.md): the formulas, the balance, and holds. - [Generate images](https://mynth.io/docs/guides/generate-images.md): send the body for real. - [Video models](https://mynth.io/docs/models/video.md): the tiers and durations that set a video's price. --- # Poll for results > Poll a task's status until it settles, then read the result, from a server with an API key or from a browser with the task's pat_ token. A create call returns a task id, not the file. Poll `GET /tasks/{id}/status` until `status` leaves `pending`, then read `GET /tasks/{id}/result`. Both are served from a cache and are the calls meant for a loop. `GET /tasks/{id}` is the full record and is not. The SDK's `generate()` does this for you. Poll yourself when you want control over the loop, or when a browser does the waiting. ## From a server ```ts const API = "https://api.mynth.io"; const headers = { Authorization: `Bearer ${process.env.MYNTH_API_KEY}` }; async function waitForTask(taskId: string, { timeoutMs = 30 * 60_000 } = {}) { const deadline = Date.now() + timeoutMs; let delay = 2_000; while (Date.now() < deadline) { const res = await fetch(`${API}/tasks/${taskId}/status`, { headers }); if (!res.ok && res.status < 500) throw new Error(`status ${res.status}`); if (res.ok) { const { data } = await res.json(); if (data.status !== "pending") { const result = await fetch(`${API}/tasks/${taskId}/result`, { headers }); return (await result.json()).data; // { id, type, status, result } } } await new Promise((resolve) => setTimeout(resolve, delay)); delay = Math.min(delay * 1.5, 10_000); } throw new Error(`Task ${taskId} still pending after ${timeoutMs} ms`); } ``` Start near 2 seconds and back off to about 10. Most images finish in seconds. Video takes minutes, so give it a longer timeout. Retry `5xx` and network errors. A `4xx` fails the same way every time. When the loop returns: - `status: "completed"`: check `status` on each item in `result.images` or `result.videos`, because a completed task can hold failed items. See [tasks](https://mynth.io/docs/concepts/tasks.md#completed-does-not-mean-every-item-worked). - `status: "failed"`: `/result` does not include `errors`. Read `GET /tasks/{id}` for the codes. Copy the file when you see `completed` if you need it for more than 7 days and did not name a [destination](https://mynth.io/docs/concepts/destinations.md). ## From a browser `POST /image/generate`, `POST /image/remove-background`, `POST /image/upscale`, and `POST /video/generate` return `data.public_access_token`, a `pat_` token for that one task, when the request sets `generate_public_access_token: true`. It works as a bearer token on the status and result calls for that task and nowhere else, and it expires one hour after it is issued. Only those two paths accept cross-origin requests. Create the task on your server and hand the browser only the id and the token: ```ts // server import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); const pending = await mynth.image.generateAsync({ model: "black-forest-labs/flux.2-pro", prompt: "A lighthouse at dusk, film grain", generate_public_access_token: true, }); return Response.json({ taskId: pending.id, token: pending.access.publicAccessToken }); ``` ```ts // browser async function pollInBrowser(taskId: string, token: string) { const headers = { Authorization: `Bearer ${token}` }; for (;;) { const res = await fetch(`https://api.mynth.io/tasks/${taskId}/status`, { headers }); if (res.status === 401) throw new Error("Token expired or invalid. Ask the server again."); const { data } = await res.json(); if (data.status !== "pending") { const result = await fetch(`https://api.mynth.io/tasks/${taskId}/result`, { headers }); return (await result.json()).data; } await new Promise((resolve) => setTimeout(resolve, 3_000)); } } ``` - If Mynth creates the task but cannot generate its token, the create answers `500 public_access_token_failed` with the task's id in `error.task_id` (in the SDK, `MynthAPIError.taskId`). Don't resend: poll that task from the server. - An expired token answers `401 token_expired`. A long video render can outlive the hour, so use a [webhook](https://mynth.io/docs/concepts/webhooks.md) or server-side polling for those. - Without `generate_public_access_token: true` no token is issued. Leave it out when only your server polls or you use webhooks. > **Warning** > > Never send the `mak_` key to the browser. The `pat_` token can read one task's status and result > and do nothing else. ## From the CLI ```bash npx @mynthio/cli task wait tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP --json ``` `task wait` prints the finished task. A failed task prints too, with a non-zero exit code: `5` for `restricted_content` and `1` for anything else. The default timeout is 30 minutes. Change it with `--timeout `. ## Next steps - [Webhooks](https://mynth.io/docs/concepts/webhooks.md): get called instead of polling. - [Tasks and polling](https://mynth.io/docs/sdks/typescript/tasks.md): how the SDK waits. - [The task object](https://mynth.io/docs/api-reference/task-object.md#status-and-result): the status and result responses. --- # Introduction > The Mynth HTTP API. Base URL, authentication, response shapes, and every endpoint, generated from the OpenAPI document. Every endpoint lives under `https://api.mynth.io`. It speaks JSON, except `POST /image/upload`, which takes multipart form data. It authenticates with a bearer token. No SDK is required. The endpoint pages are generated from the API's OpenAPI document, so every parameter they list is one the API validates. The document itself is at [api.mynth.io/openapi.json](https://api.mynth.io/openapi.json). ```bash curl https://api.mynth.io/image/generate \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"black-forest-labs/flux.2-pro","prompt":"A lighthouse at dusk"}' ``` ## Conventions - **Generation is asynchronous.** A generate call returns a task, not media. Poll the task or receive a [webhook](https://mynth.io/docs/concepts/webhooks.md). [The task object](https://mynth.io/docs/api-reference/task-object.md) covers every field. - **Successful bodies are wrapped in `data`, errors in `error`.** An error is `{ "error": { "code", "message" } }`, with a lowercase `snake_case` code. See [errors](https://mynth.io/docs/api-reference/errors.md). - **Field names are `snake_case`**, in requests, responses, and webhook events. - **Money is a decimal string in USD**, such as `"0.03"`. Parse it with a decimal type, not a float. - **Ids have prefixes:** `tsk_` tasks, `img_` images, `vid_` videos, `ak_` API keys, `wbh_` webhooks, `dst_` destinations. - **Unknown request fields are rejected** with `400 validation_error`, which names each one, so a typo never runs as a silently ignored setting. - **There are no idempotency keys.** Every create is a new task and a new hold. See [what is safe to retry](https://mynth.io/docs/api-reference/errors.md#what-is-safe-to-retry). ## Credentials **API key** ```bash curl https://api.mynth.io/tasks \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` **Public access token** ```bash curl https://api.mynth.io/tasks/tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP/status \ -H "Authorization: Bearer pat_eyJhbGciOiJIUzI1NiJ9..." ``` An API key belongs on a server. A public access token comes with a task that asked for one with `generate_public_access_token: true`, expires after an hour, and can only read that task's status and result, which is why it is safe in browser code. [Authentication](https://mynth.io/docs/api-reference/authentication.md) has the rules for both, and the scope each endpoint needs. ## Endpoints ### Image - `POST /image/generate` [Generate image](https://mynth.io/docs/api-reference/endpoints/image/generate.md) - `POST /image/generate/estimate` [Estimate cost](https://mynth.io/docs/api-reference/endpoints/image/estimate.md) - `POST /image/upload` [Upload images](https://mynth.io/docs/api-reference/endpoints/image/upload.md) - `POST /image/alt` [Generate alt text](https://mynth.io/docs/api-reference/endpoints/image/alt.md) - `POST /image/review` [Review image](https://mynth.io/docs/api-reference/endpoints/image/review.md) - `POST /image/rate` [Rate image](https://mynth.io/docs/api-reference/endpoints/image/rate.md) - `POST /image/remove-background` [Remove background](https://mynth.io/docs/api-reference/endpoints/image/remove-background.md) - `POST /image/upscale` [Upscale image](https://mynth.io/docs/api-reference/endpoints/image/upscale.md) ### Video - `POST /video/generate` [Generate video](https://mynth.io/docs/api-reference/endpoints/video/generate.md) - `POST /video/generate/estimate` [Estimate cost](https://mynth.io/docs/api-reference/endpoints/video/estimate.md) ### Tasks - `GET /tasks` [List tasks](https://mynth.io/docs/api-reference/endpoints/tasks/list.md) - `GET /tasks/{id}` [Get task](https://mynth.io/docs/api-reference/endpoints/tasks/get.md) - `GET /tasks/{id}/status` [Get task status](https://mynth.io/docs/api-reference/endpoints/tasks/status.md) - `GET /tasks/{id}/result` [Get task result](https://mynth.io/docs/api-reference/endpoints/tasks/result.md) ### Models - `GET /models` [List models](https://mynth.io/docs/api-reference/endpoints/models/list.md) ### Destinations - `POST /destinations` [Create destination](https://mynth.io/docs/api-reference/endpoints/destinations/create.md) - `GET /destinations` [List destinations](https://mynth.io/docs/api-reference/endpoints/destinations/list.md) - `GET /destinations/{id}` [Get destination](https://mynth.io/docs/api-reference/endpoints/destinations/get.md) - `PUT /destinations/{id}` [Update destination](https://mynth.io/docs/api-reference/endpoints/destinations/update.md) - `DELETE /destinations/{id}` [Delete destination](https://mynth.io/docs/api-reference/endpoints/destinations/delete.md) - `POST /destinations/{id}/test` [Test destination](https://mynth.io/docs/api-reference/endpoints/destinations/test.md) ### Webhooks - `POST /webhooks` [Create webhook](https://mynth.io/docs/api-reference/endpoints/webhooks/create.md) - `PUT /webhooks/{id}` [Update webhook](https://mynth.io/docs/api-reference/endpoints/webhooks/update.md) - `DELETE /webhooks/{id}` [Delete webhook](https://mynth.io/docs/api-reference/endpoints/webhooks/delete.md) ### API keys - `POST /api-keys` [Create API key](https://mynth.io/docs/api-reference/endpoints/api-keys/create.md) - `GET /api-keys` [List API keys](https://mynth.io/docs/api-reference/endpoints/api-keys/list.md) - `PUT /api-keys/{id}` [Update API key](https://mynth.io/docs/api-reference/endpoints/api-keys/update.md) - `DELETE /api-keys/{id}` [Delete API key](https://mynth.io/docs/api-reference/endpoints/api-keys/delete.md) ### Account - `GET /me` [Get account](https://mynth.io/docs/api-reference/endpoints/account/get.md) - `GET /balance` [Get balance](https://mynth.io/docs/api-reference/endpoints/account/balance.md) ### System - `GET /health` [Check health](https://mynth.io/docs/api-reference/endpoints/system/health.md) --- # Authentication > The Authorization header, the three credential types, the scope each endpoint needs, public access token rules, CORS, and authentication errors. This page is the contract: what the header looks like, what each credential reaches, and what comes back when it cannot. Creating, rotating, and capping keys is on [authentication](https://mynth.io/docs/authentication.md). ## The header Every authenticated request sends exactly one bearer token: ```text Authorization: Bearer ``` The scheme is case-insensitive. The value must be the scheme, whitespace, and the token, with nothing after it. Anything else counts as no credential. There is no query parameter, no `X-API-Key` header, and no Basic auth. The API decides what the token is from its prefix: | Prefix | Credential | Checked as | | --------- | ------------------- | --------------------------------------------- | | `mak_` | API key | A live key on your account | | `pat_` | Public access token | A signed token for one task | | any other | OAuth access token | A mynth.io session. Your code never sends one | A key pasted without its `mak_` prefix is checked as an OAuth token and fails with `401 unauthorized`, with a message that API keys start with `mak_`. Send the key exactly as it was shown. ## Credentials | Credential | Format | Issued by | Lifetime | Reaches | | ------------------- | --------------------- | ---------------------------------------------- | --------------------- | ---------------------------------------- | | API key | `mak_` + 48 hex chars | You: dashboard, CLI, or `POST /api-keys` | Until you delete it | Every endpoint its scopes allow | | Public access token | `pat_` + a signed JWT | Mynth, in a task's create response, on request | 1 hour | `/status` and `/result` of that one task | | OAuth access token | JWT | mynth.io when you sign in | Your mynth.io session | The dashboard and playground | ## Scope per endpoint An API key needs one of the scopes listed for the endpoint. OAuth sessions pass every scope check. | Endpoint | API key scope | `pat_` | | ---------------------------------------------------- | ----------------------------------- | ----------------- | | `POST /image/*`, including `/estimate` and `/upload` | `generate` | No | | `POST /video/*`, including `/estimate` | `generate` | No | | `GET /tasks`, `GET /tasks/{id}` | `generate` | No | | `GET /tasks/{id}/status`, `GET /tasks/{id}/result` | `generate` | Yes, its own task | | `/webhooks`, `/destinations`, `GET /balance` | `manage` | No | | `/api-keys` | `keys` | No | | `GET /me` | any of `generate`, `manage`, `keys` | No | | `GET /models`, `GET /health` | none, public | n/a | A key without the scope gets `403`, and the body says what was missing: ```json { "error": { "code": "insufficient_scope", "message": "This endpoint requires the `manage` scope. This key has: generate. Add the scope to this key in the dashboard, or use a key that has it.", "scopes": { "required": ["manage"], "current": ["generate"] } } } ``` Read `error.scopes.required` rather than parsing `message`. Scopes can be changed on an existing key in the dashboard, or with `PUT /api-keys/{id}`. ### Which tasks a key can read - `GET /tasks` lists only the tasks the calling key created. An OAuth session lists every task on the account. - `GET /tasks/{id}`, `/status`, and `/result` check the account, not the key. Any key on your account with `generate` can read any of your tasks by id. - Another account's task answers `404 task_not_found`, never `403`, so the API never confirms that an id exists. ## Public access tokens `POST /image/generate`, `POST /image/remove-background`, `POST /image/upscale`, and `POST /video/generate` return a token for the task they create when the body asks for one with `"generate_public_access_token": true`: ```json { "data": { "task_id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimated_cost": "0.03", "public_access_token": "pat_eyJhbGciOiJIUzI1NiJ9..." } } ``` - It works as a bearer token on `GET /tasks/{id}/status` and `GET /tasks/{id}/result` for that task. Every other endpoint rejects it. - It expires one hour after it is issued, and nothing refreshes it. - Without `generate_public_access_token: true` there is no token. Ask for one only when browser code will poll the task. - If the task is created but its token cannot be generated, the create answers `500 public_access_token_failed` with the task's id in `error.task_id`. The task runs: follow it from your server rather than resending, which would create and charge a second task. - `POST /image/rate`, `/image/alt`, and `/image/review` never return one. [Poll for results](https://mynth.io/docs/guides/poll-for-results.md#from-a-browser) has the pattern end to end. ## CORS | Paths | Allowed origins | | -------------------------------------------------- | ------------------------------------- | | `GET /tasks/{id}/status`, `GET /tasks/{id}/result` | Any | | Everything else | `https://mynth.io` and its subdomains | The allowed request headers are `Authorization` and `Content-Type`. Credentials mode is off, so cookies are never sent. A browser call to `/image/generate` from your own origin fails the preflight, whatever it sends. ## Failures | Status | Code | When | | ------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 401 | `unauthorized` | No usable header, or a key that is unknown or deleted. `message` says which: a missing header, a header without `Bearer`, a `pat_` where a key is needed, or a key that is not valid | | 401 | `invalid_token` | A `pat_` that is malformed or not signed by Mynth | | 401 | `token_expired` | A `pat_` past its hour | | 403 | `insufficient_scope` | A valid key without a scope the endpoint accepts | | 403 | `scope_escalation` | An API key tried to create or edit a key with `manage` or `keys` | | 404 | `task_not_found` | Another account's task, or a `pat_` for a different task | > **Warning** > > `/status` and `/result` treat a missing or invalid API key as no credential and answer `404 > task_not_found` instead of `401`, so an unauthenticated caller cannot learn whether a task id > exists. If polling a task you just created returns `404`, check the header before the id. A `pat_` problem is never a `404` on those two paths. A malformed or expired token fails with its own `401` first. Every code, with what to do about it, is on [errors](https://mynth.io/docs/api-reference/errors.md). ## Next steps - [Authentication](https://mynth.io/docs/authentication.md): create a key, set scopes, rotate, cap spending. - [The task object](https://mynth.io/docs/api-reference/task-object.md): what the task endpoints return. - [Errors](https://mynth.io/docs/api-reference/errors.md): every code the API can send. --- # The task object > Every field of a task, the result shape for each task type, and what the create, list, status, and result endpoints return. Every generation and analysis call creates a task, and every task endpoint returns some part of this object. The lifecycle, billing, and file lifetimes are on [tasks](https://mynth.io/docs/concepts/tasks.md). ## Where each part comes from | Call | Returns | | ---------------------------- | ----------------------------------------------------------------------------------------------------- | | A `POST` that creates a task | `task_id`, `estimated_cost`, sometimes `public_access_token`. See [create response](#create-response) | | `GET /tasks` | A page of rows: `id`, `type`, `status`, `cost`, timestamps | | `GET /tasks/{id}` | The full object | | `GET /tasks/{id}/status` | `status` only | | `GET /tasks/{id}/result` | `id`, `type`, `status`, `result` | | A webhook delivery | The full object, as the event's `data`. See [webhook payloads](https://mynth.io/docs/api-reference/webhook-payloads.md) | Poll `/status`, then read `/result`. Both are served from a cache and accept a [`pat_` token](https://mynth.io/docs/api-reference/authentication.md#public-access-tokens). `GET /tasks/{id}` reads the database and needs an API key. ## The object ```json { "data": { "id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "type": "image.generate", "status": "completed", "request": { "model": "black-forest-labs/flux.2-pro", "prompt": "A lighthouse at dusk, film grain", "count": 1, "metadata": { "order_id": "ord_42" } }, "result": { "model": "black-forest-labs/flux.2-pro", "images": [ { "status": "success", "id": "img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk", "url": "https://cdn.mynth.io/images/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp", "mynth_url": "https://cdn.mynth.io/images/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp", "size": "1536x1024", "format": "webp" } ] }, "errors": null, "cost": "0.03000000", "user_id": "user_01JD8G3W1R5T6Y7U8I9O0P1Q2W", "api_key_id": "ak_01KE7XWWEQ4MCGWKBQKJ1G47RP", "created_at": "2026-07-04T10:00:00.000Z", "updated_at": "2026-07-04T10:00:12.000Z" } } ``` | Field | Type | Notes | | ------------ | ------------------------------------ | --------------------------------------------------------------------------------------- | | `id` | string | `tsk_` + a ULID. Sorts by creation time | | `type` | string | One of the seven [task types](#task-types). Decides the shape of `request` and `result` | | `status` | `pending` \| `completed` \| `failed` | `pending` covers queued and running. A settled task never changes again | | `request` | object | The body as accepted, with defaults filled in. See [request](#request) | | `result` | object \| `null` | Set only when `completed`. Shape depends on `type` | | `errors` | `[{ code, message? }]` \| `null` | Set only when `failed` | | `cost` | decimal string \| `null` | USD charged, 8 decimal places. Set only when `completed` | | `user_id` | string | The account that owns the task | | `api_key_id` | string \| `null` | The key that created it. `null` for dashboard and playground tasks | | `created_at` | ISO 8601 string | UTC | | `updated_at` | ISO 8601 string | UTC. The last change | The combinations are fixed: | `status` | `result` | `errors` | `cost` | | ----------- | -------- | ------------------ | ------ | | `pending` | `null` | `null` | `null` | | `completed` | set | `null` | set | | `failed` | `null` | at least one entry | `null` | `completed` means the task finished and has a `result`. It does not mean every item succeeded: a generation can complete with failed images or videos, so check each entry's `status` in `result.images` or `result.videos`. `failed` means the task as a whole failed: there is no `result`, nothing is charged, and `errors` says why. [Errors](https://mynth.io/docs/api-reference/errors.md#task-failures) lists the codes. Money is always a decimal string: `cost`, `estimated_cost`, and a video's own `cost`. Add amounts up with a decimal type, not floats. ### Request `request` echoes the body after validation: - Defaults are filled in, such as `model: "auto"` and `count: 1` on `image.generate`, or `effort: "high"` on `image.review`. - Field names are `snake_case`, the same as you send them. A body with a field the endpoint does not define is rejected, so nothing is dropped. - `metadata` comes back unchanged, here and in every webhook. Put your own ids in it to match a task to your records. - URLs in `webhook.custom` are cut down to `scheme://host`, because they often carry a token. Each endpoint page under [Endpoints](https://mynth.io/docs/api-reference.md#endpoints) lists the fields its task type accepts. ## Task types | `type` | Created by | `result` holds | | ------------------------- | ------------------------------- | ------------------------------------------- | | `image.generate` | `POST /image/generate` | `model`, `images[]`, `magic_prompt?` | | `image.remove_background` | `POST /image/remove-background` | `image` | | `image.upscale` | `POST /image/upscale` | `image` | | `image.rate` | `POST /image/rate` | `level` | | `image.alt` | `POST /image/alt` | `alt` | | `image.review` | `POST /image/review` | `score`, `summary`, `findings`, `strengths` | | `video.generate` | `POST /video/generate` | `model`, `videos[]` | Fields marked `?` are absent, not `null`, when they do not apply. ### `image.generate` ```json { "model": "black-forest-labs/flux.2-pro", "images": [ { "status": "success", "id": "img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk", "url": "https://assets.example.com/renders/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp", "mynth_url": "https://cdn.mynth.io/images/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp", "size": "1536x1024", "format": "webp", "rating": { "status": "success", "level": "sfw" } }, { "status": "failed", "error": { "code": "restricted_content", "message": "The request was blocked by content moderation." } } ], "magic_prompt": { "positive": "A lone lighthouse on a rocky shore at dusk, ..." } } ``` | Field | Type | Notes | | --------------- | ------------------------- | --------------------------------------------------------------- | | `model` | string | The model that ran. With `auto`, the one Mynth picked | | `images` | array | One entry per requested image, each `success` or `failed` | | `magic_prompt?` | `{ positive, negative? }` | The rewritten prompt, when the request set `magic_prompt: true` | A successful image: | Field | Type | Notes | | ----------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | `status` | `"success"` | | | `id` | string | `img_` + 32 random characters | | `url` | string \| `null` | Your storage when a [destination](https://mynth.io/docs/concepts/destinations.md) was named, else the same as `mynth_url`. `null` if that upload failed | | `mynth_url` | string | Mynth's copy. Always set, served for 7 days | | `size` | string | Measured from the file, `{width}x{height}` | | `format` | `png` \| `jpg` \| `webp` | What `output.format` asked for, or what the provider returned | | `rating?` | object | When the request set `rating`: `{ status: "success", level }` or `{ status: "failed", error: { code } }` | A failed image is `{ "status": "failed", "error": { "code", "message?" } }` with nothing else, and is not charged. Generated images carry no `destination` block, so a failed upload shows only as `url: null`. ### `image.remove_background` ```json { "image": { "id": "img_Q8mZr2LkT0vWc5NhY7pD3xFa9GsJ1bEu", "url": "https://cdn.mynth.io/images/img_Q8mZr2LkT0vWc5NhY7pD3xFa9GsJ1bEu.png", "mynth_url": "https://cdn.mynth.io/images/img_Q8mZr2LkT0vWc5NhY7pD3xFa9GsJ1bEu.png", "size": "1024x1024", "format": "png" } } ``` `image` has the fields of a successful `image.generate` entry, without `status` and `rating`. `format` is `png` or `webp`. There is no per-item failure: if the work fails, the task is `failed`. When the request named a destination, `image` also carries `destination`: `{ status: "success", name }`, or `{ status: "failed", name, error: { code, message?, provider_response? } }` next to `url: null`. ### `image.upscale` ```json { "image": { "id": "img_Hn4cW8pLx2RsT6vYb0QjM9kE3dAf7ZuG", "url": "https://cdn.mynth.io/images/img_Hn4cW8pLx2RsT6vYb0QjM9kE3dAf7ZuG.png", "mynth_url": "https://cdn.mynth.io/images/img_Hn4cW8pLx2RsT6vYb0QjM9kE3dAf7ZuG.png", "size": "3072x4608", "format": "png" } } ``` Same shape as `image.remove_background`, with `destination` under the same rules. `format` is `png`, `jpg`, or `webp`. `size` is what the provider returned, which can differ slightly from the source times the factor. ### `image.rate` ```json { "level": "sfw" } ``` `level` is `sfw` or `nsfw` by default, or one of your `value` strings with custom levels. ### `image.alt` ```json { "alt": "A white lighthouse on dark rocks, lit by an orange sunset sky" } ``` `alt` is 1 to 160 characters. ### `image.review` ```json { "score": 3, "summary": "Clean composition with one visible defect in the left hand.", "findings": [ { "finding": "The left hand has six fingers", "category": "anatomy", "severity": "major", "where": "Bottom left, the hand resting on the railing", "confidence": "high" } ], "strengths": [{ "strength": "Warm, consistent lighting", "confidence": "medium" }] } ``` | Field | Type | Notes | | ----------------------- | -------------------------------- | ----------------------------------------------------------------------------------------- | | `score` | number, 1 to 4 | Median of the reviewers' scores. Higher is better | | `summary` | string | A short prose summary | | `findings[].category` | string | Usually `anatomy`, `text`, `composition`, `artifact`, `color`, or `lighting`. An open set | | `findings[].severity` | `critical` \| `major` \| `minor` | | | `findings[].confidence` | `low` \| `medium` \| `high` | Reviewer agreement: bare threshold, majority, strong consensus | | `strengths[]` | `{ strength, confidence }` | Same `confidence` scale | ### `video.generate` ```json { "model": "bytedance/seedance-2.0-mini", "videos": [ { "status": "success", "id": "vid_3HkP9wQz7LmR2tXc8VbN5dFy0JsA4gUe", "url": "https://cdn.mynth.io/videos/vid_3HkP9wQz7LmR2tXc8VbN5dFy0JsA4gUe.mp4", "mynth_url": "https://cdn.mynth.io/videos/vid_3HkP9wQz7LmR2tXc8VbN5dFy0JsA4gUe.mp4", "cost": "0.40500000", "duration": 5, "resolution": "720p", "aspect_ratio": "9:16", "audio": true } ] } ``` | Field | Type | Notes | | -------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | `vid_` + 32 random characters | | `url` | string \| `null` | The same as `mynth_url` today. Nullable, as on images, so video destinations can be added without a breaking change. Write `url ?? mynth_url` | | `mynth_url` | string | Mynth's copy. Always set | | `cost` | decimal string | What this video cost | | `duration` | number | Seconds | | `resolution` | `480p` \| `720p` \| `1080p` \| `4k` | | | `aspect_ratio` | string \| `null` | The ratio it was rendered at, such as `9:16`. `null` when a first frame set the shape | | `audio` | boolean | Whether the video has generated audio | A failed video is `{ "status": "failed", "error": { "code", "message?" } }`, like a failed image. ## Create response Every `POST` that creates a task answers `201`: ```json { "data": { "task_id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimated_cost": "0.03", "public_access_token": "pat_eyJhbGciOiJIUzI1NiJ9..." } } ``` | Field | Notes | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `task_id` | Pass it to every task endpoint | | `estimated_cost` | USD held on your balance until the task settles. The final `cost` is normally equal or lower | | `public_access_token?` | Only when the request set `generate_public_access_token: true`, on image generate, remove background, upscale, and video generate. [Rules](https://mynth.io/docs/api-reference/authentication.md#public-access-tokens) | The media is never in this response. Wait for the task with [polling](https://mynth.io/docs/guides/poll-for-results.md) or a [webhook](https://mynth.io/docs/concepts/webhooks.md). ## Listing tasks `GET /tasks` returns the calling key's tasks, newest first. Rows omit `request` and `result`, so fetch a task by id for those. | Query | Default | Notes | | ------- | ------- | -------------------------------------- | | `limit` | `20` | 1 to 100 | | `after` | none | A task id. Returns tasks older than it | ```json { "data": [ { "id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "type": "image.generate", "status": "completed", "cost": "0.03000000", "created_at": "2026-07-04T10:00:00.000Z", "updated_at": "2026-07-04T10:00:12.000Z" } ] } ``` There is no `has_more` and no total. To page, pass the last `id` you received as `after`. A page shorter than `limit` is the last one. ```ts let after: string | undefined; do { const url = new URL("https://api.mynth.io/tasks"); url.searchParams.set("limit", "100"); if (after) url.searchParams.set("after", after); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}` }, }); const { data } = await res.json(); for (const task of data) console.log(task.id, task.status, task.cost); after = data.length === 100 ? data.at(-1).id : undefined; } while (after); ``` After a create request times out, look here before resending. If the task is listed, it was created, and sending the request again would create and charge a second one. ## Status and result ```json { "data": { "status": "pending" } } ``` ```json { "data": { "id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "type": "image.alt", "status": "completed", "result": { "alt": "A white lighthouse on dark rocks, lit by an orange sunset sky" } } } ``` `/result` answers `200` while the task is `pending`, with `result: null`. It never includes `errors`, so for a `failed` task read `GET /tasks/{id}`. ## Next steps - [Tasks](https://mynth.io/docs/concepts/tasks.md): statuses, billing, retries, and file lifetimes. - [Webhook payloads](https://mynth.io/docs/api-reference/webhook-payloads.md): the same result, pushed to you. - [Errors](https://mynth.io/docs/api-reference/errors.md): the codes in `errors` and on failed items. --- # Webhook payloads > The HTTP request Mynth sends when a task settles. Standard Webhooks headers and signature, event names, and the event body. When a task settles, Mynth sends one `POST` to each endpoint that subscribed. This page describes that request exactly. Registering endpoints, receiver code for each framework, and retry behavior are on [webhooks](https://mynth.io/docs/concepts/webhooks.md). Deliveries follow [Standard Webhooks](https://www.standardwebhooks.com), so any of its libraries verifies them. ```text POST https://your-app.com/api/mynth-webhook Content-Type: application/json User-Agent: Mynth-Webhook/1.0 (+https://mynth.io) webhook-id: evt_01KE7Y9M3R8T2QXN5BVC4WJ6HD webhook-timestamp: 1791108012 webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4= {"id":"evt_01KE7Y9M3R8T2QXN5BVC4WJ6HD","type":"task.image.generate.completed","timestamp":"2026-10-04T10:00:12.000Z","data":{...}} ``` ## Headers | Header | Value | | ------------------- | --------------------------------------------------------------------------------------------- | | `Content-Type` | `application/json` | | `User-Agent` | `Mynth-Webhook/1.0 (+https://mynth.io)` | | `webhook-id` | The event ID, `evt_...`. The same for every delivery of the event, and equal to the body `id` | | `webhook-timestamp` | When this attempt was sent, in Unix seconds. Each retry sends a new one | | `webhook-signature` | `v1,`. Only on endpoints registered in the dashboard or with `POST /webhooks` | ### `webhook-id` Every delivery of one event carries the same ID: to each of your endpoints, on every retry, and after Mynth recovers from an outage. Store it and skip any ID you have already handled. Treat it as an opaque string: do not parse it. An endpoint registered twice with the same URL gets the event twice, with the same ID. Deduplicating drops the second one, which is correct. ## Signature A registered endpoint gets a `whsec_` secret when it is created. Every delivery to it is signed with that secret: ```text webhook-signature: v1, signature = base64( HMAC-SHA256( key = base64-decode( secret without "whsec_" ), message = ".." ) ) ``` - The key is the bytes the secret encodes, not the secret string. - The message is the ID, a `.`, the timestamp, a `.`, then the body bytes exactly as received. Parsing and re-serializing the JSON changes the bytes and breaks the match. - Each retry is signed again with a new `webhook-timestamp`. Reject a timestamp more than five minutes from your clock: that stops replays and still accepts late retries. - The ID is signed, so a replay with a fresh `webhook-id` to get past your deduplication fails verification. - The header is a space-separated list. Mynth sends one `v1` today; accept the delivery when any `v1` entry matches. Compare in constant time. Per-request `webhook.custom` URLs get no signature. They carry `webhook-id` and `webhook-timestamp` but anyone who knows the URL can post to them: put a secret token in the URL and check it. See [per-request webhooks](https://mynth.io/docs/concepts/webhooks.md#per-request-webhooks). `verifyWebhook()` in the SDK, the SDK's framework helpers, and every [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries) verify all of this for you. ## Events The event `type` is always the exact name, `task..`: | Task type | Completed | Failed | | ------------------------- | ---------------------------------------- | ------------------------------------- | | `image.generate` | `task.image.generate.completed` | `task.image.generate.failed` | | `image.remove_background` | `task.image.remove_background.completed` | `task.image.remove_background.failed` | | `image.upscale` | `task.image.upscale.completed` | `task.image.upscale.failed` | | `image.rate` | `task.image.rate.completed` | `task.image.rate.failed` | | `image.alt` | `task.image.alt.completed` | `task.image.alt.failed` | | `image.review` | `task.image.review.completed` | `task.image.review.failed` | | `video.generate` | `task.video.generate.completed` | `task.video.generate.failed` | `completed` means the task finished with results. Items in the result can still have failed: check each one's `status`. `failed` means the task failed as a whole and has no results; `data.errors` says why. An endpoint can also subscribe to `task.completed`, `task.failed` or `all`. Those decide what reaches the endpoint, not what an event is called. An endpoint on `all` still receives `task.image.alt.completed`, never `all`. An endpoint that matches through more than one of its events gets one delivery. `all` also covers task types added later, so answer an unknown `type` with a `2xx` and ignore it. ## Body ```json { "id": "evt_01KE7Y9M3R8T2QXN5BVC4WJ6HD", "type": "task.image.alt.completed", "timestamp": "2026-10-04T10:00:12.000Z", "data": { "id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "type": "image.alt", "status": "completed", "request": { "url": "https://assets.example.com/lighthouse.webp" }, "result": { "alt": "A white lighthouse on dark rocks, lit by an orange sunset sky" }, "errors": null, "cost": "0.00040000", "user_id": "user_01KE7XS3J0V5N8QF2PZ6W4MBRC", "api_key_id": "ak_01KE7XT9K2C6M4RX8NBV1QZJ5W", "created_at": "2026-10-04T10:00:00.000Z", "updated_at": "2026-10-04T10:00:12.000Z" } } ``` | Field | Type | Notes | | ----------- | ------ | ---------------------------------------------------------------------------------------------------------------------- | | `id` | string | The event ID, equal to the `webhook-id` header | | `type` | string | See [events](#events) | | `timestamp` | string | ISO 8601. When the event happened: the task's `updated_at`, not when this attempt was sent | | `data` | object | The task exactly as [`GET /tasks/{id}`](https://mynth.io/docs/api-reference/task-object.md) returns it: same fields, same nulls, redacted | `data.request` comes back with defaults filled in and `metadata` unchanged, so match the task to your own records with `data.request.metadata`. Only image generate, remove background, upscale and video generate take `metadata`. For rate, alt and review, store the `task_id` from the create response instead. URLs in `data.request.webhook.custom` are cut down to `scheme://host`, the same as on `GET /tasks/{id}`, so a token in a per-request URL never reaches your other endpoints. ### Examples These show `data` only; the envelope around it is the same for every event. **Image, partial** ```json { "id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "type": "image.generate", "status": "completed", "request": { "model": "black-forest-labs/flux.2-pro", "prompt": "A lighthouse at dusk, film grain", "count": 2, "metadata": { "order_id": "ord_42" } }, "result": { "model": "black-forest-labs/flux.2-pro", "images": [ { "status": "success", "id": "img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk", "url": "https://cdn.mynth.io/images/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp", "mynth_url": "https://cdn.mynth.io/images/img_V1StGXR8Z5jdHi6BmyT0sC1pQ2rN4wLk.webp", "size": "1536x1024", "format": "webp" }, { "status": "failed", "error": { "code": "provider_error", "message": "The provider failed to process the request. Please try again." } } ] }, "errors": null, "cost": "0.03000000", "user_id": "user_01KE7XS3J0V5N8QF2PZ6W4MBRC", "api_key_id": "ak_01KE7XT9K2C6M4RX8NBV1QZJ5W", "created_at": "2026-10-04T10:00:00.000Z", "updated_at": "2026-10-04T10:00:12.000Z" } ``` **Image, failed** ```json { "id": "tsk_01KE7XZ8B4N3QR5SJVDVAW6TME", "type": "image.generate", "status": "failed", "request": { "model": "black-forest-labs/flux.2-pro", "prompt": "A lighthouse at dusk, film grain", "count": 1, "size": "16:9_4k" }, "result": null, "errors": [ { "code": "capability_not_supported", "message": "The request uses options that are not supported for this model." } ], "cost": null, "user_id": "user_01KE7XS3J0V5N8QF2PZ6W4MBRC", "api_key_id": "ak_01KE7XT9K2C6M4RX8NBV1QZJ5W", "created_at": "2026-10-04T10:00:00.000Z", "updated_at": "2026-10-04T10:00:03.000Z" } ``` **Remove background** ```json { "id": "tsk_01KE7Y1R6WQ2J9TM4BXC8ZKD3F", "type": "image.remove_background", "status": "completed", "request": { "url": "https://assets.example.com/product-042.jpg", "output": { "format": "png" }, "destination": "r2-prod" }, "result": { "image": { "id": "img_Q8mZr2LkT0vWc5NhY7pD3xFa9GsJ1bEu", "url": "https://assets.example.com/cutouts/img_Q8mZr2LkT0vWc5NhY7pD3xFa9GsJ1bEu.png", "mynth_url": "https://cdn.mynth.io/images/img_Q8mZr2LkT0vWc5NhY7pD3xFa9GsJ1bEu.png", "size": "1024x1024", "format": "png", "destination": { "status": "success", "name": "r2-prod" } } }, "errors": null, "cost": "0.02000000", "user_id": "user_01KE7XS3J0V5N8QF2PZ6W4MBRC", "api_key_id": "ak_01KE7XT9K2C6M4RX8NBV1QZJ5W", "created_at": "2026-10-04T10:00:00.000Z", "updated_at": "2026-10-04T10:00:09.000Z" } ``` **Upscale, failed** ```json { "id": "tsk_01KE7Y2C9DF4M8QW1RXN6TBZ5H", "type": "image.upscale", "status": "failed", "request": { "url": "https://assets.example.com/poster-6000.png", "size": "4x", "effort": "high", "metadata": { "poster_id": "pst_19" } }, "result": null, "errors": [ { "code": "output_too_large", "message": "The upscaled image would be larger than 4096x4096 pixels. Send a smaller image or a lower factor." } ], "cost": null, "user_id": "user_01KE7XS3J0V5N8QF2PZ6W4MBRC", "api_key_id": "ak_01KE7XT9K2C6M4RX8NBV1QZJ5W", "created_at": "2026-10-04T10:00:00.000Z", "updated_at": "2026-10-04T10:00:02.000Z" } ``` **Rate** ```json { "id": "tsk_01KE7Y3B0GZC5V7NHXA2RMQ8TP", "type": "image.rate", "status": "completed", "request": { "url": "https://assets.example.com/upload-7731.jpg", "mode": "custom", "levels": [ { "value": "<13", "description": "Safe for Children" }, { "value": "<18", "description": "Safe for Teens" }, { "value": "18+", "description": "Adults Only" } ] }, "result": { "level": "<13" }, "errors": null, "cost": "0.00020000", "user_id": "user_01KE7XS3J0V5N8QF2PZ6W4MBRC", "api_key_id": "ak_01KE7XT9K2C6M4RX8NBV1QZJ5W", "created_at": "2026-10-04T10:00:00.000Z", "updated_at": "2026-10-04T10:00:02.000Z" } ``` **Review** ```json { "id": "tsk_01KE7Y4D8KM1P6WRZ3QXB9TJ2C", "type": "image.review", "status": "failed", "request": { "url": "https://assets.example.com/gone.webp", "effort": "high" }, "result": null, "errors": [{ "code": "fetch_failed", "message": "The image could not be fetched." }], "cost": null, "user_id": "user_01KE7XS3J0V5N8QF2PZ6W4MBRC", "api_key_id": "ak_01KE7XT9K2C6M4RX8NBV1QZJ5W", "created_at": "2026-10-04T10:00:00.000Z", "updated_at": "2026-10-04T10:00:04.000Z" } ``` **Video** ```json { "id": "tsk_01KE7Y5T2HB9R4XQN7CZW3KM6D", "type": "video.generate", "status": "completed", "request": { "model": "bytedance/seedance-2.0-mini", "prompt": "A cat surfing a wave at sunset", "duration": 5, "size": "9:16_720p" }, "result": { "model": "bytedance/seedance-2.0-mini", "videos": [ { "status": "success", "id": "vid_3HkP9wQz7LmR2tXc8VbN5dFy0JsA4gUe", "url": "https://cdn.mynth.io/videos/vid_3HkP9wQz7LmR2tXc8VbN5dFy0JsA4gUe.mp4", "mynth_url": "https://cdn.mynth.io/videos/vid_3HkP9wQz7LmR2tXc8VbN5dFy0JsA4gUe.mp4", "cost": "0.40500000", "duration": 5, "resolution": "720p", "aspect_ratio": "9:16", "audio": true } ] }, "errors": null, "cost": "0.40500000", "user_id": "user_01KE7XS3J0V5N8QF2PZ6W4MBRC", "api_key_id": null, "created_at": "2026-10-04T10:00:00.000Z", "updated_at": "2026-10-04T10:02:41.000Z" } ``` > **Warning** > > `task.image.generate.completed` and `task.video.generate.completed` do not mean the media exists. > Walk `data.result.images` or `data.result.videos` and check each `status`. A completed task can > hold only failed items. ## TypeScript types `verifyWebhook()` returns the event typed as a union discriminated on `type`: ```ts import { verifyWebhook } from "@mynthio/sdk"; const event = await verifyWebhook({ body: await request.text(), headers: request.headers }); switch (event.type) { case "task.image.generate.completed": for (const image of event.data.result.images) { if (image.status === "success") console.log(image.url ?? image.mynth_url); } break; case "task.image.generate.failed": console.error(event.data.id, event.data.errors); break; } ``` The type is `MynthSDKTypes.WebhookEvent`, and one variant is `MynthSDKTypes.WebhookEventOf<"image.alt", "completed">`. The framework helpers in `@mynthio/sdk/next`, `/tanstack-start` and `/convex` map completed events to the same results as polling. See [receive events](https://mynth.io/docs/concepts/webhooks.md#receive-events). ## Responding Answer any `2xx` within 30 seconds, then do the slow work. Mynth does not follow redirects and ignores the response body. Anything else gets retried with the same `webhook-id`. The full schedule is in [delivery behavior](https://mynth.io/docs/concepts/webhooks.md#delivery-behavior). ## Next steps - [Webhooks](https://mynth.io/docs/concepts/webhooks.md): register an endpoint and verify deliveries. - [The task object](https://mynth.io/docs/api-reference/task-object.md): every field inside `data`. --- # Errors > The shapes an error takes, every request error code and task failure code, how a failed task differs from a rejected request, and what is safe to retry. Two different things fail, and they look nothing alike. A **request error** rejects the call before anything is queued, with a `4xx` or `5xx` right away. A **task failure** happens after a `201`: the task runs and the work fails seconds or minutes later. ```text POST /image/generate │ ├─ rejected 4xx / 5xx { "error": { "code": "unauthorized", "message": "..." } } │ nothing queued, held, or charged │ └─ 201 { task_id } ──▶ task runs ──┬─▶ completed items can still carry an error └─▶ failed errors: [{ code, message? }] ``` Handle both. ## Request errors Every request error has the same body: an `error` object with a `code` in lowercase `snake_case` and a `message` that says what to change: ```json { "error": { "code": "insufficient_balance", "message": "This request needs $0.12, which is more than your available balance. Top up at https://mynth.io/dashboard/wallet, or lower the cost (fewer images, a cheaper model). Nothing was queued or charged." } } ``` Every error has this envelope, including a `404` for an unknown path, a `415`, and a `500`. No error response has `data`, and no success response has `error`. Branch on `code`, never on `message`. The message is written for people and may change. It is optional: `invalid_token` and `token_expired` have none. `validation_error` can add an `issues` array, `insufficient_scope` and `scope_escalation` add a `scopes` object, and `public_access_token_failed` adds `task_id`, all inside `error`. | Status | Code | Means | Do | | ------ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | | 400 | `validation_error` | The body, query, or path failed validation. See below | Fix what `message` names | | 400 | `invalid_json` | The body is not valid JSON. `message` includes the parser's position | Fix the JSON | | 400 | `api_key_limit_reached` | The account already has 100 live keys | Delete a key first | | 400 | `webhook_api_key_not_found` | `api_key_ids` names a key you don't have | Drop the unknown id | | 400 | `destination_invalid_provider` | A destination update changed `provider.id` | Create a new destination | | 401 | `unauthorized` | No usable bearer header, or an unknown or deleted key. `message` says which | Fix the credential | | 401 | `invalid_token` | A `pat_` that is malformed or not signed by Mynth | Get a new token from your server | | 401 | `token_expired` | A `pat_` past its hour | Poll from the server, or use a webhook | | 402 | `insufficient_balance` | Available balance is below the task's estimate. `message` states the estimate | Top up. Nothing was queued | | 402 | `spending_limit_exceeded` | The key's cap for the current day, week, or month is used up. `message` states the cap, what is used, and what the request needs | Wait for the period, or raise the cap | | 403 | `insufficient_scope` | Valid key, missing scope. Adds `scopes.required` and `scopes.current` | Use a key with the scope, or add it | | 403 | `scope_escalation` | An API key asked for `manage` or `keys`. Adds `scopes.requested`, `scopes.allowed` | Do it in the dashboard | | 404 | `task_not_found` | Unknown task, another account's task, or a `pat_` for a different task | Check the id and the credential | | 404 | `api_key_not_found` | Unknown key id | Check the id | | 404 | `destination_not_found` | Unknown destination id | Check the id | | 404 | `webhook_not_found` | Unknown webhook id | Check the id | | 404 | `not_found` | No route matches the method and path. `message` names both | Check the URL | | 409 | `destination_name_taken` | You already have a destination with that name | Pick another name | | 413 | `payload_too_large` | `POST /image/upload` body over 100 MB | Upload fewer or smaller files | | 415 | `unsupported_media_type` | A JSON endpoint without `Content-Type: application/json`, or an upload without `multipart/form-data` | Set the `Content-Type` header | | 500 | `internal_server_error` | Our fault, including a failure to store or read a destination secret | Retry, after checking `GET /tasks` | | 500 | `public_access_token_failed` | The task was created and will run, but its `pat_` could not be generated. Adds `task_id` | Follow `task_id`. Don't resend | | 502 | `destination_test_failed` | Your storage rejected the test upload. `message` is the provider's | Fix the credentials or path | A `429` means back off and retry. There is no `Retry-After` header. [Rate limits](https://mynth.io/docs/concepts/rate-limits.md) has the rest. ### Validation errors `validation_error` covers two kinds of check, with one shape. A field that fails the schema: a missing `prompt`, a `count` outside 1 to 20 or not a whole number, an unknown model id, a `size` that is not a size, or a field the endpoint does not define. These add `issues`, one entry per invalid field, and `message` states all of them: ```json { "error": { "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. count: Too big: expected number to be <=20.", "issues": [ { "path": ["model"], "message": "\"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." }, { "path": ["count"], "message": "Too big: expected number to be <=20" } ] } } ``` `path` points into the body, query, or path parameters: `["inputs", 0, "source"]` is `inputs[0].source`, and an empty `path` means the body as a whole. Map `issues` onto your own form fields, or show `message` as it is. The response does not echo your request. Each issue in `message` starts with its field path, such as `count:` or `inputs[0].source:`. An issue about the whole body has no path prefix. Use `code` and `issues[].path` to handle errors in code, since the wording of `message` can change. A request that fits the schema but not the model or your account has no `issues`, only `message`: - a `size` the pinned model cannot produce, such as `16:9_4k` on a model without 4k. The message names the size to use instead - `inputs` the pinned model cannot take, or too many of them - a video resolution tier, `duration`, or `audio` the model does not offer. The message lists the values it does - `metadata` over 2048 bytes - a `destination` name you don't own - an upload with no files, more than 10 files, a file that is not JPEG, PNG, or WEBP, or a file under 1 KB or over 10 MB. The message names the file With `model: "auto"`, input and size checks wait until the worker picks a model; see [task failures](#task-failures). Every request body rejects fields it does not define, at any depth. A typo such as `{"prompt": "...", "widht": 512}` answers `400` with an issue at `["widht"]` instead of running without it. A camelCase name, such as `aspectRatio`, gets a message saying field names are `snake_case`. ### Malformed requests Before the body is validated, it has to be JSON, sent as JSON: ```json { "error": { "code": "unsupported_media_type", "message": "The request has no Content-Type header. Send the body as application/json and set Content-Type: application/json." } } ``` ```json { "error": { "code": "invalid_json", "message": "The request body is not valid JSON (Expected double-quoted property name in JSON at position 17 (line 1 column 18)). Check for trailing commas, unquoted keys, or a truncated body." } } ``` `POST /image/upload` is the exception: it takes `multipart/form-data`, and a JSON body there answers `415 unsupported_media_type`. ## Task failures An accepted task settles as `completed` or `failed`, and both can carry error codes. Where the code lands depends on the task type: | Task type | A failure appears in | | ------------------------------------------------------------------------------------- | ----------------------------------------------- | | `image.generate` | `result.images[].error`, task stays `completed` | | `video.generate` | `result.videos[].error`, task stays `completed` | | `image.remove_background`, `image.upscale`, `image.rate`, `image.alt`, `image.review` | `errors[]`, task is `failed` | So a refused prompt on `POST /image/generate` gives you `201` at creation and a `completed` task whose only image failed. Checking `status === "completed"` is not enough. Walk the array. ```json { "status": "completed", "result": { "model": "black-forest-labs/flux.2-pro", "images": [ { "status": "failed", "error": { "code": "restricted_content", "message": "The request was blocked by content moderation." } } ] } } ``` An `image.generate` or `video.generate` task reaches `failed` only when the work around generation broke: resolving the model, the inputs, or the size, queueing the task, or the daily sweep that expires a task stuck on `pending`. With a pinned model, inputs and sizes it cannot serve are rejected at create with `400 validation_error`. With `model: "auto"` they are only known once a model is picked, so a `_4k` size or an edit that lands on a model without support fails the task with `capability_not_supported`. ### Codes | Code | Means | Retry helps | | -------------------------- | ------------------------------------------------------------------ | --------------------------------------- | | `restricted_content` | The provider or the model refused the content. Mynth did not | Mynth does not retry. You may resend | | `invalid_prompt` | The prompt was empty or too long for the model | No | | `invalid_input` | An input image could not be fetched or read | No. Check the URL is publicly reachable | | `capability_not_supported` | The model does not support an option you sent | No. Use another model or option | | `output_too_large` | `image.upscale` would produce more than 4096x4096 pixels | No. Send a smaller image or factor | | `providers_busy` | No provider for that model has capacity right now | Yes, later | | `rate_limited` | Providers kept rate-limiting the request | Yes, shortly | | `timeout` | The provider did not answer in time | Yes | | `provider_error` | The provider failed for another reason | Yes | | `fetch_failed` | `image.rate`, `image.alt`, or `image.review` could not fetch `url` | Only if the URL was flaky | | `rating_failed` | `image.rate` ran but produced nothing usable | Yes | | `alt_generation_failed` | `image.alt` ran but produced nothing usable | Yes | | `review_failed` | `image.review` ran but produced nothing usable | Yes | | `task_expired` | Still `pending` 24 hours after its last update, then swept | Yes. Something broke on our side | | `enqueue_failed` | Queueing failed right after creation. The create answered `500` | Yes | | `unknown_error` | Nothing else matched | Once | Before a retryable code reaches you, Mynth has already retried across providers: up to four attempts per image and three per video. See [tasks](https://mynth.io/docs/concepts/tasks.md#retries-and-timeouts). What `restricted_content` means is on [prompts and images](https://mynth.io/docs/concepts/prompts.md). Webhooks carry the same values: `.completed` events carry `result`, and `.failed` events carry `errors`. ## What is safe to retry - `400`, `401`, `403`, `404`, `409`, `413`, and `415` fail the same way every time. Fix the request. - `402 insufficient_balance` clears when you top up. `402 spending_limit_exceeded` clears when the key's period rolls over or you raise the cap. - `502 destination_test_failed` is your storage rejecting the upload. Fix the destination. - `500` and connection failures are worth retrying, with one catch: **there are no idempotency keys.** Every create mints a new task and holds cost again, so resending a create that actually succeeded runs and charges it twice. If a create times out, check `GET /tasks` before sending it again. - A create that fails while queueing marks its task `failed` with `enqueue_failed`, releases the hold, and answers `500`. Retrying that one is safe. - `500 public_access_token_failed` is the opposite: the task was created and will run. Follow `error.task_id` with your API key instead of resending. - A settled task never runs again. Retrying a task failure means a new request. Failed items are not charged. ## In the SDK and CLI The SDK throws `MynthAPIError` for a request error, with `status`, `code`, `message`, and `issues`, and `TaskAsync*` errors while waiting on a task. [SDK errors](https://mynth.io/docs/sdks/typescript/errors.md) covers both. The CLI prints the message to stderr and exits with a code scripts can branch on: `2` usage, `validation_error`, `invalid_json`, and `unsupported_media_type`, `3` auth, `4` balance or spending limit, `5` `restricted_content`, `6` a `429`, `1` anything else. [CLI commands](https://mynth.io/docs/sdks/cli/commands.md#exit-codes) has the table. ## Next steps - [Tasks](https://mynth.io/docs/concepts/tasks.md): statuses, partial failure, and attempts. - [Authentication](https://mynth.io/docs/api-reference/authentication.md): the auth codes in context. - [Webhooks](https://mynth.io/docs/concepts/webhooks.md): the same codes, pushed to you. --- # Generate image > Queues an image generation and returns the task straight away — generation itself is asynchronous. Follow the task by polling its status or by receiving a webhook. The cost is reserved up front and reconciled when the task finishes, so a partial failure is refunded. Creates an image task and returns its id. Nothing in the response is an image yet. Wait for the task by [polling](https://mynth.io/docs/guides/poll-for-results.md) or with a [webhook](https://mynth.io/docs/concepts/webhooks.md), or let the SDK's `image.generate()` wait for you. Always pass `model`. When it is omitted, the API uses `auto`, which is experimental. See [the model field](https://mynth.io/docs/models.md#auto-is-experimental). [Generate images](https://mynth.io/docs/guides/generate-images.md) covers sizes and the result. `POST /image/generate` **Auth:** API key or OAuth token ## Request body - `model` (string) — Default `"auto"`. One of: `"alibaba/qwen-image-2.0"`, `"alibaba/qwen-image-2.0-pro"`, `"alibaba/qwen-image-2.1-pro"`, `"alibaba/qwen-image-3.0"`, `"alibaba/qwen-image-3.0-pro"`, `"bytedance/seedream-5.0-lite"`, `"bytedance/seedream-pro"`, `"bytedance/seedream-v5-flash"`, `"black-forest-labs/flux-1-schnell"`, `"black-forest-labs/flux.1-dev"`, `"black-forest-labs/flux.2-dev"`, `"black-forest-labs/flux.2-pro"`, `"black-forest-labs/flux.2-flex"`, `"black-forest-labs/flux.2-max"`, `"black-forest-labs/flux.2-klein-4b"`, `"black-forest-labs/flux-3"`, `"bria/fibo-edit-1.5"`, `"bria/fibo-generate-1.5"`, `"circlestone-labs/anima"`, `"goofy-ai/prefect-pony-xl-lora"`, `"google/gemini-3.1-flash-lite-image"`, `"google/gemini-3.1-flash-image"`, `"google/gemini-nano-banana-2.1"`, `"google/gemini-3-pro-image-preview"`, `"ideogram/ideogram-4.5"`, `"imagineart/imagineart-1.5-pro"`, `"imagineart/imagineart-2.0"`, `"john6666/bismuth-illustrious-mix"`, `"klingai/kling-image-3.0"`, `"klingai/kling-image-o3"`, `"krea/krea-2-turbo"`, `"krea/krea-2-medium"`, `"krea/krea-2-large"`, `"luma/uni-1"`, `"luma/uni-1-max"`, `"maxfeifei8/one-obsession"`, `"meta/muse-image"`, `"microsoft/mai-image-2.6"`, `"microsoft/mai-image-2.6-flash"`, `"minimax/h3"`, `"openai/gpt-image-2"`, `"openai/gpt-image-2.5-flare"`, `"openai/gpt-image-2.5-sunburst"`, `"purplesmartai/pony-diffusion-v6-xl"`, `"recraft/recraft-v4"`, `"recraft/recraft-v4.1-flash"`, `"recraft/recraft-v4-pro"`, `"reve/reve"`, `"reve/reve-remix"`, `"sourceful/riverflow-2.0-pro"`, `"tongyi-mai/z-image"`, `"tongyi-mai/z-image-turbo"`, `"wan/wan2.6-image"`, `"wan/wan2.7-image"`, `"wan/wan2.7-image-pro"`, `"xai/grok-imagine-image"`, `"xai/grok-imagine-image-2.0"`, `"xai/grok-imagine-image-quality"`, `"auto"`. Example `"black-forest-labs/flux.2-pro"`. - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"Cute Cat"`. - `negative_prompt` (string) — ≤ 8192 characters. - `magic_prompt` (boolean) - `count` (integer) — Default `1`. ≥ 1, ≤ 20. - `size` (object | string) — One of: `"auto"`, `"square"`, `"portrait"`, `"landscape"`, `"portrait_tall"`, `"landscape_wide"`, `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`, `"2:1"`, `"1:2"`, `"1:1_4k"`, `"2:3_4k"`, `"3:2_4k"`, `"3:4_4k"`, `"4:3_4k"`, `"4:5_4k"`, `"5:4_4k"`, `"9:16_4k"`, `"16:9_4k"`, `"21:9_4k"`, `"2:1_4k"`, `"1:2_4k"`. - **auto** - `type` (required, "auto") - **aspect_ratio** - `type` (required, "aspect_ratio") - `aspect_ratio` (required, string) — One of: `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`, `"2:1"`, `"1:2"`. - `scale` (string) — Default `"base"`. One of: `"base"`, `"4k"`. - `inputs` ((string | object)[]) — ≤ 20 items. - **type + source** - `type` (required, "image") - `as` (string) — One of: `"auto"`, `"source"`, `"reference"`. - `source` (required, object) - `type` (required, "url") - `url` (required, string) — format: uri. - `rating` (object | boolean) — One of: `true`. - **nsfw_sfw** - `mode` ("nsfw_sfw") — Default `"nsfw_sfw"`. - **custom** - `mode` (required, "custom") - `levels` (required, object[]) — ≥ 2 items, ≤ 7 items. - `value` (required, string) — ≥ 1 characters, ≤ 24 characters. - `description` (required, string) — ≥ 1 characters, ≤ 150 characters. - `output` (object) - `format` (string) — One of: `"png"`, `"jpg"`, `"webp"`. - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - `registered` (boolean) — Set to false to skip the webhooks registered on the account for this task. Custom webhooks in `custom` are still sent. - `custom` (object[]) — ≥ 1 items, ≤ 5 items. - `url` (required, string) — Receives this task's events, unsigned. Use https and a public host, and put a secret token in the query string to verify deliveries: only `scheme://host` is ever shown back. Example `"https://example.com/hooks/mynth?token=your-secret-token"`. - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) ## Response ### 201 — Image generation task created - `data` (required, object) - `task_id` (required, string) — Task ID Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. - `estimated_cost` (required, string) — Estimated cost in USD reserved for this task. Work that fails is refunded, so the final cost may be lower. Example `"0.03"`. - `public_access_token` (string) — Short-lived public access token for polling this task's status and result. Safe to hand to browser code instead of your API key. Returned only when the request set `generate_public_access_token: true`. Example `"pat_eyJhbGciOi..."`. ```json { "data": { "task_id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimated_cost": "0.03", "public_access_token": "pat_eyJhbGciOi..." } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/image/generate \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "black-forest-labs/flux.2-pro", "prompt": "Cute Cat", "destination": "bunny-prod" }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/image/generate", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "model": "black-forest-labs/flux.2-pro", "prompt": "Cute Cat", "destination": "bunny-prod" }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Estimate cost > Prices a request without running it. Takes the same body as generate, validates it the same way, and charges nothing. With `model: "auto"` the figure is an upper bound, because the model is not chosen until the task runs. The same body as [generate](https://mynth.io/docs/api-reference/endpoints/image/generate.md), validated the same way, priced rather than run. Nothing is created or held. Use it to show a cost before a user commits, or to reject a request that would break a budget. With an explicit model the figure is exact. With `model: "auto"` it is the flat $0.20 per image that `auto` holds, marked `upper_bound`. [Estimate cost](https://mynth.io/docs/guides/estimate-cost.md) has the patterns. `POST /image/generate/estimate` **Auth:** API key or OAuth token ## Request body - `model` (string) — Default `"auto"`. One of: `"alibaba/qwen-image-2.0"`, `"alibaba/qwen-image-2.0-pro"`, `"alibaba/qwen-image-2.1-pro"`, `"alibaba/qwen-image-3.0"`, `"alibaba/qwen-image-3.0-pro"`, `"bytedance/seedream-5.0-lite"`, `"bytedance/seedream-pro"`, `"bytedance/seedream-v5-flash"`, `"black-forest-labs/flux-1-schnell"`, `"black-forest-labs/flux.1-dev"`, `"black-forest-labs/flux.2-dev"`, `"black-forest-labs/flux.2-pro"`, `"black-forest-labs/flux.2-flex"`, `"black-forest-labs/flux.2-max"`, `"black-forest-labs/flux.2-klein-4b"`, `"black-forest-labs/flux-3"`, `"bria/fibo-edit-1.5"`, `"bria/fibo-generate-1.5"`, `"circlestone-labs/anima"`, `"goofy-ai/prefect-pony-xl-lora"`, `"google/gemini-3.1-flash-lite-image"`, `"google/gemini-3.1-flash-image"`, `"google/gemini-nano-banana-2.1"`, `"google/gemini-3-pro-image-preview"`, `"ideogram/ideogram-4.5"`, `"imagineart/imagineart-1.5-pro"`, `"imagineart/imagineart-2.0"`, `"john6666/bismuth-illustrious-mix"`, `"klingai/kling-image-3.0"`, `"klingai/kling-image-o3"`, `"krea/krea-2-turbo"`, `"krea/krea-2-medium"`, `"krea/krea-2-large"`, `"luma/uni-1"`, `"luma/uni-1-max"`, `"maxfeifei8/one-obsession"`, `"meta/muse-image"`, `"microsoft/mai-image-2.6"`, `"microsoft/mai-image-2.6-flash"`, `"minimax/h3"`, `"openai/gpt-image-2"`, `"openai/gpt-image-2.5-flare"`, `"openai/gpt-image-2.5-sunburst"`, `"purplesmartai/pony-diffusion-v6-xl"`, `"recraft/recraft-v4"`, `"recraft/recraft-v4.1-flash"`, `"recraft/recraft-v4-pro"`, `"reve/reve"`, `"reve/reve-remix"`, `"sourceful/riverflow-2.0-pro"`, `"tongyi-mai/z-image"`, `"tongyi-mai/z-image-turbo"`, `"wan/wan2.6-image"`, `"wan/wan2.7-image"`, `"wan/wan2.7-image-pro"`, `"xai/grok-imagine-image"`, `"xai/grok-imagine-image-2.0"`, `"xai/grok-imagine-image-quality"`, `"auto"`. Example `"black-forest-labs/flux.2-pro"`. - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"Cute Cat"`. - `negative_prompt` (string) — ≤ 8192 characters. - `magic_prompt` (boolean) - `count` (integer) — Default `1`. ≥ 1, ≤ 20. - `size` (object | string) — One of: `"auto"`, `"square"`, `"portrait"`, `"landscape"`, `"portrait_tall"`, `"landscape_wide"`, `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`, `"2:1"`, `"1:2"`, `"1:1_4k"`, `"2:3_4k"`, `"3:2_4k"`, `"3:4_4k"`, `"4:3_4k"`, `"4:5_4k"`, `"5:4_4k"`, `"9:16_4k"`, `"16:9_4k"`, `"21:9_4k"`, `"2:1_4k"`, `"1:2_4k"`. - **auto** - `type` (required, "auto") - **aspect_ratio** - `type` (required, "aspect_ratio") - `aspect_ratio` (required, string) — One of: `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`, `"2:1"`, `"1:2"`. - `scale` (string) — Default `"base"`. One of: `"base"`, `"4k"`. - `inputs` ((string | object)[]) — ≤ 20 items. - **type + source** - `type` (required, "image") - `as` (string) — One of: `"auto"`, `"source"`, `"reference"`. - `source` (required, object) - `type` (required, "url") - `url` (required, string) — format: uri. - `rating` (object | boolean) — One of: `true`. - **nsfw_sfw** - `mode` ("nsfw_sfw") — Default `"nsfw_sfw"`. - **custom** - `mode` (required, "custom") - `levels` (required, object[]) — ≥ 2 items, ≤ 7 items. - `value` (required, string) — ≥ 1 characters, ≤ 24 characters. - `description` (required, string) — ≥ 1 characters, ≤ 150 characters. - `output` (object) - `format` (string) — One of: `"png"`, `"jpg"`, `"webp"`. - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - `registered` (boolean) — Set to false to skip the webhooks registered on the account for this task. Custom webhooks in `custom` are still sent. - `custom` (object[]) — ≥ 1 items, ≤ 5 items. - `url` (required, string) — Receives this task's events, unsigned. Use https and a public host, and put a secret token in the query string to verify deliveries: only `scheme://host` is ever shown back. Example `"https://example.com/hooks/mynth?token=your-secret-token"`. - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) ## Response ### 200 — Image generation request validated and cost estimated - `data` (required, object) - `estimated_cost` (required, string) — Estimated cost in USD for the request. Nothing is generated or charged. Example `"0.03"`. - `currency` (required, "usd") - `estimate_kind` (required, string) — "exact" when the model is pinned; "upper_bound" for `model: "auto"`, which reserves a flat per-image ceiling until a concrete model is selected. One of: `"exact"`, `"upper_bound"`. ```json { "data": { "estimated_cost": "0.03", "currency": "usd", "estimate_kind": "exact" } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/image/generate/estimate \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "black-forest-labs/flux.2-pro", "prompt": "Cute Cat", "destination": "bunny-prod" }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/image/generate/estimate", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "model": "black-forest-labs/flux.2-pro", "prompt": "Cute Cat", "destination": "bunny-prod" }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Upload images > Hosts your own images so they can be referenced as `inputs` by an edit, review or rating task. Send them as multipart form fields named `images`. The whole body is capped at 100 MB. The one endpoint that is not JSON. Send `multipart/form-data` with the files under a repeated `images` field: up to 10 JPEG, PNG, or WEBP files, each 1 KB to 10 MB, and at most 100 MB in total. A JSON body answers `415 unsupported_media_type`, and a file outside the limits answers `400 validation_error` naming the file. Uploading is free, and the returned URLs are served for 1 day. Put the URLs in `inputs` on generate. If your images already have public URLs, skip this and pass those. [Image to image](https://mynth.io/docs/guides/image-to-image.md) shows both. `POST /image/upload` **Auth:** API key or OAuth token ## Request body Sent as `multipart/form-data`. - `images` (required, string[]) — The image files to host. Repeat the field for several. ## Response ### 200 — Images uploaded - `data` (required, object) - `urls` (required, string[]) ```json { "data": { "urls": [ "string" ] } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/image/upload \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -F "images=@./photo.png" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/image/upload", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, }, }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Generate alt text > Queues a description of an image, written to be read aloud. Returns a task; the text arrives on its result. `POST /image/alt` **Auth:** API key or OAuth token ## Request body - `url` (required, string) — format: uri. ## Response ### 201 — Image alt text task created - `data` (required, object) - `task_id` (required, string) — Task ID Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. - `estimated_cost` (required, string) — Estimated cost in USD reserved for this task. Work that fails is refunded, so the final cost may be lower. Example `"0.03"`. ```json { "data": { "task_id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimated_cost": "0.03" } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/image/alt \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "string" }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/image/alt", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "url": "string" }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Review image > Queues a judgement of an image against what was asked for — whether it holds up, and where it does not. `effort` trades latency and cost for thoroughness. `POST /image/review` **Auth:** API key or OAuth token ## Request body - `url` (required, string) — format: uri. - `effort` (string) — Reviewer panel to run. 'high' runs five strong vision models; 'low' runs three smaller ones for triage. Default `"high"`. One of: `"low"`, `"high"`. ## Response ### 201 — Image review task created - `data` (required, object) - `task_id` (required, string) — Task ID Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. - `estimated_cost` (required, string) — Estimated cost in USD reserved for this task. Work that fails is refunded, so the final cost may be lower. Example `"0.03"`. ```json { "data": { "task_id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimated_cost": "0.03" } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/image/review \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "string" }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/image/review", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "url": "string" }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Rate image > Queues a content rating for an image and returns the task. The result is a single level, so this is the cheap check to run before publishing something generated. `POST /image/rate` **Auth:** API key or OAuth token ## Request body ### nsfw_sfw - `url` (required, string) — format: uri. - `mode` ("nsfw_sfw") — Default `"nsfw_sfw"`. ### custom - `url` (required, string) — format: uri. - `mode` (required, "custom") - `levels` (required, object[]) — ≥ 2 items, ≤ 7 items. - `value` (required, string) — ≥ 1 characters, ≤ 24 characters. - `description` (required, string) — ≥ 1 characters, ≤ 150 characters. ## Response ### 201 — Image rating task created - `data` (required, object) - `task_id` (required, string) — Task ID Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. - `estimated_cost` (required, string) — Estimated cost in USD reserved for this task. Work that fails is refunded, so the final cost may be lower. Example `"0.03"`. ```json { "data": { "task_id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimated_cost": "0.03" } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/image/rate \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "string" }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/image/rate", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "url": "string" }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Remove background > Queues a cutout of the subject and returns the task. The result is a transparent image, so ask for a format that carries an alpha channel. `POST /image/remove-background` **Auth:** API key or OAuth token ## Request body - `url` (required, string) — format: uri. - `output` (object) - `format` (string) — One of: `"png"`, `"webp"`. - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - `registered` (boolean) — Set to false to skip the webhooks registered on the account for this task. Custom webhooks in `custom` are still sent. - `custom` (object[]) — ≥ 1 items, ≤ 5 items. - `url` (required, string) — Receives this task's events, unsigned. Use https and a public host, and put a secret token in the query string to verify deliveries: only `scheme://host` is ever shown back. Example `"https://example.com/hooks/mynth?token=your-secret-token"`. - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) ## Response ### 201 — Image background removal task created - `data` (required, object) - `task_id` (required, string) — Task ID Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. - `estimated_cost` (required, string) — Estimated cost in USD reserved for this task. Work that fails is refunded, so the final cost may be lower. Example `"0.03"`. - `public_access_token` (string) — Short-lived public access token for polling this task's status and result. Safe to hand to browser code instead of your API key. Returned only when the request set `generate_public_access_token: true`. Example `"pat_eyJhbGciOi..."`. ```json { "data": { "task_id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimated_cost": "0.03", "public_access_token": "pat_eyJhbGciOi..." } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/image/remove-background \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "string", "destination": "bunny-prod" }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/image/remove-background", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "url": "string", "destination": "bunny-prod" }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Upscale image > Beta. Queues an enlargement of an image and returns the task. `effort` picks the upscaler and sets a flat price; `size` never changes it. The result can be at most 4096x4096 pixels, so a larger request fails with `output_too_large` and is not charged. `POST /image/upscale` **Auth:** API key or OAuth token ## Request body - `url` (required, string) — format: uri. - `size` (required, object | string) — How much to enlarge each side. The upscaled image can be at most 4096x4096 pixels. One of: `"2x"`, `"4x"`. - `type` (required, "scale") - `factor` (required, number) — One of: `2`, `4`. - `effort` (required, string) — Upscaler to run, and the price. 'low' is fast and sharp; 'high' rebuilds fine detail such as small text and faces. One of: `"low"`, `"high"`. - `output` (object) - `format` (string) — One of: `"png"`, `"jpg"`, `"webp"`. - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - `registered` (boolean) — Set to false to skip the webhooks registered on the account for this task. Custom webhooks in `custom` are still sent. - `custom` (object[]) — ≥ 1 items, ≤ 5 items. - `url` (required, string) — Receives this task's events, unsigned. Use https and a public host, and put a secret token in the query string to verify deliveries: only `scheme://host` is ever shown back. Example `"https://example.com/hooks/mynth?token=your-secret-token"`. - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) ## Response ### 201 — Image upscale task created - `data` (required, object) - `task_id` (required, string) — Task ID Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. - `estimated_cost` (required, string) — Estimated cost in USD reserved for this task. Work that fails is refunded, so the final cost may be lower. Example `"0.03"`. - `public_access_token` (string) — Short-lived public access token for polling this task's status and result. Safe to hand to browser code instead of your API key. Returned only when the request set `generate_public_access_token: true`. Example `"pat_eyJhbGciOi..."`. ```json { "data": { "task_id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimated_cost": "0.03", "public_access_token": "pat_eyJhbGciOi..." } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/image/upscale \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "string", "size": "2x", "effort": "low", "destination": "bunny-prod" }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/image/upscale", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "url": "string", "size": "2x", "effort": "low", "destination": "bunny-prod" }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Generate video > Queues a video generation and returns the task straight away. Video takes minutes rather than seconds, so a webhook is the better way to hear about it than polling. Creates a video task on the model you name. There is no `auto` and no `count`: one request renders one video. A render takes minutes, so plan for a [webhook](https://mynth.io/docs/concepts/webhooks.md) rather than holding a request open. Resolution, duration, audio, and frame inputs are checked against the model at create. [Video models](https://mynth.io/docs/models/video.md) lists what each model accepts, and [generate video](https://mynth.io/docs/guides/generate-video.md) covers the result. `POST /video/generate` **Auth:** API key or OAuth token ## Request body - `model` (required, string) — One of: `"bytedance/seedance-2.0-mini"`, `"google/gemini-omni-flash-1.1"`, `"prunaai/p-video"`, `"xai/grok-imagine-video-1.5"`. Example `"bytedance/seedance-2.0-mini"`. - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"A cat surfing a wave at sunset"`. - `negative_prompt` (string) — ≤ 8192 characters. - `duration` (number) — Video duration in seconds. Defaults to the model's default duration. ≥ 1, ≤ 60. - `size` (object | string) — Aspect ratio and resolution tier. The aspect ratio is best effort: the closest one the model supports. With image inputs the first frame sets the shape. Defaults to "auto". One of: `"auto"`, `"square"`, `"portrait"`, `"landscape"`, `"portrait_tall"`, `"landscape_wide"`, `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`, `"2:1"`, `"1:2"`, `"auto_480p"`, `"auto_720p"`, `"auto_1080p"`, `"auto_4k"`, `"square_480p"`, `"square_720p"`, `"square_1080p"`, `"square_4k"`, `"portrait_480p"`, `"portrait_720p"`, `"portrait_1080p"`, `"portrait_4k"`, `"landscape_480p"`, `"landscape_720p"`, `"landscape_1080p"`, `"landscape_4k"`, `"portrait_tall_480p"`, `"portrait_tall_720p"`, `"portrait_tall_1080p"`, `"portrait_tall_4k"`, `"landscape_wide_480p"`, `"landscape_wide_720p"`, `"landscape_wide_1080p"`, `"landscape_wide_4k"`, `"1:1_480p"`, `"1:1_720p"`, `"1:1_1080p"`, `"1:1_4k"`, `"2:3_480p"`, `"2:3_720p"`, `"2:3_1080p"`, `"2:3_4k"`, `"3:2_480p"`, `"3:2_720p"`, `"3:2_1080p"`, `"3:2_4k"`, `"3:4_480p"`, `"3:4_720p"`, `"3:4_1080p"`, `"3:4_4k"`, `"4:3_480p"`, `"4:3_720p"`, `"4:3_1080p"`, `"4:3_4k"`, `"4:5_480p"`, `"4:5_720p"`, `"4:5_1080p"`, `"4:5_4k"`, `"5:4_480p"`, `"5:4_720p"`, `"5:4_1080p"`, `"5:4_4k"`, `"9:16_480p"`, `"9:16_720p"`, `"9:16_1080p"`, `"9:16_4k"`, `"16:9_480p"`, `"16:9_720p"`, `"16:9_1080p"`, `"16:9_4k"`, `"21:9_480p"`, `"21:9_720p"`, `"21:9_1080p"`, `"21:9_4k"`, `"2:1_480p"`, `"2:1_720p"`, `"2:1_1080p"`, `"2:1_4k"`, `"1:2_480p"`, `"1:2_720p"`, `"1:2_1080p"`, `"1:2_4k"`. - **auto** - `type` (required, "auto") - `resolution` (string) — Resolution tier. Each model supports a subset, and a tier it does not support is rejected. Defaults to the model's default tier. One of: `"480p"`, `"720p"`, `"1080p"`, `"4k"`. - **aspect_ratio** - `type` (required, "aspect_ratio") - `aspect_ratio` (required, string) — One of: `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`, `"2:1"`, `"1:2"`. - `resolution` (string) — Resolution tier. Each model supports a subset, and a tier it does not support is rejected. Defaults to the model's default tier. One of: `"480p"`, `"720p"`, `"1080p"`, `"4k"`. - `audio` (boolean) — Enable model-native generated audio. Only supported by models with audio capability. - `inputs` ((string | object)[]) — ≤ 5 items. - **type + source** - `type` (required, "image") - `as` (string) — One of: `"auto"`, `"first_frame"`, `"last_frame"`, `"reference"`. - `source` (required, object) - `type` (required, "url") - `url` (required, string) — format: uri. - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - `registered` (boolean) — Set to false to skip the webhooks registered on the account for this task. Custom webhooks in `custom` are still sent. - `custom` (object[]) — ≥ 1 items, ≤ 5 items. - `url` (required, string) — Receives this task's events, unsigned. Use https and a public host, and put a secret token in the query string to verify deliveries: only `scheme://host` is ever shown back. Example `"https://example.com/hooks/mynth?token=your-secret-token"`. - `metadata` (object) ## Response ### 201 — Video generation task created - `data` (required, object) - `task_id` (required, string) — Task ID Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. - `estimated_cost` (required, string) — Estimated cost in USD reserved for this task. Work that fails is refunded, so the final cost may be lower. Example `"0.03"`. - `public_access_token` (string) — Short-lived public access token for polling this task's status and result. Safe to hand to browser code instead of your API key. Returned only when the request set `generate_public_access_token: true`. Example `"pat_eyJhbGciOi..."`. ```json { "data": { "task_id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimated_cost": "0.03", "public_access_token": "pat_eyJhbGciOi..." } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/video/generate \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "bytedance/seedance-2.0-mini", "prompt": "A cat surfing a wave at sunset" }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/video/generate", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "model": "bytedance/seedance-2.0-mini", "prompt": "A cat surfing a wave at sunset" }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Estimate cost > Prices a video request without running it. Same body as generate, nothing generated and nothing charged. The same body as [generate](https://mynth.io/docs/api-reference/endpoints/video/generate.md), validated the same way, priced rather than run. The figure is always exact, because the model is always explicit. Resolution and duration move a video's price a lot, so check it before a long render. `POST /video/generate/estimate` **Auth:** API key or OAuth token ## Request body - `model` (required, string) — One of: `"bytedance/seedance-2.0-mini"`, `"google/gemini-omni-flash-1.1"`, `"prunaai/p-video"`, `"xai/grok-imagine-video-1.5"`. Example `"bytedance/seedance-2.0-mini"`. - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"A cat surfing a wave at sunset"`. - `negative_prompt` (string) — ≤ 8192 characters. - `duration` (number) — Video duration in seconds. Defaults to the model's default duration. ≥ 1, ≤ 60. - `size` (object | string) — Aspect ratio and resolution tier. The aspect ratio is best effort: the closest one the model supports. With image inputs the first frame sets the shape. Defaults to "auto". One of: `"auto"`, `"square"`, `"portrait"`, `"landscape"`, `"portrait_tall"`, `"landscape_wide"`, `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`, `"2:1"`, `"1:2"`, `"auto_480p"`, `"auto_720p"`, `"auto_1080p"`, `"auto_4k"`, `"square_480p"`, `"square_720p"`, `"square_1080p"`, `"square_4k"`, `"portrait_480p"`, `"portrait_720p"`, `"portrait_1080p"`, `"portrait_4k"`, `"landscape_480p"`, `"landscape_720p"`, `"landscape_1080p"`, `"landscape_4k"`, `"portrait_tall_480p"`, `"portrait_tall_720p"`, `"portrait_tall_1080p"`, `"portrait_tall_4k"`, `"landscape_wide_480p"`, `"landscape_wide_720p"`, `"landscape_wide_1080p"`, `"landscape_wide_4k"`, `"1:1_480p"`, `"1:1_720p"`, `"1:1_1080p"`, `"1:1_4k"`, `"2:3_480p"`, `"2:3_720p"`, `"2:3_1080p"`, `"2:3_4k"`, `"3:2_480p"`, `"3:2_720p"`, `"3:2_1080p"`, `"3:2_4k"`, `"3:4_480p"`, `"3:4_720p"`, `"3:4_1080p"`, `"3:4_4k"`, `"4:3_480p"`, `"4:3_720p"`, `"4:3_1080p"`, `"4:3_4k"`, `"4:5_480p"`, `"4:5_720p"`, `"4:5_1080p"`, `"4:5_4k"`, `"5:4_480p"`, `"5:4_720p"`, `"5:4_1080p"`, `"5:4_4k"`, `"9:16_480p"`, `"9:16_720p"`, `"9:16_1080p"`, `"9:16_4k"`, `"16:9_480p"`, `"16:9_720p"`, `"16:9_1080p"`, `"16:9_4k"`, `"21:9_480p"`, `"21:9_720p"`, `"21:9_1080p"`, `"21:9_4k"`, `"2:1_480p"`, `"2:1_720p"`, `"2:1_1080p"`, `"2:1_4k"`, `"1:2_480p"`, `"1:2_720p"`, `"1:2_1080p"`, `"1:2_4k"`. - **auto** - `type` (required, "auto") - `resolution` (string) — Resolution tier. Each model supports a subset, and a tier it does not support is rejected. Defaults to the model's default tier. One of: `"480p"`, `"720p"`, `"1080p"`, `"4k"`. - **aspect_ratio** - `type` (required, "aspect_ratio") - `aspect_ratio` (required, string) — One of: `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`, `"2:1"`, `"1:2"`. - `resolution` (string) — Resolution tier. Each model supports a subset, and a tier it does not support is rejected. Defaults to the model's default tier. One of: `"480p"`, `"720p"`, `"1080p"`, `"4k"`. - `audio` (boolean) — Enable model-native generated audio. Only supported by models with audio capability. - `inputs` ((string | object)[]) — ≤ 5 items. - **type + source** - `type` (required, "image") - `as` (string) — One of: `"auto"`, `"first_frame"`, `"last_frame"`, `"reference"`. - `source` (required, object) - `type` (required, "url") - `url` (required, string) — format: uri. - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - `registered` (boolean) — Set to false to skip the webhooks registered on the account for this task. Custom webhooks in `custom` are still sent. - `custom` (object[]) — ≥ 1 items, ≤ 5 items. - `url` (required, string) — Receives this task's events, unsigned. Use https and a public host, and put a secret token in the query string to verify deliveries: only `scheme://host` is ever shown back. Example `"https://example.com/hooks/mynth?token=your-secret-token"`. - `metadata` (object) ## Response ### 200 — Video generation request validated and cost estimated - `data` (required, object) - `estimated_cost` (required, string) — Estimated cost in USD for the request. Nothing is generated or charged. Example `"0.05"`. - `currency` (required, "usd") - `estimate_kind` (required, "exact") — Video generation always requires a pinned model, so the estimate is exact. ```json { "data": { "estimated_cost": "0.05", "currency": "usd", "estimate_kind": "exact" } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/video/generate/estimate \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "bytedance/seedance-2.0-mini", "prompt": "A cat surfing a wave at sunset" }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/video/generate/estimate", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "model": "bytedance/seedance-2.0-mini", "prompt": "A cat surfing a wave at sunset" }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # List tasks > Your tasks, newest first, in pages. Each row carries the status and cost but not the request or the result — fetch those from the task itself. `GET /tasks` **Auth:** API key or OAuth token ## Query parameters - `limit` (integer) — How many tasks to return. Default `20`. ≥ 1, ≤ 100. - `after` (string) — Cursor: return tasks created before this task ID. Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Response ### 200 — Tasks returned - `data` (required, object[]) - `id` (required, string) - `type` (required, string) — One of: `"image.generate"`, `"image.rate"`, `"image.alt"`, `"image.review"`, `"image.remove_background"`, `"image.upscale"`, `"video.generate"`. - `status` (required, string) — One of: `"pending"`, `"completed"`, `"failed"`. - `cost` (required, string | null) - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. ```json { "data": [ { "id": "string", "type": "image.generate", "status": "pending", "cost": "string", "created_at": "string", "updated_at": "string" } ] } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl https://api.mynth.io/tasks?after=tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/tasks?after=tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", { method: "GET", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, }, }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Get task > One task in full: what was asked for, what came back, what it cost. Owner-only, so it takes an API key or an OAuth token rather than a public access token. The whole task: the request as accepted, the result, the cost, and any errors. It takes an API key, not a public access token, and it reads the database, so use it for failed tasks, audits, and dashboards rather than in a polling loop. Poll [status](https://mynth.io/docs/api-reference/endpoints/tasks/status.md) instead. `GET /tasks/{id}` **Auth:** API key or OAuth token ## Path parameters - `id` (required, string) — The task to fetch. Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Response ### 200 — Task returned - `data` (required, object) - **image.generate · pending** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[] | null) — Why the task failed. Null unless `status` is `failed`. - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.generate") - `request` (required, object) - `model` (string) — Default `"auto"`. One of: `"alibaba/qwen-image-2.0"`, `"alibaba/qwen-image-2.0-pro"`, `"alibaba/qwen-image-2.1-pro"`, `"alibaba/qwen-image-3.0"`, `"alibaba/qwen-image-3.0-pro"`, `"bytedance/seedream-5.0-lite"`, `"bytedance/seedream-pro"`, `"bytedance/seedream-v5-flash"`, `"black-forest-labs/flux-1-schnell"`, `"black-forest-labs/flux.1-dev"`, `"black-forest-labs/flux.2-dev"`, `"black-forest-labs/flux.2-pro"`, `"black-forest-labs/flux.2-flex"`, `"black-forest-labs/flux.2-max"`, `"black-forest-labs/flux.2-klein-4b"`, `"black-forest-labs/flux-3"`, `"bria/fibo-edit-1.5"`, `"bria/fibo-generate-1.5"`, `"circlestone-labs/anima"`, `"goofy-ai/prefect-pony-xl-lora"`, `"google/gemini-3.1-flash-lite-image"`, `"google/gemini-3.1-flash-image"`, `"google/gemini-nano-banana-2.1"`, `"google/gemini-3-pro-image-preview"`, `"ideogram/ideogram-4.5"`, `"imagineart/imagineart-1.5-pro"`, `"imagineart/imagineart-2.0"`, `"john6666/bismuth-illustrious-mix"`, `"klingai/kling-image-3.0"`, `"klingai/kling-image-o3"`, `"krea/krea-2-turbo"`, `"krea/krea-2-medium"`, `"krea/krea-2-large"`, `"luma/uni-1"`, `"luma/uni-1-max"`, `"maxfeifei8/one-obsession"`, `"meta/muse-image"`, `"microsoft/mai-image-2.6"`, `"microsoft/mai-image-2.6-flash"`, `"minimax/h3"`, `"openai/gpt-image-2"`, `"openai/gpt-image-2.5-flare"`, `"openai/gpt-image-2.5-sunburst"`, `"purplesmartai/pony-diffusion-v6-xl"`, `"recraft/recraft-v4"`, `"recraft/recraft-v4.1-flash"`, `"recraft/recraft-v4-pro"`, `"reve/reve"`, `"reve/reve-remix"`, `"sourceful/riverflow-2.0-pro"`, `"tongyi-mai/z-image"`, `"tongyi-mai/z-image-turbo"`, `"wan/wan2.6-image"`, `"wan/wan2.7-image"`, `"wan/wan2.7-image-pro"`, `"xai/grok-imagine-image"`, `"xai/grok-imagine-image-2.0"`, `"xai/grok-imagine-image-quality"`, `"auto"`. Example `"black-forest-labs/flux.2-pro"`. - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"Cute Cat"`. - `negative_prompt` (string) — ≤ 8192 characters. - `magic_prompt` (boolean) - `count` (integer) — Default `1`. ≥ 1, ≤ 20. - `size` (object | string) — One of: `"auto"`, `"square"`, `"portrait"`, `"landscape"`, `"portrait_tall"`, `"landscape_wide"`, `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`, `"2:1"`, `"1:2"`, `"1:1_4k"`, `"2:3_4k"`, `"3:2_4k"`, `"3:4_4k"`, `"4:3_4k"`, `"4:5_4k"`, `"5:4_4k"`, `"9:16_4k"`, `"16:9_4k"`, `"21:9_4k"`, `"2:1_4k"`, `"1:2_4k"`. - … - `inputs` ((string | object)[]) — ≤ 20 items. - … - `rating` (object | boolean) — One of: `true`. - … - `output` (object) - … - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - … - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) - `result` (required, null) - `status` (required, "pending") - **image.rate · pending** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[] | null) — Why the task failed. Null unless `status` is `failed`. - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.rate") - `request` (required, object) - **nsfw_sfw** - `url` (required, string) — format: uri. - `mode` ("nsfw_sfw") — Default `"nsfw_sfw"`. - **custom** - `url` (required, string) — format: uri. - `mode` (required, "custom") - `levels` (required, object[]) — ≥ 2 items, ≤ 7 items. - … - `result` (required, null) - `status` (required, "pending") - **image.alt · pending** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[] | null) — Why the task failed. Null unless `status` is `failed`. - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.alt") - `request` (required, object) - `url` (required, string) — format: uri. - `result` (required, null) - `status` (required, "pending") - **image.review · pending** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[] | null) — Why the task failed. Null unless `status` is `failed`. - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.review") - `request` (required, object) - `url` (required, string) — format: uri. - `effort` (string) — Reviewer panel to run. 'high' runs five strong vision models; 'low' runs three smaller ones for triage. Default `"high"`. One of: `"low"`, `"high"`. - `result` (required, null) - `status` (required, "pending") - **image.remove_background · pending** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[] | null) — Why the task failed. Null unless `status` is `failed`. - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.remove_background") - `request` (required, object) - `url` (required, string) — format: uri. - `output` (object) - … - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - … - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) - `result` (required, null) - `status` (required, "pending") - **image.upscale · pending** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[] | null) — Why the task failed. Null unless `status` is `failed`. - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.upscale") - `request` (required, object) - `url` (required, string) — format: uri. - `size` (required, object | string) — How much to enlarge each side. The upscaled image can be at most 4096x4096 pixels. One of: `"2x"`, `"4x"`. - … - `effort` (required, string) — Upscaler to run, and the price. 'low' is fast and sharp; 'high' rebuilds fine detail such as small text and faces. One of: `"low"`, `"high"`. - `output` (object) - … - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - … - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) - `result` (required, null) - `status` (required, "pending") - **video.generate · pending** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[] | null) — Why the task failed. Null unless `status` is `failed`. - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "video.generate") - `request` (required, object) - `model` (required, string) — One of: `"bytedance/seedance-2.0-mini"`, `"google/gemini-omni-flash-1.1"`, `"prunaai/p-video"`, `"xai/grok-imagine-video-1.5"`. Example `"bytedance/seedance-2.0-mini"`. - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"A cat surfing a wave at sunset"`. - `negative_prompt` (string) — ≤ 8192 characters. - `duration` (number) — Video duration in seconds. Defaults to the model's default duration. ≥ 1, ≤ 60. - `size` (object | string) — Aspect ratio and resolution tier. The aspect ratio is best effort: the closest one the model supports. With image inputs the first frame sets the shape. Defaults to "auto". One of: `"auto"`, `"square"`, `"portrait"`, `"landscape"`, `"portrait_tall"`, `"landscape_wide"`, `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`, `"2:1"`, `"1:2"`, `"auto_480p"`, `"auto_720p"`, `"auto_1080p"`, `"auto_4k"`, `"square_480p"`, `"square_720p"`, `"square_1080p"`, `"square_4k"`, `"portrait_480p"`, `"portrait_720p"`, `"portrait_1080p"`, `"portrait_4k"`, `"landscape_480p"`, `"landscape_720p"`, `"landscape_1080p"`, `"landscape_4k"`, `"portrait_tall_480p"`, `"portrait_tall_720p"`, `"portrait_tall_1080p"`, `"portrait_tall_4k"`, `"landscape_wide_480p"`, `"landscape_wide_720p"`, `"landscape_wide_1080p"`, `"landscape_wide_4k"`, `"1:1_480p"`, `"1:1_720p"`, `"1:1_1080p"`, `"1:1_4k"`, `"2:3_480p"`, `"2:3_720p"`, `"2:3_1080p"`, `"2:3_4k"`, `"3:2_480p"`, `"3:2_720p"`, `"3:2_1080p"`, `"3:2_4k"`, `"3:4_480p"`, `"3:4_720p"`, `"3:4_1080p"`, `"3:4_4k"`, `"4:3_480p"`, `"4:3_720p"`, `"4:3_1080p"`, `"4:3_4k"`, `"4:5_480p"`, `"4:5_720p"`, `"4:5_1080p"`, `"4:5_4k"`, `"5:4_480p"`, `"5:4_720p"`, `"5:4_1080p"`, `"5:4_4k"`, `"9:16_480p"`, `"9:16_720p"`, `"9:16_1080p"`, `"9:16_4k"`, `"16:9_480p"`, `"16:9_720p"`, `"16:9_1080p"`, `"16:9_4k"`, `"21:9_480p"`, `"21:9_720p"`, `"21:9_1080p"`, `"21:9_4k"`, `"2:1_480p"`, `"2:1_720p"`, `"2:1_1080p"`, `"2:1_4k"`, `"1:2_480p"`, `"1:2_720p"`, `"1:2_1080p"`, `"1:2_4k"`. - … - `audio` (boolean) — Enable model-native generated audio. Only supported by models with audio capability. - `inputs` ((string | object)[]) — ≤ 5 items. - … - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - … - `metadata` (object) - `result` (required, null) - `status` (required, "pending") - **image.generate · completed** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[] | null) — Why the task failed. Null unless `status` is `failed`. - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.generate") - `request` (required, object) - `model` (string) — Default `"auto"`. One of: `"alibaba/qwen-image-2.0"`, `"alibaba/qwen-image-2.0-pro"`, `"alibaba/qwen-image-2.1-pro"`, `"alibaba/qwen-image-3.0"`, `"alibaba/qwen-image-3.0-pro"`, `"bytedance/seedream-5.0-lite"`, `"bytedance/seedream-pro"`, `"bytedance/seedream-v5-flash"`, `"black-forest-labs/flux-1-schnell"`, `"black-forest-labs/flux.1-dev"`, `"black-forest-labs/flux.2-dev"`, `"black-forest-labs/flux.2-pro"`, `"black-forest-labs/flux.2-flex"`, `"black-forest-labs/flux.2-max"`, `"black-forest-labs/flux.2-klein-4b"`, `"black-forest-labs/flux-3"`, `"bria/fibo-edit-1.5"`, `"bria/fibo-generate-1.5"`, `"circlestone-labs/anima"`, `"goofy-ai/prefect-pony-xl-lora"`, `"google/gemini-3.1-flash-lite-image"`, `"google/gemini-3.1-flash-image"`, `"google/gemini-nano-banana-2.1"`, `"google/gemini-3-pro-image-preview"`, `"ideogram/ideogram-4.5"`, `"imagineart/imagineart-1.5-pro"`, `"imagineart/imagineart-2.0"`, `"john6666/bismuth-illustrious-mix"`, `"klingai/kling-image-3.0"`, `"klingai/kling-image-o3"`, `"krea/krea-2-turbo"`, `"krea/krea-2-medium"`, `"krea/krea-2-large"`, `"luma/uni-1"`, `"luma/uni-1-max"`, `"maxfeifei8/one-obsession"`, `"meta/muse-image"`, `"microsoft/mai-image-2.6"`, `"microsoft/mai-image-2.6-flash"`, `"minimax/h3"`, `"openai/gpt-image-2"`, `"openai/gpt-image-2.5-flare"`, `"openai/gpt-image-2.5-sunburst"`, `"purplesmartai/pony-diffusion-v6-xl"`, `"recraft/recraft-v4"`, `"recraft/recraft-v4.1-flash"`, `"recraft/recraft-v4-pro"`, `"reve/reve"`, `"reve/reve-remix"`, `"sourceful/riverflow-2.0-pro"`, `"tongyi-mai/z-image"`, `"tongyi-mai/z-image-turbo"`, `"wan/wan2.6-image"`, `"wan/wan2.7-image"`, `"wan/wan2.7-image-pro"`, `"xai/grok-imagine-image"`, `"xai/grok-imagine-image-2.0"`, `"xai/grok-imagine-image-quality"`, `"auto"`. Example `"black-forest-labs/flux.2-pro"`. - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"Cute Cat"`. - `negative_prompt` (string) — ≤ 8192 characters. - `magic_prompt` (boolean) - `count` (integer) — Default `1`. ≥ 1, ≤ 20. - `size` (object | string) — One of: `"auto"`, `"square"`, `"portrait"`, `"landscape"`, `"portrait_tall"`, `"landscape_wide"`, `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`, `"2:1"`, `"1:2"`, `"1:1_4k"`, `"2:3_4k"`, `"3:2_4k"`, `"3:4_4k"`, `"4:3_4k"`, `"4:5_4k"`, `"5:4_4k"`, `"9:16_4k"`, `"16:9_4k"`, `"21:9_4k"`, `"2:1_4k"`, `"1:2_4k"`. - … - `inputs` ((string | object)[]) — ≤ 20 items. - … - `rating` (object | boolean) — One of: `true`. - … - `output` (object) - … - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - … - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) - `result` (required, object) - `model` (required, string) — One of: `"alibaba/qwen-image-2.0"`, `"alibaba/qwen-image-2.0-pro"`, `"alibaba/qwen-image-2.1-pro"`, `"alibaba/qwen-image-3.0"`, `"alibaba/qwen-image-3.0-pro"`, `"bytedance/seedream-5.0-lite"`, `"bytedance/seedream-pro"`, `"bytedance/seedream-v5-flash"`, `"black-forest-labs/flux-1-schnell"`, `"black-forest-labs/flux.1-dev"`, `"black-forest-labs/flux.2-dev"`, `"black-forest-labs/flux.2-pro"`, `"black-forest-labs/flux.2-flex"`, `"black-forest-labs/flux.2-max"`, `"black-forest-labs/flux.2-klein-4b"`, `"black-forest-labs/flux-3"`, `"bria/fibo-edit-1.5"`, `"bria/fibo-generate-1.5"`, `"circlestone-labs/anima"`, `"goofy-ai/prefect-pony-xl-lora"`, `"google/gemini-3.1-flash-lite-image"`, `"google/gemini-3.1-flash-image"`, `"google/gemini-nano-banana-2.1"`, `"google/gemini-3-pro-image-preview"`, `"ideogram/ideogram-4.5"`, `"imagineart/imagineart-1.5-pro"`, `"imagineart/imagineart-2.0"`, `"john6666/bismuth-illustrious-mix"`, `"klingai/kling-image-3.0"`, `"klingai/kling-image-o3"`, `"krea/krea-2-turbo"`, `"krea/krea-2-medium"`, `"krea/krea-2-large"`, `"luma/uni-1"`, `"luma/uni-1-max"`, `"maxfeifei8/one-obsession"`, `"meta/muse-image"`, `"microsoft/mai-image-2.6"`, `"microsoft/mai-image-2.6-flash"`, `"minimax/h3"`, `"openai/gpt-image-2"`, `"openai/gpt-image-2.5-flare"`, `"openai/gpt-image-2.5-sunburst"`, `"purplesmartai/pony-diffusion-v6-xl"`, `"recraft/recraft-v4"`, `"recraft/recraft-v4.1-flash"`, `"recraft/recraft-v4-pro"`, `"reve/reve"`, `"reve/reve-remix"`, `"sourceful/riverflow-2.0-pro"`, `"tongyi-mai/z-image"`, `"tongyi-mai/z-image-turbo"`, `"wan/wan2.6-image"`, `"wan/wan2.7-image"`, `"wan/wan2.7-image-pro"`, `"xai/grok-imagine-image"`, `"xai/grok-imagine-image-2.0"`, `"xai/grok-imagine-image-quality"`. - `images` (required, object[]) - … - `magic_prompt` (object) - … - `status` (required, "completed") - **image.rate · completed** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[] | null) — Why the task failed. Null unless `status` is `failed`. - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.rate") - `request` (required, object) - **nsfw_sfw** - `url` (required, string) — format: uri. - `mode` ("nsfw_sfw") — Default `"nsfw_sfw"`. - **custom** - `url` (required, string) — format: uri. - `mode` (required, "custom") - `levels` (required, object[]) — ≥ 2 items, ≤ 7 items. - … - `result` (required, object) - `level` (required, string) — The rating level ≥ 1 characters, ≤ 24 characters. - `status` (required, "completed") - **image.alt · completed** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[] | null) — Why the task failed. Null unless `status` is `failed`. - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.alt") - `request` (required, object) - `url` (required, string) — format: uri. - `result` (required, object) - `alt` (required, string) — ≥ 1 characters, ≤ 160 characters. - `status` (required, "completed") - **image.review · completed** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[] | null) — Why the task failed. Null unless `status` is `failed`. - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.review") - `request` (required, object) - `url` (required, string) — format: uri. - `effort` (string) — Reviewer panel to run. 'high' runs five strong vision models; 'low' runs three smaller ones for triage. Default `"high"`. One of: `"low"`, `"high"`. - `result` (required, object) - `score` (required, number) — Median of independent reviewer ballots. Higher is better, 1 to 4. - `summary` (required, string) - `findings` (required, object[]) - … - `strengths` (required, object[]) - … - `status` (required, "completed") - **image.remove_background · completed** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[] | null) — Why the task failed. Null unless `status` is `failed`. - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.remove_background") - `request` (required, object) - `url` (required, string) — format: uri. - `output` (object) - … - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - … - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) - `result` (required, object) - `image` (required, object) - … - `status` (required, "completed") - **image.upscale · completed** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[] | null) — Why the task failed. Null unless `status` is `failed`. - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.upscale") - `request` (required, object) - `url` (required, string) — format: uri. - `size` (required, object | string) — How much to enlarge each side. The upscaled image can be at most 4096x4096 pixels. One of: `"2x"`, `"4x"`. - … - `effort` (required, string) — Upscaler to run, and the price. 'low' is fast and sharp; 'high' rebuilds fine detail such as small text and faces. One of: `"low"`, `"high"`. - `output` (object) - … - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - … - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) - `result` (required, object) - `image` (required, object) - … - `status` (required, "completed") - **video.generate · completed** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[] | null) — Why the task failed. Null unless `status` is `failed`. - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "video.generate") - `request` (required, object) - `model` (required, string) — One of: `"bytedance/seedance-2.0-mini"`, `"google/gemini-omni-flash-1.1"`, `"prunaai/p-video"`, `"xai/grok-imagine-video-1.5"`. Example `"bytedance/seedance-2.0-mini"`. - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"A cat surfing a wave at sunset"`. - `negative_prompt` (string) — ≤ 8192 characters. - `duration` (number) — Video duration in seconds. Defaults to the model's default duration. ≥ 1, ≤ 60. - `size` (object | string) — Aspect ratio and resolution tier. The aspect ratio is best effort: the closest one the model supports. With image inputs the first frame sets the shape. Defaults to "auto". One of: `"auto"`, `"square"`, `"portrait"`, `"landscape"`, `"portrait_tall"`, `"landscape_wide"`, `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`, `"2:1"`, `"1:2"`, `"auto_480p"`, `"auto_720p"`, `"auto_1080p"`, `"auto_4k"`, `"square_480p"`, `"square_720p"`, `"square_1080p"`, `"square_4k"`, `"portrait_480p"`, `"portrait_720p"`, `"portrait_1080p"`, `"portrait_4k"`, `"landscape_480p"`, `"landscape_720p"`, `"landscape_1080p"`, `"landscape_4k"`, `"portrait_tall_480p"`, `"portrait_tall_720p"`, `"portrait_tall_1080p"`, `"portrait_tall_4k"`, `"landscape_wide_480p"`, `"landscape_wide_720p"`, `"landscape_wide_1080p"`, `"landscape_wide_4k"`, `"1:1_480p"`, `"1:1_720p"`, `"1:1_1080p"`, `"1:1_4k"`, `"2:3_480p"`, `"2:3_720p"`, `"2:3_1080p"`, `"2:3_4k"`, `"3:2_480p"`, `"3:2_720p"`, `"3:2_1080p"`, `"3:2_4k"`, `"3:4_480p"`, `"3:4_720p"`, `"3:4_1080p"`, `"3:4_4k"`, `"4:3_480p"`, `"4:3_720p"`, `"4:3_1080p"`, `"4:3_4k"`, `"4:5_480p"`, `"4:5_720p"`, `"4:5_1080p"`, `"4:5_4k"`, `"5:4_480p"`, `"5:4_720p"`, `"5:4_1080p"`, `"5:4_4k"`, `"9:16_480p"`, `"9:16_720p"`, `"9:16_1080p"`, `"9:16_4k"`, `"16:9_480p"`, `"16:9_720p"`, `"16:9_1080p"`, `"16:9_4k"`, `"21:9_480p"`, `"21:9_720p"`, `"21:9_1080p"`, `"21:9_4k"`, `"2:1_480p"`, `"2:1_720p"`, `"2:1_1080p"`, `"2:1_4k"`, `"1:2_480p"`, `"1:2_720p"`, `"1:2_1080p"`, `"1:2_4k"`. - … - `audio` (boolean) — Enable model-native generated audio. Only supported by models with audio capability. - `inputs` ((string | object)[]) — ≤ 5 items. - … - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - … - `metadata` (object) - `result` (required, object) - `model` (required, string) — One of: `"bytedance/seedance-2.0-mini"`, `"google/gemini-omni-flash-1.1"`, `"prunaai/p-video"`, `"xai/grok-imagine-video-1.5"`. - `videos` (required, object[]) - … - `status` (required, "completed") - **image.generate · failed** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[]) - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.generate") - `request` (required, object) - `model` (string) — Default `"auto"`. One of: `"alibaba/qwen-image-2.0"`, `"alibaba/qwen-image-2.0-pro"`, `"alibaba/qwen-image-2.1-pro"`, `"alibaba/qwen-image-3.0"`, `"alibaba/qwen-image-3.0-pro"`, `"bytedance/seedream-5.0-lite"`, `"bytedance/seedream-pro"`, `"bytedance/seedream-v5-flash"`, `"black-forest-labs/flux-1-schnell"`, `"black-forest-labs/flux.1-dev"`, `"black-forest-labs/flux.2-dev"`, `"black-forest-labs/flux.2-pro"`, `"black-forest-labs/flux.2-flex"`, `"black-forest-labs/flux.2-max"`, `"black-forest-labs/flux.2-klein-4b"`, `"black-forest-labs/flux-3"`, `"bria/fibo-edit-1.5"`, `"bria/fibo-generate-1.5"`, `"circlestone-labs/anima"`, `"goofy-ai/prefect-pony-xl-lora"`, `"google/gemini-3.1-flash-lite-image"`, `"google/gemini-3.1-flash-image"`, `"google/gemini-nano-banana-2.1"`, `"google/gemini-3-pro-image-preview"`, `"ideogram/ideogram-4.5"`, `"imagineart/imagineart-1.5-pro"`, `"imagineart/imagineart-2.0"`, `"john6666/bismuth-illustrious-mix"`, `"klingai/kling-image-3.0"`, `"klingai/kling-image-o3"`, `"krea/krea-2-turbo"`, `"krea/krea-2-medium"`, `"krea/krea-2-large"`, `"luma/uni-1"`, `"luma/uni-1-max"`, `"maxfeifei8/one-obsession"`, `"meta/muse-image"`, `"microsoft/mai-image-2.6"`, `"microsoft/mai-image-2.6-flash"`, `"minimax/h3"`, `"openai/gpt-image-2"`, `"openai/gpt-image-2.5-flare"`, `"openai/gpt-image-2.5-sunburst"`, `"purplesmartai/pony-diffusion-v6-xl"`, `"recraft/recraft-v4"`, `"recraft/recraft-v4.1-flash"`, `"recraft/recraft-v4-pro"`, `"reve/reve"`, `"reve/reve-remix"`, `"sourceful/riverflow-2.0-pro"`, `"tongyi-mai/z-image"`, `"tongyi-mai/z-image-turbo"`, `"wan/wan2.6-image"`, `"wan/wan2.7-image"`, `"wan/wan2.7-image-pro"`, `"xai/grok-imagine-image"`, `"xai/grok-imagine-image-2.0"`, `"xai/grok-imagine-image-quality"`, `"auto"`. Example `"black-forest-labs/flux.2-pro"`. - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"Cute Cat"`. - `negative_prompt` (string) — ≤ 8192 characters. - `magic_prompt` (boolean) - `count` (integer) — Default `1`. ≥ 1, ≤ 20. - `size` (object | string) — One of: `"auto"`, `"square"`, `"portrait"`, `"landscape"`, `"portrait_tall"`, `"landscape_wide"`, `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`, `"2:1"`, `"1:2"`, `"1:1_4k"`, `"2:3_4k"`, `"3:2_4k"`, `"3:4_4k"`, `"4:3_4k"`, `"4:5_4k"`, `"5:4_4k"`, `"9:16_4k"`, `"16:9_4k"`, `"21:9_4k"`, `"2:1_4k"`, `"1:2_4k"`. - … - `inputs` ((string | object)[]) — ≤ 20 items. - … - `rating` (object | boolean) — One of: `true`. - … - `output` (object) - … - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - … - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) - `result` (required, null) - `status` (required, "failed") - **image.rate · failed** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[]) - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.rate") - `request` (required, object) - **nsfw_sfw** - `url` (required, string) — format: uri. - `mode` ("nsfw_sfw") — Default `"nsfw_sfw"`. - **custom** - `url` (required, string) — format: uri. - `mode` (required, "custom") - `levels` (required, object[]) — ≥ 2 items, ≤ 7 items. - … - `result` (required, null) - `status` (required, "failed") - **image.alt · failed** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[]) - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.alt") - `request` (required, object) - `url` (required, string) — format: uri. - `result` (required, null) - `status` (required, "failed") - **image.review · failed** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[]) - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.review") - `request` (required, object) - `url` (required, string) — format: uri. - `effort` (string) — Reviewer panel to run. 'high' runs five strong vision models; 'low' runs three smaller ones for triage. Default `"high"`. One of: `"low"`, `"high"`. - `result` (required, null) - `status` (required, "failed") - **image.remove_background · failed** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[]) - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.remove_background") - `request` (required, object) - `url` (required, string) — format: uri. - `output` (object) - … - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - … - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) - `result` (required, null) - `status` (required, "failed") - **image.upscale · failed** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[]) - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "image.upscale") - `request` (required, object) - `url` (required, string) — format: uri. - `size` (required, object | string) — How much to enlarge each side. The upscaled image can be at most 4096x4096 pixels. One of: `"2x"`, `"4x"`. - … - `effort` (required, string) — Upscaler to run, and the price. 'low' is fast and sharp; 'high' rebuilds fine detail such as small text and faces. One of: `"low"`, `"high"`. - `output` (object) - … - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - … - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) - `result` (required, null) - `status` (required, "failed") - **video.generate · failed** - `id` (required, string) - `cost` (required, string | null) — USD charged. Set once the task completes; null while pending or failed. - `errors` (required, object[]) - `code` (required, string) - `message` (string) - `user_id` (required, string) - `api_key_id` (required, string | null) — The API key that created the task. Null for tasks created without one. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `type` (required, "video.generate") - `request` (required, object) - `model` (required, string) — One of: `"bytedance/seedance-2.0-mini"`, `"google/gemini-omni-flash-1.1"`, `"prunaai/p-video"`, `"xai/grok-imagine-video-1.5"`. Example `"bytedance/seedance-2.0-mini"`. - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"A cat surfing a wave at sunset"`. - `negative_prompt` (string) — ≤ 8192 characters. - `duration` (number) — Video duration in seconds. Defaults to the model's default duration. ≥ 1, ≤ 60. - `size` (object | string) — Aspect ratio and resolution tier. The aspect ratio is best effort: the closest one the model supports. With image inputs the first frame sets the shape. Defaults to "auto". One of: `"auto"`, `"square"`, `"portrait"`, `"landscape"`, `"portrait_tall"`, `"landscape_wide"`, `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`, `"2:1"`, `"1:2"`, `"auto_480p"`, `"auto_720p"`, `"auto_1080p"`, `"auto_4k"`, `"square_480p"`, `"square_720p"`, `"square_1080p"`, `"square_4k"`, `"portrait_480p"`, `"portrait_720p"`, `"portrait_1080p"`, `"portrait_4k"`, `"landscape_480p"`, `"landscape_720p"`, `"landscape_1080p"`, `"landscape_4k"`, `"portrait_tall_480p"`, `"portrait_tall_720p"`, `"portrait_tall_1080p"`, `"portrait_tall_4k"`, `"landscape_wide_480p"`, `"landscape_wide_720p"`, `"landscape_wide_1080p"`, `"landscape_wide_4k"`, `"1:1_480p"`, `"1:1_720p"`, `"1:1_1080p"`, `"1:1_4k"`, `"2:3_480p"`, `"2:3_720p"`, `"2:3_1080p"`, `"2:3_4k"`, `"3:2_480p"`, `"3:2_720p"`, `"3:2_1080p"`, `"3:2_4k"`, `"3:4_480p"`, `"3:4_720p"`, `"3:4_1080p"`, `"3:4_4k"`, `"4:3_480p"`, `"4:3_720p"`, `"4:3_1080p"`, `"4:3_4k"`, `"4:5_480p"`, `"4:5_720p"`, `"4:5_1080p"`, `"4:5_4k"`, `"5:4_480p"`, `"5:4_720p"`, `"5:4_1080p"`, `"5:4_4k"`, `"9:16_480p"`, `"9:16_720p"`, `"9:16_1080p"`, `"9:16_4k"`, `"16:9_480p"`, `"16:9_720p"`, `"16:9_1080p"`, `"16:9_4k"`, `"21:9_480p"`, `"21:9_720p"`, `"21:9_1080p"`, `"21:9_4k"`, `"2:1_480p"`, `"2:1_720p"`, `"2:1_1080p"`, `"2:1_4k"`, `"1:2_480p"`, `"1:2_720p"`, `"1:2_1080p"`, `"1:2_4k"`. - … - `audio` (boolean) — Enable model-native generated audio. Only supported by models with audio capability. - `inputs` ((string | object)[]) — ≤ 5 items. - … - `generate_public_access_token` (boolean) — Return a short-lived public access token as `public_access_token`, for polling this task from browser code without your API key. Defaults to false. - `webhook` (object) - … - `metadata` (object) - `result` (required, null) - `status` (required, "failed") ```json { "data": { "id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "type": "image.generate", "status": "completed", "request": { "model": "black-forest-labs/flux.2-pro", "prompt": "A futuristic cityscape at sunset with flying cars", "count": 1, "size": "landscape", "output": { "format": "webp" } }, "result": { "model": "black-forest-labs/flux.2-pro", "images": [ { "status": "success", "id": "img_01KE7XWWEQ4MCGWKBQKJ1G47RP", "url": "https://cdn.example.com/generated/image1.webp", "mynth_url": "https://mynthcdn.example.com/generated/image1.webp", "size": "1536x1024", "format": "webp" } ] }, "errors": null, "cost": "0.01250000", "user_id": "user_01KE7XWWEQ4MCGWKBQKJ1G47RP", "api_key_id": "ak_01KE7XWWEQ4MCGWKBQKJ1G47RP", "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T10:30:12.000Z" } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl https://api.mynth.io/tasks/tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/tasks/tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", { method: "GET", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, }, }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Get task status > The status and nothing else — the endpoint to poll on an interval. It also accepts a public access token, so browser code can follow a task without holding an API key. The endpoint to poll. It returns the status and nothing else, from a cache, and it accepts the task's public access token, so browser code can follow a task without an API key. [Poll for results](https://mynth.io/docs/guides/poll-for-results.md) has a loop with backoff. A [webhook](https://mynth.io/docs/concepts/webhooks.md) avoids polling entirely. `GET /tasks/{id}/status` **Auth:** API key or Public access token ## Path parameters - `id` (required, string) — The task to poll. Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Response ### 200 — Task status returned - `data` (required, object) - `status` (required, string) — One of: `"pending"`, `"completed"`, `"failed"`. ```json { "data": { "status": "pending" } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl https://api.mynth.io/tasks/tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP/status \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/tasks/tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP/status", { method: "GET", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, }, }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Get task result > What the task produced, once it is finished. Like the status endpoint it accepts a public access token, so the media can be collected client-side. What the task produced. Like [status](https://mynth.io/docs/api-reference/endpoints/tasks/status.md), it accepts the task's public access token, so a browser can collect the media. The shape of `result` follows the task's `type`. It is `null` while the task is pending. `errors` is not included, so read a failed task with [get task](https://mynth.io/docs/api-reference/endpoints/tasks/get.md). `GET /tasks/{id}/result` **Auth:** API key or Public access token ## Path parameters - `id` (required, string) — The task to read. Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Response ### 200 — Task result returned - `data` (required, object) - **image.generate · pending** - `id` (required, string) - `type` (required, "image.generate") - `result` (required, null) - `status` (required, "pending") - **image.rate · pending** - `id` (required, string) - `type` (required, "image.rate") - `result` (required, null) - `status` (required, "pending") - **image.alt · pending** - `id` (required, string) - `type` (required, "image.alt") - `result` (required, null) - `status` (required, "pending") - **image.review · pending** - `id` (required, string) - `type` (required, "image.review") - `result` (required, null) - `status` (required, "pending") - **image.remove_background · pending** - `id` (required, string) - `type` (required, "image.remove_background") - `result` (required, null) - `status` (required, "pending") - **image.upscale · pending** - `id` (required, string) - `type` (required, "image.upscale") - `result` (required, null) - `status` (required, "pending") - **video.generate · pending** - `id` (required, string) - `type` (required, "video.generate") - `result` (required, null) - `status` (required, "pending") - **image.generate · completed** - `id` (required, string) - `type` (required, "image.generate") - `result` (required, object) - `model` (required, string) — One of: `"alibaba/qwen-image-2.0"`, `"alibaba/qwen-image-2.0-pro"`, `"alibaba/qwen-image-2.1-pro"`, `"alibaba/qwen-image-3.0"`, `"alibaba/qwen-image-3.0-pro"`, `"bytedance/seedream-5.0-lite"`, `"bytedance/seedream-pro"`, `"bytedance/seedream-v5-flash"`, `"black-forest-labs/flux-1-schnell"`, `"black-forest-labs/flux.1-dev"`, `"black-forest-labs/flux.2-dev"`, `"black-forest-labs/flux.2-pro"`, `"black-forest-labs/flux.2-flex"`, `"black-forest-labs/flux.2-max"`, `"black-forest-labs/flux.2-klein-4b"`, `"black-forest-labs/flux-3"`, `"bria/fibo-edit-1.5"`, `"bria/fibo-generate-1.5"`, `"circlestone-labs/anima"`, `"goofy-ai/prefect-pony-xl-lora"`, `"google/gemini-3.1-flash-lite-image"`, `"google/gemini-3.1-flash-image"`, `"google/gemini-nano-banana-2.1"`, `"google/gemini-3-pro-image-preview"`, `"ideogram/ideogram-4.5"`, `"imagineart/imagineart-1.5-pro"`, `"imagineart/imagineart-2.0"`, `"john6666/bismuth-illustrious-mix"`, `"klingai/kling-image-3.0"`, `"klingai/kling-image-o3"`, `"krea/krea-2-turbo"`, `"krea/krea-2-medium"`, `"krea/krea-2-large"`, `"luma/uni-1"`, `"luma/uni-1-max"`, `"maxfeifei8/one-obsession"`, `"meta/muse-image"`, `"microsoft/mai-image-2.6"`, `"microsoft/mai-image-2.6-flash"`, `"minimax/h3"`, `"openai/gpt-image-2"`, `"openai/gpt-image-2.5-flare"`, `"openai/gpt-image-2.5-sunburst"`, `"purplesmartai/pony-diffusion-v6-xl"`, `"recraft/recraft-v4"`, `"recraft/recraft-v4.1-flash"`, `"recraft/recraft-v4-pro"`, `"reve/reve"`, `"reve/reve-remix"`, `"sourceful/riverflow-2.0-pro"`, `"tongyi-mai/z-image"`, `"tongyi-mai/z-image-turbo"`, `"wan/wan2.6-image"`, `"wan/wan2.7-image"`, `"wan/wan2.7-image-pro"`, `"xai/grok-imagine-image"`, `"xai/grok-imagine-image-2.0"`, `"xai/grok-imagine-image-quality"`. - `images` (required, object[]) - … - `magic_prompt` (object) - … - `status` (required, "completed") - **image.rate · completed** - `id` (required, string) - `type` (required, "image.rate") - `result` (required, object) - `level` (required, string) — The rating level ≥ 1 characters, ≤ 24 characters. - `status` (required, "completed") - **image.alt · completed** - `id` (required, string) - `type` (required, "image.alt") - `result` (required, object) - `alt` (required, string) — ≥ 1 characters, ≤ 160 characters. - `status` (required, "completed") - **image.review · completed** - `id` (required, string) - `type` (required, "image.review") - `result` (required, object) - `score` (required, number) — Median of independent reviewer ballots. Higher is better, 1 to 4. - `summary` (required, string) - `findings` (required, object[]) - … - `strengths` (required, object[]) - … - `status` (required, "completed") - **image.remove_background · completed** - `id` (required, string) - `type` (required, "image.remove_background") - `result` (required, object) - `image` (required, object) - … - `status` (required, "completed") - **image.upscale · completed** - `id` (required, string) - `type` (required, "image.upscale") - `result` (required, object) - `image` (required, object) - … - `status` (required, "completed") - **video.generate · completed** - `id` (required, string) - `type` (required, "video.generate") - `result` (required, object) - `model` (required, string) — One of: `"bytedance/seedance-2.0-mini"`, `"google/gemini-omni-flash-1.1"`, `"prunaai/p-video"`, `"xai/grok-imagine-video-1.5"`. - `videos` (required, object[]) - … - `status` (required, "completed") - **image.generate · failed** - `id` (required, string) - `type` (required, "image.generate") - `result` (required, null) - `status` (required, "failed") - **image.rate · failed** - `id` (required, string) - `type` (required, "image.rate") - `result` (required, null) - `status` (required, "failed") - **image.alt · failed** - `id` (required, string) - `type` (required, "image.alt") - `result` (required, null) - `status` (required, "failed") - **image.review · failed** - `id` (required, string) - `type` (required, "image.review") - `result` (required, null) - `status` (required, "failed") - **image.remove_background · failed** - `id` (required, string) - `type` (required, "image.remove_background") - `result` (required, null) - `status` (required, "failed") - **image.upscale · failed** - `id` (required, string) - `type` (required, "image.upscale") - `result` (required, null) - `status` (required, "failed") - **video.generate · failed** - `id` (required, string) - `type` (required, "video.generate") - `result` (required, null) - `status` (required, "failed") ```json { "data": { "id": "string", "type": "image.generate", "result": null, "status": "pending" } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl https://api.mynth.io/tasks/tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP/result \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/tasks/tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP/result", { method: "GET", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, }, }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # List models > Every model you can name in a `model` field, with what it accepts and what it costs. Public: no credential required. `GET /models` ## Response ### 200 — Available models returned - `data` (required, object[]) - **image** - `id` (required, string) — Stable model identifier, in `vendor/model` form. - `display_name` (required, string | null) — Human-readable name as published by the model's creator. - `type` (required, "image") - `modes` (required, object) — Generation modes the model currently serves, with their input contracts. - `txt->img` (object) - … - `img->img` (object) - … - `pricing` (required, object | null) - `per_image` (required, object) - … - `per_input` (string) — USD charged per input image supplied with the request. - **video** - `id` (required, string) — Stable model identifier, in `vendor/model` form. - `display_name` (required, string | null) — Human-readable name as published by the model's creator. - `type` (required, "video") - `modes` (required, object) — Generation modes the model currently serves, with their input contracts. - `txt->vid` (object) - … - `img->vid` (object) - … - `pricing` (required, object | null) - `per_second` (required, object) — Keyed by resolution tier. A tier the model cannot produce is absent. - … - `audio` (object) — Present only when audio is billed on top of the per-second video rate. - … ```json { "data": [ { "id": "string", "display_name": "string", "type": "image", "modes": { "txt->img": { "inputs": { "rules": null, "max_total": null } }, "img->img": { "inputs": { "rules": null, "max_total": null } } }, "pricing": { "per_image": { "base": "string", "4k": "string" }, "per_input": "string" } } ] } ``` ## Request samples ### cURL ```bash curl https://api.mynth.io/models ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/models", { method: "GET", }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Create destination > Registers a bucket Mynth can deliver finished media to. The secret is write-only: it is stored encrypted and never returned by any endpoint. `name` is the slug you reference from a task's `destination` field, and it cannot be changed afterwards. A destination is storage of yours that Mynth writes finished images into, so `url` on the result points at your own domain. Name it on a generation request with its `name`, not its id. The secret is write-only: it is stored in a vault and no endpoint returns it. `name` cannot be changed later. [Destinations](https://mynth.io/docs/concepts/destinations.md#path-and-url-templates) covers the path and URL templates, which is where most of the configuration is. `POST /destinations` **Auth:** API key or OAuth token ## Request body ### bunny - `name` (required, string) — Destination slug. Immutable after creation. Matches /^[a-z0-9-]+$/. ≥ 1 characters, ≤ 64 characters, matches `^[a-z0-9-]+$`. - `config` (required, object) - `path_template` (required, string) — ≤ 2048 characters. - `url_template` (string) — URL template used to create an image URL. It must include a `{path}` which is a resolved path to file from `path_template`. Example: path_template -> "/images/{id}" On upload `path` becomes `/images/img_si1EywtFZMvDArkcVSSsN759VFNbHtE_.webp` url_template: "https://my-cdn.my-domain.com/{path}" The `url` in the response will be: "https://my-cdn.my-domain.com/images/img_si1EywtFZMvDArkcVSSsN759VFNbHtE_.webp" Note: The double `//` are automatically removed and replaced with single slash. format: includes, matches `\{path\}`. - `provider` (required, object) - `id` (required, "bunny") - `storage_zone` (required, string) - `region` (string) — One of: `"de"`, `"uk"`, `"ny"`, `"la"`, `"sg"`, `"se"`, `"br"`, `"jh"`, `"syd"`. - `secret` (required, object) - `password` (required, string) ### r2 - `name` (required, string) — Destination slug. Immutable after creation. Matches /^[a-z0-9-]+$/. ≥ 1 characters, ≤ 64 characters, matches `^[a-z0-9-]+$`. - `config` (required, object) - `path_template` (required, string) — ≤ 2048 characters. - `url_template` (string) — URL template used to create an image URL. It must include a `{path}` which is a resolved path to file from `path_template`. Example: path_template -> "/images/{id}" On upload `path` becomes `/images/img_si1EywtFZMvDArkcVSSsN759VFNbHtE_.webp` url_template: "https://my-cdn.my-domain.com/{path}" The `url` in the response will be: "https://my-cdn.my-domain.com/images/img_si1EywtFZMvDArkcVSSsN759VFNbHtE_.webp" Note: The double `//` are automatically removed and replaced with single slash. format: includes, matches `\{path\}`. - `provider` (required, object) - `id` (required, "r2") - `account_id` (required, string) - `bucket` (required, string) - `jurisdiction` (string) — One of: `"default"`, `"eu"`, `"fedramp"`. - `secret` (required, object) - `access_key_id` (required, string) - `secret_access_key` (required, string) ### s3 - `name` (required, string) — Destination slug. Immutable after creation. Matches /^[a-z0-9-]+$/. ≥ 1 characters, ≤ 64 characters, matches `^[a-z0-9-]+$`. - `config` (required, object) - `path_template` (required, string) — ≤ 2048 characters. - `url_template` (string) — URL template used to create an image URL. It must include a `{path}` which is a resolved path to file from `path_template`. Example: path_template -> "/images/{id}" On upload `path` becomes `/images/img_si1EywtFZMvDArkcVSSsN759VFNbHtE_.webp` url_template: "https://my-cdn.my-domain.com/{path}" The `url` in the response will be: "https://my-cdn.my-domain.com/images/img_si1EywtFZMvDArkcVSSsN759VFNbHtE_.webp" Note: The double `//` are automatically removed and replaced with single slash. format: includes, matches `\{path\}`. - `provider` (required, object) - `id` (required, "s3") - `bucket` (required, string) - `region` (required, string) - `endpoint` (string) - `force_path_style` (boolean) - `secret` (required, object) - `access_key_id` (required, string) - `secret_access_key` (required, string) ## Response ### 201 — Destination created - `data` (required, object) - `id` (required, string) - `name` (required, string) - `provider` (required, json) - `config` (required, json) - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. ```json { "data": { "id": "string", "name": "string", "provider": {}, "config": {}, "created_at": "string", "updated_at": "string" } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/destinations \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "string", "config": { "path_template": "string" }, "provider": { "id": "bunny", "storage_zone": "string" }, "secret": { "password": "string" } }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/destinations", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "name": "string", "config": { "path_template": "string" }, "provider": { "id": "bunny", "storage_zone": "string" }, "secret": { "password": "string" } }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # List destinations > Every destination on the account, without its secrets. `GET /destinations` **Auth:** API key or OAuth token ## Response ### 200 — Destinations returned - `data` (required, object[]) - `id` (required, string) - `name` (required, string) - `provider` (required, json) - `config` (required, json) - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. ```json { "data": [ { "id": "string", "name": "string", "provider": {}, "config": {}, "created_at": "string", "updated_at": "string" } ] } ``` ## Request samples ### cURL ```bash curl https://api.mynth.io/destinations \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/destinations", { method: "GET", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, }, }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Get destination > One destination and its path configuration, without its secret. `GET /destinations/{id}` **Auth:** API key or OAuth token ## Path parameters - `id` (required, string) — The destination to fetch. Example `"dst_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Response ### 200 — Destination returned - `data` (required, object) - `id` (required, string) - `name` (required, string) - `provider` (required, json) - `config` (required, json) - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. ```json { "data": { "id": "string", "name": "string", "provider": {}, "config": {}, "created_at": "string", "updated_at": "string" } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl https://api.mynth.io/destinations/dst_01KE7XWWEQ4MCGWKBQKJ1G47RP \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/destinations/dst_01KE7XWWEQ4MCGWKBQKJ1G47RP", { method: "GET", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, }, }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Update destination > Replaces the destination's configuration. Leave `secret` out to keep the credentials already stored; send it to rotate them. The provider and the slug are fixed at creation. `PUT /destinations/{id}` **Auth:** API key or OAuth token ## Path parameters - `id` (required, string) — The destination to update. Example `"dst_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Request body ### bunny - `config` (required, object) - `path_template` (required, string) — ≤ 2048 characters. - `url_template` (string) — URL template used to create an image URL. It must include a `{path}` which is a resolved path to file from `path_template`. Example: path_template -> "/images/{id}" On upload `path` becomes `/images/img_si1EywtFZMvDArkcVSSsN759VFNbHtE_.webp` url_template: "https://my-cdn.my-domain.com/{path}" The `url` in the response will be: "https://my-cdn.my-domain.com/images/img_si1EywtFZMvDArkcVSSsN759VFNbHtE_.webp" Note: The double `//` are automatically removed and replaced with single slash. format: includes, matches `\{path\}`. - `provider` (required, object) - `id` (required, "bunny") - `storage_zone` (required, string) - `region` (string) — One of: `"de"`, `"uk"`, `"ny"`, `"la"`, `"sg"`, `"se"`, `"br"`, `"jh"`, `"syd"`. - `secret` (object) - `password` (required, string) ### r2 - `config` (required, object) - `path_template` (required, string) — ≤ 2048 characters. - `url_template` (string) — URL template used to create an image URL. It must include a `{path}` which is a resolved path to file from `path_template`. Example: path_template -> "/images/{id}" On upload `path` becomes `/images/img_si1EywtFZMvDArkcVSSsN759VFNbHtE_.webp` url_template: "https://my-cdn.my-domain.com/{path}" The `url` in the response will be: "https://my-cdn.my-domain.com/images/img_si1EywtFZMvDArkcVSSsN759VFNbHtE_.webp" Note: The double `//` are automatically removed and replaced with single slash. format: includes, matches `\{path\}`. - `provider` (required, object) - `id` (required, "r2") - `account_id` (required, string) - `bucket` (required, string) - `jurisdiction` (string) — One of: `"default"`, `"eu"`, `"fedramp"`. - `secret` (object) - `access_key_id` (required, string) - `secret_access_key` (required, string) ### s3 - `config` (required, object) - `path_template` (required, string) — ≤ 2048 characters. - `url_template` (string) — URL template used to create an image URL. It must include a `{path}` which is a resolved path to file from `path_template`. Example: path_template -> "/images/{id}" On upload `path` becomes `/images/img_si1EywtFZMvDArkcVSSsN759VFNbHtE_.webp` url_template: "https://my-cdn.my-domain.com/{path}" The `url` in the response will be: "https://my-cdn.my-domain.com/images/img_si1EywtFZMvDArkcVSSsN759VFNbHtE_.webp" Note: The double `//` are automatically removed and replaced with single slash. format: includes, matches `\{path\}`. - `provider` (required, object) - `id` (required, "s3") - `bucket` (required, string) - `region` (required, string) - `endpoint` (string) - `force_path_style` (boolean) - `secret` (object) - `access_key_id` (required, string) - `secret_access_key` (required, string) ## Response ### 200 — Destination updated - `data` (required, object) - `id` (required, string) - `name` (required, string) - `provider` (required, json) - `config` (required, json) - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. ```json { "data": { "id": "string", "name": "string", "provider": {}, "config": {}, "created_at": "string", "updated_at": "string" } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X PUT https://api.mynth.io/destinations/dst_01KE7XWWEQ4MCGWKBQKJ1G47RP \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "config": { "path_template": "string" }, "provider": { "id": "bunny", "storage_zone": "string" } }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/destinations/dst_01KE7XWWEQ4MCGWKBQKJ1G47RP", { method: "PUT", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "config": { "path_template": "string" }, "provider": { "id": "bunny", "storage_zone": "string" } }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Delete destination > Removes the destination and its stored credentials. Media already delivered to your bucket stays where it is; tasks still naming this destination start failing delivery. `DELETE /destinations/{id}` **Auth:** API key or OAuth token ## Path parameters - `id` (required, string) — The destination to delete. Example `"dst_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Response ### 204 — Destination deleted No body. ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X DELETE https://api.mynth.io/destinations/dst_01KE7XWWEQ4MCGWKBQKJ1G47RP \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/destinations/dst_01KE7XWWEQ4MCGWKBQKJ1G47RP", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, }, }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Test destination > Uploads a small probe object to the given path with the stored credentials, so a misconfigured bucket is found here rather than on a real task. Uploads a small WEBP probe to `path` with the credentials on file and answers `204`. Call it after creating a destination or rotating its secret: a wrong key or a missing permission shows up here as `502 destination_test_failed`, not on a generation you already paid for. Delete the probe afterwards. `POST /destinations/{id}/test` **Auth:** API key or OAuth token ## Path parameters - `id` (required, string) — The destination to test. Example `"dst_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Request body - `path` (required, string) — Destination path for the test upload. Used verbatim, not templated. ≥ 1 characters, ≤ 2048 characters. ## Response ### 204 — Destination test completed No body. ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/destinations/dst_01KE7XWWEQ4MCGWKBQKJ1G47RP/test \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "path": "string" }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/destinations/dst_01KE7XWWEQ4MCGWKBQKJ1G47RP/test", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "path": "string" }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Create webhook > Registers a URL that receives task events as they happen, so you do not have to poll. Deliveries are signed (Standard Webhooks); the signing secret is returned here and nowhere else. Registers an endpoint that Mynth POSTs to when a task settles. Deliveries are signed with the `whsec_` secret in this response, following [Standard Webhooks](https://www.standardwebhooks.com). The API returns it only here, so store it now. The dashboard also shows it on the endpoint's page. [Webhooks](https://mynth.io/docs/concepts/webhooks.md) has the signature check, events, and sources. [Webhook payloads](https://mynth.io/docs/api-reference/webhook-payloads.md) has the body of each event. `POST /webhooks` **Auth:** API key or OAuth token ## Request body - `enabled` (boolean) — Whether the webhook is enabled. When disabled, no events will be sent to the webhook. Default `true`. - `url` (required, string) — Receives the events. Must be https and a public host. Example `"https://example.com/hooks/mynth"`. - `events` (required, string[]) — The events to send. `["all"]` sends every event, including ones added later. `task.completed` and `task.failed` cover every task type. ≥ 1 items. - `api_key_ids` (string[] | null) — Tasks created with these API keys are sent. Omitted or null: every key, including keys created later. An empty list: no key. ≤ 1000 items. - `include_session_tasks` (boolean) — Also send tasks created without an API key: playground runs and other signed-in (OAuth) sessions. Tasks the CLI creates carry its API key, so `api_key_ids` decides those. Default `false`. ## Response ### 201 — Webhook created - `data` (required, object) - `id` (required, string) - `user_id` (required, string) - `enabled` (required, boolean) - `url` (required, string) - `events` (required, string[]) - `api_key_ids` (required, string[] | null) - `include_session_tasks` (required, boolean) - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. - `secret` (required, string) — Signs every delivery to this webhook (Standard Webhooks). Returned here and nowhere else in the API. Example `"whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"`. ```json { "data": { "id": "string", "user_id": "string", "enabled": false, "url": "string", "events": [ "all" ], "api_key_ids": [ "string" ], "include_session_tasks": false, "created_at": "string", "updated_at": "string", "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw" } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/webhooks \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/mynth", "events": [ "all" ] }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/webhooks", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "url": "https://example.com/hooks/mynth", "events": [ "all" ] }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Update webhook > Replaces the webhook's URL, event subscription and sources. A field left out takes its default. The signing secret is unchanged. `PUT /webhooks/{id}` **Auth:** API key or OAuth token ## Path parameters - `id` (required, string) — The webhook to update. Example `"wbh_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Request body - `enabled` (required, boolean) — Whether the webhook is enabled. When disabled, no events will be sent to the webhook. - `url` (required, string) — Receives the events. Must be https and a public host. Example `"https://example.com/hooks/mynth"`. - `events` (required, string[]) — The events to send. `["all"]` sends every event, including ones added later. `task.completed` and `task.failed` cover every task type. ≥ 1 items. - `api_key_ids` (string[] | null) — Tasks created with these API keys are sent. Omitted or null: every key, including keys created later. An empty list: no key. ≤ 1000 items. - `include_session_tasks` (boolean) — Also send tasks created without an API key: playground runs and other signed-in (OAuth) sessions. Tasks the CLI creates carry its API key, so `api_key_ids` decides those. Default `false`. ## Response ### 200 — Webhook updated - `data` (required, object) - `id` (required, string) - `user_id` (required, string) - `enabled` (required, boolean) - `url` (required, string) - `events` (required, string[]) - `api_key_ids` (required, string[] | null) - `include_session_tasks` (required, boolean) - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. ```json { "data": { "id": "string", "user_id": "string", "enabled": false, "url": "string", "events": [ "all" ], "api_key_ids": [ "string" ], "include_session_tasks": false, "created_at": "string", "updated_at": "string" } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X PUT https://api.mynth.io/webhooks/wbh_01KE7XWWEQ4MCGWKBQKJ1G47RP \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": false, "url": "https://example.com/hooks/mynth", "events": [ "all" ] }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/webhooks/wbh_01KE7XWWEQ4MCGWKBQKJ1G47RP", { method: "PUT", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "enabled": false, "url": "https://example.com/hooks/mynth", "events": [ "all" ] }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Delete webhook > Stops delivery for good. To pause instead, update the webhook with `enabled: false` and keep its configuration. `DELETE /webhooks/{id}` **Auth:** API key or OAuth token ## Path parameters - `id` (required, string) — The webhook to delete. Example `"wbh_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Response ### 204 — Webhook deleted No body. ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X DELETE https://api.mynth.io/webhooks/wbh_01KE7XWWEQ4MCGWKBQKJ1G47RP \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/webhooks/wbh_01KE7XWWEQ4MCGWKBQKJ1G47RP", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, }, }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Create API key > Mints a key and returns it in full, once. `raw` is never retrievable again, so store it when you receive it. A key authenticated with an API key cannot grant scopes it does not itself hold. `POST /api-keys` **Auth:** API key or OAuth token ## Request body - `name` (string) — ≥ 1 characters, ≤ 256 characters. - `scopes` (string[]) — Default `["generate"]`. ≥ 1 items. ## Response ### 201 — API key created - `data` (required, object) - `raw` (required, string) - `api_key` (required, object) - `id` (required, string) - `name` (required, string | null) - `key_preview` (required, string) - `user_id` (required, string) - `scopes` (required, string[]) - `spending_limit` (required, string | null) - `spending_limit_period` (required, string | null) — One of: `"day"`, `"week"`, `"month"`. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. ```json { "data": { "raw": "string", "api_key": { "id": "string", "name": "string", "key_preview": "string", "user_id": "string", "scopes": [ "generate" ], "spending_limit": "string", "spending_limit_period": "day", "created_at": "string", "updated_at": "string" } } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/api-keys \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/api-keys", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({}), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # List API keys > Every key on the account, with its scopes and spending limit. Only the preview of each key is returned — the secret itself exists once, at creation. `GET /api-keys` **Auth:** API key or OAuth token ## Response ### 200 — API keys returned - `data` (required, object[]) - `id` (required, string) - `name` (required, string | null) - `key_preview` (required, string) - `user_id` (required, string) - `scopes` (required, string[]) - `spending_limit` (required, string | null) - `spending_limit_period` (required, string | null) — One of: `"day"`, `"week"`, `"month"`. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. ```json { "data": [ { "id": "string", "name": "string", "key_preview": "string", "user_id": "string", "scopes": [ "generate" ], "spending_limit": "string", "spending_limit_period": "day", "created_at": "string", "updated_at": "string" } ] } ``` ## Request samples ### cURL ```bash curl https://api.mynth.io/api-keys \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/api-keys", { method: "GET", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, }, }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Update API key > Replaces the key's name, scopes and spending limit. `spending_limit` is a decimal string in USD, sent with its `spending_limit_period`; `null` removes the limit. The secret does not change, so live traffic keeps working. `PUT /api-keys/{id}` **Auth:** API key or OAuth token ## Path parameters - `id` (required, string) — The API key to update. Example `"ak_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Request body - `name` (string | null) — ≥ 1 characters, ≤ 256 characters. - `scopes` (required, string[]) — ≥ 1 items. - `spending_limit` (required, string | null) — Most this key can spend per period, in USD. `null` removes the limit. A limit of `0` blocks every request. matches `^(0|[1-9]\d{0,7})(\.\d{1,2})?$`. Example `"25.00"`. - `spending_limit_period` (string | null) — When the limit resets, at the start of each UTC day, week or month. Required with a limit; leave it out, or null, without one. One of: `"day"`, `"week"`, `"month"`. ## Response ### 200 — API key updated - `data` (required, object) - `id` (required, string) - `name` (required, string | null) - `key_preview` (required, string) - `user_id` (required, string) - `scopes` (required, string[]) - `spending_limit` (required, string | null) - `spending_limit_period` (required, string | null) — One of: `"day"`, `"week"`, `"month"`. - `created_at` (required, string) — format: date-time. - `updated_at` (required, string) — format: date-time. ```json { "data": { "id": "string", "name": "string", "key_preview": "string", "user_id": "string", "scopes": [ "generate" ], "spending_limit": "string", "spending_limit_period": "day", "created_at": "string", "updated_at": "string" } } ``` ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X PUT https://api.mynth.io/api-keys/ak_01KE7XWWEQ4MCGWKBQKJ1G47RP \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "scopes": [ "generate" ], "spending_limit": "25.00" }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/api-keys/ak_01KE7XWWEQ4MCGWKBQKJ1G47RP", { method: "PUT", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "scopes": [ "generate" ], "spending_limit": "25.00" }), }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Delete API key > Revokes the key immediately. Requests carrying it start failing on the next call; tasks it already created are unaffected. `DELETE /api-keys/{id}` **Auth:** API key or OAuth token ## Path parameters - `id` (required, string) — The API key to revoke. Example `"ak_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Response ### 204 — API key deleted No body. ### 400 — The request was rejected before anything was queued. `error.message` says what to change. A field that failed the schema, or one the endpoint does not take, is also listed in `error.issues`. - `error` (required, object) - `code` (required, string) — `validation_error` for a body that failed validation, `invalid_json` for one that is not JSON. One of: `"validation_error"`, `"invalid_json"`. - `message` (required, string) — Every problem, in one sentence each, led by the field it is about. - `issues` (object[]) — One entry per field that failed the schema, and one per field the endpoint does not take. - `path` (required, (string | integer)[]) — Where the problem is, e.g. `["inputs", 0, "source"]`. Empty for the body as a whole. - `message` (required, string) ```json { "error": { "code": "validation_error", "message": "count: Too big: expected number to be <=20. colour: Unknown field.", "issues": [ { "path": [ "count" ], "message": "Too big: expected number to be <=20" }, { "path": [ "colour" ], "message": "Unknown field." } ] } } ``` ## Request samples ### cURL ```bash curl -X DELETE https://api.mynth.io/api-keys/ak_01KE7XWWEQ4MCGWKBQKJ1G47RP \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/api-keys/ak_01KE7XWWEQ4MCGWKBQKJ1G47RP", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, }, }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Get account > Who the credential in the request belongs to, and — for an API key — which scopes it holds and how much of its spending limit is left. `GET /me` **Auth:** API key or OAuth token ## Response ### 200 — The authenticated identity - `data` (required, object) - `user_id` (required, string) — Example `"user_01JD8G3W1R5T6Y7U8I9O0P1Q2W"`. - `auth` (required, object) - `method` (required, string) — One of: `"api-key"`, `"oauth"`. - `api_key` (object) — Present when authenticated with an API key. - `id` (required, string) - `name` (required, string | null) - `key_preview` (required, string) — Example `"mak_abc...xyz"`. - `scopes` (required, string[]) - `spending` (required, object) - … ```json { "data": { "user_id": "user_01JD8G3W1R5T6Y7U8I9O0P1Q2W", "auth": { "method": "api-key", "api_key": { "id": "string", "name": "string", "key_preview": "mak_abc...xyz", "scopes": [ "generate" ], "spending": { "mode": "unlimited" } } } } } ``` ## Request samples ### cURL ```bash curl https://api.mynth.io/me \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/me", { method: "GET", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, }, }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Get balance > What is left to spend. `reserved` is held against tasks still running and is released or charged when each one finishes. `GET /balance` **Auth:** API key or OAuth token ## Response ### 200 — Account balance - `data` (required, object) - `balance` (required, string) — Current account balance in USD. - `reserved` (required, string) — Amount reserved by in-flight tasks. - `available` (required, string) — Spendable balance: balance minus reserved. - `currency` (required, "usd") ```json { "data": { "balance": "string", "reserved": "string", "available": "string", "currency": "usd" } } ``` ## Request samples ### cURL ```bash curl https://api.mynth.io/balance \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/balance", { method: "GET", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, }, }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Check health > Returns 200 while the API is accepting traffic. Public, and cheap to call. `GET /health` ## Response ### 200 — API is ready - `status` (required, "ok") ```json { "status": "ok" } ``` ## Request samples ### cURL ```bash curl https://api.mynth.io/health ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/health", { method: "GET", }); const { data } = await response.json(); ``` The full schema for this endpoint is in the [OpenAPI document](https://api.mynth.io/openapi.json). --- # Overview > @mynthio/sdk is the TypeScript client for Mynth. It creates tasks, waits for them, uploads local files, and ships webhook helpers for Next.js, TanStack Start, and Convex. `@mynthio/sdk` is the TypeScript client for Mynth. Construct one client, pass a model id and a prompt, and read the file URLs from the result. ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); // reads MYNTH_API_KEY const task = await mynth.image.generate({ model: "black-forest-labs/flux.2-pro", prompt: "A lighthouse at dusk, film grain", }); console.log(task.urls[0]); ``` `generate()` creates a task and polls it until it settles. Use `generateAsync()` when the caller cannot wait, and finish on a webhook. ## What the SDK covers | Surface | Methods | | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `mynth.image` | `generate`, `rate`, `alt`, `review`, `removeBackground`, `upscale`, each with an `Async` twin, and `upload` | | `mynth.video` | `generate`, `generateAsync`, `estimate`, `upload` | | `mynth.models` | `list`, which needs no API key | | `@mynthio/sdk/next`, `/tanstack-start`, `/convex` | Webhook helpers that verify the signature and call a handler per event. `verifyWebhook()` for anything else | The SDK does not wrap account management: keys, webhooks, destinations, and balance. Use the REST API or the CLI for those. ## Rules - Keep the `mak_` key on the server. A browser polls one task with the `pat_` token from the create response. See [tasks and polling](https://mynth.io/docs/sdks/typescript/tasks.md#poll-from-the-browser). - Pass a model id. Omitting `model` makes the API use `auto`, which is experimental. See [the model field](https://mynth.io/docs/models.md#auto-is-experimental). - `urls` and `images` skip failed images. Compare with the `count` you sent when a partial result matters. ## In this section | Page | Covers | | ---------------------------------------------------------- | -------------------------------------------------------------- | | [Installation](https://mynth.io/docs/sdks/typescript/installation.md) | The package, the key, and the entry points | | [Client](https://mynth.io/docs/sdks/typescript/client.md) | `apiKey`, `baseUrl`, and the default destination | | [Generating media](https://mynth.io/docs/sdks/typescript/generating-media.md) | Image, video, and tool calls, and what the result objects hold | | [Tasks and polling](https://mynth.io/docs/sdks/typescript/tasks.md) | `generateAsync()`, poll intervals, and the browser token | | [Errors](https://mynth.io/docs/sdks/typescript/errors.md) | `MynthAPIError` and the errors a wait throws | ## Next steps - [Installation](https://mynth.io/docs/sdks/typescript/installation.md): add the package and set the key. - [Generate images](https://mynth.io/docs/guides/generate-images.md): every field `image.generate()` sends. - [Webhooks](https://mynth.io/docs/concepts/webhooks.md): the helpers, mounted per framework. --- # Installation > Install @mynthio/sdk, set MYNTH_API_KEY, and import from the right entry point. **pnpm** ```bash pnpm add @mynthio/sdk ``` **npm** ```bash npm install @mynthio/sdk ``` **bun** ```bash bun add @mynthio/sdk ``` **yarn** ```bash yarn add @mynthio/sdk ``` The package is ESM only. Import the default client: ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); ``` ## API key Create a `mak_` key with `npx @mynthio/cli api-key create my-app` or in the [dashboard](https://mynth.io/dashboard/keys), and put it where the server process reads environment variables: ```bash MYNTH_API_KEY=mak_... ``` `new Mynth()` does not check the key. The first read of `mynth.image` or `mynth.video` throws a plain `Error` when there is no `MYNTH_API_KEY` and no `apiKey` option. `mynth.models.list()` needs no key. [Authentication](https://mynth.io/docs/authentication.md) covers scopes and spending limits. ## Entry points The package has four entry points. Import only these. | Import | Exports | | ----------------------------- | ------------------------------------------------------------------------------------ | | `@mynthio/sdk` | `Mynth`, `verifyWebhook`, result types, error classes, and the `MynthSDKTypes` types | | `@mynthio/sdk/next` | `mynthWebhookHandler` for a Next.js route handler | | `@mynthio/sdk/tanstack-start` | `mynthWebhookHandler` for a TanStack Start route | | `@mynthio/sdk/convex` | `mynthWebhookAction` for a Convex HTTP action | There is no `@mynthio/sdk/webhooks` entry point. `verifyWebhook()` in the main entry point verifies a delivery in any other framework and returns the typed event. `convex` is an optional peer dependency, needed only for `@mynthio/sdk/convex`. The webhook helpers and `verifyWebhook()` read `MYNTH_WEBHOOK_SECRET` unless you pass a secret. [Webhooks](https://mynth.io/docs/concepts/webhooks.md#receive-events) shows each one mounted. ## Optional environment | Variable | Used for | | ---------------------- | --------------------------------------------------------------------------- | | `MYNTH_API_KEY` | The API key, unless you pass `apiKey` | | `MYNTH_DESTINATION` | Default destination for image generation, background removal, and upscaling | | `MYNTH_WEBHOOK_SECRET` | The `whsec_` secret the webhook helpers verify against | ## Next steps - [Client](https://mynth.io/docs/sdks/typescript/client.md): the constructor options. - [Generating media](https://mynth.io/docs/sdks/typescript/generating-media.md): the first real call. --- # Client > The Mynth constructor options, where the key, base URL, and default destination come from, and the image, video, and models clients. `new Mynth()` with no arguments reads `MYNTH_API_KEY` and sends requests to `https://api.mynth.io`. Pass options only when a default is wrong: ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth({ apiKey: process.env.MYNTH_API_KEY, baseUrl: "https://api.mynth.io", destination: "product-photos", }); ``` | Option | Default | What it does | | ------------- | ---------------------- | -------------------------------------------------------------------------------- | | `apiKey` | `MYNTH_API_KEY` | The bearer key. Missing, it throws on first use of `image` or `video` | | `baseUrl` | `https://api.mynth.io` | A proxy or test host. A trailing slash is removed | | `destination` | `MYNTH_DESTINATION` | Default destination name for image generation, background removal, and upscaling | An option wins over its environment variable, and a `destination` on a request wins over both. Video has no destinations, so `mynth.video` ignores the option. ## The three clients | Property | Holds | | -------------- | --------------------------------------------------------------------------------- | | `mynth.image` | Image generation, rating, alt text, review, background removal, upscaling, upload | | `mynth.video` | Video generation, estimates, upload | | `mynth.models` | `list()`: the public catalog, `GET /models`, no key needed | `image` and `video` are created the first time you read them, and that is when a missing key throws: ```text Mynth API key is required. Either pass it as an option or set the MYNTH_API_KEY environment variable. ``` The throw is a plain `Error`, not a `MynthAPIError`. `models.list()` returns image and video entries together. Filter on `type`. [The model field](https://mynth.io/docs/models.md#reading-a-catalog-entry) describes each entry. `MynthImage` and `MynthVideo` are also exported for code that needs only one side. `MynthVideo` takes `apiKey` and `baseUrl`. Types live on `MynthSDKTypes`: ```ts import type { MynthSDKTypes } from "@mynthio/sdk"; ``` ## Next steps - [Generating media](https://mynth.io/docs/sdks/typescript/generating-media.md): `image.generate()` and `video.generate()`. - [Tasks and polling](https://mynth.io/docs/sdks/typescript/tasks.md): keep the wait out of a request handler. --- # Generating media > Generate images and video with the TypeScript client, run the image tools, upload local files, and read plain result objects. `image.generate()` and `video.generate()` send one request body, wait for the task, and return the finished result. The body is the REST body, with the same fields. [Generate images](https://mynth.io/docs/guides/generate-images.md) and [generate video](https://mynth.io/docs/guides/generate-video.md) are the field references. ## Images ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); const task = await mynth.image.generate({ model: "black-forest-labs/flux.2-pro", prompt: "A lighthouse at dusk, film grain", size: "landscape", count: 2, metadata: { orderId: "ord_42" }, }); for (const image of task.images) { console.log(image.url ?? image.mynthUrl, image.width, image.height); } ``` The value is an `ImageGenerationResult`: | Member | Holds | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `taskId`, `cost` | The task ID and final USD charge | | `model`, `magicPrompt` | The resolved model and rewritten prompt | | `urls` | `url` of each successful image, skipping any whose `url` is `null` | | `images` | Successful images with `id`, `url`, `mynthUrl`, `width`, `height`, `size`, `format`, `mimeType`, `destination`, and `rating` | | `failures` | Each failed image's `{ code, message? }` | | `metadata` | The metadata you sent, typed from the request | | `raw` | The API's request and result before mapping | > **Warning** > > `urls` and `images` skip failed images, and `urls` also skips images whose destination upload > failed. A task can complete with a short list. Compare `images.length` with the `count` you sent, > and read `failures` to see why. A task that settles as `failed` is not returned: `generate()` throws `TaskAsyncTaskFailedError`. See [errors](https://mynth.io/docs/sdks/typescript/errors.md). Pass a model id on every call. Omitting `model` makes the API use `auto`, which is experimental. See [the model field](https://mynth.io/docs/models.md#auto-is-experimental). The SDK has no `image.estimate()`. Price an image body with `POST /image/generate/estimate`. See [estimate cost](https://mynth.io/docs/guides/estimate-cost.md). ## Local files In `inputs`, a string is a URL, and a `File` or `Blob` is uploaded first. All files of one call go up in a single upload, which accepts at most 10 files. ```ts const task = await mynth.image.generate({ model: "black-forest-labs/flux.2-pro", prompt: "The same lighthouse, at noon", inputs: [file], }); ``` `image.upload(file)` or `image.upload([a, b])` runs that upload on its own and returns `{ urls }`. Uploaded URLs are served for 1 day. [Image to image](https://mynth.io/docs/guides/image-to-image.md) covers input roles. ## Video ```ts const task = await mynth.video.generate({ model: "bytedance/seedance-2.0-mini", prompt: "A lighthouse beam sweeping over waves", duration: 5, size: "9:16_720p", }); console.log(task.urls[0]); ``` The result is a plain `VideoGenerationResult` with `taskId`, `cost`, `model`, `videos`, `urls`, `failures`, `metadata`, and `raw`. `videos` holds successful outputs, and `failures` lists the errors for failed outputs. Each video uses `mynthUrl` for Mynth's copy. `generate()` waits up to an hour. `video.estimate()` prices the same body without creating a task. A `File` in `inputs` is uploaded first, as on `generate()`. ```ts const quote = await mynth.video.estimate({ model: "bytedance/seedance-2.0-mini", prompt: "A lighthouse beam sweeping over waves", duration: 5, }); console.log(quote.estimated_cost, quote.estimate_kind); // "0.405", "exact" ``` `AVAILABLE_VIDEO_MODELS` lists each video model's resolutions, durations, and frame inputs. [Video models](https://mynth.io/docs/models/video.md) has the same table. ## Rating, alt text, review, background removal, upscaling These take no `model`, and each takes a `url` or a local `file`. Each method waits, and each has an `Async` twin. ```ts const rating = await mynth.image.rate({ url }); const alt = await mynth.image.alt({ url }); const review = await mynth.image.review({ url, effort: "low" }); const cutout = await mynth.image.removeBackground({ file, output: { format: "png" } }); const upscaled = await mynth.image.upscale({ file, size: "2x", effort: "low" }); ``` | Method | Result members | | -------------------- | ------------------------------------------------------------- | | `rate()` | `taskId`, `cost`, `level` | | `alt()` | `taskId`, `cost`, `alt` | | `review()` | `taskId`, `cost`, `score`, `summary`, `findings`, `strengths` | | `removeBackground()` | `taskId`, `cost`, `image`, `metadata` | | `upscale()` | `taskId`, `cost`, `image`, `metadata` | Every finished result uses `taskId` and includes `raw`. `removeBackground()` and `upscale()` return the same image fields as generation and also take `destination`, `webhook`, and `metadata`. `upscale()` requires `size` and `effort`. The guides cover each tool: [rate](https://mynth.io/docs/guides/rate-images.md), [alt text](https://mynth.io/docs/guides/alt-text.md), [review](https://mynth.io/docs/guides/review-images.md), [remove background](https://mynth.io/docs/guides/remove-background.md), and [upscale](https://mynth.io/docs/guides/upscale-images.md). ## Next steps - [Tasks and polling](https://mynth.io/docs/sdks/typescript/tasks.md): `generateAsync()`, the browser token, and poll intervals. - [Errors](https://mynth.io/docs/sdks/typescript/errors.md): a rejected request and a failed task. - [Tasks](https://mynth.io/docs/concepts/tasks.md#file-lifetimes): how long the files last. --- # Tasks and polling > Wait with generate(), return early with generateAsync(), how long the SDK polls, and how to hand a browser the pat_ token. Every SDK call that does work creates a task. `generate()` polls that task for you. `generateAsync()` returns a `TaskAsync` right after the task is created, and you decide who waits. ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); const pending = await mynth.image.generateAsync({ model: "black-forest-labs/flux.2-pro", prompt: "A lighthouse at dusk, film grain", }); console.log(pending.id); // tsk_... const task = await pending.wait(); console.log(task.urls[0]); ``` `image.generate()` is `generateAsync()` followed by `wait()`. The tools follow the same pattern: `rate()` and `rateAsync()`, `alt()` and `altAsync()`, `review()` and `reviewAsync()`, `removeBackground()` and `removeBackgroundAsync()`, `upscale()` and `upscaleAsync()`, `video.generate()` and `video.generateAsync()`. `upload()` and `video.estimate()` do not create tasks. ## `TaskAsync` | Member | Holds | | -------- | -------------------------------------------------------------------------- | | `id` | The task id | | `access` | `{ publicAccessToken? }`, the browser token when the request asked for one | | `wait()` | Polls until the task settles, then returns a plain result object | `wait()` polls until `completed`, then loads the full task and builds the result. Calling it twice returns the same promise. A task that settles as `failed` makes it throw `TaskAsyncTaskFailedError`. [Errors](https://mynth.io/docs/sdks/typescript/errors.md) lists every throw. ## How long it polls | Wait | Interval | Gives up after | | ------------------ | ---------------------------------------------- | -------------- | | Images and tools | every 2.5 s for the first 12 s, then every 5 s | 30 minutes | | `video.generate()` | every 10 s | 1 hour | Each sleep adds up to 500 ms of jitter. After the budget runs out, `wait()` throws `TaskAsyncTimeoutError`. The task itself keeps running, and you can still read it by id. Status reads that fail with a network error, a `404`, or a `5xx` are retried, up to 20 in a row. Hold the connection only where that wait is acceptable, such as a script or a background worker. A request handler should store `pending.id`, respond, and finish in a [webhook](https://mynth.io/docs/concepts/webhooks.md). ## Poll from the browser `image.generateAsync()`, `image.removeBackgroundAsync()`, `image.upscaleAsync()`, and `video.generateAsync()` expose the task's `pat_` token as `pending.access.publicAccessToken` when the request sets `generate_public_access_token: true`. Rate, alt text, and review do not issue one. ```ts // server const pending = await mynth.image.generateAsync({ model: "black-forest-labs/flux.2-pro", prompt: "A lighthouse at dusk, film grain", generate_public_access_token: true, }); return Response.json({ taskId: pending.id, token: pending.access.publicAccessToken }); ``` The browser calls `GET /tasks/{id}/status` and `GET /tasks/{id}/result` with `Authorization: Bearer pat_...`. [Poll for results](https://mynth.io/docs/guides/poll-for-results.md#from-a-browser) has the browser loop. - The token works only on those two routes and expires one hour after it is issued. A long video render can outlive it. - If the task is created but its token cannot be generated, `generateAsync()` throws `MynthAPIError` with `code` `public_access_token_failed` and the task's id in `taskId`. Don't resend: poll `GET /tasks/{id}/status` from the server, or let a webhook report it. - `wait()` is for the server. Its last call is `GET /tasks/{id}`, which a `pat_` cannot make. ## Partial results `urls`, `images`, and `videos` skip failed items. Compare `images.length` with `count` when a short list is a problem. A video request renders one video, so a failed render shows up as an empty `urls` on a completed task. [Tasks](https://mynth.io/docs/concepts/tasks.md#completed-does-not-mean-every-item-worked) explains why a completed task can hold failed items. ## Next steps - [Errors](https://mynth.io/docs/sdks/typescript/errors.md): what `wait()` throws, and how to read the code. - [Webhooks](https://mynth.io/docs/concepts/webhooks.md): take the result without a polling loop. - [Poll for results](https://mynth.io/docs/guides/poll-for-results.md): the REST loop. --- # Errors > MynthAPIError for a rejected request, the TaskAsync errors a wait can throw, and how to read the codes of a task that failed. The SDK surfaces failures three ways: | What failed | What you get | | ----------------------------------------------- | -------------------------------------------------- | | The HTTP request was rejected | `MynthAPIError` is thrown | | The task settled as `failed`, or the wait broke | A `TaskAsync*` error is thrown | | Some items failed on a `completed` task | Nothing is thrown. `urls` and `images` are shorter | The codes themselves are on [errors](https://mynth.io/docs/api-reference/errors.md). This page is how the SDK reports them. ```ts import Mynth, { MynthAPIError, TaskAsyncTaskFailedError } from "@mynthio/sdk"; const mynth = new Mynth(); const pending = await mynth.image .generateAsync({ model: "black-forest-labs/flux.2-pro", prompt: "A lighthouse at dusk, film grain", }) .catch((error: unknown) => { if (error instanceof MynthAPIError && error.code === "insufficient_balance") { // rejected at create: nothing queued, nothing held } throw error; }); try { const task = await pending.wait(); console.log(task.urls[0]); } catch (error) { if (error instanceof TaskAsyncTaskFailedError) { // read GET /tasks/{pending.id} for errors[].code } throw error; } ``` Keep the id from `generateAsync()` before you wait. When `generate()` throws, the task id is only in the error message, and the error classes have no `taskId` property. ## `MynthAPIError` Thrown for any non-2xx response on a create, upload, estimate, or models call. | Property | Holds | | --------- | -------------------------------------------------------------------------- | | `status` | The HTTP status | | `code` | The body's `error.code`, when there is one | | `message` | The body's `error.message`, or a generic fallback | | `issues` | One `{ path, message }` per invalid field, on a `validation_error` | | `taskId` | The task the create made before it failed, on `public_access_token_failed` | `message` says what to change, so it is safe to show as it is. When a field failed the schema, `issues` has the same problems one by one, with `path` pointing into the request body: ```ts try { await mynth.image.generate({ model: "black-forest-labs/flux.2-pro", prompt: "A lighthouse at dusk", count: 50, }); } catch (error) { if (error instanceof MynthAPIError && error.code === "validation_error") { for (const issue of error.issues ?? []) { console.error(issue.path.join("."), issue.message); // count count must be at most 20 (was 50) } } throw error; } ``` A validation error that is not about one field, such as a size the model cannot produce, has no `issues`. See [validation errors](https://mynth.io/docs/api-reference/errors.md#validation-errors). Two failures are not `MynthAPIError`: - A missing API key throws a plain `Error` when you first read `mynth.image` or `mynth.video`. - A network failure on the create request throws whatever `fetch` throws. The create is not retried. There are no idempotency keys, so check `GET /tasks` before you resend a create that may have gone through. ## Errors while waiting All are exported from `@mynthio/sdk`, and none carries a task error `code`. | Class | Thrown when | | ---------------------------- | -------------------------------------------------------------------------------- | | `TaskAsyncTaskFailedError` | The task settled as `failed`. The message includes the task id | | `TaskAsyncTimeoutError` | The poll budget ran out: 30 minutes for images and tools, one hour for video | | `TaskAsyncUnauthorizedError` | Status or the task record answered `401` or `403` | | `TaskAsyncFetchError` | Status reads kept failing. `cause` is the last network error, when there was one | | `TaskAsyncTaskFetchError` | The task completed, but loading the record kept failing | For `TaskAsyncTaskFailedError`, read `GET /tasks/{id}`. Its `errors` array is `[{ code, message? }]`. Branch on `code`. After `TaskAsyncTimeoutError` the task is still running on Mynth. Read it by id later instead of creating it again. ## `restricted_content` A provider refusal usually fails one output, not the task. `wait()` returns, and `urls` is shorter than `count`. Read `failures` for each `code`. If the refusal failed the whole task, `wait()` throws `TaskAsyncTaskFailedError`, and the code is on `GET /tasks/{id}`. [Prompts and images](https://mynth.io/docs/concepts/prompts.md) explains the refusal. ## Next steps - [Tasks and polling](https://mynth.io/docs/sdks/typescript/tasks.md): the poll budget behind `TaskAsyncTimeoutError`. - [Errors](https://mynth.io/docs/api-reference/errors.md): every HTTP and task code. --- # Overview > Install the Mynth CLI, log in with a browser approval, and generate images, manage keys, webhooks and destinations, and read these docs from the terminal. The Mynth CLI works from a terminal or an agent's shell. It generates and analyzes images, reads tasks, and manages API keys, webhooks, and destinations. With `--json`, its output is stable enough to parse, and its exit codes tell scripts what went wrong. ## Install Node.js 20 or newer. Install globally for the `mynth` binary: ```bash npm install -g @mynthio/cli mynth --help ``` Or run any command without installing, as the examples in these docs do: ```bash npx @mynthio/cli --help ``` ## Log in ```bash npx @mynthio/cli auth login ``` The CLI prints a one-time code and opens your browser to approve it. It then creates an API key and stores it on this machine. [Logging in](https://mynth.io/docs/sdks/cli/logging-in.md) covers where the key lives and which credential each command sends. ## Generate an image ```bash npx @mynthio/cli image generate \ -m black-forest-labs/flux.2-pro \ -p "A lighthouse at dusk, film grain" ``` The command waits and prints the image URL. Add `-o ./images` to save the file, or `--json` for machine-readable output. ## Without an account `docs` and `models list` work without logging in: ```bash npx @mynthio/cli docs get getting-started npx @mynthio/cli models list --type image ``` ## Next steps - [Logging in](https://mynth.io/docs/sdks/cli/logging-in.md): the stored key, and which credential is sent. - [Commands](https://mynth.io/docs/sdks/cli/commands.md): every command, its flags, and exit codes. --- # Logging in > Approve a one-time code in the browser. The CLI stores an API key on this machine. `auth login` is a browser approval. You confirm a one-time code, and the CLI creates an API key and stores it for the commands that follow. Every later command is an ordinary API key request. ## Log in ```bash npx @mynthio/cli auth login ``` On a desktop terminal the CLI opens the approval page and prints the URL. When the page already shows a code, confirm it matches the terminal. When it asks you to type one, enter the code the CLI printed. Over SSH, in CI, when output is piped, or with `--no-browser`, the CLI only prints the URL. The approval is used once, to create a key named `mynth-cli ()`. Its default scopes are `generate`, `manage`, and `keys`. Narrow them at login: ```bash npx @mynthio/cli auth login --scopes generate,manage ``` The key is listed in the [dashboard](https://mynth.io/dashboard/keys) with your other keys. Set a spending limit there if this machine is shared. [Authentication](https://mynth.io/docs/authentication.md) is the scope list. If `MYNTH_API_KEY` is already set, `auth login` exits 3 and does not start the browser flow. Unset it, or keep using the variable. ## Where the key is stored The file is `~/.config/mynth/credentials.json`, mode `0600`. When `XDG_CONFIG_HOME` is set, the directory is `$XDG_CONFIG_HOME/mynth` instead. The file holds the API key and its id. The name, the scopes, and the spending limit stay on the server, and `whoami` reads them live. Nothing from the browser session is written next to the key. To store a key you already have, skip the browser: ```bash npx @mynthio/cli config set api-key - ``` The `-` reads the key from stdin. That copy has no id in the file, so `auth logout` leaves it active on the account and only removes the local copy. ## Which key is used When `MYNTH_API_KEY` is set, every command sends that key. The file is not read. With the variable unset, commands send the stored key. With neither, the CLI exits 3 and tells you to run `auth login` or set `MYNTH_API_KEY`. `GET /balance` needs the `manage` scope. A generate-only key cannot read it, and `balance` exits 3 with `insufficient_scope`. `balance` works after `auth login`, because that key includes `manage` unless you narrowed `--scopes`. ## Check and sign out ```bash npx @mynthio/cli auth status npx @mynthio/cli whoami npx @mynthio/cli auth logout ``` `auth status` reports whether the credential is the environment variable or the file. It does not call the API. `whoami` calls `GET /me`, so a revoked key fails here instead of halfway through a generation. The same command is `auth whoami`. `auth logout` revokes the key this login created, then deletes the file. A key you stored with `config set api-key` is cleared locally and left active. Revoke that one in the dashboard, or with `api-key delete`. While `MYNTH_API_KEY` is set, logout cannot see the stored key, so it still deletes the file, leaves that key active, and warns that the variable will be used. `config unset api-key` deletes the file and does not revoke anything. ## Next - [Commands](https://mynth.io/docs/sdks/cli/commands.md): what to run once you are signed in. - [Authentication](https://mynth.io/docs/authentication.md): scopes, spending limits, and `mak_` keys. --- # Commands > Every Mynth CLI command and its main flags, with exit codes and environment variables. Images, tasks, models, balance, API keys, webhooks, destinations, and docs. Login, logout, and `whoami` are on [logging in](https://mynth.io/docs/sdks/cli/logging-in.md). Every command takes `--json` for machine-readable output, and `npx @mynthio/cli --help` lists every flag. ## Generate an image ```bash npx @mynthio/cli image generate \ -m black-forest-labs/flux.2-pro \ -p "A lighthouse at dusk, film grain" \ -s landscape \ -o ./images ``` The command waits for the task, then prints the image URL. `-o` downloads the files into that directory and creates it if needed. | Flag | What it does | | -------------------------------- | ------------------------------------------------------------------------------- | | `-m, --model` | Model id. Always pass one. See `models list` | | `-p, --prompt` | The prompt | | `-n, --negative` | Negative prompt | | `-s, --size` | A preset such as `landscape`, a ratio such as `16:9`, `16:9_4k`, or `auto` | | `-c, --count` | How many images, 1 to 20. Default 1 | | `-f, --format` | `png`, `jpg`, or `webp` | | `-i, --input` | Input image, repeatable, up to 20: a URL or a local file, with an optional role | | `--magic-prompt` | Rewrite the prompt before generating | | `--content-rating` | Rate each image sfw/nsfw | | `-l, --level` | Custom rating level as `value=description`, repeatable, 2 to 7 | | `--levels-file`, `--levels-json` | Custom rating levels as a JSON array, from a file (or `-` for stdin) or inline | | `--metadata` | Inline JSON object stored on the task, up to 2 KB | | `--destination` | Destination name. Defaults to `MYNTH_DESTINATION` | | `--webhook-url` | Per-request webhook URL, repeatable, up to 5 | | `--no-registered-webhooks` | Skip your registered webhook endpoints for this task | | `-o, --output-dir` | Save the files into this directory. Ignored with `--async` | | `--dry-run` | Print the estimated cost. Nothing is generated or uploaded | | `--async` | Print the task id and its `pat_` token, and return without waiting | | `--detailed` | With `--json`, print the full task record | Omitting `-m` leaves `model` off the request, so the API uses `auto`, which is experimental. See [the model field](https://mynth.io/docs/models.md#auto-is-experimental). An input can carry a role: `-i source:./edit.png` or `-i reference:https://example.com/a.png`. Roles are `source`, `reference`, and `auto`. Local files are uploaded first, in one upload of at most 10 files. [Generate images](https://mynth.io/docs/guides/generate-images.md) covers every field. `--dry-run` calls `POST /image/generate/estimate`. For `auto` the line says `(upper bound)`. [Estimate cost](https://mynth.io/docs/guides/estimate-cost.md) is the same call over HTTP. ## Other image commands These take one image, as a URL or a local file, and have no `--model` flag. ```bash npx @mynthio/cli image rate ./shot.png npx @mynthio/cli image alt ./shot.png npx @mynthio/cli image review ./shot.png --effort low npx @mynthio/cli image remove-background ./shot.png -f png -o ./out npx @mynthio/cli image upscale ./shot.png --size 2x --effort low -o ./out npx @mynthio/cli image upload ./a.png ./b.webp ``` - `rate` uses sfw/nsfw unless you pass custom levels with `-l`, `--levels-file`, or `--levels-json`. - `review` defaults to `--effort high`. `low` is the cheaper, faster panel. - `remove-background` outputs `png` or `webp` with `-f`, and also takes `--async`, `-o`, `--metadata`, `--destination`, `--webhook-url`, and `--no-registered-webhooks`. - `upscale` requires `-s, --size` (`2x` or `4x`) and `--effort` (`low` or `high`). Effort sets the price, so it has no default. It outputs `png`, `jpg`, or `webp` with `-f`, and takes the same other flags as `remove-background`. - `upload` sends up to 10 local JPEG, PNG, or WEBP files and prints a URL for each. The URLs are served for 1 day. The CLI has no video command. Use the SDK or the REST API for video. ## Models ```bash npx @mynthio/cli models list --type image --capability img2img --max-price 0.05 ``` `models list` reads the public catalog and needs no account. The filters run locally: | Flag | Keeps | | ---------------------------- | ------------------------------------------------------------- | | `-s, --search` | Fuzzy matches on the id and display name | | `--org` | One vendor, fuzzy matched, so `bfl` finds `black-forest-labs` | | `--type` | `image` or `video` | | `--capability` | `txt2img`, `img2img`, `txt2vid`, or `img2vid` | | `--4k` | Models with a 4k price | | `--max-price`, `--min-price` | The cheapest rate: per image, or per second for video | ## Tasks ```bash npx @mynthio/cli task list --limit 10 npx @mynthio/cli task get tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP npx @mynthio/cli task result tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP npx @mynthio/cli task wait tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP --timeout 120 ``` `list` is newest first. `--limit` is 1 to 100, default 20, and `--after` takes a task id to page back from. `result` prints only the result as JSON. `wait` blocks until the task settles, for up to 30 minutes unless you pass `--timeout `. `wait` takes `--detailed` to print the full record instead of a summary. ## Balance ```bash npx @mynthio/cli balance ``` Prints balance, reserved, and available, then the key's spending limit when it has one. It needs a key with the `manage` scope, which the key from `auth login` has. [Pricing](https://mynth.io/docs/pricing.md#balance) explains the numbers. ## API keys ```bash npx @mynthio/cli api-key create my-app npx @mynthio/cli api-key list npx @mynthio/cli api-key delete ak_01KE7XWWEQ4MCGWKBQKJ1G47RP --yes ``` `create` prints a `mak_` key once, with the `generate` scope. The CLI authenticates with an API key, and an API key can only create `generate` keys: asking for `manage` or `keys` fails with `403 scope_escalation` and exit `3`. Create wider keys in the [dashboard](https://mynth.io/dashboard/keys). `list` shows each key's id, name, preview, and scopes, never the key itself. Spending limits are set in the dashboard. `delete` needs `--yes`. ## Webhooks ```bash npx @mynthio/cli webhook create --url https://example.com/api/mynth -e all ``` | Flag | What it does | | ------------------------- | ------------------------------------------------------------------------------ | | `--url` | Required. The endpoint URL, `https` on a public host | | `-e, --event` | Required, repeatable. An event name, `task.completed`, `task.failed`, or `all` | | `--api-key-id` | Repeatable. Deliver only tasks from these keys. Omit for every key | | `--include-session-tasks` | Also deliver tasks with no API key, such as playground runs | | `--disabled` | Create the endpoint disabled | The `whsec_` signing secret prints once. Tasks from the CLI carry the key from `auth login`, so they deliver without `--include-session-tasks`. `webhook update ` replaces the whole configuration: pass `--url` and every `-e` again. Leaving out `--include-session-tasks` turns it off, and leaving out `--api-key-id` goes back to every key. `webhook delete ` needs `--yes`. [Webhooks](https://mynth.io/docs/concepts/webhooks.md) covers the signature and the payload. ## Destinations ```bash npx @mynthio/cli destination create bunny-prod \ --provider bunny \ --storage-zone my-zone \ --region de \ --path-template 'images/{id}' \ --url-template 'https://cdn.example.com/{path}' \ --secret - ``` The name is 1 to 64 characters of `a-z`, `0-9`, and `-`, and cannot change later. Providers are `bunny`, `r2`, and `s3`. `--secret` takes a file path, or `-` for stdin, never the secret itself. Bunny takes the storage password as plain text. `r2` and `s3` take JSON with `access_key_id` and `secret_access_key`. `--file` sends a whole JSON body instead of the typed flags. ```bash npx @mynthio/cli destination test dst_01KE7XWWEQ4MCGWKBQKJ1G47RP npx @mynthio/cli image generate -m black-forest-labs/flux.2-pro -p "A lighthouse" --destination bunny-prod ``` `test` uploads a probe to a unique path, or to `--path`. `list` and `get` show what is stored, without the secret. `update` replaces provider and config. `delete` needs `--yes`. These commands need the `manage` scope. [Destinations](https://mynth.io/docs/concepts/destinations.md) covers the templates. ## Config ```bash npx @mynthio/cli config set api-key - npx @mynthio/cli config unset api-key ``` `set` stores an existing key, read from stdin, in the credentials file. `unset` deletes the file and does not revoke the key. [Logging in](https://mynth.io/docs/sdks/cli/logging-in.md) says when to use these instead of `auth login`. ## Docs ```bash npx @mynthio/cli docs list npx @mynthio/cli docs get concepts/tasks npx @mynthio/cli docs get index ``` No account needed. `get` takes the page path without `.md`, and `index` is the introduction. `list` prints [/llms.txt](https://mynth.io/llms.txt). Pages come from `https://mynth.io/docs/.md`. Set `MYNTH_DOCS_URL` to read another host. ## Exit codes The message goes to stderr. Branch on the exit code, not the text. | Exit | Meaning | From | | ---- | ----------------- | -------------------------------------------------------------------------- | | 0 | Success | | | 1 | Any other failure | | | 2 | Usage | Bad flags, `validation_error`, `invalid_json`, or `unsupported_media_type` | | 3 | Auth | `unauthorized`, `insufficient_scope`, any `401` or `403` | | 4 | Out of credit | `insufficient_balance`, `spending_limit_exceeded`, any `402` | | 5 | Refused | `restricted_content` | | 6 | Rate limited | Any `429` | How a task failure maps to an exit code depends on the command: - `rate`, `alt`, `review`, `remove-background`, and `upscale` fail when the task fails: exit `5` for `restricted_content`, `1` otherwise. - `task wait` prints the failed task and exits `5` if any code on the task or its images is `restricted_content`, `1` otherwise. - `image generate` prints the result and exits `0` even when the task failed or an image was refused. Read `status` and `images[].error.code` in the `--json` output. `MYNTH_DEBUG=1` adds the cause and stack trace to stderr. [Errors](https://mynth.io/docs/api-reference/errors.md) has every code. ## Environment | Variable | Effect | | ------------------- | --------------------------------------------------------- | | `MYNTH_API_KEY` | Sent instead of the stored key | | `MYNTH_DESTINATION` | Default `--destination` | | `MYNTH_DEBUG=1` | Error cause and stack trace on stderr | | `MYNTH_DOCS_URL` | Docs host for `docs get`. Default `https://mynth.io/docs` | | `MYNTH_API_URL` | API host. Default `https://api.mynth.io` | | `XDG_CONFIG_HOME` | Directory for the credentials file. Default `~/.config` | ## Next steps - [Logging in](https://mynth.io/docs/sdks/cli/logging-in.md): the stored key and which credential is sent. - [Generate images](https://mynth.io/docs/guides/generate-images.md): the request fields behind the flags. - [Errors](https://mynth.io/docs/api-reference/errors.md): request errors and task failures. --- # Convex > Start a generation from a Convex action and receive the signed webhook on a Convex HTTP route with mynthWebhookAction. Generate from a Convex **action** with the normal SDK, and receive the result on an HTTP route with `mynthWebhookAction` from `@mynthio/sdk/convex`. The API key stays in the Convex environment. ## Setup ```bash npm install @mynthio/sdk ``` `@mynthio/sdk/convex` imports `convex/server`, which every Convex project already has. Set two environment variables on the Convex deployment: | Variable | Value | | ---------------------- | ---------------------------------------------------------- | | `MYNTH_API_KEY` | A `mak_` key | | `MYNTH_WEBHOOK_SECRET` | The `whsec_` secret printed when you register the endpoint | ## Generate from an action Queries and mutations cannot make network calls, so use an action. Create the task with `generateAsync()` so the action returns right away, and collect the file in the webhook. ```ts // convex/generate.ts import { v } from "convex/values"; import Mynth from "@mynthio/sdk"; import { action } from "./_generated/server"; export const generate = action({ args: { prompt: v.string() }, handler: async (_ctx, args) => { const mynth = new Mynth(); // reads MYNTH_API_KEY const pending = await mynth.image.generateAsync({ model: "black-forest-labs/flux.2-pro", prompt: args.prompt, metadata: { source: "convex" }, generate_public_access_token: true, }); return { taskId: pending.id, publicAccessToken: pending.access.publicAccessToken }; }, }); ``` Return the task id to the client if the UI needs it. `publicAccessToken` lets the browser poll that one task. It expires after an hour. Never return the API key. ## Receive the webhook Register your deployment's HTTP actions URL plus the route path, for example `https://.convex.site/mynth-webhook`, as a webhook endpoint. ```ts // convex/http.ts import { httpRouter } from "convex/server"; import { mynthWebhookAction } from "@mynthio/sdk/convex"; import { internal } from "./_generated/api"; const http = httpRouter(); export const mynthWebhook = mynthWebhookAction({ imageTaskCompleted: async (result, { context }) => { await context.runMutation(internal.images.save, { taskId: result.taskId, images: result.images, }); }, imageTaskFailed: async (result) => { console.error(result.taskId, result.errors); }, }); http.route({ path: "/mynth-webhook", method: "POST", handler: mynthWebhook }); export default http; ``` `internal.images.save` is a mutation you write. Each handler gets a mapped result or task failure and `{ context, request, event }`. `context` is the Convex action context, so `context.runQuery` and `context.runMutation` are how it touches your data. `event.id` is the `webhook-id`, the same on every retry, so store it to skip events you have already handled. The handler names are the same as in every SDK helper. See the [table on webhooks](https://mynth.io/docs/concepts/webhooks.md#receive-events). ## Behavior `mynthWebhookAction` returns a Convex HTTP action, so pass it straight to `http.route` without wrapping it in `httpAction`. It runs the same checks as the [Next.js and TanStack Start helpers](https://mynth.io/docs/concepts/webhooks.md#receive-events): - It reads `MYNTH_WEBHOOK_SECRET` when a request arrives, so deploying works before the variable is set. Pass `{ webhookSecret }` as the second argument to use another source. - A missing or bad signature, a missing `webhook-id`, a `webhook-timestamp` more than five minutes off, or a body whose `id` is not the `webhook-id` answers `400`. - A signed event with no handler answers `200`. An error thrown by a handler fails the request, and Mynth retries the delivery. - Unsigned deliveries from per-request `webhook.custom` URLs are rejected, unless you pass `{ unsigned: { verify } }` to check a token you put in the URL. See [per-request webhooks](https://mynth.io/docs/concepts/webhooks.md#per-request-webhooks). ## Next steps - [Webhooks](https://mynth.io/docs/concepts/webhooks.md): registering the endpoint, events, sources, and retries. - [Tasks](https://mynth.io/docs/concepts/tasks.md): statuses and how long the file lasts. --- # TanStack Start > Receive signed Mynth webhooks in a TanStack Start server route, generate from a server function, and use the TanStack AI image adapter. `@mynthio/sdk/tanstack-start` exports `mynthWebhookHandler` for a Start server route. Generation from a server function uses the normal SDK. If the app already uses TanStack AI's `generateImage()`, the separate `@mynthio/tanstack-ai-adapter` package plugs Mynth into it. ## Receive the webhook ```bash npm install @mynthio/sdk ``` Put `MYNTH_WEBHOOK_SECRET`, the `whsec_` secret from registering the endpoint, in the server environment. ```ts // src/routes/api/mynth-webhook.ts import { createFileRoute } from "@tanstack/react-router"; import { mynthWebhookHandler } from "@mynthio/sdk/tanstack-start"; export const Route = createFileRoute("/api/mynth-webhook")({ server: { handlers: { POST: mynthWebhookHandler({ imageTaskCompleted: async (result, { request }) => { console.log(result.taskId, result.images, request.url); }, imageTaskFailed: async (result) => { console.error(result.taskId, result.errors); }, }), }, }, }); ``` The handler verifies the Standard Webhooks `webhook-signature` against `webhook-id`, `webhook-timestamp` and the raw body, and requires the body's `id` to be the `webhook-id`. A missing or wrong signature, or a timestamp more than five minutes off, answers `400`. An event with no handler, or one it does not know, answers `200`. It reads the secret on each request, from `MYNTH_WEBHOOK_SECRET` or from `{ webhookSecret }` as the second argument, and throws if neither is set. It reads a clone of the request, so the route's `request` stays unread. Each callback receives a mapped result or task failure and the route context: `request`, `params`, and middleware `context`, plus the verified `event`. `event.id` is the `webhook-id`, the same on every retry, so store it to skip events you have already handled. The handler names are the same as in every SDK helper. See the [table on webhooks](https://mynth.io/docs/concepts/webhooks.md#receive-events). Unsigned per-request `webhook.custom` deliveries are rejected unless you pass `{ unsigned: { verify } }`. See [per-request webhooks](https://mynth.io/docs/concepts/webhooks.md#per-request-webhooks). ## Generate on the server Keep `MYNTH_API_KEY` on the server and call the SDK from a server function: ```ts import Mynth from "@mynthio/sdk"; const mynth = new Mynth(); const pending = await mynth.image.generateAsync({ model: "black-forest-labs/flux.2-pro", prompt: "A lighthouse at dusk, film grain", generate_public_access_token: true, }); return { taskId: pending.id, token: pending.access.publicAccessToken }; ``` `generate()` waits for the task. `generateAsync()` returns the id right away, plus a browser token when the request sets `generate_public_access_token`, for when the caller cannot wait. [Tasks and polling](https://mynth.io/docs/sdks/typescript/tasks.md) covers both. ## TanStack AI `@mynthio/tanstack-ai-adapter` is for apps that already call `generateImage()` from `@tanstack/ai`. It calls the Mynth API itself, so it needs no other package: only `@tanstack/ai` 0.61.0 or newer as a peer. It does not verify webhooks. The route above still does that. ```bash npm install @mynthio/tanstack-ai-adapter @tanstack/ai ``` ```ts import { generateImage } from "@tanstack/ai"; import { mynthImage } from "@mynthio/tanstack-ai-adapter"; const result = await generateImage({ adapter: mynthImage("black-forest-labs/flux.2-pro"), prompt: "A lighthouse at dusk, film grain", }); console.log(result.images[0]?.url); ``` - `mynthImage(model, config?)` binds one model. The key comes from `MYNTH_API_KEY` or `config.apiKey`. - `createMynthImage({ apiKey, baseURL, defaultHeaders, destination })` returns a function that takes a model, for several adapters sharing one config. - Image content parts become Mynth `inputs` on models listed in `MYNTH_IMAGE_INPUT_MODELS`. Other Mynth fields go in `modelOptions`. - `MYNTH_IMAGE_MODELS` is the id list built into the package version, and it includes `auto`. Pass an explicit id. The live list is `GET https://api.mynth.io/models`. - Failures are `MynthError` subclasses with a `code` that TanStack AI reports on `RUN_ERROR`: the API's own code (such as `insufficient_balance`) for `MynthAPIError`, or `MYNTH_TASK_FAILED`, `MYNTH_NO_IMAGES` and `MYNTH_TASK_TIMEOUT`. The adapter creates the task, polls it until it settles, and returns the finished images. [tanstack-start-ai-mynth-adapter](https://github.com/mynthio/oss/tree/main/examples/tanstack-start-ai-mynth-adapter) is a complete Start app that streams the TanStack call. ## Next steps - [Webhooks](https://mynth.io/docs/concepts/webhooks.md): registering the endpoint, events, sources, and retries. - [Generate images](https://mynth.io/docs/guides/generate-images.md): the request fields. --- # llms.txt > Read these docs as plain markdown, one page, the index, or every page in one file, over HTTP or with the CLI. No account needed. Every docs page is also plain markdown, written to be read by coding agents. No account is needed for any of it. | What | URL | | -------------------- | -------------------------------------------------------- | | One page | `https://mynth.io/docs/.md` | | The introduction | [/docs.md](https://mynth.io/docs.md) | | The index | [/llms.txt](https://mynth.io/llms.txt) | | Every page, one file | [/llms-full.txt](https://mynth.io/llms-full.txt) | | The model catalog | [/models.json](https://mynth.io/models.json), [/models.txt](https://mynth.io/models.txt) | | The OpenAPI document | `https://api.mynth.io/openapi.json` | ## One page Append `.md` to a docs URL: [/docs/getting-started.md](https://mynth.io/docs/getting-started.md). The same URL without `.md` also returns markdown when the request asks for it: an `Accept` header with `text/markdown` or `text/plain` and without `text/html`. `curl`, `wget`, and `httpie` get markdown when `Accept` is empty or `*/*`. Browsers send `text/html`, so they still get the page. An unknown `.md` URL is a plain `404`. The markdown keeps every code example. Tabbed examples come out as labelled code blocks, and SDK, REST, and CLI variants of a page come out as separate "Using the SDK", "Using the REST API", and "Using the CLI" sections. ## The index and the full file [/llms.txt](https://mynth.io/llms.txt) lists every page in reading order, grouped by section, with its markdown URL and a one-line description. It ends with links to the changelog, the model catalog, and the OpenAPI document. [/llms-full.txt](https://mynth.io/llms-full.txt) is every page in the same order, separated by `---`. It is large. Prefer the index plus the pages you need. ## With the CLI ```bash npx @mynthio/cli docs list npx @mynthio/cli docs get getting-started npx @mynthio/cli docs get api-reference/webhook-payloads --json ``` `docs list` prints the index and `docs get ` prints one page. The slug is the URL path after `/docs/`, without `.md`, and `index` is the introduction. `--json` wraps the output: `{ path, content }` for `get` and `{ content }` for `list`. The CLI rejects an empty path, a `.md` suffix, a URL, a query, a fragment, a backslash, and `..` segments. `MYNTH_DOCS_URL` points it at another host. ## Prices and models change Pages are cached for up to an hour. Read prices and the model list live from `GET https://api.mynth.io/models` or [/models.json](https://mynth.io/models.json), and price a real request with an [estimate](https://mynth.io/docs/guides/estimate-cost.md), rather than copying numbers out of a page. ## Next steps - [Agent prompt](https://mynth.io/docs/sdks/agents/agent-prompt.md): a prompt that points an agent at these docs. - [Skills](https://mynth.io/docs/sdks/agents/skills.md): the Mynth skill for coding agents. --- # Skills > The Mynth agent skill, a SKILL.md with task-specific references, from the oss repository. The Mynth skill is the `skills/mynth` directory in the [oss repository](https://github.com/mynthio/oss/tree/main/skills/mynth). `SKILL.md` is what the agent reads first. It picks a file from `references/` for the task at hand. ## Add it Copy the `skills/mynth` directory into the skills folder your agent reads, and keep `SKILL.md` and `references/` together. The repository has no install command. ## What it covers The skill covers image generation with `@mynthio/sdk` and the REST API, choosing a model, browser polling with a `pat_`, webhooks, the image tools, and destinations. It tells the agent to keep the API key on the server. | Task | Reference | | -------------------------------- | --------------------------------------- | | Survey a repo before migrating | `references/analyze-repo.md` | | Call the SDK | `references/sdk-usage.md` | | Call the REST API | `references/rest-api.md` | | `generateImage()` in TanStack AI | `references/tanstack-ai.md` | | Registered webhooks | `references/webhooks.md` | | Convex webhooks | `references/convex.md` | | Browser polling with a `pat_` | `references/public-access-tokens.md` | | Rate an image | `references/image-rating.md` | | Alt text | `references/image-alt.md` | | Remove a background | `references/image-remove-background.md` | | Upscale an image | `references/image-upscale.md` | | Deliver to your storage | `references/destinations.md` | ## How it picks a model The skill never hands the agent a model to use. It tells the agent to read the live catalog with `npx @mynthio/cli models list` or `GET /models`, filter by what the feature needs, and let you choose from two or three candidates. It tells the agent never to omit `model` or send `auto`, and to ask before saving the chosen id anywhere. The id in its code samples is marked as an example. Video, image review, and the TanStack Start helper have no reference file. `SKILL.md` points the agent at the markdown docs for those. These docs are the source of truth for paths and fields. See [llms.txt](https://mynth.io/docs/sdks/agents/llms-txt.md). ## Next steps - [Agent prompt](https://mynth.io/docs/sdks/agents/agent-prompt.md): a prompt and a rule list to give the agent. - [Webhooks](https://mynth.io/docs/concepts/webhooks.md): helpers for Next.js, TanStack Start, and Convex. --- # Agent prompt > A prompt to paste into a coding agent before it integrates Mynth, and the rules it should follow. Paste this into your coding agent before it writes an integration. It tells the agent to read these docs first, ask before it touches credentials, pass an explicit model, and not invent endpoints. > **Prompt to give a coding agent** > > Integrate Mynth into this project. Before you write code, run `npx @mynthio/cli docs list` and > `npx @mynthio/cli docs get getting-started`, and read the page for each call you are about to > make. Docs need no account. Follow the docs exactly: no invented endpoints or > fields, no model aliases. Pass an explicit model id such as `black-forest-labs/flux.2-pro`, never > `auto`. Keep the API key on the server. Check the status of every image in a completed task. > Before you create or store a credential, ask me which I want: I set `MYNTH_API_KEY` myself, or > you run `npx @mynthio/cli auth login` and `npx @mynthio/cli api-key create ` after I > approve the login. ## Rules for agents These are the mistakes agents make most often with Mynth. Each links to the page that explains it. | Rule | Why | | --------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | The base URL is `https://api.mynth.io` | [API reference](https://mynth.io/docs/api-reference.md) | | Pass an explicit model id from the catalog | [`auto` is experimental](https://mynth.io/docs/models.md#auto-is-experimental) | | Generation returns a task, not a file | [Tasks](https://mynth.io/docs/concepts/tasks.md) | | Check `status` on every item of a completed task | [Partial failure](https://mynth.io/docs/concepts/tasks.md#completed-does-not-mean-every-item-worked) | | Never send the `mak_` key to a browser. Browsers poll with the `pat_` | [Authentication](https://mynth.io/docs/authentication.md#browser-polling) | | Copy files you need for more than 7 days, or use a destination | [File lifetimes](https://mynth.io/docs/concepts/tasks.md#file-lifetimes) | | Verify webhooks against the raw body, and deduplicate on `webhook-id` | [Webhooks](https://mynth.io/docs/concepts/webhooks.md) | | Do not blindly retry a create. There are no idempotency keys | [What is safe to retry](https://mynth.io/docs/api-reference/errors.md#what-is-safe-to-retry) | | Read prices from the catalog or an estimate, never from these docs | [Pricing](https://mynth.io/docs/pricing.md) | | In TypeScript, use `@mynthio/sdk` and its webhook helpers | [TypeScript SDK](https://mynth.io/docs/sdks/typescript.md) | ## Next steps - [llms.txt](https://mynth.io/docs/sdks/agents/llms-txt.md): every way to read these docs as markdown. - [Skills](https://mynth.io/docs/sdks/agents/skills.md): the Mynth skill for coding agents. - [Getting started](https://mynth.io/docs/getting-started.md): a key and a first image.