# Watermark API Watermark, resize and convert images, videos and PDFs over HTTP. Send a public file URL and a list of operations; the API processes the file in the background and gives you a link to the result. ## How a request works 1. `POST` a source URL and operations to a processing endpoint. The API answers `202 Accepted` with a request in status `queued`. 2. Poll `GET /v1/requests/{id}` until it is `succeeded`, `failed` or `aborted`, or pass `webhook_url` to have it posted to you. 3. Download `result.files[0].url`. Failed and aborted requests are free. ```bash curl -X POST https://api.watermark.ws/v1/images/process \ -H "Authorization: Bearer $WATERMARK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "https://example.com/photo.jpg", "operations": [ {"type": "resize", "width": 1600, "height": 1600}, {"type": "watermark", "image_url": "https://example.com/logo.png", "x": 40, "y": 40} ], "output": {"format": "webp"}, "customer": "cus_42" }' ``` ## Endpoints | Endpoint | What it does | Price | | --- | --- | --- | | [`POST /v1/images/process`](https://api.watermark.ws/docs/reference.md#process-image) | Process image. | $0.002 per request | | [`POST /v1/images/read`](https://api.watermark.ws/docs/reference.md#read-image) | Read image. | $0.0005 per request | | [`POST /v1/videos/process`](https://api.watermark.ws/docs/reference.md#process-video) | Process video. | $0.02 per started minute of video, accounts that have bought credits only | | [`POST /v1/videos/read`](https://api.watermark.ws/docs/reference.md#read-video) | Read video. | $0.001 per request, accounts that have bought credits only | | [`POST /v1/videos/screenshot`](https://api.watermark.ws/docs/reference.md#screenshot-video) | Screenshot video. | $0.002 per request, accounts that have bought credits only | | [`POST /v1/documents/process`](https://api.watermark.ws/docs/reference.md#process-document) | Process document. | $0.004 per page | | [`POST /v1/documents/read`](https://api.watermark.ws/docs/reference.md#read-document) | Read document. | $0.0005 per request | | [`POST /v1/documents/snapshot`](https://api.watermark.ws/docs/reference.md#snapshot-document) | Snapshot document. | $0.002 per request | | [`GET /v1/requests`](https://api.watermark.ws/docs/reference.md#list-requests) | List requests. | free | | [`GET /v1/requests/{id}`](https://api.watermark.ws/docs/reference.md#get-request) | Get request. | free | | [`GET /v1/results/{id}`](https://api.watermark.ws/docs/reference.md#download-result) | Download result. | free | | [`POST /v1/requests/{id}/abort`](https://api.watermark.ws/docs/reference.md#abort-request) | Abort request. | free | | [`GET /v1/account`](https://api.watermark.ws/docs/reference.md#get-account) | Get account. | free | | [`GET /v1/usage`](https://api.watermark.ws/docs/reference.md#get-usage) | Get usage. | free | | [`GET /v1/operations`](https://api.watermark.ws/docs/reference.md#list-operations) | List operations. | free | ## Operations | Operation | Images | Videos | PDFs | What it does | | --- | --- | --- | --- | --- | | [`watermark`](https://api.watermark.ws/docs/operations.md#watermark) | yes | yes | yes | Place an image (a logo, usually a transparent PNG) over the file, at its own size. Resize the watermark image beforehand. | | [`border`](https://api.watermark.ws/docs/operations.md#border) | yes | yes | | Add a border of a color on each side. | | [`resize`](https://api.watermark.ws/docs/operations.md#resize) | yes | yes | | Resize to a width and height. | | [`crop`](https://api.watermark.ws/docs/operations.md#crop) | yes | yes | | Cut out a rectangle. | | [`thumbnail`](https://api.watermark.ws/docs/operations.md#thumbnail) | yes | | | Make a small, fast-loading copy, metadata stripped. | | [`rotate`](https://api.watermark.ws/docs/operations.md#rotate) | yes | yes | | Rotate by degrees, clockwise, and optionally mirror. | | [`flip`](https://api.watermark.ws/docs/operations.md#flip) | yes | yes | | Mirror the file. | | [`orient`](https://api.watermark.ws/docs/operations.md#orient) | yes | | | Turn the image upright from its EXIF orientation. | | [`grayscale`](https://api.watermark.ws/docs/operations.md#grayscale) | yes | yes | | Remove the color. | | [`sepia`](https://api.watermark.ws/docs/operations.md#sepia) | yes | | | Give the image a sepia tone. | | [`exposure`](https://api.watermark.ws/docs/operations.md#exposure) | yes | | | Adjust brightness and contrast. | | [`filter`](https://api.watermark.ws/docs/operations.md#filter) | yes | | | Apply a photo filter. | | [`remove_background`](https://api.watermark.ws/docs/operations.md#remove_background) | yes | | | Make the background color transparent (output PNG or WebP to keep it). | | [`compress`](https://api.watermark.ws/docs/operations.md#compress) | yes | | | Re-encode at a quality, 1-100. | ## Libraries - PHP: `composer require watermark/watermark-php`. See [PHP library](https://api.watermark.ws/docs/php.md). - Command line: `npm install -g @watermark/cli`, or `brew install watermarkws/tap/watermark`. See [Command line](https://api.watermark.ws/docs/cli.md). - Any other language: JSON over HTTP, as in the [API reference](https://api.watermark.ws/docs/reference.md), which shows every call with curl, PHP and the CLI. ## Docs - [Quickstart](https://api.watermark.ws/docs/quickstart.md) - [Authentication](https://api.watermark.ws/docs/authentication.md) - [Requests, results and retries](https://api.watermark.ws/docs/requests.md) - [Webhooks](https://api.watermark.ws/docs/webhooks.md) - [Errors](https://api.watermark.ws/docs/errors.md) - [PHP library](https://api.watermark.ws/docs/php.md) - [Command line](https://api.watermark.ws/docs/cli.md) - [Pricing and limits](https://api.watermark.ws/docs/pricing.md) - [Operations](https://api.watermark.ws/docs/operations.md) - [API reference](https://api.watermark.ws/docs/reference.md) ## Machine-readable - [llms.txt](https://api.watermark.ws/llms.txt), and every page in one file: [llms-full.txt](https://api.watermark.ws/llms-full.txt) - [OpenAPI 3.1 spec](https://api.watermark.ws/openapi.json) - [Prices and limits as JSON](https://api.watermark.ws/pricing.json) - Any docs page as Markdown: add `.md` to its URL, or send `Accept: text/markdown`. --- # Quickstart The Watermark API watermarks, resizes and converts images, videos and PDFs. You send a URL and a list of operations; the API fetches the file, processes it in the background and gives you a link to the result. ## 1. Get a key Sign in at [watermark.ws/developers](https://watermark.ws/developers), open an account and create a key. Keys start with `wm_live_` (charged) or `wm_test_` (free, with small limits). A new account comes with free credit to try the API. ## 2. Start a request ```bash curl https://api.watermark.ws/v1/images/process \ -H "Authorization: Bearer $WATERMARK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "https://example.com/photo.jpg", "operations": [ {"type": "resize", "width": 1600, "height": 1600}, {"type": "watermark", "image_url": "https://example.com/logo.png", "x": 40, "y": 40} ], "output": {"format": "webp"} }' ``` Using PHP or a terminal? The same call with the [PHP library](https://api.watermark.ws/docs/php.md) or the [command line](https://api.watermark.ws/docs/cli.md) also waits for the result for you. The API answers `202 Accepted` with the request, which is `queued`: ```json {"id": 1234, "object": "request", "endpoint": "images/process", "status": "queued", "livemode": true, "created_at": "2026-10-11T15:00:00Z"} ``` ## 3. Get the result Poll the request until its status is `succeeded`, `failed` or `aborted`, or pass `webhook_url` and the API posts the request to you when it finishes (see [Webhooks](https://api.watermark.ws/docs/webhooks.md)). ```bash curl https://api.watermark.ws/v1/requests/1234 -H "Authorization: Bearer $WATERMARK_API_KEY" ``` ```json { "id": 1234, "status": "succeeded", "result": { "data": {"width": 1600, "height": 1067, "format": "WEBP"}, "files": [{"url": "https://…", "url_expires_at": "2026-10-18T15:00:04Z", "format": "webp", "size": 183021}], "deleted_at": "2026-10-18T15:00:04Z" }, "price": {"amount": "0.002000", "currency": "usd", "micro_usd": 2000} } ``` Download `result.files[0].url`. The link works until the result is deleted, 7 days after the request finished. ## What next - Every operation, its parameters and a picture of its result: [Operations](https://api.watermark.ws/docs/operations.md), or `GET /v1/operations`. - Every endpoint, field and response: the [API reference](https://api.watermark.ws/docs/reference.md). - Prices and limits: [Pricing and limits](https://api.watermark.ws/docs/pricing.md). - Machine-readable: the [OpenAPI spec](https://api.watermark.ws/openapi.json), and [llms.txt](https://api.watermark.ws/llms.txt) for AI agents. Add `.md` to any docs URL for its Markdown. --- # Authentication Send your key in the `Authorization` header on every request: ``` Authorization: Bearer wm_live_… ``` - **Live keys** (`wm_live_…`) run on your account's credit. - **Test keys** (`wm_test_…`) are free and return real results, but are limited to 10 requests a minute, 100 a day and 10 MB sources. Use them while you build. Keys belong to an account. Create, name and revoke them at [watermark.ws/developers](https://watermark.ws/developers); a key is shown in full only when it is created. A revoked key stops working at once. Keep keys on your server. Anyone with a live key can spend your credit. [`GET /v1/account`](https://api.watermark.ws/docs/reference.md#get-account) shows the account behind a key: its balance, tier, and the key's limits. --- # Requests, results and retries Every processing endpoint starts a **request** that runs in the background. The response is the request itself, with status `queued`. ## Statuses | Status | Meaning | | --- | --- | | `queued` | Accepted, waiting for a worker. | | `processing` | Running. Video requests report `progress` (0–100). | | `succeeded` | Finished. `result` holds the data and the files. | | `failed` | Finished without a result. `error` says why. Not charged. | | `aborted` | Stopped with `POST /v1/requests/{id}/abort`. Not charged. | Poll `GET /v1/requests/{id}` every few seconds (images take about a second; videos take longer), or pass `webhook_url` to be told (see [Webhooks](https://api.watermark.ws/docs/webhooks.md)). ## Results `result.files[].url` is a download link that works until the file is deleted, 7 days after the request finishes (`url_expires_at`, the same moment as `result.deleted_at`). Anyone with the link can download the file, so share it only where the file may go. Opening it redirects to the file itself. `result.data` describes the output: dimensions and format for images, duration and codec for videos, pages and size for PDFs. Read endpoints (`images/read`, `videos/read`, `documents/read`) return `result.data` only. ## Sources `source` must be a public `http` or `https` URL on the default port. The API refuses private, loopback and cloud-metadata addresses, follows at most 3 redirects, and only processes these formats: - Images: JPEG, PNG, GIF, WebP, TIFF, BMP, HEIC, AVIF - Videos: MP4, MOV, WebM, MKV, AVI, FLV, MPEG, MPEG-TS - Documents: PDF ## Idempotency Send an `Idempotency-Key` header (any unique string up to 255 characters) to make a retry safe. The same key with the same body returns the first request (`200`, header `Idempotent-Replayed: true`) instead of starting and charging a second one. The same key with a different body is a `409`. ## Your own references `customer` (up to 64 characters) and `metadata` (up to 20 keys of text) are stored on the request, returned with it and sent in its webhooks. Use them to tie requests to your own users and orders; [`GET /v1/usage?group_by=customer`](https://api.watermark.ws/docs/reference.md#get-usage) totals them. --- # Webhooks Pass `webhook_url` on a request, or set a default for the account at [watermark.ws/developers](https://watermark.ws/developers), and the API posts the finished request to it as JSON: ```json { "id": "evt_1234_done", "object": "event", "type": "request.succeeded", "created": 1791738000, "data": { "id": 1234, "object": "request", "status": "succeeded", "result": { "…": "…" } } } ``` Event types: `request.succeeded`, `request.failed`, `request.aborted`. Answer with any `2xx` within 10 seconds; anything else is retried 3 times with backoff (about 4, 5 and 7 seconds apart). Use the event `id` to ignore a repeat. ## Verifying the signature Every delivery carries a `Watermark-Signature` header: ``` Watermark-Signature: t=1791738000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd ``` `v1` is the HMAC-SHA256 of `"{t}.{raw body}"`, keyed with your account's webhook secret (`whsec_…`, shown in the Developers area). Compare it in constant time, and refuse a `t` more than 5 minutes old so a recorded delivery cannot be replayed. ### PHP ```php [$t, $v1] = sscanf($_SERVER['HTTP_WATERMARK_SIGNATURE'], 't=%d,v1=%s'); $body = file_get_contents('php://input'); $valid = hash_equals(hash_hmac('sha256', "{$t}.{$body}", $secret), $v1) && abs(time() - $t) <= 300; ``` ### Node.js ```js const crypto = require('crypto'); const [, t, v1] = req.headers['watermark-signature'].match(/^t=(\d+),v1=([0-9a-f]+)$/); const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) && Math.abs(Date.now() / 1000 - t) <= 300; ``` ### Python ```python import hmac, hashlib, re, time t, v1 = re.match(r"t=(\d+),v1=([0-9a-f]+)", signature_header).groups() expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() valid = hmac.compare_digest(expected, v1) and abs(time.time() - int(t)) <= 300 ``` Webhook URLs follow the same rules as sources: public `http` or `https` addresses only. --- # Errors Every error has the same shape and an HTTP status that matches its `type`: ```json { "error": { "type": "invalid_request", "code": "validation_failed", "message": "The operations.0.width field must be between 1 and 20000.", "param": "operations.0.width", "request_id": "1234" } } ``` Branch on `code`; show `message` to people. A validation error also lists every field under `errors`. | Status | Type | Codes | | --- | --- | --- | | 401 | `authentication` | `missing_api_key`, `invalid_api_key` | | 402 | `payment_required` | `insufficient_credits`, `paid_account_required` | | 403 | `permission` | `account_suspended` | | 404 | `not_found` | `request_not_found`, `not_found` | | 409 | `conflict` | `request_finished`, `idempotency_key_reused`, `idempotency_key_in_use` | | 422 | `invalid_request` | `validation_failed`, `url_invalid`, `url_blocked`, `url_unresolvable` | | 429 | `rate_limited` | `rate_limited` (wait for `Retry-After`), `concurrency_limited` (wait for one of your requests to finish), `test_mode_daily_limit` | | 500 | `server_error` | `server_error`: retry; write to support@watermark.ws if it persists | ## A request that fails while processing A request that was accepted but could not be processed ends with status `failed` and an `error` on the request (and its webhook), not an HTTP error: | Code | Meaning | | --- | --- | | `url_blocked`, `url_invalid`, `url_unresolvable` | The source or a watermark image points somewhere the API will not fetch. | | `source_unreachable` | The source could not be downloaded (not found, timed out, refused). | | `source_too_large` | The source is larger than your tier allows. | | `unsupported_media` | The file is not a format the endpoint accepts. | | `encrypted_document` | The PDF is password-protected. | | `processing_failed` | The file could not be processed. | Failed requests are not charged. --- # PHP library The official PHP client for the Watermark API. One `Client` reaches every endpoint, waits for results, retries safely and checks webhook signatures. ## Install ```bash composer require watermark/watermark-php ``` It needs PHP 8.1 or newer. Composer also installs Guzzle 7, which it uses for HTTP. ## Set your key Create a key at [watermark.ws/developers](https://watermark.ws/developers) and keep it out of your code. A `wm_test_…` key is free and returns real results, so use one while you build. ```bash export WATERMARK_API_KEY=wm_test_… ``` ```php use Watermark\Api\Client; $client = new Client(getenv('WATERMARK_API_KEY')); ``` In Laravel, add the key to `config/services.php` and build the client from config: ```php // config/services.php 'watermark' => [ 'key' => env('WATERMARK_API_KEY'), 'webhook_secret' => env('WATERMARK_WEBHOOK_SECRET'), ], // anywhere $client = new Client(config('services.watermark.key')); ``` ## Watermark an image ```php $request = $client->images->process('https://example.com/photo.jpg', [ ['type' => 'resize', 'width' => 1600, 'height' => 1600], ['type' => 'watermark', 'image_url' => 'https://example.com/logo.png', 'x' => 40, 'y' => 40], ], ['output' => ['format' => 'webp']]); $done = $client->requests->wait($request['id']); if ($done['status'] === 'succeeded') { copy($done['result']['files'][0]['url'], 'photo.webp'); } else { echo "{$done['error']['code']}: {$done['error']['message']}"; } ``` `process()` returns at once with the request in status `queued`. `wait()` polls it, every second at first and up to every 5 seconds, until it finishes, and throws `TimeoutException` after 300 seconds (pass a second argument to change that). A request that fails is not an exception: its `status` is `failed`, its `error` says why, and it is not charged. The source must be a public `http` or `https` URL. Operations run in order; every operation and its parameters are on the [Operations](https://api.watermark.ws/docs/operations.md) page. ## More examples ### Put the logo in the bottom-right corner Read the image first to learn its size, then place the logo from the right and bottom edges. ```php $photo = 'https://example.com/photo.jpg'; $info = $client->requests->wait($client->images->read($photo)['id']); ['width' => $width, 'height' => $height] = $info['result']['data']; $client->images->process($photo, [ ['type' => 'watermark', 'image_url' => 'https://example.com/logo.png', 'x' => $width - 316 - 32, 'y' => $height - 44 - 32], ]); ``` ### Watermark every page of a PDF ```php $request = $client->documents->process('https://example.com/contract.pdf', [ ['type' => 'watermark', 'image_url' => 'https://example.com/logo.png', 'x' => 148, 'y' => 712, 'diagonal_lines_color' => '#c0392b', 'diagonal_lines_opacity' => 35], ]); ``` ### Process a video and be told when it is done Videos take longer, so let the API post the result to you instead of waiting in the request that started it. ```php $client->videos->process('https://example.com/clip.mp4', [ ['type' => 'resize', 'width' => 1280, 'height' => 720], ['type' => 'watermark', 'image_url' => 'https://example.com/logo.png', 'x' => 32, 'y' => 32], ], [ 'webhook_url' => 'https://example.com/hooks/watermark', 'customer' => 'cus_42', 'metadata' => ['order' => '1001'], ]); ``` ### Save one frame of a video, or one page of a PDF ```php $frame = $client->videos->screenshot('https://example.com/clip.mp4', 3, ['output' => ['format' => 'jpg']]); $page = $client->documents->snapshot('https://example.com/contract.pdf', 1, ['output' => ['format' => 'png']]); ``` ### Retry a call safely The client already retries rate limits, server errors and dropped connections. To make your own retry safe too (after a crash, say), pass the same `idempotency_key`: a repeat returns the first request instead of starting another. ```php $client->images->process($photo, $operations, ['idempotency_key' => "order-1001-photo-3"]); ``` ### See what each customer used ```php $usage = $client->account->usage(['group_by' => 'customer', 'from' => '2026-10-01']); foreach ($usage['data'] as $row) { echo "{$row['customer']}: {$row['requests']} requests, \${$row['spent']}\n"; } ``` ## Calls | Call | Endpoint | | --- | --- | | `$client->images->process($source, $operations, $options)` | `POST /v1/images/process` | | `$client->images->read($source, $options)` | `POST /v1/images/read` | | `$client->videos->process($source, $operations, $options)` | `POST /v1/videos/process` | | `$client->videos->read($source, $options)` | `POST /v1/videos/read` | | `$client->videos->screenshot($source, $seconds, $options)` | `POST /v1/videos/screenshot` | | `$client->documents->process($source, $operations, $options)` | `POST /v1/documents/process` | | `$client->documents->read($source, $options)` | `POST /v1/documents/read` | | `$client->documents->snapshot($source, $page, $options)` | `POST /v1/documents/snapshot` | | `$client->requests->get($id)` | `GET /v1/requests/{id}` | | `$client->requests->list($limit, $before)` | `GET /v1/requests` | | `$client->requests->abort($id)` | `POST /v1/requests/{id}/abort` | | `$client->requests->wait($id, $timeout)` | Polls `GET /v1/requests/{id}` | | `$client->account->get()` | `GET /v1/account` | | `$client->account->usage($query)` | `GET /v1/usage` | | `$client->account->operations()` | `GET /v1/operations` | Every call returns the decoded JSON as an array, exactly as the [API reference](https://api.watermark.ws/docs/reference.md) shows it. `$options` on the processing calls: | Option | What it does | | --- | --- | | `output` | `['format' => 'webp']`. The formats each endpoint takes are in the reference. | | `webhook_url` | Post the finished request here. See [Webhooks](https://api.watermark.ws/docs/webhooks.md). | | `customer` | Your own reference, up to 64 characters, totalled by usage. | | `metadata` | Up to 20 keys of your own, returned with the request. | | `idempotency_key` | Sent as the `Idempotency-Key` header. | | `max_duration`, `max_pages` | Videos and documents: keep only the first seconds or pages. | The client takes options of its own as a second argument: ```php $client = new Client(getenv('WATERMARK_API_KEY'), [ 'timeout' => 30, // seconds per HTTP call 'max_retries' => 2, // for 429, 5xx and dropped connections 'http_client' => $yourGuzzleClient, // proxies, logging middleware, tests ]); ``` ## Errors When the API refuses a call, the client throws a subclass of `Watermark\Api\Exceptions\ApiException`. Branch on the class or on `getErrorCode()`, and show `getMessage()` to people. ```php use Watermark\Api\Exceptions\ApiException; use Watermark\Api\Exceptions\InvalidRequestException; use Watermark\Api\Exceptions\PaymentRequiredException; try { $client->images->process($photo, $operations); } catch (InvalidRequestException $e) { echo "{$e->getParam()}: {$e->getMessage()}"; // operations.0.width: must be between 1 and 20000 } catch (PaymentRequiredException $e) { // insufficient_credits: buy credit at watermark.ws/developers } catch (ApiException $e) { report($e->getErrorCode()); } ``` | Exception | Status | Codes | | --- | --- | --- | | `AuthenticationException` | 401 | `missing_api_key`, `invalid_api_key` | | `PaymentRequiredException` | 402 | `insufficient_credits`, `paid_account_required` | | `PermissionException` | 403 | `account_suspended` | | `NotFoundException` | 404 | `request_not_found` | | `ConflictException` | 409 | `request_finished`, `idempotency_key_reused` | | `InvalidRequestException` | 422 | `validation_failed`, `url_invalid`, `url_blocked` | | `RateLimitException` | 429 | `rate_limited`, `concurrency_limited` (after the retries ran out) | | `ApiException` | 500 | `server_error` | Every code is explained in [Errors](https://api.watermark.ws/docs/errors.md). ## Webhooks `Webhook::constructEvent()` checks the `Watermark-Signature` header against your webhook secret (`whsec_…`, in the Developers area), refuses deliveries more than 5 minutes old, and returns the event. ```php use Illuminate\Http\Request; use Watermark\Api\Exceptions\WebhookSignatureException; use Watermark\Api\Webhook; Route::post('/hooks/watermark', function (Request $request) { try { $event = Webhook::constructEvent( $request->getContent(), $request->header('Watermark-Signature', ''), config('services.watermark.webhook_secret'), ); } catch (WebhookSignatureException $e) { abort(400); } if ($event['type'] === 'request.succeeded') { SaveWatermarkedFile::dispatch($event['data']); } return response()->noContent(); }); ``` Answer within 10 seconds and do the slow work in a queued job. Leave the route out of CSRF protection. Use the event `id` to ignore a delivery you have already handled. ## Source MIT licensed, on GitHub at [watermarkws/watermark-php](https://github.com/watermarkws/watermark-php). Changes are listed in its releases. --- # Command line The `watermark` command runs the Watermark API from a terminal, a shell script or a CI job. It waits for each request, downloads the result, and with `--json` prints output that scripts and AI agents can read. ## Install Run it once without installing anything (Node.js 18 or newer): ```bash npx @watermark/cli --version ``` Or install it, so `watermark` is on your path: ```bash npm install -g @watermark/cli ``` On macOS or Linux with Homebrew: ```bash brew install watermarkws/tap/watermark ``` It has no dependencies besides Node.js. Check the install with `watermark --version`. ## Set your key Create a key at [watermark.ws/developers](https://watermark.ws/developers) and export it. A `wm_test_…` key is free and returns real results. ```bash export WATERMARK_API_KEY=wm_test_… ``` `--api-key` on a single command works too, but leaves the key in your shell history. ## Watermark an image ```console $ watermark image process https://example.com/photo.jpg \ --op resize:width=1600,height=1600 \ --op watermark:image_url=https://example.com/logo.png,x=40,y=40 \ --format webp --out photo.webp Request 1234 (images/process): succeeded Result: {"width":1600,"height":1067,"format":"WEBP","size":183021} File: https://… (link valid until 2026-10-18T15:00:04Z) Saved to photo.webp Price: $0.002000 ``` The command waits for the request to finish (up to `--timeout`, 300 seconds), and `--out` downloads the result. The source must be a public `http` or `https` URL. ## Operations Each `--op` is one operation, and they run in the order you give them: ```text --op type:param=value,param=value ``` Numbers and `true`/`false` are sent as such; everything else as text. For anything awkward to write that way, pass the whole list as JSON: ```bash watermark image process https://example.com/photo.jpg \ --ops-json '[{"type":"filter","name":"nashville","strength":50},{"type":"compress","quality":70}]' ``` `watermark operations image` (or `video`, `document`) lists every operation and its parameters, read live from the API. The [Operations](https://api.watermark.ws/docs/operations.md) page shows each with a picture. ## More examples ### Watermark every page of a PDF ```bash watermark document process https://example.com/contract.pdf \ --op watermark:image_url=https://example.com/logo.png,x=148,y=712,diagonal_lines_color=#c0392b,diagonal_lines_opacity=35 \ --out contract-watermarked.pdf ``` ### Watermark the first minute of a video ```console $ watermark video process https://example.com/clip.mp4 \ --op resize:width=1280,height=720 \ --op watermark:image_url=https://example.com/logo.png,x=32,y=32 \ --max-duration 60 --out clip.mp4 Request 1240: processing 45% ``` Video requests report their progress while they run. ### Save one frame, or one page ```bash watermark video screenshot https://example.com/clip.mp4 --seconds 3 --out poster.jpg watermark document snapshot https://example.com/contract.pdf --page 1 --format png --out page-1.png ``` ### Start now, collect later ```bash watermark image process https://example.com/photo.jpg --op grayscale --no-wait --json | jq .id watermark request get 1234 ``` ### A whole list of files ```bash mkdir -p out while read -r url; do watermark image process "$url" \ --op watermark:image_url=https://example.com/logo.png,x=40,y=40 \ --customer cus_42 --out "out/$(basename "$url")" done < urls.txt ``` ### Read a file's details in a script ```bash width=$(watermark image read https://example.com/photo.jpg --json | jq '.result.data.width') ``` ## For scripts and AI agents - `--json` prints the request exactly as the API returns it, and nothing else on stdout. Progress goes to stderr. - The exit code says what happened: | Code | Meaning | | --- | --- | | `0` | Succeeded (or queued, with `--no-wait`). | | `1` | The API refused the call, or the request failed or was aborted. The error code is printed. | | `2` | The command was used wrongly. | - Rate limits and server errors are retried twice with the same `Idempotency-Key`. Pass `--idempotency-key` to make a retry of the whole command safe too. ## Commands ```text watermark image process --op [--op ...] [--format webp] [--out file] watermark image read watermark video process --op [--max-duration 30] [--out file] watermark video read watermark video screenshot --seconds 5 [--format png] [--out file] watermark document process --op [--max-pages 10] [--out file] watermark document read watermark document snapshot [--page 1] [--format png] [--out file] watermark request get watermark request abort watermark requests [--limit 20] watermark account watermark usage [--from 2026-10-01] [--to 2026-10-31] [--group-by day|endpoint|customer|status] watermark operations [image|video|document] ``` | Option | What it does | | --- | --- | | `--out ` | Wait, then download the result to this file. | | `--no-wait` | Return as soon as the request is queued. | | `--timeout ` | How long to wait. Default 300. | | `--format ` | The output format, such as `webp` or `png`. | | `--webhook ` | Post the finished request here. | | `--customer ` | Your own reference for who the request is for. | | `--metadata key=value` | Repeatable. Returned with the request. | | `--idempotency-key ` | Make a retry of this command safe. | | `--json` | Print JSON only. | | `--api-key ` | Or set `WATERMARK_API_KEY`. | | `--base-url ` | Or set `WATERMARK_API_URL`. Default `https://api.watermark.ws`. | ## Source MIT licensed, on GitHub at [watermarkws/watermark-cli](https://github.com/watermarkws/watermark-cli). --- # Pricing and limits You buy credit in advance at [watermark.ws/developers](https://watermark.ws/developers); each finished request takes its price from it. Failed and aborted requests are free, and so is everything run with a test key. A new account starts with $1.00 of free credit, which covers images and documents. Video needs an account that has bought credit. | Endpoint | Price | | --- | --- | | `images/process` | $0.002 per request | | `images/read` | $0.0005 per request | | `videos/process` | $0.02 per started minute of video (accounts that have bought credits only) | | `videos/read` | $0.001 per request (accounts that have bought credits only) | | `videos/screenshot` | $0.002 per request (accounts that have bought credits only) | | `documents/process` | $0.004 per page | | `documents/read` | $0.0005 per request | | `documents/snapshot` | $0.002 per request | ## Limits | Key | Requests a minute | Running at once | Largest source | Requests a day | | --- | --- | --- | --- | --- | | Test key | 10 | 2 | 10 MB | 100 | | Live key, free account | 30 | 2 | 25 MB | no limit | | Live key, paid account | 120 | 10 | 2 GB | no limit | An account with no credit left gets `402 insufficient_credits`. Going over requests a minute gets `429 rate_limited` with `Retry-After`. "Running at once" counts the account's requests that are queued or processing; one more gets `429 concurrency_limited` until one of them finishes. --- # Operations Operations run in order, top to bottom. Each is an object with a `type` and that operation's parameters; anything else is refused with `422`. The same catalog is served as JSON by `GET /v1/operations`, and with a picture of each example at https://api.watermark.ws/docs/operations. | Operation | Images | Videos | PDFs | What it does | | --- | --- | --- | --- | --- | | [`watermark`](#watermark) | yes | yes | yes | Place an image (a logo, usually a transparent PNG) over the file, at its own size. Resize the watermark image beforehand. | | [`border`](#border) | yes | yes | | Add a border of a color on each side. | | [`resize`](#resize) | yes | yes | | Resize to a width and height. | | [`crop`](#crop) | yes | yes | | Cut out a rectangle. | | [`thumbnail`](#thumbnail) | yes | | | Make a small, fast-loading copy, metadata stripped. | | [`rotate`](#rotate) | yes | yes | | Rotate by degrees, clockwise, and optionally mirror. | | [`flip`](#flip) | yes | yes | | Mirror the file. | | [`orient`](#orient) | yes | | | Turn the image upright from its EXIF orientation. | | [`grayscale`](#grayscale) | yes | yes | | Remove the color. | | [`sepia`](#sepia) | yes | | | Give the image a sepia tone. | | [`exposure`](#exposure) | yes | | | Adjust brightness and contrast. | | [`filter`](#filter) | yes | | | Apply a photo filter. | | [`remove_background`](#remove_background) | yes | | | Make the background color transparent (output PNG or WebP to keep it). | | [`compress`](#compress) | yes | | | Re-encode at a quality, 1-100. | Output formats: images `jpg`, `png`, `webp`, `gif`, `tif` (the first is the default); videos `mp4`; PDFs `pdf`. ## Brand it Put your mark on a photo, a video or every page of a PDF. ### watermark Place your logo, usually a transparent PNG, over a photo, every frame of a video or every page of a PDF. It goes on at the logo's own size, so resize the logo first. `x` and `y` count pixels from the top-left corner. Works on: Images, Videos, PDFs. Tip: To use another corner, read the file first: for the bottom right, x is the image width minus the logo width minus a margin (900 − 316 − 32 = 552). Tile spacing is the step from one copy to the next, so make it wider than the logo. | Parameter | Accepts | Required | Default | Description | | --- | --- | --- | --- | --- | | `image_url` | an http(s) URL | yes | | URL of the watermark image (JPEG, PNG, GIF or WebP). | | `x` | a whole number, -20000 to 20000 | yes | | The x position in pixels, from the top left. | | `y` | a whole number, -20000 to 20000 | yes | | The y position in pixels, from the top left. | | `tile_spacing_x` | a number, 0 to 20000 | no | | Repeat the watermark across the file: the step in pixels from one copy's left edge to the next (with tile_spacing_y). | | `tile_spacing_y` | a number, 0 to 20000 | no | | When tiling, the step in pixels from one copy's top edge to the next. | | `diagonal_lines_color` | a hex color | no | | Also draw two diagonal lines corner to corner in this color. A hex color, such as "#ffffff". | | `diagonal_lines_opacity` | a whole number, 0 to 100 | no | | Opacity of the diagonal lines, 0-100. | A logo in the bottom-right corner (`POST /v1/images/process`; the result is 900 × 600 · JPG · 96 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "watermark", "image_url": "https://example.com/logo.png", "x": 552, "y": 524} ] } ``` Repeated across the whole photo (`POST /v1/images/process`; the result is 900 × 600 · JPG · 110 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "watermark", "image_url": "https://example.com/logo.png", "x": 40, "y": 36, "tile_spacing_x": 440, "tile_spacing_y": 140} ] } ``` On every page of a PDF, with diagonal lines (`POST /v1/documents/process`; the result is PDF · 100 KB): ```json { "source": "https://example.com/contract.pdf", "operations": [ {"type": "watermark", "image_url": "https://example.com/logo.png", "x": 148, "y": 712, "diagonal_lines_color": "#c0392b", "diagonal_lines_opacity": 35} ] } ``` ### border Add a frame of one color, as wide as you like on each side. The file grows by the border. Works on: Images, Videos. | Parameter | Accepts | Required | Default | Description | | --- | --- | --- | --- | --- | | `top` | a whole number, 0 to 2000 | yes | | Top border in pixels. | | `bottom` | a whole number, 0 to 2000 | yes | | Bottom border in pixels. | | `left` | a whole number, 0 to 2000 | yes | | Left border in pixels. | | `right` | a whole number, 0 to 2000 | yes | | Right border in pixels. | | `color` | a hex color | yes | | The border color. A hex color, such as "#ffffff". | A white frame, wider at the bottom (`POST /v1/images/process`; the result is 948 × 696 · JPG · 96 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "border", "top": 24, "bottom": 72, "left": 24, "right": 24, "color": "#ffffff"} ] } ``` ## Size and shape Fit a file to where it is going: a size, a square, the right way up. ### resize Make the file a new size. By default it fits inside the width and height you give and keeps its shape; `crop_center` fills the box instead and trims what overflows. Works on: Images, Videos. | Parameter | Accepts | Required | Default | Description | | --- | --- | --- | --- | --- | | `width` | a whole number, 1 to 20000 | yes | | The width in pixels. | | `height` | a whole number, 1 to 20000 | yes | | The height in pixels. | | `preserve_aspect_ratio` | true or false | no | `true` | Fit inside the box instead of stretching to it. | | `crop_center` | true or false | no | `false` | Fill the box and crop what overflows, centered. | Fit inside 450 × 450 (`POST /v1/images/process`; the result is 450 × 300 · JPG · 26 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "resize", "width": 450, "height": 450} ] } ``` Fill a 400 × 400 square (`POST /v1/images/process`; the result is 400 × 400 · JPG · 29 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "resize", "width": 400, "height": 400, "crop_center": true} ] } ``` ### crop Cut out a rectangle: its top-left corner at `x`, `y`, and its width and height, all in pixels. Works on: Images, Videos. | Parameter | Accepts | Required | Default | Description | | --- | --- | --- | --- | --- | | `x` | a whole number, 0 to 20000 | yes | | Left edge in pixels. | | `y` | a whole number, 0 to 20000 | yes | | Top edge in pixels. | | `width` | a whole number, 1 to 20000 | yes | | The width in pixels. | | `height` | a whole number, 1 to 20000 | yes | | The height in pixels. | A 400 × 400 square from the right side (`POST /v1/images/process`; the result is 400 × 400 · JPG · 42 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "crop", "x": 470, "y": 130, "width": 400, "height": 400} ] } ``` ### thumbnail A small, fast-loading copy for previews and lists: resized, compressed and stripped of metadata in one step. Leave out the height to keep the shape. Works on: Images. | Parameter | Accepts | Required | Default | Description | | --- | --- | --- | --- | --- | | `width` | a whole number, 1 to 4000 | yes | | The width in pixels. | | `height` | a whole number, 1 to 4000 | no | | The height in pixels; kept in proportion when left out. | | `quality` | a whole number, 1 to 100 | no | | Compression quality, 1-100. | 300 pixels wide, quality 70 (`POST /v1/images/process`; the result is 300 × 200 · JPG · 8 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "thumbnail", "width": 300, "quality": 70} ] } ``` ### rotate Turn the file clockwise by any number of degrees (negative turns it the other way), and mirror it at the same time if you need to. Works on: Images, Videos. | Parameter | Accepts | Required | Default | Description | | --- | --- | --- | --- | --- | | `degrees` | a whole number, -360 to 360 | yes | | Degrees clockwise. | | `flip_horizontal` | true or false | no | `false` | Mirror left to right. | | `flip_vertical` | true or false | no | `false` | Mirror top to bottom. | A quarter turn clockwise (`POST /v1/images/process`; the result is 600 × 900 · JPG · 95 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "rotate", "degrees": 90} ] } ``` ### flip Mirror the file left to right, top to bottom, or both. Works on: Images, Videos. | Parameter | Accepts | Required | Default | Description | | --- | --- | --- | --- | --- | | `horizontal` | true or false | no | `false` | Mirror left to right. | | `vertical` | true or false | no | `false` | Mirror top to bottom. | Mirrored left to right (`POST /v1/images/process`; the result is 900 × 600 · JPG · 96 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "flip", "horizontal": true} ] } ``` ### orient Phones often save a photo sideways and only note which way is up. This turns the pixels upright, so every app shows it the right way. Works on: Images. No parameters: `{"type": "orient"}` is the whole operation. A phone photo stored sideways, turned upright (`POST /v1/images/process`; the result is 900 × 600 · JPG · 96 KB): ```json { "source": "https://example.com/phone-photo.jpg", "operations": [ {"type": "orient"} ] } ``` ## Look and file size Change the colors, cut out a background, or make the file smaller. ### grayscale Remove the color. Works on: Images, Videos. No parameters: `{"type": "grayscale"}` is the whole operation. Black and white (`POST /v1/images/process`; the result is 900 × 600 · JPG · 88 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "grayscale"} ] } ``` ### sepia A warm, old-photo tone. `threshold` sets how strong it is. Works on: Images. | Parameter | Accepts | Required | Default | Description | | --- | --- | --- | --- | --- | | `threshold` | a whole number, 0 to 100 | yes | | How strong the tone is, 0-100. | Threshold 80 (`POST /v1/images/process`; the result is 900 × 600 · JPG · 98 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "sepia", "threshold": 80} ] } ``` ### exposure Brighten or darken, and add or take away contrast, each from -100 to 100. Works on: Images. | Parameter | Accepts | Required | Default | Description | | --- | --- | --- | --- | --- | | `brightness` | a whole number, -100 to 100 | no | `0` | From -100 to 100. | | `contrast` | a whole number, -100 to 100 | no | `0` | From -100 to 100. | Brighter, with more contrast (`POST /v1/images/process`; the result is 900 × 600 · JPG · 101 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "exposure", "brightness": 10, "contrast": 45} ] } ``` ### filter One of 18 photo filters, at full strength or blended in. `strength` 50 is half the look. Works on: Images. | Parameter | Accepts | Required | Default | Description | | --- | --- | --- | --- | --- | | `name` | one of amaro, apollo, brannan, earlybird, gotham, hefe, inkwell, kelvin, lomo, nashville, poprocket, rise, sierra, toaster, valencia, walden, willow, xpro2 | yes | | Which one to use. | | `strength` | a whole number, 0 to 100 | no | `100` | How much of the look, 0-100. | Toaster (`POST /v1/images/process`; the result is 900 × 600 · JPG · 91 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "filter", "name": "toaster"} ] } ``` Nashville at half strength (`POST /v1/images/process`; the result is 900 × 600 · JPG · 85 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "filter", "name": "nashville", "strength": 50} ] } ``` ### remove_background Make the background color transparent: the color in the corners, and everything close to it within `tolerance`. Ask for PNG or WebP output to keep the transparency. Made for logos, signatures and stamps on a plain background. Works on: Images. | Parameter | Accepts | Required | Default | Description | | --- | --- | --- | --- | --- | | `tolerance` | a whole number, 0 to 100 | yes | | How far from the background color still counts as background, 0-100. | | `blur` | a whole number, 0 to 10 | no | `0` | Soften the cut edge, 0-10. | A signature lifted off the paper (`POST /v1/images/process`; the result is 900 × 420 · PNG · 332 KB): ```json { "source": "https://example.com/signature.jpg", "operations": [ {"type": "remove_background", "tolerance": 20, "blur": 1} ], "output": {"format": "png"} } ``` ### compress Save at a lower quality to make the file smaller. Around 70 is hard to tell apart from the original. Works on: Images. | Parameter | Accepts | Required | Default | Description | | --- | --- | --- | --- | --- | | `quality` | a whole number, 1 to 100 | yes | | Compression quality, 1-100. | Quality 40 (`POST /v1/images/process`; the result is 900 × 600 · JPG · 39 KB): ```json { "source": "https://example.com/photo.jpg", "operations": [ {"type": "compress", "quality": 40} ] } ``` ## Other endpoints These take a file and one value instead of a list of operations. ### Screenshot a video Save one frame of a video as a JPEG or PNG: a poster, a preview, a cover. `POST /v1/videos/screenshot`: ```json { "source": "https://example.com/clip.mp4", "seconds": 3, "output": {"format": "jpg"} } ``` ### Snapshot a PDF page Render one page of a PDF as an image, to preview a document without a PDF viewer. `POST /v1/documents/snapshot`: ```json { "source": "https://example.com/contract.pdf", "page": 1, "output": {"format": "png"} } ``` ### Read a file Find out what a file is before you change it: its size and format, a video's duration, a PDF's pages. Use it to place a watermark in a corner. `POST /v1/images/read`: ```json { "source": "https://example.com/photo.jpg" } ``` `result.data`: ```json { "frames": 1, "orientation": "Undefined", "format": "JPEG", "colorspace": "sRGB", "width": 900, "height": 600, "size": 97451 } ``` --- # API reference All endpoints are under https://api.watermark.ws. Send JSON bodies and your key in the `Authorization` header. Processing endpoints answer `202 Accepted` with a request in status `queued`; read it with `GET /v1/requests/{id}`, or receive its webhook. The same reference as an OpenAPI 3.1 spec: https://api.watermark.ws/openapi.json. Headers on every processing endpoint: | Name | Type | Required | Description | | --- | --- | --- | --- | | `Authorization` | header | yes | `Bearer wm_live_…` or `Bearer wm_test_…`. See [Authentication](https://api.watermark.ws/docs/authentication.md). | | `Idempotency-Key` | header, up to 255 characters | no | Makes a retry safe: the same key with the same body returns the first request instead of starting another. See [Idempotency](https://api.watermark.ws/docs/requests.md#idempotency). | ## The request object What every processing endpoint returns, and what `GET /v1/requests/{id}` and webhooks carry. | Name | Type | Description | | --- | --- | --- | | `id` | integer | The request's id. | | `object` | string | Always `request`. | | `endpoint` | string | Which endpoint started it, such as `images/process`. | | `status` | string | Where the request is. Only `succeeded` is charged. One of: `queued`, `processing`, `succeeded`, `failed`, `aborted`. | | `livemode` | boolean | `false` for a test key's request. | | `source` | string | The URL the file was downloaded from. | | `input` | object | What was sent besides the source: operations, output and the endpoint's own fields. | | `progress` | integer, 0 to 100 | How far a video has got while it is processing. | | `result` | object | Once succeeded. | | `result.data` | object | About the output: dimensions and format for images, duration and codec for videos, pages and size for PDFs. | | `result.files` | array | The files made. Read endpoints make none. | | `result.files[].url` | URL | A signed link to download the file. | | `result.files[].url_expires_at` | timestamp | When the link stops working: when the result is deleted. | | `result.files[].format` | string | The file format, such as `webp`. | | `result.files[].size` | integer or null | Bytes. | | `result.deleted_at` | timestamp | When the files are deleted, 7 days after the request finished. | | `error` | object | Once failed. See [Errors](https://api.watermark.ws/docs/errors.md#a-request-that-fails-while-processing). | | `error.code` | string | Stable; branch on it. | | `error.message` | string | For people. | | `price` | Money object | An amount of money. | | `price.amount` | string | Dollars, six decimals. | | `price.currency` | string | Always `usd`. | | `price.micro_usd` | integer | Millionths of a dollar. | | `customer` | string | As sent. | | `metadata` | object | As sent. | | `created_at` | timestamp | When the request was accepted. | | `completed_at` | timestamp | When it finished. | ```json { "id": 1234, "object": "request", "endpoint": "images/process", "status": "succeeded", "livemode": true, "source": "https://example.com/photo.jpg", "result": { "data": { "width": 1600, "height": 1067, "format": "WEBP", "size": 183021 }, "files": [ { "url": "https://api.watermark.ws/v1/results/1234?expires=1792335604&signature=…", "url_expires_at": "2026-10-18T15:00:04Z", "format": "webp", "size": 183021 } ], "deleted_at": "2026-10-18T15:00:04Z" }, "price": { "amount": "0.002000", "currency": "usd", "micro_usd": 2000 }, "customer": "cus_42", "created_at": "2026-10-11T15:00:00Z", "completed_at": "2026-10-11T15:00:04Z" } ``` ## The event object Posted to `webhook_url` when a request finishes, signed in the `Watermark-Signature` header. See [Webhooks](https://api.watermark.ws/docs/webhooks.md). | Name | Type | Description | | --- | --- | --- | | `id` | string | The same on every retry of a delivery. Use it to ignore a repeat. | | `object` | string | Always `event`. | | `type` | string | What happened. One of: `request.succeeded`, `request.failed`, `request.aborted`. | | `created` | integer | Unix time. | | `data` | Request object | | ```json { "id": "evt_1234_done", "object": "event", "type": "request.succeeded", "created": 1791738000, "data": { "id": 1234, "object": "request", "status": "succeeded", "result": { "…": "…" } } } ``` ## Errors Every error has one shape, and an HTTP status that matches its `type`. See [Errors](https://api.watermark.ws/docs/errors.md) for every code. | Status | Meaning | | --- | --- | | 401 | No key, or not a valid one. | | 402 | No credit left, or the endpoint needs an account that has paid. | | 404 | Not found on this account. | | 409 | A conflict: the request has finished, or the Idempotency-Key was used for another request. | | 422 | Invalid input, or a URL the API will not use. | | 429 | Too many requests a minute (rate_limited, see Retry-After), or too many of the account's requests running at once (concurrency_limited). | ```json { "error": { "type": "invalid_request", "code": "validation_failed", "message": "The operations.0.width field must be between 1 and 20000.", "param": "operations.0.width", "request_id": "1234" } } ``` ## Images JPEG, PNG, GIF, WebP, TIFF, BMP, HEIC and AVIF in; JPEG, PNG, WebP, GIF or TIFF out. ### POST /v1/images/process Process image. Run operations on an image, in order, and save the result. Price: $0.002 per request. Failed requests are free. Body parameters: | Name | Type | Required | Description | | --- | --- | --- | --- | | `source` | URL | yes | A public `http` or `https` URL of the file. See [Sources](https://api.watermark.ws/docs/requests.md#sources). | | `operations` | array of operations, 1 to 20 | yes | Run in order, top to bottom. Each is `{"type": …}` plus that operation's parameters. See [Operations](https://api.watermark.ws/docs/operations.md). | | `output` | object | no | How to save the result. | | `output.format` | string | no | The file format of the result. One of: `jpg`, `png`, `webp`, `gif`, `tif`. Default: `jpg`. | | `webhook_url` | URL | no | Where to post the request when it finishes. Defaults to the account's webhook, if set. See [Webhooks](https://api.watermark.ws/docs/webhooks.md). | | `customer` | string, up to 64 characters | no | Your own reference for who the request is for. Returned with it, and totalled by `GET /v1/usage?group_by=customer`. | | `metadata` | object, up to 20 keys | no | Up to 20 keys of your own, returned with the request and in its webhooks. | cURL: ```bash curl -X POST https://api.watermark.ws/v1/images/process \ -H "Authorization: Bearer $WATERMARK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "https://example.com/photo.jpg", "operations": [ {"type": "resize", "width": 1600, "height": 1600}, {"type": "watermark", "image_url": "https://example.com/logo.png", "x": 40, "y": 40} ], "output": {"format": "webp"}, "customer": "cus_42" }' ``` PHP: ```php $client = new \Watermark\Api\Client(getenv('WATERMARK_API_KEY')); $request = $client->images->process('https://example.com/photo.jpg', [ ['type' => 'resize', 'width' => 1600, 'height' => 1600], ['type' => 'watermark', 'image_url' => 'https://example.com/logo.png', 'x' => 40, 'y' => 40], ], [ 'output' => ['format' => 'webp'], 'customer' => 'cus_42', ]); ``` CLI: ```bash # Waits for the result; add --no-wait to return as soon as it is queued. watermark image process https://example.com/photo.jpg \ --op resize:width=1600,height=1600 \ --op watermark:image_url=https://example.com/logo.png,x=40,y=40 \ --format webp --customer cus_42 --out photo-watermarked.webp ``` Returns `202`: Accepted and queued. Returns `200`: The request first sent with this Idempotency-Key. ```json { "id": 1234, "object": "request", "endpoint": "images/process", "status": "queued", "livemode": true, "source": "https://example.com/photo.jpg", "created_at": "2026-10-11T15:00:00Z" } ``` Errors: 401, 402, 409, 422, 429. ### POST /v1/images/read Read image. Read dimensions, format, colorspace and orientation. Price: $0.0005 per request. Failed requests are free. Body parameters: | Name | Type | Required | Description | | --- | --- | --- | --- | | `source` | URL | yes | A public `http` or `https` URL of the file. See [Sources](https://api.watermark.ws/docs/requests.md#sources). | | `webhook_url` | URL | no | Where to post the request when it finishes. Defaults to the account's webhook, if set. See [Webhooks](https://api.watermark.ws/docs/webhooks.md). | | `customer` | string, up to 64 characters | no | Your own reference for who the request is for. Returned with it, and totalled by `GET /v1/usage?group_by=customer`. | | `metadata` | object, up to 20 keys | no | Up to 20 keys of your own, returned with the request and in its webhooks. | cURL: ```bash curl -X POST https://api.watermark.ws/v1/images/read \ -H "Authorization: Bearer $WATERMARK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "https://example.com/photo.jpg" }' ``` PHP: ```php $client = new \Watermark\Api\Client(getenv('WATERMARK_API_KEY')); $request = $client->images->read('https://example.com/photo.jpg'); ``` CLI: ```bash # Waits for the result; add --no-wait to return as soon as it is queued. watermark image read https://example.com/photo.jpg ``` Returns `202`: Accepted and queued. Returns `200`: The request first sent with this Idempotency-Key. ```json { "id": 1234, "object": "request", "endpoint": "images/read", "status": "queued", "livemode": true, "source": "https://example.com/photo.jpg", "created_at": "2026-10-11T15:00:00Z" } ``` `result.data` once it has succeeded: ```json { "frames": 1, "orientation": "Undefined", "format": "JPEG", "colorspace": "sRGB", "width": 900, "height": 600, "size": 97451 } ``` Errors: 401, 402, 409, 422, 429. ## Videos MP4, MOV, WebM, MKV, AVI, FLV, MPEG and MPEG-TS in; MP4 out. Needs an account that has bought credits. ### POST /v1/videos/process Process video. Run operations on a video, in order, and save an MP4. Reports `progress` while it runs. Price: $0.02 per started minute of video, accounts that have bought credits only. Failed requests are free. Body parameters: | Name | Type | Required | Description | | --- | --- | --- | --- | | `source` | URL | yes | A public `http` or `https` URL of the file. See [Sources](https://api.watermark.ws/docs/requests.md#sources). | | `operations` | array of operations, 1 to 20 | yes | Run in order, top to bottom. Each is `{"type": …}` plus that operation's parameters. See [Operations](https://api.watermark.ws/docs/operations.md). | | `output` | object | no | How to save the result. | | `output.format` | string | no | The file format of the result. One of: `mp4`. Default: `mp4`. | | `max_duration` | integer, 1 to 7200 | no | Only keep the first this many seconds. | | `webhook_url` | URL | no | Where to post the request when it finishes. Defaults to the account's webhook, if set. See [Webhooks](https://api.watermark.ws/docs/webhooks.md). | | `customer` | string, up to 64 characters | no | Your own reference for who the request is for. Returned with it, and totalled by `GET /v1/usage?group_by=customer`. | | `metadata` | object, up to 20 keys | no | Up to 20 keys of your own, returned with the request and in its webhooks. | cURL: ```bash curl -X POST https://api.watermark.ws/v1/videos/process \ -H "Authorization: Bearer $WATERMARK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "https://example.com/clip.mp4", "operations": [ {"type": "watermark", "image_url": "https://example.com/logo.png", "x": 24, "y": 24} ], "webhook_url": "https://example.com/hooks/watermark" }' ``` PHP: ```php $client = new \Watermark\Api\Client(getenv('WATERMARK_API_KEY')); $request = $client->videos->process('https://example.com/clip.mp4', [ ['type' => 'watermark', 'image_url' => 'https://example.com/logo.png', 'x' => 24, 'y' => 24], ], [ 'webhook_url' => 'https://example.com/hooks/watermark', ]); ``` CLI: ```bash # Waits for the result; add --no-wait to return as soon as it is queued. watermark video process https://example.com/clip.mp4 \ --op watermark:image_url=https://example.com/logo.png,x=24,y=24 \ --webhook https://example.com/hooks/watermark --out clip-watermarked.mp4 ``` Returns `202`: Accepted and queued. Returns `200`: The request first sent with this Idempotency-Key. ```json { "id": 1234, "object": "request", "endpoint": "videos/process", "status": "queued", "livemode": true, "source": "https://example.com/clip.mp4", "created_at": "2026-10-11T15:00:00Z" } ``` Errors: 401, 402, 409, 422, 429. ### POST /v1/videos/read Read video. Read dimensions, duration, codec, bitrate and frame rate. Price: $0.001 per request, accounts that have bought credits only. Failed requests are free. Body parameters: | Name | Type | Required | Description | | --- | --- | --- | --- | | `source` | URL | yes | A public `http` or `https` URL of the file. See [Sources](https://api.watermark.ws/docs/requests.md#sources). | | `webhook_url` | URL | no | Where to post the request when it finishes. Defaults to the account's webhook, if set. See [Webhooks](https://api.watermark.ws/docs/webhooks.md). | | `customer` | string, up to 64 characters | no | Your own reference for who the request is for. Returned with it, and totalled by `GET /v1/usage?group_by=customer`. | | `metadata` | object, up to 20 keys | no | Up to 20 keys of your own, returned with the request and in its webhooks. | cURL: ```bash curl -X POST https://api.watermark.ws/v1/videos/read \ -H "Authorization: Bearer $WATERMARK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "https://example.com/clip.mp4" }' ``` PHP: ```php $client = new \Watermark\Api\Client(getenv('WATERMARK_API_KEY')); $request = $client->videos->read('https://example.com/clip.mp4'); ``` CLI: ```bash # Waits for the result; add --no-wait to return as soon as it is queued. watermark video read https://example.com/clip.mp4 ``` Returns `202`: Accepted and queued. Returns `200`: The request first sent with this Idempotency-Key. ```json { "id": 1234, "object": "request", "endpoint": "videos/read", "status": "queued", "livemode": true, "source": "https://example.com/clip.mp4", "created_at": "2026-10-11T15:00:00Z" } ``` Errors: 401, 402, 409, 422, 429. ### POST /v1/videos/screenshot Screenshot video. Save one frame as an image. Price: $0.002 per request, accounts that have bought credits only. Failed requests are free. Body parameters: | Name | Type | Required | Description | | --- | --- | --- | --- | | `source` | URL | yes | A public `http` or `https` URL of the file. See [Sources](https://api.watermark.ws/docs/requests.md#sources). | | `seconds` | integer, 0 to 86400 | yes | Which moment, in seconds from the start. | | `output` | object | no | How to save the result. | | `output.format` | string | no | The file format of the result. One of: `jpg`, `png`. Default: `jpg`. | | `webhook_url` | URL | no | Where to post the request when it finishes. Defaults to the account's webhook, if set. See [Webhooks](https://api.watermark.ws/docs/webhooks.md). | | `customer` | string, up to 64 characters | no | Your own reference for who the request is for. Returned with it, and totalled by `GET /v1/usage?group_by=customer`. | | `metadata` | object, up to 20 keys | no | Up to 20 keys of your own, returned with the request and in its webhooks. | cURL: ```bash curl -X POST https://api.watermark.ws/v1/videos/screenshot \ -H "Authorization: Bearer $WATERMARK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "https://example.com/clip.mp4", "seconds": 5, "output": { "format": "jpg" } }' ``` PHP: ```php $client = new \Watermark\Api\Client(getenv('WATERMARK_API_KEY')); $request = $client->videos->screenshot('https://example.com/clip.mp4', 5, [ 'output' => ['format' => 'jpg'], ]); ``` CLI: ```bash # Waits for the result; add --no-wait to return as soon as it is queued. watermark video screenshot https://example.com/clip.mp4 \ --format jpg --seconds 5 --out clip-frame.jpg ``` Returns `202`: Accepted and queued. Returns `200`: The request first sent with this Idempotency-Key. ```json { "id": 1234, "object": "request", "endpoint": "videos/screenshot", "status": "queued", "livemode": true, "source": "https://example.com/clip.mp4", "created_at": "2026-10-11T15:00:00Z" } ``` Errors: 401, 402, 409, 422, 429. ## Documents PDF in and out. ### POST /v1/documents/process Process document. Run operations on every page of a PDF. Price: $0.004 per page. Failed requests are free. Body parameters: | Name | Type | Required | Description | | --- | --- | --- | --- | | `source` | URL | yes | A public `http` or `https` URL of the file. See [Sources](https://api.watermark.ws/docs/requests.md#sources). | | `operations` | array of operations, 1 to 20 | yes | Run in order, top to bottom. Each is `{"type": …}` plus that operation's parameters. See [Operations](https://api.watermark.ws/docs/operations.md). | | `max_pages` | integer, 1 to 2000 | no | Only keep the first this many pages. | | `webhook_url` | URL | no | Where to post the request when it finishes. Defaults to the account's webhook, if set. See [Webhooks](https://api.watermark.ws/docs/webhooks.md). | | `customer` | string, up to 64 characters | no | Your own reference for who the request is for. Returned with it, and totalled by `GET /v1/usage?group_by=customer`. | | `metadata` | object, up to 20 keys | no | Up to 20 keys of your own, returned with the request and in its webhooks. | cURL: ```bash curl -X POST https://api.watermark.ws/v1/documents/process \ -H "Authorization: Bearer $WATERMARK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "https://example.com/contract.pdf", "operations": [ {"type": "watermark", "image_url": "https://example.com/logo.png", "x": 36, "y": 36, "diagonal_lines_color": "#c0c0c0", "diagonal_lines_opacity": 40} ] }' ``` PHP: ```php $client = new \Watermark\Api\Client(getenv('WATERMARK_API_KEY')); $request = $client->documents->process('https://example.com/contract.pdf', [ ['type' => 'watermark', 'image_url' => 'https://example.com/logo.png', 'x' => 36, 'y' => 36, 'diagonal_lines_color' => '#c0c0c0', 'diagonal_lines_opacity' => 40], ]); ``` CLI: ```bash # Waits for the result; add --no-wait to return as soon as it is queued. watermark document process https://example.com/contract.pdf \ --op watermark:image_url=https://example.com/logo.png,x=36,y=36,diagonal_lines_color=#c0c0c0,diagonal_lines_opacity=40 \ --out contract-watermarked.pdf ``` Returns `202`: Accepted and queued. Returns `200`: The request first sent with this Idempotency-Key. ```json { "id": 1234, "object": "request", "endpoint": "documents/process", "status": "queued", "livemode": true, "source": "https://example.com/contract.pdf", "created_at": "2026-10-11T15:00:00Z" } ``` Errors: 401, 402, 409, 422, 429. ### POST /v1/documents/read Read document. Read the page count and page size. Price: $0.0005 per request. Failed requests are free. Body parameters: | Name | Type | Required | Description | | --- | --- | --- | --- | | `source` | URL | yes | A public `http` or `https` URL of the file. See [Sources](https://api.watermark.ws/docs/requests.md#sources). | | `webhook_url` | URL | no | Where to post the request when it finishes. Defaults to the account's webhook, if set. See [Webhooks](https://api.watermark.ws/docs/webhooks.md). | | `customer` | string, up to 64 characters | no | Your own reference for who the request is for. Returned with it, and totalled by `GET /v1/usage?group_by=customer`. | | `metadata` | object, up to 20 keys | no | Up to 20 keys of your own, returned with the request and in its webhooks. | cURL: ```bash curl -X POST https://api.watermark.ws/v1/documents/read \ -H "Authorization: Bearer $WATERMARK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "https://example.com/contract.pdf" }' ``` PHP: ```php $client = new \Watermark\Api\Client(getenv('WATERMARK_API_KEY')); $request = $client->documents->read('https://example.com/contract.pdf'); ``` CLI: ```bash # Waits for the result; add --no-wait to return as soon as it is queued. watermark document read https://example.com/contract.pdf ``` Returns `202`: Accepted and queued. Returns `200`: The request first sent with this Idempotency-Key. ```json { "id": 1234, "object": "request", "endpoint": "documents/read", "status": "queued", "livemode": true, "source": "https://example.com/contract.pdf", "created_at": "2026-10-11T15:00:00Z" } ``` Errors: 401, 402, 409, 422, 429. ### POST /v1/documents/snapshot Snapshot document. Render one page as an image. Price: $0.002 per request. Failed requests are free. Body parameters: | Name | Type | Required | Description | | --- | --- | --- | --- | | `source` | URL | yes | A public `http` or `https` URL of the file. See [Sources](https://api.watermark.ws/docs/requests.md#sources). | | `page` | integer, 1 to 2000 | no | Which page, from 1. Default: `1`. | | `output` | object | no | How to save the result. | | `output.format` | string | no | The file format of the result. One of: `jpg`, `png`. Default: `jpg`. | | `webhook_url` | URL | no | Where to post the request when it finishes. Defaults to the account's webhook, if set. See [Webhooks](https://api.watermark.ws/docs/webhooks.md). | | `customer` | string, up to 64 characters | no | Your own reference for who the request is for. Returned with it, and totalled by `GET /v1/usage?group_by=customer`. | | `metadata` | object, up to 20 keys | no | Up to 20 keys of your own, returned with the request and in its webhooks. | cURL: ```bash curl -X POST https://api.watermark.ws/v1/documents/snapshot \ -H "Authorization: Bearer $WATERMARK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "https://example.com/contract.pdf", "page": 1, "output": { "format": "png" } }' ``` PHP: ```php $client = new \Watermark\Api\Client(getenv('WATERMARK_API_KEY')); $request = $client->documents->snapshot('https://example.com/contract.pdf', 1, [ 'output' => ['format' => 'png'], ]); ``` CLI: ```bash # Waits for the result; add --no-wait to return as soon as it is queued. watermark document snapshot https://example.com/contract.pdf \ --format png --page 1 --out contract-page.png ``` Returns `202`: Accepted and queued. Returns `200`: The request first sent with this Idempotency-Key. ```json { "id": 1234, "object": "request", "endpoint": "documents/snapshot", "status": "queued", "livemode": true, "source": "https://example.com/contract.pdf", "created_at": "2026-10-11T15:00:00Z" } ``` Errors: 401, 402, 409, 422, 429. ## Requests Follow, list and abort requests. ### GET /v1/requests List requests. The account's requests, newest first. Query parameters: | Name | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer, 1 to 100 | no | How many requests to return. Default: `20`. | | `before` | integer | no | Only requests older than this id. Pass the last id of a page to get the next one. | cURL: ```bash curl "https://api.watermark.ws/v1/requests?limit=20" \ -H "Authorization: Bearer $WATERMARK_API_KEY" ``` PHP: ```php $client = new \Watermark\Api\Client(getenv('WATERMARK_API_KEY')); $requests = $client->requests->list(20); ``` CLI: ```bash watermark requests --limit 20 ``` Returns `200`: A page of requests. ```json { "object": "list", "data": [ { "id": 1234, "object": "request", "endpoint": "images/process", "status": "succeeded", "livemode": true, "created_at": "2026-10-11T15:00:00Z" } ], "has_more": true } ``` Errors: 401, 422, 429. ### GET /v1/requests/{id} Get request. One request, with its result once it has succeeded. Download links in the result work until the result is deleted. Path parameters: | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | The request's id. | cURL: ```bash curl https://api.watermark.ws/v1/requests/1234 \ -H "Authorization: Bearer $WATERMARK_API_KEY" ``` PHP: ```php $client = new \Watermark\Api\Client(getenv('WATERMARK_API_KEY')); $request = $client->requests->get(1234); ``` CLI: ```bash watermark request get 1234 ``` Returns `200`: The request. ```json { "id": 1234, "object": "request", "endpoint": "images/process", "status": "succeeded", "livemode": true, "source": "https://example.com/photo.jpg", "result": { "data": { "width": 1600, "height": 1067, "format": "WEBP", "size": 183021 }, "files": [ { "url": "https://api.watermark.ws/v1/results/1234?expires=1792335604&signature=…", "url_expires_at": "2026-10-18T15:00:04Z", "format": "webp", "size": 183021 } ], "deleted_at": "2026-10-18T15:00:04Z" }, "price": { "amount": "0.002000", "currency": "usd", "micro_usd": 2000 }, "customer": "cus_42", "created_at": "2026-10-11T15:00:00Z", "completed_at": "2026-10-11T15:00:04Z" } ``` Errors: 401, 404, 429. ### GET /v1/results/{id} Download result. The link in `result.files[].url`: open it as given, with no key. It works until the result is deleted (`url_expires_at`) and redirects to the file. Anyone with the link can download the file. Query parameters: | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | The request's id. | | `expires` | integer | yes | Part of the link. | | `signature` | string | yes | Part of the link. | cURL: ```bash curl -L -o photo-watermarked.webp "https://api.watermark.ws/v1/results/1234?expires=1792335604&signature=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" ``` PHP: ```php $request = $client->requests->get(1234); file_put_contents('photo-watermarked.webp', file_get_contents($request['result']['files'][0]['url'])); ``` CLI: ```bash # --out on a process command downloads the file for you; a link you already have is a plain download. curl -L -o photo-watermarked.webp "https://api.watermark.ws/v1/results/1234?expires=1792335604&signature=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" ``` Returns `302`: Redirects to the file. Errors: 403, 410. ### POST /v1/requests/{id}/abort Abort request. Stop a request that has not finished. It is not charged. Path parameters: | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | The request's id. | cURL: ```bash curl -X POST https://api.watermark.ws/v1/requests/1235/abort \ -H "Authorization: Bearer $WATERMARK_API_KEY" ``` PHP: ```php $client = new \Watermark\Api\Client(getenv('WATERMARK_API_KEY')); $request = $client->requests->abort(1235); ``` CLI: ```bash watermark request abort 1235 ``` Returns `200`: The aborted request. One that has already finished is a `409 request_finished`. ```json { "id": 1235, "object": "request", "endpoint": "videos/process", "status": "aborted", "livemode": true, "source": "https://example.com/clip.mp4", "price": { "amount": "0.000000", "currency": "usd", "micro_usd": 0 }, "created_at": "2026-10-11T15:00:00Z", "completed_at": "2026-10-11T15:00:09Z" } ``` Errors: 401, 404, 409, 429. ## Account Balance, usage and the operation catalog. ### GET /v1/account Get account. The account behind the key: balance, tier, and the key's mode and limits. cURL: ```bash curl https://api.watermark.ws/v1/account \ -H "Authorization: Bearer $WATERMARK_API_KEY" ``` PHP: ```php $client = new \Watermark\Api\Client(getenv('WATERMARK_API_KEY')); $account = $client->account->get(); ``` CLI: ```bash watermark account ``` Returns `200`: The account. ```json { "object": "account", "id": 7, "name": "Acme Photos", "email": "dev@example.com", "tier": "paid", "balance": { "amount": "18.420000", "currency": "usd", "micro_usd": 18420000 }, "webhook_url": "https://example.com/hooks/watermark", "livemode": true, "limits": { "requests_per_minute": 120, "concurrent_requests": 10, "max_source_bytes": 2147483648 }, "suspended": false, "created_at": "2026-10-11T14:52:10Z" } ``` Errors: 401, 429. ### GET /v1/usage Get usage. Requests, spend and bytes over at most 92 days, grouped by day, endpoint, customer or status. Query parameters: | Name | Type | Required | Description | | --- | --- | --- | --- | | `from` | date | no | First day, `YYYY-MM-DD`. Defaults to 29 days ago. | | `to` | date | no | Last day, `YYYY-MM-DD`. Defaults to today. | | `group_by` | string | no | What each row totals. One of: `day`, `endpoint`, `customer`, `status`. Default: `day`. | | `customer` | string, up to 64 characters | no | Only this customer's requests. | cURL: ```bash curl "https://api.watermark.ws/v1/usage?from=2026-10-01&group_by=endpoint" \ -H "Authorization: Bearer $WATERMARK_API_KEY" ``` PHP: ```php $client = new \Watermark\Api\Client(getenv('WATERMARK_API_KEY')); $usage = $client->account->usage([ 'from' => '2026-10-01', 'group_by' => 'endpoint', ]); ``` CLI: ```bash watermark usage --from 2026-10-01 --group-by endpoint ``` Returns `200`: Usage. ```json { "object": "usage", "from": "2026-10-01", "to": "2026-10-13", "group_by": "endpoint", "data": [ { "endpoint": "images/process", "requests": 1840, "succeeded": 1822, "failed": 18, "spent": "3.644000", "spent_micro_usd": 3644000, "source_bytes": 912455120, "result_bytes": 301228873 } ] } ``` Errors: 401, 422, 429. ### GET /v1/operations List operations. Every operation, its parameters and the output formats, per media type: what a client needs to build requests. The same catalog, with pictures, is at https://api.watermark.ws/docs/operations. cURL: ```bash curl https://api.watermark.ws/v1/operations \ -H "Authorization: Bearer $WATERMARK_API_KEY" ``` PHP: ```php $client = new \Watermark\Api\Client(getenv('WATERMARK_API_KEY')); $catalog = $client->account->operations(); ``` CLI: ```bash watermark operations ``` Returns `200`: The catalog. The example shows one operation of one media type. ```json { "object": "operations", "data": { "image": { "formats": [ "jpg", "png", "webp", "gif", "tif" ], "operations": { "resize": { "description": "Resize to a width and height.", "params": { "width": { "description": "The width in pixels.", "required": true }, "height": { "description": "The height in pixels.", "required": true }, "preserve_aspect_ratio": { "description": "Fit inside the box instead of stretching to it.", "required": false, "default": true }, "crop_center": { "description": "Fill the box and crop what overflows, centered.", "required": false, "default": false } } } } } } } ``` Errors: 401, 429.