API Reference

API Documentation

Add text or image watermarks to images, videos, and PDFs with one HTTP call. Synchronous for image and PDF, async for video. No SDK required.

Sub-second for images25 MB max filemultipart/form-dataOpenAPI 3.1

#Quick start

POST your image and watermark config to /watermark. The response body is the watermarked image binary.

cURL
bash
curl -X POST https://api.apiwatermark.com/watermark \
  -H "Authorization: Bearer aw_••••••••" \
  -F "image=@photo.jpg" \
  -F "type=text" \
  -F "text=© 2026 MyBrand" \
  -F "position=bottom-right" \
  -F "opacity=0.5" \
  -F "font_size=24" \
  -F "output_format=webp" \
  -o watermarked.webp
JavaScript
js
const form = new FormData();
form.append("image", fileInput.files[0]);
form.append("type", "text");
form.append("text", "© 2026 MyBrand");
form.append("position", "bottom-right");
form.append("opacity", "0.5");
form.append("output_format", "webp");

const res = await fetch("https://api.apiwatermark.com/watermark", {
  method: "POST",
  headers: { Authorization: "Bearer aw_••••••••" },
  body: form,
});

const blob = await res.blob();
// Save or display the watermarked image
Python
python
import requests

with open("photo.jpg", "rb") as f:
    res = requests.post(
        "https://api.apiwatermark.com/watermark",
        headers={"Authorization": "Bearer aw_••••••••"},
        files={"image": f},
        data={
            "type": "text",
            "text": "© 2026 MyBrand",
            "position": "bottom-right",
            "opacity": "0.5",
            "font_size": "24",
            "output_format": "webp",
        },
    )

with open("watermarked.webp", "wb") as f:
    f.write(res.content)

#Base URL & authentication

All examples use the canonical API subdomain:

https://api.apiwatermark.com

The same routes are also available at https://apiwatermark.com/api/* — both URLs hit the same handlers via a small Next.js rewrite. Pick whichever you prefer.

Authentication

Send your API key as a Bearer token:

Authorization header
http
Authorization: Bearer aw_••••••••••••••••

Image / PDF / batch: API key is optional but recommended for production. Without it, requests fall back to anonymous IP rate limits.

Video: Required — either an API key or an authenticated dashboard session.

Get a key: /app/api-keys (free, instant).

#POST/watermark

Accepts multipart/form-data. Returns the watermarked file as binary with the appropriate Content-Type header. Source can be an image (JPEG, PNG, WebP, AVIF) or a PDF — the engine auto-detects.

POSThttps://api.apiwatermark.com/watermark

Content-Type: multipart/form-data · Max body: 4.5 MB direct or 25 MB via R2

#Parameters

All parameters are sent as multipart form fields. File fields are standard file uploads; all other fields are strings (numbers serialized as strings).

Source

ParameterTypeDefaultDescription
imagerequiredFileSource file — image (JPEG, PNG, WebP, AVIF) or PDF. Required unless r2_key is set. Direct upload is capped at ~4.5 MB on Vercel; use r2_key for larger files (up to 25 MB).
r2_keystringR2 object key returned by /upload/presign. Use this for files above the direct-upload limit.

Watermark config

ParameterTypeDefaultDescription
type"text" | "image""text"Watermark kind: text overlay or image overlay.
textstring"© Your Brand"Watermark text (type=text). Supports dynamic variables: {{email}}, {{name}}, {{ip_address}}, {{timestamp}} — resolved server-side per request.
font_sizenumber24Font size in pixels (type=text).
font_familystring"sans-serif"CSS font family (type=text). Server-side fonts are limited.
colorstring"#ffffff"Hex color for the text watermark, including the # prefix.
watermark_imageFileImage used as the overlay (PNG with transparency works best). Required when type=image.
watermark_widthnumber150Pixel width to resize the overlay image to. Height scales proportionally.

Position & layout

