PHP library
Install it with Composer and call the API from Laravel or any PHP 8.1 app.
The official PHP client for the Watermark API. One Client reaches every endpoint, waits for results, retries safely and checks webhook signatures.
Install
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 and keep it out of your code. A wm_test_… key is free and returns real results, so use one while you build.
export WATERMARK_API_KEY=wm_test_…
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:
// 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
$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 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.
$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
$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.
$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
$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.
$client->images->process($photo, $operations, ['idempotency_key' => "order-1001-photo-3"]);
See what each customer used
$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 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. |
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:
$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.
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.
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.
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. Changes are listed in its releases.