How to Add an Image Watermark to a PDF with PHP cURL
Send a PDF and image as multipart form data with PHP cURL, validate the response, select pages, and troubleshoot common watermarking errors.
Use a multipart cURL request to upload the source PDF and watermark image, then save the returned PDF only after checking the HTTP status and response content. PDF Blocks documents this endpoint: POST https://api.pdfblocks.com/v1/add_image_watermark. The PDF is sent in the file field and the image in the image field.
1. Complete PHP cURL example
This script reads input.pdf and logo.png, sends them with an API key, and writes watermarked.pdf after a successful response.
<?php
declare(strict_types=1);
$apiKey = getenv('PDF_BLOCKS_API_KEY');
$inputPath = __DIR__ . '/input.pdf';
$imagePath = __DIR__ . '/logo.png';
$outputPath = __DIR__ . '/watermarked.pdf';
if (!$apiKey) {
throw new RuntimeException('Set PDF_BLOCKS_API_KEY in the environment.');
}
if (!is_file($inputPath) || !is_readable($inputPath)) {
throw new RuntimeException("Cannot read {$inputPath}");
}
if (!is_file($imagePath) || !is_readable($imagePath)) {
throw new RuntimeException("Cannot read {$imagePath}");
}
$ch = curl_init('https://api.pdfblocks.com/v1/add_image_watermark');
if ($ch === false) {
throw new RuntimeException('Could not initialize cURL.');
}
$postFields = [
'file' => new CURLFile($inputPath, 'application/pdf', basename($inputPath)),
'image' => new CURLFile($imagePath, 'image/png', basename($imagePath)),
// Optional fields documented in the example:
'transparency' => '60',
'pages' => '1',
];
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postFields,
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . $apiKey,
'Accept: application/pdf',
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_CONNECTTIMEOUT => 15,
CURLOPT_TIMEOUT => 120,
]);
$responseBody = curl_exec($ch);
if ($responseBody === false) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException('cURL request failed: ' . $error);
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
if ($status !== 200) {
$preview = substr($responseBody, 0, 1000);
throw new RuntimeException("Watermark API returned HTTP {$status}: {$preview}");
}
// Do not silently save an HTML or JSON error page as a .pdf file.
if ($contentType !== '' && stripos($contentType, 'pdf') === false) {
throw new RuntimeException("Unexpected response Content-Type: {$contentType}");
}
if (file_put_contents($outputPath, $responseBody) === false) {
throw new RuntimeException("Could not write {$outputPath}");
}
echo "Wrote {$outputPath}\n";
The request shape and the X-API-Key, file, image, transparency, and pages fields follow the documented PDF Blocks PHP example. Confirm the current endpoint reference for the supported image formats, page syntax, limits, and parameter ranges before production use.
2. What each part of the request does
| Part | Purpose |
|---|---|
CURLOPT_POST |
Uses an HTTP POST request. |
CURLFile |
Encodes the PDF and image as multipart file parts. |
file |
The source PDF upload field. |
image |
The watermark image upload field. |
X-API-Key |
Authenticates the request with the provider API key. |
transparency |
Optional appearance control shown in the provider example. |
pages |
Optional page selection control shown in the provider example. |
CURLOPT_RETURNTRANSFER |
Returns response bytes to PHP so they can be validated and saved. |
3. Minimal command-line cURL equivalent
curl --fail-with-body \
-X POST "https://api.pdfblocks.com/v1/add_image_watermark" \
-H "X-API-Key: $PDF_BLOCKS_API_KEY" \
-F "file=@input.pdf;type=application/pdf" \
-F "image=@logo.png;type=image/png" \
-F "transparency=60" \
-F "pages=1" \
-o watermarked.pdf
--fail-with-body makes recent cURL versions return a failure status for HTTP errors while retaining the response body for diagnosis. If your cURL does not support it, omit that option and inspect %{http_code} separately.
4. Python and Node.js multipart examples
Python
import os
import requests
endpoint = "https://api.pdfblocks.com/v1/add_image_watermark"
headers = {"X-API-Key": os.environ["PDF_BLOCKS_API_KEY"]}
data = {"transparency": "60", "pages": "1"}
with open("input.pdf", "rb") as pdf, open("logo.png", "rb") as image:
files = {
"file": ("input.pdf", pdf, "application/pdf"),
"image": ("logo.png", image, "image/png"),
}
response = requests.post(endpoint, headers=headers, data=data, files=files, timeout=120)
response.raise_for_status()
content_type = response.headers.get("content-type", "")
if content_type and "pdf" not in content_type.lower():
raise RuntimeError(f"Unexpected content type: {content_type}")
with open("watermarked.pdf", "wb") as output:
output.write(response.content)
Node.js 18+
import fs from "node:fs";
import FormData from "form-data";
const form = new FormData();
form.append("file", fs.createReadStream("input.pdf"), {
filename: "input.pdf",
contentType: "application/pdf"
});
form.append("image", fs.createReadStream("logo.png"), {
filename: "logo.png",
contentType: "image/png"
});
form.append("transparency", "60");
form.append("pages", "1");
const response = await fetch("https://api.pdfblocks.com/v1/add_image_watermark", {
method: "POST",
headers: {
...form.getHeaders(),
"X-API-Key": process.env.PDF_BLOCKS_API_KEY
},
body: form
});
const body = Buffer.from(await response.arrayBuffer());
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${body.toString("utf8", 0, 1000)}`);
}
if (response.headers.get("content-type")?.includes("pdf") === false) {
throw new Error(`Unexpected content type: ${response.headers.get("content-type")}`);
}
fs.writeFileSync("watermarked.pdf", body);
5. Selecting pages and transparency
The documented example includes transparency values such as 60 and 85, and a pages value of 1. Treat these as examples rather than a complete specification. Page numbering, ranges, lists, transparency scale, positioning, scaling, and accepted image formats must be checked against the current endpoint documentation.
- Start with one page and a moderate transparency while validating output.
- Open the returned PDF in a viewer and check that the watermark is visible but does not hide body text.
- For a multi-page document, confirm whether the API expects a single page number, a range, or another syntax before sending production jobs.
6. Error handling and troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| cURL cannot initialize | PHP cURL extension is missing or disabled. | Enable the cURL extension in the PHP runtime used by the application and verify with php -m. |
| “File not found” or empty upload | Relative path, permissions, or a worker running from another directory. | Use absolute paths, check is_readable(), and log the resolved path. |
| HTTP 401/403 | Missing, invalid, or unauthorized API key. | Send X-API-Key exactly as documented and keep the key in an environment variable. |
| HTTP 400/422 | Invalid field name, image type, page value, or transparency value. | Compare the multipart names and parameter syntax with the current API reference. |
| Output is JSON or HTML | The API returned an error body that was saved as a PDF. | Check the HTTP status and Content-Type before writing output. |
| PDF opens but watermark is missing | Wrong page selection, unsupported image, placement, or transparency. | Test page 1, use a clearly visible PNG, and reduce transparency while diagnosing. |
| Request times out | Large PDF, slow upload, or server-side processing. | Set a reasonable connect and total timeout, retry only when safe, and avoid duplicate writes. |
| Retries create duplicate work | A client retry happened after the server completed but before the response arrived. | Store a job or request identifier if the provider supplies one, and design downstream processing to be idempotent. |
7. Security, reliability, and cost considerations
- Protect credentials: use environment variables or a secret manager; never commit the API key or place it in browser-side code.
- Protect uploaded documents: this workflow sends the PDF and image to a hosted service. The reviewed documentation does not establish current retention, privacy, pricing, or data-processing terms, so verify those terms before uploading confidential files.
- Validate inputs: restrict accepted extensions and MIME types, enforce your own size limits, and reject unexpected paths.
- Validate outputs: check status, content type, and that the response is non-empty before moving it into downstream storage.
- Use bounded timeouts: separate connection timeout from total request timeout so a stalled connection does not occupy a worker indefinitely.
- Retry carefully: retry transient network failures with exponential backoff, but do not blindly retry authentication or validation errors.
- Measure your workload: record input size, page count, elapsed time, status code, and output size. The available research does not provide a benchmark or file-size guarantee.
8. Alternative: a watermark PDF workflow with Adobe PDF Services
Adobe PDF Services documents a cloud operation that takes an input-document asset and a separate watermark-document asset, then applies the watermark PDF to selected pages. Its examples use an API key and bearer token and show page ranges plus appearance controls such as opacity and foreground placement. This differs from the direct image multipart upload above: you prepare a watermark PDF instead of sending a PNG or other image directly. Compare the input workflow, page controls, authentication, privacy, retention, pricing, and usage terms before choosing an approach.
9. Or skip the browser setup
ScreenshotNeo is a website screenshot API, so it does not add watermarks to existing PDFs. It is useful when the asset you need to watermark is a webpage capture and you want to avoid maintaining a browser stack. A single request returns a PNG, JPEG, WebP, or PDF.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Before capture, ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots. See the ScreenshotNeo API documentation for options and ScreenshotNeo for the service. Create a free ScreenshotNeo account.
10. FAQ
Can I watermark every page?
The example exposes a pages field, but the reviewed material does not define the complete page-selection syntax. Check the current endpoint reference before sending a range or “all pages” value.
Can I use JPEG instead of PNG?
The documented PHP example uses image/png. Confirm accepted image formats with the current API documentation before changing the MIME type.
Why should I inspect Content-Type?
An API error may be JSON or HTML. Checking status and content type prevents writing that diagnostic response into a file named .pdf.
Is local processing available?
A search result for ajaxray/php-watermark describes a PHP library using ImageMagick and Ghostscript for PDF watermarking, but its current maintenance state and compatibility were not verified here. Treat it as a lead for separate local-processing research.
Where should the API key live?
Keep it outside publicly served source code, normally in an environment variable or secret manager, and restrict access to the process that performs the upload.