ParameterTypeDefaultDescription
positionstring"bottom-right"One of: "top-left", "top-center", "top-right", "center-left", "center", "center-right", "bottom-left", "bottom-center", "bottom-right", "tiled", "custom".
custom_xnumber0.5X position as a 0-1 ratio (position=custom).
custom_ynumber0.5Y position as a 0-1 ratio (position=custom).
opacitynumber0.50 = invisible, 1 = fully opaque.
paddingnumber20Distance from the edge in pixels (preset positions).
rotationnumber0Watermark rotation in degrees, -180 to 180.
tile_gapnumber100Gap between repeated watermarks in pixels (position=tiled).
tile_anglenumber-30Angle of the tile grid in degrees, -90 to 90 (position=tiled).

Output

ParameterTypeDefaultDescription
output_format"jpeg" | "png" | "webp" | "avif""png"Output image format. Ignored for PDF sources — PDFs always return application/pdf.
output_qualitynumber90Compression quality 1-100 (JPEG, WebP, AVIF only).

#Text watermark

Set type=text and provide text, font_size, color, and opacity. The text is rendered as an SVG layer and composited into the source.

Text watermark
bash
curl -X POST https://api.apiwatermark.com/watermark \
  -H "Authorization: Bearer aw_••••••••" \
  -F "image=@photo.jpg" \
  -F "type=text" \
  -F "text=© 2026 MyBrand" \
  -F "position=bottom-right" \
  -F "opacity=0.5" \
  -F "font_size=24" \
  -F "output_format=webp" \
  -o watermarked.webp

#Image watermark (logo)

Set type=image and attach a watermark_image file. PNG with transparency works best. Resize via watermark_width.

Image watermark
bash
curl -X POST https://api.apiwatermark.com/watermark \
  -H "Authorization: Bearer aw_••••••••" \
  -F "image=@photo.jpg" \
  -F "type=image" \
  -F "watermark_image=@logo.png" \
  -F "position=center" \
  -F "opacity=0.3" \
  -F "watermark_width=200" \
  -o branded.jpg

#Tiled / repeated watermark

position=tiled repeats the watermark diagonally across the entire canvas — crop-proof for proof protection. Control spacing with tile_gapand angle with tile_angle.

Tiled watermark
bash
curl -X POST https://api.apiwatermark.com/watermark \
  -H "Authorization: Bearer aw_••••••••" \
  -F "image=@photo.jpg" \
  -F "type=text" \
  -F "text=DRAFT" \
  -F "position=tiled" \
  -F "opacity=0.15" \
  -F "rotation=-30" \
  -F "tile_gap=120" \
  -F "tile_angle=-30" \
  -F "font_size=48" \
  -F "color=#ff0000" \
  -o draft.png

#PDF watermark

Send a PDF in the image field (or via r2_key) and the engine auto-detects it. Every page receives the same watermark; original text remains selectable. Output is always application/pdf output_format is ignored for PDF sources.

PDF with dynamic email stamp
bash
# Source PDF is auto-detected — output is always PDF.
# Every page receives the same watermark.
curl -X POST https://api.apiwatermark.com/watermark \
  -H "Authorization: Bearer aw_••••••••" \
  -F "image=@contract.pdf" \
  -F "type=text" \
  -F "text=CONFIDENTIAL · {{email}}" \
  -F "position=tiled" \
  -F "opacity=0.15" \
  -o stamped.pdf

Dynamic variables resolve server-side per request: {{email}}, {{name}}, {{ip_address}}, {{timestamp}}. Perfect for burning the viewer's identity into the document for audit trails.

#Batch processing

POST /watermark/batch processes up to 20 files at once and returns a ZIP archive. Mix images and PDFs freely — each output keeps its source extension. Total upload capped at 100 MB.

