# AI agents The Watermark API is built to be called by AI agents as much as by people. This page says when an agent should reach for it, the three ways to connect (an MCP server, the command line, plain HTTP), the rules that keep an agent out of trouble, and three worked examples. If you are a model reading this: the rules and examples below are meant for you. ## When to call Watermark A code sandbox can stamp one JPEG with a library. Call the API instead when the job is any of these: - **A batch.** Tens or hundreds of files, each with the same operations. One request per file, run in parallel, with results you can download or have written to the user's own bucket. - **Video.** A logo on every frame, resized and re-encoded, without a sandbox trying to run ffmpeg for minutes. - **PDF and Word.** Every page watermarked, and the file stays a PDF or a DOCX. - **Camera files.** HEIC, AVIF, Canon CR3, Nikon NEF, Sony ARW and other RAW formats, PSD, wide-gamut photos: formats a sandbox's image library usually cannot open. - **Fonts and text.** Text in a built-in font (`GET /v1/fonts`) or the user's own font file, made into a transparent stamp once and used on every file after. - **Delivery.** Results written straight to the user's S3 bucket or to a presigned URL, so no file travels through the conversation. For one small image that the sandbox can already open, local code is fine. ## Three ways in ### 1. The MCP server `https://watermark.ws/mcp` is a Model Context Protocol server over Streamable HTTP. It signs in with a watermark.ws account (OAuth, the connector's "Sign in" option) or with an API key sent as the bearer token: `Authorization: Bearer wm_live_…`. Its tools, each the thin shape of one endpoint: | Tool | What it does | | --- | --- | | `list_operations` | Every operation and its parameters, per media type (`GET /v1/operations`). | | `request_files` | Shows the user a drop zone in the chat (Claude and ChatGPT) for files on their computer; they are uploaded from the user's browser and the user's next message carries the `upl_…` ids. | | `create_upload` | An upload for a file that is not on the web (`POST /v1/uploads`): the agent `PUT`s the bytes itself, for a file in its own workspace. | | `process_image`, `process_video`, `process_document` | Run operations on a file (`POST /v1/{images,videos,documents}/process`). | | `create_image` | Draw on a blank canvas: a text stamp, a logo (`POST /v1/images/create`). | | `read_file` | A file's dimensions, format, duration or page count (`POST /v1/{images,videos,documents}/read`). | | `video_screenshot`, `document_snapshot` | One frame of a video, or one page of a document, as an image. | | `get_request`, `list_requests`, `abort_request` | Follow, list and stop requests. | | `get_file` | A fresh download link for a file a request made. | | `list_fonts` | The built-in fonts a `text` operation can name. | | `get_account`, `get_usage` | Balance, limits and spend. | Arguments are the endpoint's JSON body: `source`, `operations`, `output`, `wait` and so on, as the [API reference](https://api.watermark.ws/docs/reference.md) lists them. **Claude.ai:** Customize → Connectors → Add custom connector. Name it Watermark, give it the URL `https://watermark.ws/mcp`, and sign in when asked, or set the `Authorization` header to `Bearer wm_live_…` under advanced settings. Team and Enterprise admins add it once for everyone under Organization settings → Connectors. **ChatGPT:** Settings → Apps & Connectors → turn on developer mode → add an MCP server with the same URL. **Claude Code:** ```bash claude mcp add --transport http watermark https://watermark.ws/mcp --header "Authorization: Bearer wm_test_…" ``` **Cursor, VS Code and other MCP clients:** add an HTTP (Streamable HTTP) MCP server with the URL and an `Authorization: Bearer wm_live_…` header. **Your own agent on the Claude API:** attach the server to a Messages request with the MCP connector. Both halves are needed: the server, and a toolset that names it. ```bash curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: mcp-client-2025-11-20" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 16000, "mcp_servers": [{"type": "url", "url": "https://watermark.ws/mcp", "name": "watermark", "authorization_token": "wm_live_…"}], "tools": [{"type": "mcp_toolset", "mcp_server_name": "watermark"}], "messages": [{"role": "user", "content": "Put https://example.com/logo.png in the bottom-right corner of https://example.com/photo.jpg and give me the link."}] }' ``` ### 2. The command line For an agent with a shell (Claude Code, Codex, Cursor, a CI job), the [`watermark` command](https://api.watermark.ws/docs/cli.md) is the shortest path: a local file given as the source is uploaded for you, the command waits for the result, and `--out` downloads it. ```bash npm install -g @watermarkws/cli # or: brew install watermarkws/tap/watermark, or run once with npx @watermarkws/cli export WATERMARK_API_KEY=wm_test_… # from https://watermark.ws/developers watermark image process ./IMG_4471.heic \ --op resize:width=1600,height=1600 \ --op watermark:image_url=https://example.com/logo.png,x=40,y=40 \ --format webp --out IMG_4471.webp --json ``` `--json` prints the finished request as the API returns it and nothing else on stdout; the exit code is `0` when the request succeeded, `1` when the API refused the call or the request failed, `2` when the command was used wrongly. A ready-made skill file for the CLI is at [github.com/watermarkws/watermark-cli/SKILL.md](https://raw.githubusercontent.com/watermarkws/watermark-cli/main/SKILL.md): save it as `.claude/skills/watermark/SKILL.md` in a project for Claude Code, or paste it into any agent's instructions. ### 3. Plain HTTP Any agent that can make HTTP requests can use the API as the [Quickstart](https://api.watermark.ws/docs/quickstart.md) shows. A file that is not on the web takes two calls more: ask for an upload, then `PUT` the bytes to the URL it gives you. ```bash # 1. Ask for an upload curl https://api.watermark.ws/v1/uploads \ -H "Authorization: Bearer $WATERMARK_API_KEY" \ -H "Content-Type: application/json" \ -d '{"filename": "photo.jpg", "size": 2481193, "content_type": "image/jpeg"}' # → {"id": "upl_9f3Tq1LmA2bC4dE6fG8hJ0", "upload": {"url": "https://….s3.amazonaws.com/…?X-Amz-Signature=…", "headers": {"Content-Type": "image/jpeg"}}, …} # 2. PUT the file to upload.url (UPLOAD_URL here) with upload.headers, and no API key curl -X PUT "$UPLOAD_URL" -H "Content-Type: image/jpeg" --data-binary @photo.jpg # 3. Process it, waiting up to 20 seconds for the result 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": "watermark", "image_url": "https://example.com/logo.png", "x": 40, "y": 40}], "wait": 20}' ``` The machine-readable docs are [llms.txt](https://api.watermark.ws/llms.txt), [llms-full.txt](https://api.watermark.ws/llms-full.txt) and the [OpenAPI spec](https://api.watermark.ws/openapi.json); any docs page is Markdown with `.md` on its URL. ## Rules for agents - **Never put a file's bytes in arguments.** Tool arguments and JSON bodies carry references, not files: a public URL, an upload id (`upl_…`), the id of a file an earlier request made (`file_…`), or `{"storage": "stg_…", "key": "…"}` for an object in the user's connected bucket. Do not base64 a file into `source` or `image_url`; it is refused. - **A local file is uploaded, not hosted.** A file in your workspace, attached to the chat or on the user's computer is not on the web. Create an upload (`create_upload`, `POST /v1/uploads`, or just give the CLI the path) and `PUT` the bytes there. Do not ask the user to put the file online first. - **Send `wait`.** `"wait": 20` (at most 25 seconds) returns `200` with the finished request when it finishes in time, which images, reads, snapshots and short videos usually do. Otherwise you get `202` and poll `GET /v1/requests/{id}?wait=25` (`get_request`) until the status is `succeeded`, `failed` or `aborted`. Do not poll in a tight loop. - **The result is `result.files[0].url`.** Give the user that link, or download from it. It works for 7 days (`url_expires_at`); `GET /v1/files/{id}` (`get_file`) gives a fresh link while the file is kept. An output with a `destination` has no link: the file was written to the user's storage, and `result.files[0].destination` says where. - **Reuse a watermark.** Run a logo or a text stamp through one image request, then pass its `file_…` id as the `watermark` operation's `image` on every file after. One upload, not one per file. - **Ask before spending.** Credit is prepaid and every finished request is charged: $0.002 an image, $0.004 a document page, $0.02 a started minute of video. Before processing more than about 20 files, a video longer than about 10 minutes, or a document with hundreds of pages, tell the user what it will cost and ask. Work in parallel once they agree, within the account's "running at once" limit. - **Failed and aborted requests are free.** A `failed` request carries `error.code` and `error.message`; fix the cause (usually the source, a parameter or the format) and send it again. Do not retry an identical request blindly. - **Test keys return real results.** `wm_test_…` keys are free and process files for real, within small limits: 10 requests a minute, 100 a day, 2 running at once, sources up to 10 MB. Video needs a live key on an account that has bought credit. Build and test with a test key, then switch the key, not the code. - **Look before guessing.** `list_operations` (`GET /v1/operations`) is the catalog, with every parameter and its range; `read_file` gives a file's dimensions, duration or page count; `list_fonts` gives the names a `text` operation accepts. A wrong parameter is a `422` with `error.errors` naming the field, and nothing is charged. ## Worked examples Each example shows the MCP tool calls and the same job on the command line. A tool's arguments are the endpoint's JSON body, except that the tools take `format` for `output.format` and `size_bytes` for an upload's `size`. The images used as watermarks are placed at `x` and `y` pixels from the top left, at their own size; resize a logo first if it is too big. ### A folder of photos: logo, 1600 px, WebP Prepare the logo once, so every photo refers to the same `file_…` id rather than uploading the logo again. With a logo that is already online, skip the first two calls and send `image_url` instead of `image`. ```json create_upload {"filename": "logo.png", "size_bytes": 48213, "content_type": "image/png"} → PUT the file to upload.url with upload.headers process_image {"source": "upl_logoUpload…", "operations": [{"type": "orient"}], "format": "png", "wait": 20} → result.files[0].id = "file_2kqV7PzM1aT4hQ0bR9sX3c" ``` Then one request per photo, several at a time (ask first if there are more than about 20): ```json create_upload {"filename": "IMG_4471.heic", "size_bytes": 3812044, "content_type": "image/heic"} → PUT the file process_image { "source": "upl_photoUpload…", "operations": [ {"type": "resize", "width": 1600, "height": 1600}, {"type": "watermark", "image": "file_2kqV7PzM1aT4hQ0bR9sX3c", "x": 40, "y": 40} ], "format": "webp", "wait": 20 } → result.files[0].url, valid 7 days ``` `resize` keeps the proportions by default (`preserve_aspect_ratio`), so a landscape photo comes out 1600 px wide and a portrait one 1600 px tall. The same with the CLI, which uploads each local file itself: ```bash logo=$(watermark image process ./logo.png --op orient --format png --json | jq -r '.result.files[0].id') mkdir -p out for f in photos/*; do name=$(basename "${f%.*}") watermark image process "$f" \ --op resize:width=1600,height=1600 \ --op "watermark:image=$logo,x=40,y=40" \ --format webp --out "out/$name.webp" done ``` ### A 30-second preview of a video, with a logo `max_duration` keeps only the first so many seconds, so a long video becomes a short preview, priced by the minutes of the source. Video needs a live key on an account that has bought credit, and a long video rarely finishes within `wait`: expect `202`, then follow the request. ```json process_video { "source": "https://example.com/talk.mp4", "operations": [ {"type": "resize", "width": 1280, "height": 720}, {"type": "watermark", "image_url": "https://example.com/logo.png", "x": 24, "y": 24} ], "max_duration": 30, "wait": 20 } → 202, status "queued" get_request {"id": 1240, "wait": 25} → status "processing", progress 45 … then "succeeded", result.files[0].url ``` ```bash watermark video process https://example.com/talk.mp4 \ --op resize:width=1280,height=720 \ --op watermark:image_url=https://example.com/logo.png,x=24,y=24 \ --max-duration 30 --out preview.mp4 ``` ### CONFIDENTIAL across every page of a PDF, kept as a PDF A document's `watermark` operation places an image on every page, so make the stamp first: a `text` operation on a transparent canvas, turned 30 degrees and trimmed to the text, saved as PNG. Then watermark the PDF with the stamp's `file_…` id. The output stays a PDF (and a DOCX stays a DOCX). ```json create_image { "width": 800, "height": 800, "operations": [ {"type": "text", "text": "CONFIDENTIAL", "font": "inter-bold", "size": 80, "color": "#c0392b", "opacity": 35, "position": "center", "rotation": -30}, {"type": "trim"} ], "format": "png", "wait": 20 } → result.files[0].id = "file_stampId…", about 560 × 340 px create_upload {"filename": "contract.pdf", "size_bytes": 1204911, "content_type": "application/pdf"} → PUT the file process_document { "source": "upl_contractUpload…", "operations": [{"type": "watermark", "image": "file_stampId…", "x": 20, "y": 230}], "wait": 20 } → result.files[0].url: the PDF, one stamp on each page ``` At the default `density` of 72, one pixel of the stamp is one point on the page, so a 560 × 340 px stamp at `x` 20, `y` 230 sits across the middle of a Letter or A4 page. Pass `max_pages` to stamp only the first pages. ```bash stamp=$(watermark image create --width 800 --height 800 \ --op 'text:text=CONFIDENTIAL,font=inter-bold,size=80,color=#c0392b,opacity=35,position=center,rotation=-30' \ --op trim --format png --json | jq -r '.result.files[0].id') watermark document process ./contract.pdf \ --op "watermark:image=$stamp,x=20,y=230" \ --out contract-confidential.pdf ``` ## Read on - [Files in and out](https://api.watermark.ws/docs/files.md): every way to give the API a file and every place it can put a result. - [Requests, results and retries](https://api.watermark.ws/docs/requests.md): statuses, `wait`, idempotency and `result.files`. - [Operations](https://api.watermark.ws/docs/operations.md): every operation, its parameters and a picture of what it does. - [Pricing and limits](https://api.watermark.ws/docs/pricing.md): prices per endpoint and the limits of each kind of key.