Watermark API
Libraries

PHP library

View as Markdown

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

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

Shell
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 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 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:

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.

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. Changes are listed in its releases.