Accepted inputs: images[] for direct uploads or r2_keys[] for pre-uploaded R2 objects. Watermark fields are the same as the single-file endpoint.
Batch ZIP
bash
curl -X POST https://api.apiwatermark.com/watermark/batch \
  -H "Authorization: Bearer aw_••••••••" \
  -F "images[]=@photo-1.jpg" \
  -F "images[]=@photo-2.jpg" \
  -F "images[]=@photo-3.jpg" \
  -F "type=text" \
  -F "text=© 2026 MyBrand" \
  -F "position=bottom-right" \
  -o watermarked-batch.zip

# Up to 20 files per request · 100 MB total · returns a ZIP.
# Mix images and PDFs freely — outputs keep their source extension.

#Video jobs (async)

Video processing is asynchronous and runs on dedicated FFmpeg workers. The flow is: upload source → create job → poll status → download result. Either an API key or an authenticated dashboard session owns the job and can poll it.

1

Upload

PUT the source video to a presigned R2 URL.

2

Create

POST /video/watermark with the r2 key and watermark config.

3

Poll

GET /video/watermark/{jobId} every 2–5s until status is completed or failed.

Full video flow
bash
# 1. Get a presigned R2 upload URL
PRESIGN=$(curl -s -X POST https://api.apiwatermark.com/upload/presign \
  -H "Content-Type: application/json" \
  -d '{"contentType":"video/mp4","fileName":"clip.mp4","fileSize":125000000}')
R2_KEY=$(echo "$PRESIGN" | jq -r .key)
UPLOAD_URL=$(echo "$PRESIGN" | jq -r .uploadUrl)

# 2. PUT the source video to R2
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: video/mp4" \
  --data-binary @clip.mp4

# 3. Create the asynchronous video job
JOB=$(curl -s -X POST https://api.apiwatermark.com/video/watermark \
  -H "Authorization: Bearer aw_••••••••" \
  -H "Content-Type: application/json" \
  -d '{
    "sourceR2Key": "'$R2_KEY'",
    "sourceFileName": "clip.mp4",
    "sourceFileSize": 125000000,
    "outputFormat": "mp4",
    "watermarkConfig": {
      "type": "text",
      "text": "© {{email}}",
      "fontSize": 32,
      "color": "#ffffff",
      "position": "bottom-right",
      "opacity": 0.5
    }
  }')
JOB_ID=$(echo "$JOB" | jq -r .jobId)

# 4. Poll status (2-5s intervals are fine; polling is free)
while true; do
  STATUS=$(curl -s -H "Authorization: Bearer aw_••••••••" \
    https://api.apiwatermark.com/video/watermark/$JOB_ID)
  STATE=$(echo "$STATUS" | jq -r .status)
  echo "[$STATE] $(echo "$STATUS" | jq -r .progress)%"
  case "$STATE" in
    completed) echo "$STATUS" | jq -r .downloadUrl; break ;;
    failed)    echo "$STATUS" | jq -r .error >&2; exit 1 ;;
  esac
  sleep 3
done

# 5. (Shortcut) Skip steps 4 with ?download=1 — redirects to the asset URL
curl -L -H "Authorization: Bearer aw_••••••••" \
  "https://api.apiwatermark.com/video/watermark/$JOB_ID?download=1" -o watermarked.mp4

watermarkConfig shape: same field names as the synchronous endpoint, in camelCase JSON instead of snake_case form fields. So font_size becomes fontSize, tile_gap becomes tileGap, etc.

Polling is free: status checks don't count against your monthly quota. Only the initial job creation does.

#Large files (R2 upload)

Direct uploads to /watermark are capped at ~4.5 MB by Vercel. For larger files (up to 25 MB images/PDF, 500 MB video) use the presigned R2 flow.

  1. 1Get a presigned URLPOST to /upload/presign with the file's MIME type and size.
  2. 2PUT directly to R2using the returned uploadUrl. The bytes never touch Vercel.
  3. 3Processby passing the returned key as r2_key in your watermark request. The source object is auto-deleted after processing.

#POST/upload/presign

