# 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 { taskId, estimatedCost } 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": { "taskId": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimatedCost": "0.03", "access": { "publicAccessToken": "pat_eyJhbGciOiJIUzI1NiJ9..." } } } ``` 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`. There is no `/v1` prefix. 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-key \ -X POST \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "my-app" }' ``` `POST /api-key` 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 `keyPreview`, 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": { "userId": "user_01JD8G3W1R5T6Y7U8I9O0P1Q2W", "auth": { "method": "api-key", "apiKey": { "id": "ak_01KE7XWWEQ4MCGWKBQKJ1G47RP", "name": "my-app", "keyPreview": "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` | `/webhook`, [`/destinations`](https://mynth.io/docs/concepts/destinations.md), `/balance`, `/me` | | `keys` | `/api-key`, `/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-key/{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-key` and `PUT /api-key/{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. Set it on the key's page in the dashboard, or send `spendingLimit` and `spendingLimitPeriod` to `PUT /api-key/{id}`. The create call does not take one. 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 `429 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`, and `POST /video/generate` return `data.access.publicAccessToken`, a `pat_` token for that one task. 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 `wbs_` 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.perImage.base` | One image at the model's normal size | | `pricing.perImage["4k"]` | One image at a `_4k` size. Absent when the model has none | | `pricing.perInput` | Each input image, charged per output. Absent means $0 | | `pricing.perSecond` | 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 = perImage[scale] + perInput × 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 `perImage.base` at $0.03 and `perInput` 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 a 4k price fails the task with `CAPABILITY_NOT_SUPPORTED`, 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 = (perSecond[resolution] + audio.perSecond) × duration ``` An omitted `resolution`, `duration`, or `audio` uses the model's default, and the estimate includes it. `audio.perSecond` applies only when the catalog lists it. The video models available today include audio in `perSecond`. ## 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 | | `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 estimatedCost 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 `estimatedCost`. - If `available` is below the hold, the create fails with `422 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 `429 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. --- # 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/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": { "taskId": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimatedCost": "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 { taskId, estimatedCost } 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.rate`, `image.alt`, `image.review`) have no items. When their work fails, the task is `failed`. > **Warning** > > In the SDK, `urls` and `getImages()` skip failed images without saying so. If a partial result is > not acceptable, compare `getImages().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.getImages(); console.log(image?.url); // your storage, or null if the upload failed console.log(image?.mynth_url); // Mynth's copy, always set ``` ## Rules - `destination` takes the destination's **name**, not its `dst_` id. - Only `POST /image/generate` and `POST /image/remove-background` accept it. `POST /video/generate` drops the field without an 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 DESTINATIONS_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` | On `image.remove_background`, 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 > `getImages()` when a destination is in use. ## Errors | Status | Code | When | | ------ | ------------------------------- | ------------------------------------------------------------ | | 400 | `VALIDATION_ERROR` | A generation request named a destination you don't own | | 400 | `DESTINATIONS_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 | `UNKNOWN_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 the signature, 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, 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 X-Mynth-Delivery. > 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: X-Mynth-Event, X-Mynth-Delivery, X-Mynth-Signature body: { event, task: { id }, request, result | errors } │ ├─ 2xx within 30 s ──▶ done └─ anything else ────▶ retried: 5 s, 10 s, 20 s … up to 1 h apart, 12 attempts same X-Mynth-Delivery on every attempt ``` 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/webhook \ -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 response carries a `wbs_...` signing secret. Store it where your app reads environment variables: ```bash MYNTH_WEBHOOK_SECRET=wbs_... ``` 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 /webhook/{id}` replaces the whole configuration, so send `url`, `events`, and `enabled` every time. An omitted `oauthEnabled` becomes `false`, and an omitted `apiKeyIds` becomes every key. `npx @mynthio/cli webhook update` behaves the same way. `DELETE /webhook/{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, so subscribe only to what you handle. Every helper takes the handlers below, image and video alike. | 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` | | `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 (payload) => { for (const image of payload.result.images) { if (image.status === "success") console.log(payload.task.id, image.url ?? image.mynth_url); } }, imageTaskFailed: async (payload) => { console.error(payload.task.id, payload.errors); }, }); ``` The handler reads the secret from `MYNTH_WEBHOOK_SECRET` on each request, or from `{ webhookSecret }` as the second argument. It rejects a bad or missing signature, a timestamp more than five minutes off, a missing `X-Mynth-Delivery`, and an `X-Mynth-Event` header that does not match the body, all with `400`. ### 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 (payload) => { console.log(payload.task.id, payload.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 (payload, { context }) => { await context.runMutation(internal.images.save, { taskId: payload.task.id, images: payload.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 `X-Mynth-Signature` is `t=,v1=`. `v1` is the hex HMAC-SHA256 of `.`, keyed with the whole `wbs_...` secret. ```text raw_body = the request body as bytes, before any JSON parsing t, v1 = parse X-Mynth-Signature reject unless |now_unix − t| <= 300 expected = hex(HMAC-SHA256(key = secret, message = t + "." + raw_body)) reject unless expected equals v1, compared in constant time payload = JSON.parse(raw_body) handle payload.event, return 2xx ``` In Node.js: ```js import http from "node:http"; import crypto from "node:crypto"; const SECRET = process.env.MYNTH_WEBHOOK_SECRET; if (!SECRET) throw new Error("MYNTH_WEBHOOK_SECRET is required"); function verify(header, body) { const { t, v1 } = Object.fromEntries(header.split(",").map((part) => part.split("=", 2))); if (!t || !v1) return false; if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > 300) return false; const expected = crypto.createHmac("sha256", SECRET).update(`${t}.`).update(body).digest("hex"); return ( v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) ); } http .createServer((req, res) => { const chunks = []; req.on("data", (chunk) => chunks.push(chunk)); req.on("end", () => { const body = Buffer.concat(chunks); if (!verify(String(req.headers["x-mynth-signature"] ?? ""), body)) { res.writeHead(400).end("Bad signature"); return; } const payload = JSON.parse(body.toString("utf8")); res.writeHead(200).end("OK"); // do the slow work after answering console.log(payload.event, payload.task.id); }); }) .listen(3000); ``` > **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`, or `video.generate` | | `task.completed`, `task.failed` | Any task type settling that way | | `all` | Every event, including future task types | The delivered event 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 | `apiKeyIds` (CLI: repeat `--api-key-id`) limits it to those keys | | Tasks with no API key, such as playground runs | off | `oauthEnabled: true` (CLI: `--oauth-events`) | 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 | 12 attempts, 5 s apart at first, doubling up to 1 h apart. About 2.5 h in total | | Duplicates | Possible. Every attempt repeats the same `X-Mynth-Delivery`, 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 `X-Mynth-Delivery` and skip a value you have already handled. The SDK helpers pass it to every handler as `deliveryId`. Answer first and do slow work afterwards, because a handler that runs past 30 seconds counts as a failure and is retried. ## Per-request webhooks `POST /image/generate`, `POST /image/remove-background`, 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": { "dashboard": false, "custom": [{ "url": "https://your-app.com/hooks/one-off/9f2c1e" }] } } ``` `custom` takes up to five URLs. `dashboard: false` skips your registered endpoints for this task. Rate, alt text, and review requests do not take `webhook`. > **Warning** > > Custom URLs get no `X-Mynth-Signature`, and the SDK helpers reject unsigned deliveries. Anyone who > learns the URL can POST to it, so treat the payload as unverified, or put a hard-to-guess token in > the URL path. The task record and other payloads show only the first 12 characters of that path. ## 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 > The 429 you can hit is a key's spending limit. Provider rate limits are retried for you and surface on the task, not as an HTTP error. 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 A request refused before a task exists answers `429`. The `429` you can hit through your own settings is `SPENDING_LIMIT_EXCEEDED`, the [spending limit](https://mynth.io/docs/authentication.md#spending-limits) on a key. It clears when the key's day, week, or month rolls over, not on a retry. A key without a limit never returns it. Capacity limits can also apply to protect the service. Do not build code that assumes a key can send without limit. If you get a `429` with a code other than `SPENDING_LIMIT_EXCEEDED`, slow down and retry with backoff. There is no `Retry-After` header. The CLI exits with code `6` for such a `429`. ## 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 fails schema validation with a `400`. See [errors](https://mynth.io/docs/api-reference/errors.md#schema-rejections-use-a-different-shape). `/image/rate`, `/image/alt`, `/image/review`, and `/image/remove-background` 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", "displayName": "FLUX.2 Pro", "type": "image", "modes": { "txt->img": {}, "img->img": { "inputs": { "rules": [{ "type": "image", "max": 3 }] } } }, "pricing": { "perImage": { "base": "0.03" }, "perInput": "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. `maxTotal` 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 `estimateKind: "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 model without 4k | 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 | | `resolution`, `duration`, or `audio` a video model rejects | `400 VALIDATION_ERROR` at create | | A field that is not in the schema | Dropped, no error | 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.perImage["4k"]`, or `4k` in `supports` | The task fails on a `_4k` size | | What does it cost? | `pricing.perImage.base`, plus `perInput` | 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.perSecond` on `GET https://api.mynth.io/models` are the live source for inputs and resolution tiers. > **Warning** > > `resolution`, `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": { "taskId": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimatedCost": "0.03", "access": { "publicAccessToken": "pat_eyJhbGciOiJIUzI1NiJ9..." } } } ``` 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` | 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 | | `access` | token issued | `{ "pat": { "enabled": false } }` skips the browser token | Fields the schema does not know are dropped without an error. If a setting seems to have no effect, check the task's `request`, which shows the body as it was accepted. ## 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", "aspectRatio": "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 `perImage["4k"]` and needs a model that has it. On a model without 4k the task fails with `CAPABILITY_NOT_SUPPORTED`. ## 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 `estimatedCost`, 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, resolution: "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,"resolution":"720p"}' ``` The REST call returns a `taskId` and `estimatedCost` 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 | | `resolution` | `480p`, `720p`, `1080p`, or `4k`. Defaults to the model's default | | `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 | | `access` | `{ "pat": { "enabled": false } }` skips the browser token | Video does not take `destination`, `size`, `count`, `magic_prompt`, or `output`. `destination` is dropped without an error, so a video request that names one still writes nothing to your storage. Every value is checked against the model when the task is created. A resolution, duration, audio setting, or input the model does not serve answers `400 VALIDATION_ERROR`, and nothing is queued. ## 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", "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 `perSecond[resolution] × duration`, 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 `perInput` 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 `perInput` 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.result?.magic_prompt?.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. ## 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 | | `access` | `{ "pat": { "enabled": false } }` skips the browser token | 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. --- # 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": { "estimatedCost": "0.06", "currency": "usd", "estimateKind": "exact" } } ``` | Field | Meaning | | --------------- | ------------------------------------------------------------------------------------------ | | `estimatedCost` | USD as a decimal string. The amount a real request would hold | | `estimateKind` | `exact` for an explicit model. `upper_bound` for image `auto`, which holds $0.20 per image | Keep `estimatedCost` 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`, and `POST /video/generate` return `data.access.publicAccessToken`, a `pat_` token for that one task. 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", }); 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)); } } ``` - Treat `access` as optional. If Mynth could not sign a token, the task is still created and `access` is missing. Fall back to polling 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. - Send `"access": { "pat": { "enabled": false } }` on the create request to skip issuing a token. > **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`, with no version prefix. 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 are not: they carry `code` and usually `message` at the top level. See [errors](https://mynth.io/docs/api-reference/errors.md). - **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 dropped**, not rejected. Check the task's echoed `request` when a setting seems to have no effect. - **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, 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) ### 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 /webhook` [Create webhook](https://mynth.io/docs/api-reference/endpoints/webhooks/create.md) - `PUT /webhook/{id}` [Update webhook](https://mynth.io/docs/api-reference/endpoints/webhooks/update.md) - `DELETE /webhook/{id}` [Delete webhook](https://mynth.io/docs/api-reference/endpoints/webhooks/delete.md) ### API keys - `POST /api-key` [Create API key](https://mynth.io/docs/api-reference/endpoints/api-keys/create.md) - `GET /api-key` [List API keys](https://mynth.io/docs/api-reference/endpoints/api-keys/list.md) - `PUT /api-key/{id}` [Update API key](https://mynth.io/docs/api-reference/endpoints/api-keys/update.md) - `DELETE /api-key/{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`. 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-key` | Until you delete it | Every endpoint its scopes allow | | Public access token | `pat_` + a signed JWT | Mynth, in a task's create response | 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 | | `/webhook`, `/destinations`, `GET /balance` | `manage` | No | | `/api-key` | `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 { "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 `scopes.required` rather than parsing `message`. Scopes can be changed on an existing key in the dashboard, or with `PUT /api-key/{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`, and `POST /video/generate` return a token for the task they create: ```json { "data": { "taskId": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimatedCost": "0.03", "access": { "publicAccessToken": "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. - Send `"access": { "pat": { "enabled": false } }` in the create body to skip it. - Treat `data.access` as optional even when enabled. If signing fails, the task is still created and the field is missing. - `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 | | 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 | `taskId`, `estimatedCost`, sometimes `access`. 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 | `task.id`, `request`, and `result` or `errors`. 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": { "orderId": "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", "userId": "user_01JD8G3W1R5T6Y7U8I9O0P1Q2W", "apiKeyId": "ak_01KE7XWWEQ4MCGWKBQKJ1G47RP", "createdAt": "2026-07-04T10:00:00.000Z", "updatedAt": "2026-07-04T10:00:12.000Z" } } ``` | Field | Type | Notes | | ----------- | ------------------------------------ | ------------------------------------------------------------------------------------- | | `id` | string | `tsk_` + a ULID. Sorts by creation time | | `type` | string | One of the six [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` | | `userId` | string | The account that owns the task | | `apiKeyId` | string \| `null` | The key that created it. `null` for dashboard and playground tasks | | `createdAt` | ISO 8601 string | UTC | | `updatedAt` | 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` | A `completed` generation can still hold failed items. Check each entry in `result.images` or `result.videos`. [Errors](https://mynth.io/docs/api-reference/errors.md#task-failures) lists the codes. Money is always a decimal string: `cost`, `estimatedCost`, 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`. - Fields the schema does not know are gone. If a setting seems ignored, look here. - `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 shortened to the scheme, host, and first 12 characters of the path, 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.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.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", "audio": true } ] } ``` | Field | Type | Notes | | ------------ | ----------------------------------- | --------------------------------------- | | `id` | string | `vid_` + 32 random characters | | `url` | string | Never `null`. Video has no destinations | | `mynth_url` | string | The same as `url` today | | `cost` | decimal string | What this video cost | | `duration` | number | Seconds | | `resolution` | `480p` \| `720p` \| `1080p` \| `4k` | | | `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": { "taskId": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "estimatedCost": "0.03", "access": { "publicAccessToken": "pat_eyJhbGciOiJIUzI1NiJ9..." } } } ``` | Field | Notes | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `taskId` | Pass it to every task endpoint | | `estimatedCost` | USD held on your balance until the task settles. The final `cost` is normally equal or lower | | `access.publicAccessToken?` | Image generate, remove background, and video generate only. [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", "createdAt": "2026-07-04T10:00:00.000Z", "updatedAt": "2026-07-04T10:00:12.000Z" } ] } ``` There is no `hasMore` 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. Headers, signature, event names, and the body for each task type. 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). ```text POST https://your-app.com/api/mynth-webhook Content-Type: application/json User-Agent: Mynth-Webhook/1.0 (+https://mynth.io) X-Mynth-Event: task.image.generate.completed X-Mynth-Delivery: tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP:dashboard:wbh_01KE7Y0C3D7XH2M9QK4VZ8TBN5:task.image.generate.completed X-Mynth-Signature: t=1783159212,v1=5f0c6e... {"event":"task.image.generate.completed","task":{"id":"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"},"request":{...},"result":{...}} ``` ## Headers | Header | Value | | ------------------- | -------------------------------------------------------------------------------------------------- | | `Content-Type` | `application/json` | | `User-Agent` | `Mynth-Webhook/1.0 (+https://mynth.io)` | | `X-Mynth-Event` | The event name. Always equal to `event` in the body | | `X-Mynth-Delivery` | A stable ID for this task, endpoint and event. Retries repeat it | | `X-Mynth-Signature` | `t=,v1=`. Only on endpoints registered in the dashboard or with `POST /webhook` | ### `X-Mynth-Delivery` The value has four parts separated by `:`: ```text ::: ``` The endpoint key is the `wbh_` id for a registered endpoint, or the first 16 hex characters of the URL's SHA-256 for a [per-request URL](https://mynth.io/docs/concepts/webhooks.md#per-request-webhooks). Every retry of one delivery sends the same value. Store it and skip any value you have already handled. Treat it as an opaque string and do not parse it. ## Signature A registered endpoint gets a `wbs_` secret when it is created. Every delivery to it is signed with that secret: ```text X-Mynth-Signature: t=,v1= v1 = hex( HMAC-SHA256( key = your full wbs_... secret, message = "." + raw request body ) ) ``` - The key is the whole secret string, `wbs_` prefix included. - The message is `t`, a literal `.`, 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 fresh `t`. A five-minute tolerance on `t` rejects replays and still accepts late retries. - Compare in constant time. Mynth sends one `v1` today. If the header ever carries several, accept the delivery when any of them matches, as the SDK does. Per-request `webhook.custom` URLs get no signature. Anyone who knows the URL can post to it. The SDK helpers verify all of this for you. Node.js code and a language-neutral version are on [webhooks](https://mynth.io/docs/concepts/webhooks.md#any-other-stack). ## Events The delivered event 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.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` | An endpoint can also subscribe to `task.completed`, `task.failed` or `all`. Those decide what reaches the endpoint, not what it 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 still gets one delivery. Branch on `event`, or on `X-Mynth-Event` if you want to route before reading the body. `all` also covers task types added later, so handle unknown events by answering `2xx` and ignoring them. ## Body ```json { "event": "task.image.alt.completed", "task": { "id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP" }, "request": { "url": "https://assets.example.com/lighthouse.webp" }, "result": { "alt": "A white lighthouse on dark rocks, lit by an orange sunset sky" } } ``` | Field | Type | Present | Notes | | --------- | ---------------------- | ------------ | --------------------------------------------------------------------------------------- | | `event` | string | Always | See [events](#events) | | `task.id` | string | Always | The `tsk_` ID. The only field under `task` | | `request` | object | Always | The task's `request`, defaults filled in, `metadata` unchanged when the type takes it | | `result` | object | `.completed` | Same shape as `result` on [the task object](https://mynth.io/docs/api-reference/task-object.md#task-types) | | `errors` | `[{ code, message? }]` | `.failed` | At least one entry. Codes are on [errors](https://mynth.io/docs/api-reference/errors.md#task-failures) | A completed payload has no `errors` key and a failed one has no `result` key. The key is missing, not `null`. The body has no `status`, `cost`, `apiKeyId`, or timestamps. If you need them, call [`GET /tasks/{id}`](https://mynth.io/docs/api-reference/endpoints/tasks/get.md) with `task.id`. To match the task to your own records, read `request.metadata`. You set it when you created the task, and it comes back unchanged. Only image generate, remove background, and video generate take `metadata`. For rate, alt, and review, store the `taskId` from the create response instead. URLs in `request.webhook.custom` are shortened to scheme, host and the first 12 characters of the path, the same as on `GET /tasks/{id}`. A token in a per-request URL never reaches your other endpoints. ### Examples **Image, partial** ```json { "event": "task.image.generate.completed", "task": { "id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP" }, "request": { "model": "black-forest-labs/flux.2-pro", "prompt": "A lighthouse at dusk, film grain", "count": 2 }, "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." } } ] } } ``` **Image, failed** ```json { "event": "task.image.generate.failed", "task": { "id": "tsk_01KE7XZ8B4N3QR5SJVDVAW6TME" }, "request": { "model": "black-forest-labs/flux.2-pro", "prompt": "A lighthouse at dusk, film grain", "count": 1, "size": "16:9_4k" }, "errors": [ { "code": "CAPABILITY_NOT_SUPPORTED", "message": "The request uses options that are not supported for this model." } ] } ``` **Remove background** ```json { "event": "task.image.remove_background.completed", "task": { "id": "tsk_01KE7Y1R6WQ2J9TM4BXC8ZKD3F" }, "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" } } } } ``` **Rate** ```json { "event": "task.image.rate.completed", "task": { "id": "tsk_01KE7Y3B0GZC5V7NHXA2RMQ8TP" }, "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" } } ``` **Review** ```json { "event": "task.image.review.failed", "task": { "id": "tsk_01KE7Y4D8KM1P6WRZ3QXB9TJ2C" }, "request": { "url": "https://assets.example.com/gone.webp", "effort": "high" }, "errors": [{ "code": "FETCH_FAILED", "message": "The image could not be fetched." }] } ``` **Video** ```json { "event": "task.video.generate.completed", "task": { "id": "tsk_01KE7Y5T2HB9R4XQN7CZW3KM6D" }, "request": { "model": "bytedance/seedance-2.0-mini", "prompt": "A cat surfing a wave at sunset", "duration": 5, "resolution": "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", "audio": true } ] } } ``` > **Warning** > > `task.image.generate.completed` and `task.video.generate.completed` do not mean the media exists. > Walk `result.images` or `result.videos` and check each `status`. A completed task can hold only > failed items. ## TypeScript types The SDK exports the whole payload as a union discriminated on `event`: ```ts import type { MynthSDKTypes } from "@mynthio/sdk"; function handle(payload: MynthSDKTypes.WebhookPayload) { switch (payload.event) { case "task.image.generate.completed": for (const image of payload.result.images) { if (image.status === "success") console.log(image.url ?? image.mynth_url); } break; case "task.image.generate.failed": console.error(payload.task.id, payload.errors); break; } } ``` Each variant also has its own name, such as `MynthSDKTypes.WebhookTaskImageAltCompletedPayload`. The framework helpers in `@mynthio/sdk/next`, `/tanstack-start` and `/convex` pass these types to your handlers already. ## Responding Answer any `2xx` within 30 seconds, then do the slow work. Mynth does not follow redirects and ignores the response body. Anything other than a `2xx` gets retried with the same `X-Mynth-Delivery`. The full schedule and ordering rules are 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 `result`. --- # 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 { "code": "UNAUTHORIZED", "message": "..." } │ nothing queued, held, or charged │ └─ 201 { taskId } ──▶ task runs ──┬─▶ completed items can still carry an error └─▶ failed errors: [{ code, message? }] ``` Handle both. ## Request errors The body is flat, with `code` at the top level in `SCREAMING_SNAKE_CASE`: ```json { "code": "INSUFFICIENT_BALANCE", "message": "Insufficient balance." } ``` Branch on `code`, never on `message`. `message` is optional: `TASK_NOT_FOUND`, `API_KEY_NOT_FOUND`, `INVALID_TOKEN`, and `TOKEN_EXPIRED` have none. `INSUFFICIENT_SCOPE` and `SCOPE_ESCALATION` add a `scopes` object. | Status | Code | Means | Do | | ------ | ------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------- | | 400 | `VALIDATION_ERROR` | A check that runs after the schema passed. See below | Fix what `message` names | | 400 | `API_KEY_LIMIT_REACHED` | The account already has 100 live keys | Delete a key first | | 400 | `WEBHOOK_API_KEY_NOT_FOUND` | `apiKeyIds` names a key you don't have | Drop the unknown id | | 400 | `DESTINATIONS_INVALID_PROVIDER` | A destination update changed `provider.id` | Create a new destination | | 401 | `UNAUTHORIZED` | No usable bearer header, or an unknown or deleted key | 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 | | 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 path | Check the URL. There is no `/v1` prefix | | 409 | `DESTINATION_NAME_TAKEN` | You already have a destination with that name | Pick another name | | 413 | `VALIDATION_ERROR` | `POST /image/upload` body over 100 MB | Upload fewer or smaller files | | 422 | `INSUFFICIENT_BALANCE` | Available balance is below the task's estimate | Top up. Nothing was queued | | 429 | `SPENDING_LIMIT_EXCEEDED` | The key's cap for the current day, week, or month is used up | Wait for the period, or raise the cap | | 500 | `INTERNAL_SERVER_ERROR` | Our fault | Retry, after checking `GET /tasks` | | 500 | `UNKNOWN_ERROR` | Storing or reading a destination secret failed | Retry | | 502 | `DESTINATION_TEST_FAILED` | Your storage rejected the test upload. `message` is the provider's | Fix the credentials or path | `VALIDATION_ERROR` covers the checks that run after the schema passed: - `metadata` over 2048 bytes - a `destination` name you don't own - `inputs` or options the chosen model cannot take - 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 A `429` other than `SPENDING_LIMIT_EXCEEDED` means back off and retry. There is no `Retry-After` header. [Rate limits](https://mynth.io/docs/concepts/rate-limits.md) has the rest. ### Schema rejections use a different shape A body that fails the schema never reaches the handler, so it gets no `code`. The validator answers `400` with its own shape, which depends on the endpoint: | Endpoints | `400` body | | ------------------------------------------------- | -------------------------------------------------------------- | | `/image/*`, `/video/*` | `{ "data": , "error": [issues], "success": false }` | | `/tasks`, `/api-key`, `/webhook`, `/destinations` | `{ "success": false, "errors": [issues] }` | | Any body that is not valid JSON | `Malformed JSON in request body` as plain text | ```json { "data": { "model": "auto", "count": 1 }, "error": [ { "path": ["prompt"], "code": "required", "expected": "a string", "actual": "missing", "message": "prompt must be a string (was missing)" } ], "success": false } ``` Read `path` and `message` on each issue. `count` outside 1 to 20, an unknown model id, and a missing `prompt` all fail here. `data` echoes the request body, so keep these responses out of logs you share. > **Warning** > > Generation bodies drop fields they do not recognize instead of rejecting them. `{"promt": "..."}` > fails on the missing `prompt`, but `{"prompt": "...", "widht": 512}` succeeds and ignores > `widht`. Check the task's echoed `request` when a setting seems to have no effect. ## 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.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 (a `_4k` size on a model without 4k fails with `CAPABILITY_NOT_SUPPORTED`), queueing the task, or the daily sweep that expires a task stuck on `pending`. ### 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 | | `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 `422` fail the same way every time. Fix the request. - `429 SPENDING_LIMIT_EXCEEDED` clears when the key's period rolls over. - `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. - 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` and `code`, and `TaskAsync*` errors while waiting on a task. [SDK errors](https://mynth.io/docs/sdks/typescript/errors.md) covers both, including why a schema rejection leaves `code` undefined. The CLI prints the message to stderr and exits with a code scripts can branch on: `2` usage and `VALIDATION_ERROR`, `3` auth, `4` balance or spending limit, `5` `RESTRICTED_CONTENT`, `6` another `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 - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"Cute Cat"`. - `model` (string) — Default `"auto"`. One of: `"alibaba/qwen-image-2.0"`, `"alibaba/qwen-image-2.0-pro"`, `"alibaba/qwen-image-3.0"`, `"alibaba/qwen-image-3.0-pro"`, `"auto"`, `"black-forest-labs/flux-1-schnell"`, `"black-forest-labs/flux.1-dev"`, `"black-forest-labs/flux.2-dev"`, `"black-forest-labs/flux.2-flex"`, `"black-forest-labs/flux.2-klein-4b"`, `"black-forest-labs/flux.2-max"`, `"black-forest-labs/flux.2-pro"`, `"bria/fibo-edit-1.5"`, `"bria/fibo-generate-1.5"`, `"bytedance/seedream-5.0-lite"`, `"bytedance/seedream-pro"`, `"circlestone-labs/anima"`, `"goofy-ai/prefect-pony-xl-lora"`, `"google/gemini-3-pro-image-preview"`, `"google/gemini-3.1-flash-image"`, `"google/gemini-3.1-flash-lite-image"`, `"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-large"`, `"krea/krea-2-medium"`, `"krea/krea-2-turbo"`, `"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-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"`. Example `"black-forest-labs/flux.2-pro"`. - `count` (number) — Default `1`. ≥ 1, ≤ 20. - `access` (object) — Controls whether the create-task response should include a short-lived Public Access Token. That token can be passed to browser or client-side code for polling task status and task images without exposing your API key. - `pat` (required, object) - `enabled` (boolean) — Default `true`. - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `inputs` ((string | object)[]) — ≤ 20 items. - **source + type** - `source` (required, object) - `type` (required, "url") - `url` (required, string) - `type` (required, "image") - `as` (string) — One of: `"auto"`, `"reference"`, `"source"`. - `magic_prompt` (boolean) - `metadata` (object) - `negative_prompt` (string) — ≤ 8192 characters. - `output` (object) - `format` (string) — One of: `"jpg"`, `"png"`, `"webp"`. - `rating` (object | boolean) — One of: `true`. - **nsfw_sfw** - `mode` ("nsfw_sfw") — Default `"nsfw_sfw"`. - **custom** - `levels` (required, object[]) — ≥ 2 items, ≤ 7 items. - `description` (required, string) — ≥ 1 characters, ≤ 150 characters. - `value` (required, string) — ≥ 1 characters, ≤ 24 characters. - `mode` (required, "custom") - `size` (object | string) — One of: `"16:9"`, `"16:9_4k"`, `"1:1"`, `"1:1_4k"`, `"1:2"`, `"1:2_4k"`, `"21:9"`, `"21:9_4k"`, `"2:1"`, `"2:1_4k"`, `"2:3"`, `"2:3_4k"`, `"3:2"`, `"3:2_4k"`, `"3:4"`, `"3:4_4k"`, `"4:3"`, `"4:3_4k"`, `"4:5"`, `"4:5_4k"`, `"5:4"`, `"5:4_4k"`, `"9:16"`, `"9:16_4k"`, `"auto"`, `"landscape"`, `"landscape_wide"`, `"portrait"`, `"portrait_tall"`, `"square"`. - **aspect_ratio** - `aspectRatio` (required, string) — One of: `"16:9"`, `"1:1"`, `"1:2"`, `"21:9"`, `"2:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`. - `type` (required, "aspect_ratio") - `scale` (string) — Default `"base"`. One of: `"4k"`, `"base"`. - **auto** - `type` (required, "auto") - `webhook` (object) - `custom` (object[]) — ≥ 1 items, ≤ 5 items. - `url` (required, string) - `dashboard` (boolean) — Set to false to disable dashboard-managed webhooks for this task. Request-level custom webhooks are still sent. ## Response ### 201 — Image generation task created - `data` (required, object) - `estimatedCost` (required, string) — Estimated cost in USD reserved for this task. Failed images are refunded, so the final cost may be lower. Example `"0.03"`. - `taskId` (required, string) — Task ID Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. - `access` (object) — Returned when `access.pat.enabled` is true. - `publicAccessToken` (required, string) — Short-lived Public Access Token for polling task status and image results. This token is safe to return to frontend code and can be used instead of your API key for polling public task state. Example `"pat_eyJhbGciOi..."`. ```json { "data": { "estimatedCost": "0.03", "taskId": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "access": { "publicAccessToken": "pat_eyJhbGciOi..." } } } ``` ### 400 — Validation Error - `success` (required, false) - `error` (required, array) - `data` (required, any) ```json { "success": false, "error": [] } ``` ## 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 '{ "prompt": "Cute Cat", "model": "black-forest-labs/flux.2-pro", "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({ "prompt": "Cute Cat", "model": "black-forest-labs/flux.2-pro", "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 - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"Cute Cat"`. - `model` (string) — Default `"auto"`. One of: `"alibaba/qwen-image-2.0"`, `"alibaba/qwen-image-2.0-pro"`, `"alibaba/qwen-image-3.0"`, `"alibaba/qwen-image-3.0-pro"`, `"auto"`, `"black-forest-labs/flux-1-schnell"`, `"black-forest-labs/flux.1-dev"`, `"black-forest-labs/flux.2-dev"`, `"black-forest-labs/flux.2-flex"`, `"black-forest-labs/flux.2-klein-4b"`, `"black-forest-labs/flux.2-max"`, `"black-forest-labs/flux.2-pro"`, `"bria/fibo-edit-1.5"`, `"bria/fibo-generate-1.5"`, `"bytedance/seedream-5.0-lite"`, `"bytedance/seedream-pro"`, `"circlestone-labs/anima"`, `"goofy-ai/prefect-pony-xl-lora"`, `"google/gemini-3-pro-image-preview"`, `"google/gemini-3.1-flash-image"`, `"google/gemini-3.1-flash-lite-image"`, `"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-large"`, `"krea/krea-2-medium"`, `"krea/krea-2-turbo"`, `"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-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"`. Example `"black-forest-labs/flux.2-pro"`. - `count` (number) — Default `1`. ≥ 1, ≤ 20. - `access` (object) — Controls whether the create-task response should include a short-lived Public Access Token. That token can be passed to browser or client-side code for polling task status and task images without exposing your API key. - `pat` (required, object) - `enabled` (boolean) — Default `true`. - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `inputs` ((string | object)[]) — ≤ 20 items. - **source + type** - `source` (required, object) - `type` (required, "url") - `url` (required, string) - `type` (required, "image") - `as` (string) — One of: `"auto"`, `"reference"`, `"source"`. - `magic_prompt` (boolean) - `metadata` (object) - `negative_prompt` (string) — ≤ 8192 characters. - `output` (object) - `format` (string) — One of: `"jpg"`, `"png"`, `"webp"`. - `rating` (object | boolean) — One of: `true`. - **nsfw_sfw** - `mode` ("nsfw_sfw") — Default `"nsfw_sfw"`. - **custom** - `levels` (required, object[]) — ≥ 2 items, ≤ 7 items. - `description` (required, string) — ≥ 1 characters, ≤ 150 characters. - `value` (required, string) — ≥ 1 characters, ≤ 24 characters. - `mode` (required, "custom") - `size` (object | string) — One of: `"16:9"`, `"16:9_4k"`, `"1:1"`, `"1:1_4k"`, `"1:2"`, `"1:2_4k"`, `"21:9"`, `"21:9_4k"`, `"2:1"`, `"2:1_4k"`, `"2:3"`, `"2:3_4k"`, `"3:2"`, `"3:2_4k"`, `"3:4"`, `"3:4_4k"`, `"4:3"`, `"4:3_4k"`, `"4:5"`, `"4:5_4k"`, `"5:4"`, `"5:4_4k"`, `"9:16"`, `"9:16_4k"`, `"auto"`, `"landscape"`, `"landscape_wide"`, `"portrait"`, `"portrait_tall"`, `"square"`. - **aspect_ratio** - `aspectRatio` (required, string) — One of: `"16:9"`, `"1:1"`, `"1:2"`, `"21:9"`, `"2:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`. - `type` (required, "aspect_ratio") - `scale` (string) — Default `"base"`. One of: `"4k"`, `"base"`. - **auto** - `type` (required, "auto") - `webhook` (object) - `custom` (object[]) — ≥ 1 items, ≤ 5 items. - `url` (required, string) - `dashboard` (boolean) — Set to false to disable dashboard-managed webhooks for this task. Request-level custom webhooks are still sent. ## Response ### 200 — Image generation request validated and cost estimated - `data` (required, object) - `currency` (required, "usd") - `estimateKind` (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"`. - `estimatedCost` (required, string) — Estimated cost in USD for the request. Nothing is generated or charged. Example `"0.03"`. ```json { "data": { "currency": "usd", "estimateKind": "exact", "estimatedCost": "0.03" } } ``` ### 400 — Validation Error - `success` (required, false) - `error` (required, array) - `data` (required, any) ```json { "success": false, "error": [] } ``` ## 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 '{ "prompt": "Cute Cat", "model": "black-forest-labs/flux.2-pro", "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({ "prompt": "Cute Cat", "model": "black-forest-labs/flux.2-pro", "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. 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) ## Response ### 201 — Image alt text task created - `data` (required, object) - `estimatedCost` (required, string) — Estimated cost in USD reserved for this task. Failed tasks are not charged. Example `"0.0002"`. - `taskId` (required, string) — Task ID Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ```json { "data": { "estimatedCost": "0.0002", "taskId": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP" } } ``` ### 400 — Validation Error - `success` (required, false) - `error` (required, array) - `data` (required, any) ```json { "success": false, "error": [] } ``` ## 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) - `effort` (string) — Reviewer panel to run. 'high' runs five strong vision models; 'low' runs three smaller ones for triage. Default `"high"`. One of: `"high"`, `"low"`. ## Response ### 201 — Image review task created - `data` (required, object) - `estimatedCost` (required, string) — Estimated cost in USD reserved for this task. Failed tasks are not charged. Example `"0.0002"`. - `taskId` (required, string) — Task ID Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ```json { "data": { "estimatedCost": "0.0002", "taskId": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP" } } ``` ### 400 — Validation Error - `success` (required, false) - `error` (required, array) - `data` (required, any) ```json { "success": false, "error": [] } ``` ## 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 ### custom - `levels` (required, object[]) — ≥ 2 items, ≤ 7 items. - `description` (required, string) — ≥ 1 characters, ≤ 150 characters. - `value` (required, string) — ≥ 1 characters, ≤ 24 characters. - `mode` (required, "custom") - `url` (required, string) ### nsfw_sfw - `url` (required, string) - `mode` ("nsfw_sfw") — Default `"nsfw_sfw"`. ## Response ### 201 — Image rating task created - `data` (required, object) - `estimatedCost` (required, string) — Estimated cost in USD reserved for this task. Failed tasks are not charged. Example `"0.0002"`. - `taskId` (required, string) — Task ID Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ```json { "data": { "estimatedCost": "0.0002", "taskId": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP" } } ``` ### 400 — Validation Error - `success` (required, false) - `error` (required, array) - `data` (required, any) ```json { "success": false, "error": [] } ``` ## 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 '{ "levels": [ { "description": "string", "value": "string" } ], "mode": "custom", "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({ "levels": [ { "description": "string", "value": "string" } ], "mode": "custom", "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) - `access` (object) — Controls whether the create-task response should include a short-lived Public Access Token. That token can be passed to browser or client-side code for polling task status and task images without exposing your API key. - `pat` (required, object) - `enabled` (boolean) — Default `true`. - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) - `output` (object) - `format` (string) — One of: `"png"`, `"webp"`. - `webhook` (object) - `custom` (object[]) — ≥ 1 items, ≤ 5 items. - `url` (required, string) - `dashboard` (boolean) — Set to false to disable dashboard-managed webhooks for this task. Request-level custom webhooks are still sent. ## Response ### 201 — Image background removal task created - `data` (required, object) - `estimatedCost` (required, string) — Estimated cost in USD reserved for this task. Failed images are refunded, so the final cost may be lower. Example `"0.03"`. - `taskId` (required, string) — Task ID Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. - `access` (object) — Returned when `access.pat.enabled` is true. - `publicAccessToken` (required, string) — Short-lived Public Access Token for polling task status and image results. This token is safe to return to frontend code and can be used instead of your API key for polling public task state. Example `"pat_eyJhbGciOi..."`. ```json { "data": { "estimatedCost": "0.03", "taskId": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "access": { "publicAccessToken": "pat_eyJhbGciOi..." } } } ``` ### 400 — Validation Error - `success` (required, false) - `error` (required, array) - `data` (required, any) ```json { "success": false, "error": [] } ``` ## 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). --- # 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"`. - `access` (object) — Controls whether the create-task response should include a short-lived Public Access Token. That token can be passed to browser or client-side code for polling task status and task videos without exposing your API key. - `pat` (required, object) - `enabled` (boolean) — Default `true`. - `audio` (boolean) — Enable model-native generated audio. Only supported by models with audio capability. - `duration` (number) — Video duration in seconds. Defaults to the model's default duration. ≥ 1, ≤ 60. - `inputs` ((string | object)[]) — ≤ 5 items. - **source + type** - `source` (required, object) - `type` (required, "url") - `url` (required, string) - `type` (required, "image") - `as` (string) — One of: `"auto"`, `"first_frame"`, `"last_frame"`, `"reference"`. - `metadata` (object) - `negative_prompt` (string) — ≤ 8192 characters. - `resolution` (string) — Resolution tier. Defaults to the model's default tier. One of: `"1080p"`, `"480p"`, `"4k"`, `"720p"`. - `webhook` (object) - `custom` (object[]) — ≥ 1 items, ≤ 5 items. - `url` (required, string) - `dashboard` (boolean) — Set to false to disable dashboard-managed webhooks for this task. Request-level custom webhooks are still sent. ## Response ### 201 — Video generation task created - `data` (required, object) - `estimatedCost` (required, string) — Estimated cost in USD reserved for this task. Failed videos are refunded, so the final cost may be lower. Example `"0.05"`. - `taskId` (required, string) — Task ID Example `"tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. - `access` (object) — Returned when `access.pat.enabled` is true. Currently the generate endpoint only returns a Public Access Token. - `publicAccessToken` (required, string) — Short-lived Public Access Token for polling task status and video results. This token is safe to return to frontend code and can be used instead of your API key for polling public task state. Example `"pat_eyJhbGciOi..."`. ```json { "data": { "estimatedCost": "0.05", "taskId": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "access": { "publicAccessToken": "pat_eyJhbGciOi..." } } } ``` ### 400 — Validation Error - `success` (required, false) - `error` (required, array) - `data` (required, any) ```json { "success": false, "error": [] } ``` ## 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"`. - `access` (object) — Controls whether the create-task response should include a short-lived Public Access Token. That token can be passed to browser or client-side code for polling task status and task videos without exposing your API key. - `pat` (required, object) - `enabled` (boolean) — Default `true`. - `audio` (boolean) — Enable model-native generated audio. Only supported by models with audio capability. - `duration` (number) — Video duration in seconds. Defaults to the model's default duration. ≥ 1, ≤ 60. - `inputs` ((string | object)[]) — ≤ 5 items. - **source + type** - `source` (required, object) - `type` (required, "url") - `url` (required, string) - `type` (required, "image") - `as` (string) — One of: `"auto"`, `"first_frame"`, `"last_frame"`, `"reference"`. - `metadata` (object) - `negative_prompt` (string) — ≤ 8192 characters. - `resolution` (string) — Resolution tier. Defaults to the model's default tier. One of: `"1080p"`, `"480p"`, `"4k"`, `"720p"`. - `webhook` (object) - `custom` (object[]) — ≥ 1 items, ≤ 5 items. - `url` (required, string) - `dashboard` (boolean) — Set to false to disable dashboard-managed webhooks for this task. Request-level custom webhooks are still sent. ## Response ### 200 — Video generation request validated and cost estimated - `data` (required, object) - `currency` (required, "usd") - `estimateKind` (required, "exact") — Video generation always requires a pinned model, so the estimate is exact. - `estimatedCost` (required, string) — Estimated cost in USD for the request. Nothing is generated or charged. Example `"0.05"`. ```json { "data": { "currency": "usd", "estimateKind": "exact", "estimatedCost": "0.05" } } ``` ### 400 — Validation Error - `success` (required, false) - `error` (required, array) - `data` (required, any) ```json { "success": false, "error": [] } ``` ## 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[]) - `cost` (required, string | null) - `createdAt` (required, any) - `id` (required, string) - `status` (required, string) — One of: `"completed"`, `"failed"`, `"pending"`. - `type` (required, string) — One of: `"image.alt"`, `"image.generate"`, `"image.rate"`, `"image.remove_background"`, `"image.review"`, `"video.generate"`. - `updatedAt` (required, any) ```json { "data": [ { "cost": "string", "id": "string", "status": "completed", "type": "image.alt" } ] } ``` ## 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.rate · completed** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, json | null) - `id` (required, string) - `request` (required, object) - **custom** - `levels` (required, object[]) — ≥ 2 items, ≤ 7 items. - … - `mode` (required, "custom") - `url` (required, string) - **nsfw_sfw** - `url` (required, string) - `mode` ("nsfw_sfw") — Default `"nsfw_sfw"`. - `result` (required, object) - `level` (required, string) — The rating level ≥ 1 characters, ≤ 24 characters. - `status` (required, "completed") - `type` (required, "image.rate") - `updatedAt` (required, any) - `userId` (required, string) - **image.rate · pending** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, json | null) - `id` (required, string) - `request` (required, object) - **custom** - `levels` (required, object[]) — ≥ 2 items, ≤ 7 items. - … - `mode` (required, "custom") - `url` (required, string) - **nsfw_sfw** - `url` (required, string) - `mode` ("nsfw_sfw") — Default `"nsfw_sfw"`. - `result` (required, null) - `status` (required, "pending") - `type` (required, "image.rate") - `updatedAt` (required, any) - `userId` (required, string) - **video.generate · completed** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, json | null) - `id` (required, string) - `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"`. - `audio` (boolean) — Enable model-native generated audio. Only supported by models with audio capability. - `duration` (number) — Video duration in seconds. Defaults to the model's default duration. ≥ 1, ≤ 60. - `inputs` ((string | object)[]) — ≤ 5 items. - … - `metadata` (object) - `negative_prompt` (string) — ≤ 8192 characters. - `resolution` (string) — Resolution tier. Defaults to the model's default tier. One of: `"1080p"`, `"480p"`, `"4k"`, `"720p"`. - `webhook` (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") - `type` (required, "video.generate") - `updatedAt` (required, any) - `userId` (required, string) - **video.generate · pending** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, json | null) - `id` (required, string) - `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"`. - `audio` (boolean) — Enable model-native generated audio. Only supported by models with audio capability. - `duration` (number) — Video duration in seconds. Defaults to the model's default duration. ≥ 1, ≤ 60. - `inputs` ((string | object)[]) — ≤ 5 items. - … - `metadata` (object) - `negative_prompt` (string) — ≤ 8192 characters. - `resolution` (string) — Resolution tier. Defaults to the model's default tier. One of: `"1080p"`, `"480p"`, `"4k"`, `"720p"`. - `webhook` (object) - … - `result` (required, null) - `status` (required, "pending") - `type` (required, "video.generate") - `updatedAt` (required, any) - `userId` (required, string) - **image.generate · completed** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, json | null) - `id` (required, string) - `request` (required, object) - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"Cute Cat"`. - `model` (string) — Default `"auto"`. One of: `"alibaba/qwen-image-2.0"`, `"alibaba/qwen-image-2.0-pro"`, `"alibaba/qwen-image-3.0"`, `"alibaba/qwen-image-3.0-pro"`, `"auto"`, `"black-forest-labs/flux-1-schnell"`, `"black-forest-labs/flux.1-dev"`, `"black-forest-labs/flux.2-dev"`, `"black-forest-labs/flux.2-flex"`, `"black-forest-labs/flux.2-klein-4b"`, `"black-forest-labs/flux.2-max"`, `"black-forest-labs/flux.2-pro"`, `"bria/fibo-edit-1.5"`, `"bria/fibo-generate-1.5"`, `"bytedance/seedream-5.0-lite"`, `"bytedance/seedream-pro"`, `"circlestone-labs/anima"`, `"goofy-ai/prefect-pony-xl-lora"`, `"google/gemini-3-pro-image-preview"`, `"google/gemini-3.1-flash-image"`, `"google/gemini-3.1-flash-lite-image"`, `"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-large"`, `"krea/krea-2-medium"`, `"krea/krea-2-turbo"`, `"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-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"`. Example `"black-forest-labs/flux.2-pro"`. - `count` (number) — Default `1`. ≥ 1, ≤ 20. - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `inputs` ((string | object)[]) — ≤ 20 items. - … - `magic_prompt` (boolean) - `metadata` (object) - `negative_prompt` (string) — ≤ 8192 characters. - `output` (object) - … - `rating` (object | boolean) — One of: `true`. - … - `size` (object | string) — One of: `"16:9"`, `"16:9_4k"`, `"1:1"`, `"1:1_4k"`, `"1:2"`, `"1:2_4k"`, `"21:9"`, `"21:9_4k"`, `"2:1"`, `"2:1_4k"`, `"2:3"`, `"2:3_4k"`, `"3:2"`, `"3:2_4k"`, `"3:4"`, `"3:4_4k"`, `"4:3"`, `"4:3_4k"`, `"4:5"`, `"4:5_4k"`, `"5:4"`, `"5:4_4k"`, `"9:16"`, `"9:16_4k"`, `"auto"`, `"landscape"`, `"landscape_wide"`, `"portrait"`, `"portrait_tall"`, `"square"`. - … - `webhook` (object) - … - `result` (required, object) - `images` (required, object[]) - … - `model` (required, string) — One of: `"alibaba/qwen-image-2.0"`, `"alibaba/qwen-image-2.0-pro"`, `"alibaba/qwen-image-3.0"`, `"alibaba/qwen-image-3.0-pro"`, `"black-forest-labs/flux-1-schnell"`, `"black-forest-labs/flux.1-dev"`, `"black-forest-labs/flux.2-dev"`, `"black-forest-labs/flux.2-flex"`, `"black-forest-labs/flux.2-klein-4b"`, `"black-forest-labs/flux.2-max"`, `"black-forest-labs/flux.2-pro"`, `"bria/fibo-edit-1.5"`, `"bria/fibo-generate-1.5"`, `"bytedance/seedream-5.0-lite"`, `"bytedance/seedream-pro"`, `"circlestone-labs/anima"`, `"goofy-ai/prefect-pony-xl-lora"`, `"google/gemini-3-pro-image-preview"`, `"google/gemini-3.1-flash-image"`, `"google/gemini-3.1-flash-lite-image"`, `"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-large"`, `"krea/krea-2-medium"`, `"krea/krea-2-turbo"`, `"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-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"`. - `magic_prompt` (object) - … - `status` (required, "completed") - `type` (required, "image.generate") - `updatedAt` (required, any) - `userId` (required, string) - **image.generate · pending** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, json | null) - `id` (required, string) - `request` (required, object) - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"Cute Cat"`. - `model` (string) — Default `"auto"`. One of: `"alibaba/qwen-image-2.0"`, `"alibaba/qwen-image-2.0-pro"`, `"alibaba/qwen-image-3.0"`, `"alibaba/qwen-image-3.0-pro"`, `"auto"`, `"black-forest-labs/flux-1-schnell"`, `"black-forest-labs/flux.1-dev"`, `"black-forest-labs/flux.2-dev"`, `"black-forest-labs/flux.2-flex"`, `"black-forest-labs/flux.2-klein-4b"`, `"black-forest-labs/flux.2-max"`, `"black-forest-labs/flux.2-pro"`, `"bria/fibo-edit-1.5"`, `"bria/fibo-generate-1.5"`, `"bytedance/seedream-5.0-lite"`, `"bytedance/seedream-pro"`, `"circlestone-labs/anima"`, `"goofy-ai/prefect-pony-xl-lora"`, `"google/gemini-3-pro-image-preview"`, `"google/gemini-3.1-flash-image"`, `"google/gemini-3.1-flash-lite-image"`, `"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-large"`, `"krea/krea-2-medium"`, `"krea/krea-2-turbo"`, `"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-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"`. Example `"black-forest-labs/flux.2-pro"`. - `count` (number) — Default `1`. ≥ 1, ≤ 20. - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `inputs` ((string | object)[]) — ≤ 20 items. - … - `magic_prompt` (boolean) - `metadata` (object) - `negative_prompt` (string) — ≤ 8192 characters. - `output` (object) - … - `rating` (object | boolean) — One of: `true`. - … - `size` (object | string) — One of: `"16:9"`, `"16:9_4k"`, `"1:1"`, `"1:1_4k"`, `"1:2"`, `"1:2_4k"`, `"21:9"`, `"21:9_4k"`, `"2:1"`, `"2:1_4k"`, `"2:3"`, `"2:3_4k"`, `"3:2"`, `"3:2_4k"`, `"3:4"`, `"3:4_4k"`, `"4:3"`, `"4:3_4k"`, `"4:5"`, `"4:5_4k"`, `"5:4"`, `"5:4_4k"`, `"9:16"`, `"9:16_4k"`, `"auto"`, `"landscape"`, `"landscape_wide"`, `"portrait"`, `"portrait_tall"`, `"square"`. - … - `webhook` (object) - … - `result` (required, null) - `status` (required, "pending") - `type` (required, "image.generate") - `updatedAt` (required, any) - `userId` (required, string) - **image.alt · completed** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, json | null) - `id` (required, string) - `request` (required, object) - `url` (required, string) - `result` (required, object) - `alt` (required, string) — ≥ 1 characters, ≤ 160 characters. - `status` (required, "completed") - `type` (required, "image.alt") - `updatedAt` (required, any) - `userId` (required, string) - **image.alt · pending** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, json | null) - `id` (required, string) - `request` (required, object) - `url` (required, string) - `result` (required, null) - `status` (required, "pending") - `type` (required, "image.alt") - `updatedAt` (required, any) - `userId` (required, string) - **image.review · completed** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, json | null) - `id` (required, string) - `request` (required, object) - `url` (required, string) - `effort` (string) — Reviewer panel to run. 'high' runs five strong vision models; 'low' runs three smaller ones for triage. Default `"high"`. One of: `"high"`, `"low"`. - `result` (required, object) - `findings` (required, object[]) - … - `score` (required, number) — Median of independent reviewer ballots. Higher is better, 1 to 4. - `strengths` (required, object[]) - … - `summary` (required, string) - `status` (required, "completed") - `type` (required, "image.review") - `updatedAt` (required, any) - `userId` (required, string) - **image.review · pending** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, json | null) - `id` (required, string) - `request` (required, object) - `url` (required, string) - `effort` (string) — Reviewer panel to run. 'high' runs five strong vision models; 'low' runs three smaller ones for triage. Default `"high"`. One of: `"high"`, `"low"`. - `result` (required, null) - `status` (required, "pending") - `type` (required, "image.review") - `updatedAt` (required, any) - `userId` (required, string) - **image.remove_background · completed** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, json | null) - `id` (required, string) - `request` (required, object) - `url` (required, string) - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) - `output` (object) - … - `webhook` (object) - … - `result` (required, object) - `image` (required, object) - … - `status` (required, "completed") - `type` (required, "image.remove_background") - `updatedAt` (required, any) - `userId` (required, string) - **image.remove_background · pending** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, json | null) - `id` (required, string) - `request` (required, object) - `url` (required, string) - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) - `output` (object) - … - `webhook` (object) - … - `result` (required, null) - `status` (required, "pending") - `type` (required, "image.remove_background") - `updatedAt` (required, any) - `userId` (required, string) - **image.rate · failed** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, object[]) - `code` (required, string) - `message` (string) - `id` (required, string) - `request` (required, object) - **custom** - `levels` (required, object[]) — ≥ 2 items, ≤ 7 items. - … - `mode` (required, "custom") - `url` (required, string) - **nsfw_sfw** - `url` (required, string) - `mode` ("nsfw_sfw") — Default `"nsfw_sfw"`. - `result` (required, null) - `status` (required, "failed") - `type` (required, "image.rate") - `updatedAt` (required, any) - `userId` (required, string) - **video.generate · failed** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, object[]) - `code` (required, string) - `message` (string) - `id` (required, string) - `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"`. - `audio` (boolean) — Enable model-native generated audio. Only supported by models with audio capability. - `duration` (number) — Video duration in seconds. Defaults to the model's default duration. ≥ 1, ≤ 60. - `inputs` ((string | object)[]) — ≤ 5 items. - … - `metadata` (object) - `negative_prompt` (string) — ≤ 8192 characters. - `resolution` (string) — Resolution tier. Defaults to the model's default tier. One of: `"1080p"`, `"480p"`, `"4k"`, `"720p"`. - `webhook` (object) - … - `result` (required, null) - `status` (required, "failed") - `type` (required, "video.generate") - `updatedAt` (required, any) - `userId` (required, string) - **image.generate · failed** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, object[]) - `code` (required, string) - `message` (string) - `id` (required, string) - `request` (required, object) - `prompt` (required, string) — Positive prompt. ≥ 1 characters, ≤ 8192 characters. Example `"Cute Cat"`. - `model` (string) — Default `"auto"`. One of: `"alibaba/qwen-image-2.0"`, `"alibaba/qwen-image-2.0-pro"`, `"alibaba/qwen-image-3.0"`, `"alibaba/qwen-image-3.0-pro"`, `"auto"`, `"black-forest-labs/flux-1-schnell"`, `"black-forest-labs/flux.1-dev"`, `"black-forest-labs/flux.2-dev"`, `"black-forest-labs/flux.2-flex"`, `"black-forest-labs/flux.2-klein-4b"`, `"black-forest-labs/flux.2-max"`, `"black-forest-labs/flux.2-pro"`, `"bria/fibo-edit-1.5"`, `"bria/fibo-generate-1.5"`, `"bytedance/seedream-5.0-lite"`, `"bytedance/seedream-pro"`, `"circlestone-labs/anima"`, `"goofy-ai/prefect-pony-xl-lora"`, `"google/gemini-3-pro-image-preview"`, `"google/gemini-3.1-flash-image"`, `"google/gemini-3.1-flash-lite-image"`, `"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-large"`, `"krea/krea-2-medium"`, `"krea/krea-2-turbo"`, `"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-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"`. Example `"black-forest-labs/flux.2-pro"`. - `count` (number) — Default `1`. ≥ 1, ≤ 20. - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `inputs` ((string | object)[]) — ≤ 20 items. - … - `magic_prompt` (boolean) - `metadata` (object) - `negative_prompt` (string) — ≤ 8192 characters. - `output` (object) - … - `rating` (object | boolean) — One of: `true`. - … - `size` (object | string) — One of: `"16:9"`, `"16:9_4k"`, `"1:1"`, `"1:1_4k"`, `"1:2"`, `"1:2_4k"`, `"21:9"`, `"21:9_4k"`, `"2:1"`, `"2:1_4k"`, `"2:3"`, `"2:3_4k"`, `"3:2"`, `"3:2_4k"`, `"3:4"`, `"3:4_4k"`, `"4:3"`, `"4:3_4k"`, `"4:5"`, `"4:5_4k"`, `"5:4"`, `"5:4_4k"`, `"9:16"`, `"9:16_4k"`, `"auto"`, `"landscape"`, `"landscape_wide"`, `"portrait"`, `"portrait_tall"`, `"square"`. - … - `webhook` (object) - … - `result` (required, null) - `status` (required, "failed") - `type` (required, "image.generate") - `updatedAt` (required, any) - `userId` (required, string) - **image.alt · failed** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, object[]) - `code` (required, string) - `message` (string) - `id` (required, string) - `request` (required, object) - `url` (required, string) - `result` (required, null) - `status` (required, "failed") - `type` (required, "image.alt") - `updatedAt` (required, any) - `userId` (required, string) - **image.review · failed** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, object[]) - `code` (required, string) - `message` (string) - `id` (required, string) - `request` (required, object) - `url` (required, string) - `effort` (string) — Reviewer panel to run. 'high' runs five strong vision models; 'low' runs three smaller ones for triage. Default `"high"`. One of: `"high"`, `"low"`. - `result` (required, null) - `status` (required, "failed") - `type` (required, "image.review") - `updatedAt` (required, any) - `userId` (required, string) - **image.remove_background · failed** - `apiKeyId` (required, string | null) - `cost` (required, string | null) - `createdAt` (required, any) - `errors` (required, object[]) - `code` (required, string) - `message` (string) - `id` (required, string) - `request` (required, object) - `url` (required, string) - `destination` (string) — ≥ 1 characters, ≤ 64 characters. Example `"bunny-prod"`. - `metadata` (object) - `output` (object) - … - `webhook` (object) - … - `result` (required, null) - `status` (required, "failed") - `type` (required, "image.remove_background") - `updatedAt` (required, any) - `userId` (required, string) ```json { "data": { "id": "tsk_01KE7XWWEQ4MCGWKBQKJ1G47RP", "apiKeyId": "ak_01KE7XWWEQ4MCGWKBQKJ1G47RP", "userId": "user_01KE7XWWEQ4MCGWKBQKJ1G47RP", "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" } ] }, "cost": "0.01250000", "type": "image.generate", "updatedAt": "2025-01-15T10:30:12.000Z", "createdAt": "2025-01-15T10:30:00.000Z" } } ``` ## 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: `"completed"`, `"failed"`, `"pending"`. ```json { "data": { "status": "completed" } } ``` ## 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.alt · completed** - `id` (required, string) - `result` (required, object) - `alt` (required, string) — ≥ 1 characters, ≤ 160 characters. - `status` (required, "completed") - `type` (required, "image.alt") - **image.review · completed** - `id` (required, string) - `result` (required, object) - `findings` (required, object[]) - … - `score` (required, number) — Median of independent reviewer ballots. Higher is better, 1 to 4. - `strengths` (required, object[]) - … - `summary` (required, string) - `status` (required, "completed") - `type` (required, "image.review") - **image.remove_background · completed** - `id` (required, string) - `result` (required, object) - `image` (required, object) - … - `status` (required, "completed") - `type` (required, "image.remove_background") - **image.generate · completed** - `id` (required, string) - `result` (required, object) - `images` (required, object[]) - … - `model` (required, string) — One of: `"alibaba/qwen-image-2.0"`, `"alibaba/qwen-image-2.0-pro"`, `"alibaba/qwen-image-3.0"`, `"alibaba/qwen-image-3.0-pro"`, `"black-forest-labs/flux-1-schnell"`, `"black-forest-labs/flux.1-dev"`, `"black-forest-labs/flux.2-dev"`, `"black-forest-labs/flux.2-flex"`, `"black-forest-labs/flux.2-klein-4b"`, `"black-forest-labs/flux.2-max"`, `"black-forest-labs/flux.2-pro"`, `"bria/fibo-edit-1.5"`, `"bria/fibo-generate-1.5"`, `"bytedance/seedream-5.0-lite"`, `"bytedance/seedream-pro"`, `"circlestone-labs/anima"`, `"goofy-ai/prefect-pony-xl-lora"`, `"google/gemini-3-pro-image-preview"`, `"google/gemini-3.1-flash-image"`, `"google/gemini-3.1-flash-lite-image"`, `"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-large"`, `"krea/krea-2-medium"`, `"krea/krea-2-turbo"`, `"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-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"`. - `magic_prompt` (object) - … - `status` (required, "completed") - `type` (required, "image.generate") - **image.rate · completed** - `id` (required, string) - `result` (required, object) - `level` (required, string) — The rating level ≥ 1 characters, ≤ 24 characters. - `status` (required, "completed") - `type` (required, "image.rate") - **video.generate · completed** - `id` (required, string) - `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") - `type` (required, "video.generate") - **image.alt · failed** - `id` (required, string) - `result` (required, null) - `status` (required, "failed") - `type` (required, "image.alt") - **image.generate · failed** - `id` (required, string) - `result` (required, null) - `status` (required, "failed") - `type` (required, "image.generate") - **image.rate · failed** - `id` (required, string) - `result` (required, null) - `status` (required, "failed") - `type` (required, "image.rate") - **image.remove_background · failed** - `id` (required, string) - `result` (required, null) - `status` (required, "failed") - `type` (required, "image.remove_background") - **image.review · failed** - `id` (required, string) - `result` (required, null) - `status` (required, "failed") - `type` (required, "image.review") - **video.generate · failed** - `id` (required, string) - `result` (required, null) - `status` (required, "failed") - `type` (required, "video.generate") - **image.alt · pending** - `id` (required, string) - `result` (required, null) - `status` (required, "pending") - `type` (required, "image.alt") - **image.generate · pending** - `id` (required, string) - `result` (required, null) - `status` (required, "pending") - `type` (required, "image.generate") - **image.rate · pending** - `id` (required, string) - `result` (required, null) - `status` (required, "pending") - `type` (required, "image.rate") - **image.remove_background · pending** - `id` (required, string) - `result` (required, null) - `status` (required, "pending") - `type` (required, "image.remove_background") - **image.review · pending** - `id` (required, string) - `result` (required, null) - `status` (required, "pending") - `type` (required, "image.review") - **video.generate · pending** - `id` (required, string) - `result` (required, null) - `status` (required, "pending") - `type` (required, "video.generate") ```json { "data": { "id": "string", "result": { "alt": "string" }, "status": "completed", "type": "image.alt" } } ``` ## 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** - `displayName` (required, string | null) — Human-readable name as published by the model's creator. - `id` (required, string) — Stable model identifier, in `vendor/model` form. - `modes` (required, object) — Generation modes the model currently serves, with their input contracts. - `img->img` (object) - … - `txt->img` (object) - … - `pricing` (required, object | null) - `perImage` (required, object) - … - `perInput` (string) — USD charged per input image supplied with the request. - `type` (required, "image") - **video** - `displayName` (required, string | null) — Human-readable name as published by the model's creator. - `id` (required, string) — Stable model identifier, in `vendor/model` form. - `modes` (required, object) — Generation modes the model currently serves, with their input contracts. - `img->vid` (object) - … - `txt->vid` (object) - … - `pricing` (required, object | null) - `perSecond` (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. - … - `type` (required, "video") ```json { "data": [ { "displayName": "string", "id": "string", "modes": { "img->img": { "inputs": { "rules": null, "maxTotal": null } }, "txt->img": { "inputs": { "rules": null, "maxTotal": null } } }, "pricing": { "perImage": { "base": "string", "4k": "string" }, "perInput": "string" }, "type": "image" } ] } ``` ## 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 ### 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. - `name` (required, string) — Destination slug. Immutable after creation. Matches /^[a-z0-9-]+$/. ≥ 1 characters, ≤ 64 characters. - `provider` (required, object) - `account_id` (required, string) - `bucket` (required, string) - `id` (required, "r2") - `jurisdiction` (string) — One of: `"default"`, `"eu"`, `"fedramp"`. - `secret` (required, 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. - `name` (required, string) — Destination slug. Immutable after creation. Matches /^[a-z0-9-]+$/. ≥ 1 characters, ≤ 64 characters. - `provider` (required, object) - `bucket` (required, string) - `id` (required, "s3") - `region` (required, string) - `endpoint` (string) - `force_path_style` (boolean) - `secret` (required, object) - `access_key_id` (required, string) - `secret_access_key` (required, string) ### 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. - `name` (required, string) — Destination slug. Immutable after creation. Matches /^[a-z0-9-]+$/. ≥ 1 characters, ≤ 64 characters. - `provider` (required, object) - `id` (required, "bunny") - `storage_zone` (required, string) - `region` (string) — One of: `"br"`, `"de"`, `"jh"`, `"la"`, `"ny"`, `"se"`, `"sg"`, `"syd"`, `"uk"`. - `secret` (required, object) - `password` (required, string) ## Response ### 201 — Destination created - `data` (required, object) - `config` (required, json | null) - `createdAt` (required, any) - `id` (required, string) - `name` (required, string) - `provider` (required, json | null) - `updatedAt` (required, any) ```json { "data": { "config": {}, "id": "string", "name": "string", "provider": {} } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/destinations \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "config": { "path_template": "string" }, "name": "string", "provider": { "account_id": "string", "bucket": "string", "id": "r2" }, "secret": { "access_key_id": "string", "secret_access_key": "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({ "config": { "path_template": "string" }, "name": "string", "provider": { "account_id": "string", "bucket": "string", "id": "r2" }, "secret": { "access_key_id": "string", "secret_access_key": "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[]) - `config` (required, json | null) - `createdAt` (required, any) - `id` (required, string) - `name` (required, string) - `provider` (required, json | null) - `updatedAt` (required, any) ```json { "data": [ { "config": {}, "id": "string", "name": "string", "provider": {} } ] } ``` ## 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) - `config` (required, json | null) - `createdAt` (required, any) - `id` (required, string) - `name` (required, string) - `provider` (required, json | null) - `updatedAt` (required, any) ```json { "data": { "config": {}, "id": "string", "name": "string", "provider": {} } } ``` ## 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 ### 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. - `provider` (required, object) - `account_id` (required, string) - `bucket` (required, string) - `id` (required, "r2") - `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. - `provider` (required, object) - `bucket` (required, string) - `id` (required, "s3") - `region` (required, string) - `endpoint` (string) - `force_path_style` (boolean) - `secret` (object) - `access_key_id` (required, string) - `secret_access_key` (required, string) ### 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. - `provider` (required, object) - `id` (required, "bunny") - `storage_zone` (required, string) - `region` (string) — One of: `"br"`, `"de"`, `"jh"`, `"la"`, `"ny"`, `"se"`, `"sg"`, `"syd"`, `"uk"`. - `secret` (object) - `password` (required, string) ## Response ### 200 — Destination updated - `data` (required, object) - `config` (required, json | null) - `createdAt` (required, any) - `id` (required, string) - `name` (required, string) - `provider` (required, json | null) - `updatedAt` (required, any) ```json { "data": { "config": {}, "id": "string", "name": "string", "provider": {} } } ``` ## 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": { "account_id": "string", "bucket": "string", "id": "r2" } }' ``` ### 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": { "account_id": "string", "bucket": "string", "id": "r2" } }), }); 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. ## 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. ## 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; 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 `wbs_` secret in this response. 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 /webhook` **Auth:** API key or OAuth token ## Request body - `events` (required, array | string) — The events to send to the webhook. When 'all', all events will be sent to the webhook. One of: `"all"`. - `url` (required, string) — The URL to send the webhook to. Must be a valid URL. - `oauthEnabled` (boolean) — Whether tasks authenticated with OAuth (playground and CLI sessions) deliver to this webhook. Default `false`. - `enabled` (boolean) — Whether the webhook is enabled. When disabled, no events will be sent to the webhook. Default `true`. - `apiKeyIds` (string[] | null) — API keys this webhook is scoped to. When empty or null, every API key delivers to it. ≤ 1000 items. ## Response ### 201 — Webhook created - `data` (required, object) - `apiKeyIds` (required, string[] | null) - `createdAt` (required, any) - `enabled` (required, boolean) - `events` (required, string[] | null) - `id` (required, string) - `oauthEnabled` (required, boolean) - `secret` (required, string) - `updatedAt` (required, any) - `url` (required, string) - `userId` (required, string) ```json { "data": { "apiKeyIds": [ "string" ], "enabled": false, "events": [ "all" ], "id": "string", "oauthEnabled": false, "secret": "string", "url": "string", "userId": "string" } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/webhook \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "events": "all", "url": "string" }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/webhook", { method: "POST", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "events": "all", "url": "string" }), }); 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 key scoping. The signing secret is unchanged. `PUT /webhook/{id}` **Auth:** API key or OAuth token ## Path parameters - `id` (required, string) — The webhook to update. Example `"whk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Request body - `enabled` (required, boolean) — Whether the webhook is enabled. When disabled, no events will be sent to the webhook. - `events` (required, array | string) — The events to send to the webhook. When 'all', all events will be sent to the webhook. One of: `"all"`. - `url` (required, string) — The URL to send the webhook to. Must be a valid URL. - `apiKeyIds` (string[] | null) — API keys this webhook is scoped to. When empty or null, every API key delivers to it. ≤ 1000 items. - `oauthEnabled` (boolean) — Whether tasks authenticated with OAuth (playground and CLI sessions) deliver to this webhook. ## Response ### 200 — Webhook updated - `data` (required, object) - `apiKeyIds` (required, string[] | null) - `enabled` (required, boolean) - `events` (required, string[] | null) - `id` (required, string) - `oauthEnabled` (required, boolean) - `url` (required, string) ```json { "data": { "apiKeyIds": [ "string" ], "enabled": false, "events": [ "all" ], "id": "string", "oauthEnabled": false, "url": "string" } } ``` ## Request samples ### cURL ```bash curl -X PUT https://api.mynth.io/webhook/whk_01KE7XWWEQ4MCGWKBQKJ1G47RP \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": false, "events": "all", "url": "string" }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/webhook/whk_01KE7XWWEQ4MCGWKBQKJ1G47RP", { method: "PUT", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "enabled": false, "events": "all", "url": "string" }), }); 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 /webhook/{id}` **Auth:** API key or OAuth token ## Path parameters - `id` (required, string) — The webhook to delete. Example `"whk_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Response ### 204 — Webhook deleted No body. ## Request samples ### cURL ```bash curl -X DELETE https://api.mynth.io/webhook/whk_01KE7XWWEQ4MCGWKBQKJ1G47RP \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/webhook/whk_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-key` **Auth:** API key or OAuth token ## Request body - `scopes` (string[]) — Default `["generate"]`. ≥ 1 items. - `name` (string) — ≥ 1 characters, ≤ 256 characters. ## Response ### 201 — API key created - `data` (required, object) - `apiKey` (required, object) - `id` (required, string) - `keyPreview` (required, string) - `scopes` (required, string[]) - `spendingLimitPeriod` (required, string | null) — One of: `"day"`, `"month"`, `"week"`. - `userId` (required, string) - `name` (string) - `spendingLimit` (string) - `raw` (required, string) ```json { "data": { "apiKey": { "id": "string", "keyPreview": "string", "scopes": [ "generate" ], "spendingLimitPeriod": "day", "userId": "string", "name": "string", "spendingLimit": "string" }, "raw": "string" } } ``` ## Request samples ### cURL ```bash curl -X POST https://api.mynth.io/api-key \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/api-key", { 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-key` **Auth:** API key or OAuth token ## Response ### 200 — API keys returned - `data` (required, object[]) - `createdAt` (required, any) - `id` (required, string) - `keyPreview` (required, string) - `name` (required, string | null) - `scopes` (required, string[]) - `spendingLimit` (required, string | null) - `spendingLimitPeriod` (required, string | null) — One of: `"day"`, `"month"`, `"week"`. - `updatedAt` (required, any) ```json { "data": [ { "id": "string", "keyPreview": "string", "name": "string", "scopes": [ "generate" ], "spendingLimit": "string", "spendingLimitPeriod": "day" } ] } ``` ## Request samples ### cURL ```bash curl https://api.mynth.io/api-key \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/api-key", { 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. The secret does not change, so live traffic keeps working. `PUT /api-key/{id}` **Auth:** API key or OAuth token ## Path parameters - `id` (required, string) — The API key to update. Example `"ak_01KE7XWWEQ4MCGWKBQKJ1G47RP"`. ## Request body - `scopes` (required, string[]) — ≥ 1 items. - `spendingLimit` (required, number | null) - `spendingLimitPeriod` (required, string | null) — One of: `"day"`, `"month"`, `"week"`. - `name` (string | null) — ≥ 1 characters, ≤ 256 characters. ## Response ### 200 — API key updated - `data` (required, object) - `id` (required, string) - `name` (required, string | null) - `scopes` (required, string[]) - `spendingLimit` (required, string | null) - `spendingLimitPeriod` (required, string | null) — One of: `"day"`, `"month"`, `"week"`. ```json { "data": { "id": "string", "name": "string", "scopes": [ "generate" ], "spendingLimit": "string", "spendingLimitPeriod": "day" } } ``` ## Request samples ### cURL ```bash curl -X PUT https://api.mynth.io/api-key/ak_01KE7XWWEQ4MCGWKBQKJ1G47RP \ -H "Authorization: Bearer $MYNTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "scopes": [ "generate" ], "spendingLimit": 0, "spendingLimitPeriod": "day" }' ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/api-key/ak_01KE7XWWEQ4MCGWKBQKJ1G47RP", { method: "PUT", headers: { Authorization: `Bearer ${process.env.MYNTH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "scopes": [ "generate" ], "spendingLimit": 0, "spendingLimitPeriod": "day" }), }); 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-key/{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. ## Request samples ### cURL ```bash curl -X DELETE https://api.mynth.io/api-key/ak_01KE7XWWEQ4MCGWKBQKJ1G47RP \ -H "Authorization: Bearer $MYNTH_API_KEY" ``` ### JavaScript ```ts const response = await fetch("https://api.mynth.io/api-key/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) - `auth` (required, object) - `method` (required, string) — One of: `"api-key"`, `"oauth"`. - `apiKey` (object) — Present when authenticated with an API key. - `id` (required, string) - `keyPreview` (required, string) — Example `"mak_abc...xyz"`. - `name` (required, string | null) - `scopes` (required, string[]) - `spending` (required, object) - … - `userId` (required, string) — Example `"user_01JD8G3W1R5T6Y7U8I9O0P1Q2W"`. ```json { "data": { "auth": { "method": "api-key", "apiKey": { "id": "string", "keyPreview": "mak_abc...xyz", "name": "string", "scopes": [ "generate" ], "spending": { "limit": "string", "mode": "limited", "period": "day", "remaining": "string", "used": "string" } } }, "userId": "user_01JD8G3W1R5T6Y7U8I9O0P1Q2W" } } ``` ## 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) - `available` (required, string) — Spendable balance: balance minus reserved. - `balance` (required, string) — Current account balance in USD. - `currency` (required, "usd") - `reserved` (required, string) — Amount reserved by in-flight tasks. ```json { "data": { "available": "string", "balance": "string", "currency": "usd", "reserved": "string" } } ``` ## 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`, 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 | 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 `getImages()` 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 classes 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`, the result classes, the 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. `convex` is an optional peer dependency, needed only for `@mynthio/sdk/convex`. The webhook helpers 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 and background removal | | `MYNTH_WEBHOOK_SECRET` | The `wbs_` 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 and background removal | 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, 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 the result classes. `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.getImages()) { console.log(image.url ?? image.mynth_url, image.size); } ``` The value is an `ImageGenerationResult`: | Member | Holds | | ------------------------------------ | ----------------------------------------------------------------------- | | `id`, `status` | The task id and status | | `result` | The raw result: `model`, `images`, `magic_prompt` | | `urls` | `url` of each successful image, skipping any whose `url` is `null` | | `getImages()` | Successful images: `id`, `url`, `mynth_url`, `size`, `format`, `rating` | | `getImages({ includeFailed: true })` | Every entry, including `{ status: "failed", error }` | | `getMetadata()` | The `metadata` you sent, typed from the request | | `data` | The whole task record | > **Warning** > > `urls` and `getImages()` skip failed images, and `urls` also skips images whose destination > upload failed. A task can complete with a short list. Compare `getImages().length` with the > `count` you sent, and pass `{ includeFailed: true }` 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, resolution: "720p", }); console.log(task.urls[0]); ``` The result is a `VideoGenerationResult` with `urls`, `getVideos()`, and `getMetadata()`. There is no `.videos` property. The raw array is `result.videos`, and `getVideos()` skips failures the same way `getImages()` does. `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.estimatedCost, quote.estimateKind); // "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 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" } }); ``` | Method | Result members | | -------------------- | ------------------------------------------------------------- | | `rate()` | `taskId`, `cost`, `level` | | `alt()` | `taskId`, `cost`, `alt` | | `review()` | `taskId`, `cost`, `score`, `summary`, `findings`, `strengths` | | `removeBackground()` | `taskId`, `cost`, `image`, `metadata` | The task id is `taskId` on these, not `id`. `removeBackground()` also takes `destination`, `webhook`, and `metadata`. 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), and [remove background](https://mynth.io/docs/guides/remove-background.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()`, `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 endpoint issues one | | `wait()` | Polls until the task settles, then returns the result class | `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()`, and `video.generateAsync()` expose the task's `pat_` token as `pending.access.publicAccessToken`. 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", }); 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. - `publicAccessToken` can be missing if Mynth failed to sign one. Fall back to polling from the server. - `wait()` is for the server. Its last call is `GET /tasks/{id}`, which a `pat_` cannot make. ## Partial results `urls`, `getImages()`, and `getVideos()` skip failed items. Compare `getImages().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 `getImages()` 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 `code`, when there is one | | `message` | The body's `message`, or a generic fallback | A schema rejection is `status` `400` with `code` undefined and an unhelpful `message`, because that body has an issue list instead of a `code`, and the SDK does not keep the list. On `error.status === 400` without a code, repeat the request with curl or `npx @mynthio/cli ... --json` to see which field failed. See [schema rejections](https://mynth.io/docs/api-reference/errors.md#schema-rejections-use-a-different-shape). 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 `getImages({ includeFailed: true })` for each `error.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-dashboard-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 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-dashboard-webhooks`. - `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 | | `-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 | | `--oauth-events` | Also deliver tasks with no API key, such as playground runs | | `--disabled` | Create the endpoint disabled | The `wbs_` signing secret prints once. Tasks from the CLI carry the key from `auth login`, so they deliver without `--oauth-events`. `webhook update ` replaces the whole configuration: pass `--url` and every `-e` again. Leaving out `--oauth-events` 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, or `VALIDATION_ERROR` | | 3 | Auth | `UNAUTHORIZED`, `INSUFFICIENT_SCOPE`, any `401` or `403` | | 4 | Out of credit | `INSUFFICIENT_BALANCE`, `SPENDING_LIMIT_EXCEEDED` | | 5 | Refused | `RESTRICTED_CONTENT` | | 6 | Rate limited | A `429` other than `SPENDING_LIMIT_EXCEEDED` | How a task failure maps to an exit code depends on the command: - `rate`, `alt`, `review`, and `remove-background` 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 `wbs_` 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" }, }); 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 (payload, { context }) => { await context.runMutation(internal.images.save, { taskId: payload.task.id, images: payload.result.images, }); }, imageTaskFailed: async (payload) => { console.error(payload.task.id, payload.errors); }, }); http.route({ path: "/mynth-webhook", method: "POST", handler: mynthWebhook }); export default http; ``` `internal.images.save` is a mutation you write. Each handler gets the payload and `{ context, request, deliveryId }`. `context` is the Convex action context, so `context.runQuery` and `context.runMutation` are how it touches your data. `deliveryId` is the `X-Mynth-Delivery` value, the same on every retry, so store it to skip deliveries 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 timestamp more than five minutes off, a missing `X-Mynth-Delivery`, or an `X-Mynth-Event` header that does not match the body 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. Register the endpoint instead. ## 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 `wbs_` 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 (payload, { request }) => { console.log(payload.task.id, payload.result.images, request.url); }, imageTaskFailed: async (payload) => { console.error(payload.task.id, payload.errors); }, }), }, }, }); ``` The handler verifies `X-Mynth-Signature` against the raw body, requires `X-Mynth-Event` to match the body's `event` and `X-Mynth-Delivery` to be present, and rejects a timestamp more than five minutes off, all with `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 the payload and the route context: `request`, `params`, and middleware `context`, plus `deliveryId`, the `X-Mynth-Delivery` value. 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. ## 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", }); return { taskId: pending.id, token: pending.access.publicAccessToken }; ``` `generate()` waits for the task. `generateAsync()` returns the id and a browser token right away, 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 depends on `@mynthio/sdk` and needs `@tanstack/ai` 0.34.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, 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`. The adapter calls the SDK's `generate()` 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` | | 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 `/v1` prefix, 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`, with no version prefix | [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 `X-Mynth-Delivery` | [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.