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 setRules#
destinationtakes the destination's name, not itsdst_id.- Only
POST /image/generateandPOST /image/remove-backgroundaccept it.POST /video/generatedrops the field without an error. - A name you don't own fails at create with
400 VALIDATION_ERRORandDestination "prod-cdn" not found. Nothing is queued or held. - The upload happens inside the task. A task that reports
completedhas already finished it. - A failed upload does not fail the image. The image succeeds with
url: null, andmynth_urlis set. See what comes back. - Uploading does not change the price.
- Every
/destinationsendpoint needs a key with themanagescope.
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: nullMynth 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#
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.jsonThe 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:
| 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.<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_01KE7XWWEQ4MCGWKBQKJ1G47RPThe 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 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:
"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#
| 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: how long
mynth_urllasts. - Webhooks: learn when the task, and its upload, is done.
- Create destination: the full request schema.