FieldTypeDescription
contentTypestringMIME type: image/jpeg, image/png, image/webp, image/avif, video/mp4, video/quicktime, video/webm, application/pdf
fileNamestringOriginal filename (used for extension detection).
fileSizenumberFile size in bytes. Validated against the 500 MB cap.
Full R2 upload flow
bash
# 1. Ask for a presigned upload URL
PRESIGN=$(curl -s -X POST https://api.apiwatermark.com/upload/presign \
  -H "Content-Type: application/json" \
  -d '{"contentType":"image/jpeg","fileName":"large.jpg","fileSize":15000000}')
UPLOAD_URL=$(echo "$PRESIGN" | jq -r .uploadUrl)
R2_KEY=$(echo "$PRESIGN" | jq -r .key)

# 2. PUT the file directly to Cloudflare R2 (bypasses Vercel body limits)
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: image/jpeg" \
  --data-binary @large.jpg

# 3. Process the uploaded R2 object
curl -X POST https://api.apiwatermark.com/watermark \
  -H "Authorization: Bearer aw_••••••••" \
  -F "r2_key=$R2_KEY" \
  -F "type=text" \
  -F "text=© MyBrand" \
  -F "position=bottom-right" \
  -o watermarked.jpg

CORS: The PUT in step 2 goes browser → R2 directly. Your R2 bucket must allow PUT from the origin you upload from. The dashboard handles this for our hosted setup; from your own frontend, add a CORS rule on the bucket.

#POST/upload/direct(small files only)

Convenience endpoint that pipes through the app server to R2. Strictly for small files (capped at 4.5 MB on Vercel, 10 MB local). For anything larger use the presigned flow above.

Direct upload (small files)
bash
# Alternative for small files (< 4.5 MB on Vercel)
curl -X POST https://api.apiwatermark.com/upload/direct \
  -F "file=@photo.jpg"
# Response: { "key": "uploads/uuid.jpg" }

#Error codes

Errors are JSON with an error field. Some 500 responses also include a details field with the underlying message.

StatusErrorCause
400Missing required field: image (or r2_key for R2 uploads)No image or r2_key provided.
400Invalid R2 key formatThe r2_key contains invalid characters or path traversal.
400Missing required field: watermark_image (for image type)type=image but no watermark_image file was attached.
400Invalid content typePresign endpoint: unsupported MIME type.
401Authentication requiredVideo endpoints require a valid API key or an authenticated session.
404Source image not found in storageR2 key references an expired or missing object.
413File too largeExceeds 25 MB (image/PDF) or 500 MB (video upload).
429Too many requests / Monthly API call limit reachedIP rate limit (anonymous) or plan monthly quota (authenticated).
500Failed to process watermarkServer error during Sharp / FFmpeg processing.
503R2 storage is not configuredServer is missing R2 environment variables.
Error response format
json
{
  "error": "Missing required field: image (or r2_key for R2 uploads)"
}

// For 500 errors:
{
  "error": "Failed to process watermark",
  "details": "Input image exceeds pixel limit"
}

#Rate limits

Two layers, applied separately

Anonymous calls are IP-rate-limited per endpoint. Authenticated calls are bounded by your plan's monthly quota (see pricing).

EndpointAnonymous limitWindow
POST /watermark20 requests1 minute
POST /watermark/batch5 requests1 minute
POST /upload/presign10 requests1 minute
POST /upload/direct10 requests1 minute
POST /video/watermarkAuth requiredMonthly quota

When rate-limited the API returns 429 Too Many Requests with Retry-After and X-RateLimit-Limit / X-RateLimit-Remaining headers.

#OpenAPI specification

A machine-readable OpenAPI 3.1 spec is at /api/openapi.json. Use it to generate client SDKs, import into Postman, or wire up any OpenAPI-compatible tooling.

Fetch the OpenAPI spec
bash
curl https://apiwatermark.com/api/openapi.json | jq .

Ready to watermark?

Try the live API builder or grab an API key in one click.