Files in and out
Send a URL, an upload or a file in your bucket; get a link, or have the result written where you want it.
Every request reads one file and writes one or more. This page lists each way to give the API a file and each place it can put a result, so you can pick the one that fits where your files are.
Choosing
Where the file you want processed is:
| Your file is | Send source as |
Needs |
|---|---|---|
| On the web, at a public URL | "https://…" |
Nothing. |
| On your computer or server, not on the web | an upload, "upl_…": see Uploading a file |
Nothing. |
| A result of an earlier request | its id, "file_…" |
Nothing. |
| In your own S3 bucket | {"storage": "stg_…", "key": "photos/IMG_4471.jpg"} |
A storage connection. |
Where you want the result:
| You want the result | Send the output's destination as |
Needs |
|---|---|---|
| As a download link (the default) | nothing | Nothing. |
| Written to a URL you signed: S3, R2, Google Cloud Storage, Azure | {"url": "https://…", "headers": {…}} |
An account that has bought credits. |
| Written into your own S3 bucket | {"storage": "stg_…", "key": "renders/IMG_4471.webp"} |
A storage connection and an account that has bought credits. |
For AI agents. A file in your workspace or on the user's computer is not on the web: upload it (two calls, below) rather than asking the user to host it first. Download the result from result.files[0].url. Use a storage connection only when the user has one set up and gives you its stg_… id; creating one needs access to their AWS account.
Sources
source is one of:
- A public
httporhttpsURL on the default port. The API refuses private, loopback and cloud-metadata addresses and follows at most 3 redirects. - A file an earlier request made, by its id:
"file_…". The API reads it from where it keeps it, so nothing is downloaded twice. - An upload:
"upl_…", for a file that is not on the web. See Uploading a file. - An object in your own bucket:
{"storage": "stg_…", "key": "photos/IMG_4471.jpg"}, through a storage connection that may read, with a key under one of its prefixes.
A watermark operation's image takes the same forms but an upload: image_url for a URL, or image for a file id or a storage object. A file id suits an image used again and again, such as a logo: upload the logo, run it through an image request once, and use the result's file_… id from then on.
The API only processes these formats:
- Images: JPEG, PNG, GIF, WebP, TIFF, BMP, HEIC, AVIF, SVG, PSD, and camera RAW (Canon CR2, CR3 and CRW, Nikon NEF and NRW, Sony ARW, DNG, Fujifilm RAF, Olympus ORF, Panasonic RW2, Pentax PEF, Samsung SRW, Sigma X3F, and others). A RAW file is told apart by its name's extension, so keep it.
- Videos: MP4, MOV, WebM, MKV, AVI, FLV, WMV, MPEG, MPEG-TS
- An SVG is drawn on its own: images, fonts or files it points at are not loaded, unless written into it as
data:URLs. - Documents: PDF and Word (DOCX). Processing keeps the type: ask for
"format": "docx"output for a Word file.
Uploading a file
Three steps: ask for an upload, PUT the file to the URL it gives you, then send the upload's id as the source.
1. Ask for an upload, with the file's name, its size in bytes and its type:
curl https://api.watermark.ws/v1/uploads \
-H "Authorization: Bearer $WATERMARK_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"filename\": \"IMG_4471.heic\", \"size\": $(wc -c < IMG_4471.heic), \"content_type\": \"image/heic\"}"
{
"id": "upl_9f3Tq1LmA2bC4dE6fG8hJ0",
"object": "upload",
"status": "pending",
"upload": {
"method": "PUT",
"url": "https://uploads.watermark.ws/…?X-Amz-Signature=…",
"headers": {"Content-Type": "image/heic"},
"expires_at": "2026-10-13T16:00:00Z"
},
"expires_at": "2026-10-14T15:00:00Z"
}
2. PUT the file to upload.url, sending every header in upload.headers:
curl -X PUT "$UPLOAD_URL" -H "Content-Type: image/heic" --data-binary @IMG_4471.heic
No API key here: the URL is signed. It works for an hour (upload.expires_at); GET /v1/uploads/{id} gives a fresh one while nothing has been uploaded.
3. Use the upload as the source of any processing request:
curl https://api.watermark.ws/v1/images/process \
-H "Authorization: Bearer $WATERMARK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"source": "upl_9f3Tq1LmA2bC4dE6fG8hJ0", "operations": [{"type": "resize", "width": 1600, "height": 1600}], "wait": 20}'
What happens to it:
- One request at a time may use an upload; a second gets
409 upload_in_use. Sending it before thePUTfinished gets409 upload_incomplete. - The first request that succeeds uses it up, and the file is deleted: send it again and you get
410 upload_consumed. Use that request'sfile_…id instead. - A request that fails leaves the upload, so you can retry without uploading again.
- An upload nobody uses is deleted after a day (
expires_at). sizemay be at most your account's largest source (see Pricing and limits).
Destinations
An output's destination says where its file goes:
| Destination | Send | Who keeps the file |
|---|---|---|
| Kept by the API (default) | nothing | The API, for 7 days; you get a download link. |
| A presigned URL | {"url": "https://…", "headers": {…}} |
You. The API makes one PUT to the URL. |
| Your bucket | {"storage": "stg_…", "key": "renders/IMG_4471.webp"} |
You. The API writes through your storage connection. |
The last two need an account that has bought credits.
Kept by the API. The result's url is a download link that works until the file is deleted: 7 days after the request finished, the same for every key. To keep a file longer, write it to your own storage. Anyone with the link can download the file. GET /v1/files/{id} gives a fresh link; DELETE /v1/files/{id} deletes it early.
A presigned URL. Sign a PUT URL with your storage provider and send it with the headers the signature covers (up to 20). The API sends those headers, plus Content-Type with the output's type (such as image/webp) when you leave it out, so sign that type if your provider checks it. The signature is used once and never stored: the request shows the URL without it.
{
"source": "https://example.com/photo.jpg",
"operations": [{"type": "resize", "width": 1600, "height": 1600}],
"outputs": [{
"name": "web",
"format": "webp",
"destination": {"url": "https://acme-photos.s3.amazonaws.com/renders/IMG_4471.webp?X-Amz-Signature=…", "headers": {"Content-Type": "image/webp"}}
}]
}
Your bucket. Through a storage connection. The key must be under one of the connection's prefixes; a file already there is replaced. cache_control and content_disposition set those headers on the object.
A file written to a presigned URL or your bucket has no download link: result.files lists it with its destination (the storage id and key, or the URL without its signature) and the etag your storage returned.
When a file cannot be written. Outputs are written in order. If one cannot be written (the role was denied, the presigned URL expired), the request fails with error.code destination_rejected, error.output naming the output and error.retryable true; the files written before it are listed in result.files with result.partial. A failed request is not charged.