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.

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.
  • Uploading does not change the price.
  • Every /destinations endpoint needs a key with the manage scope.

How the upload works#

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.idConfigsecret
s3bucket, region, optional endpoint, optional force_path_styleaccess_key_id, secret_access_key
r2account_id, bucket, optional jurisdictionaccess_key_id, secret_access_key
bunnystorage_zone, optional regionpassword

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#

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

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

TokenBecomes
{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.<key>}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.

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.

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#

{
  "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 typeUpload failedDestination deleted before the upload
image.generateurl: null, no reason givenurl is the Mynth URL; nothing reaches your storage
image.remove_backgroundurl: null, plus a destination blockurl: null, destination.error.code is DESTINATION_NOT_FOUND

On image.remove_background, the image carries a destination block:

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

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#

StatusCodeWhen
400VALIDATION_ERRORA generation request named a destination you don't own
400DESTINATIONS_INVALID_PROVIDERPUT /destinations/{id} changed provider.id
404DESTINATION_NOT_FOUNDUnknown dst_ id
409DESTINATION_NAME_TAKENYou already have a destination with that name
500UNKNOWN_ERRORStoring or reading the secret failed
502DESTINATION_TEST_FAILEDYour storage rejected the probe. message is the provider's

Next steps#