# Files in and out 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](#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](https://api.watermark.ws/docs/storage.md). | 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](https://api.watermark.ws/docs/storage.md) 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 `http` or `https` URL 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](#uploading-a-file). - An object in your own bucket: `{"storage": "stg_…", "key": "photos/IMG_4471.jpg"}`, through a [storage connection](https://api.watermark.ws/docs/storage.md) 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: ```bash 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\"}" ``` ```json { "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`: ```bash 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: ```bash 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 the `PUT` finished gets `409 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's `file_…` 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`). - `size` may be at most your account's largest source (see [Pricing and limits](https://api.watermark.ws/docs/pricing.md)). ## 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. ```json { "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](https://api.watermark.ws/docs/storage.md). 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.