# 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.