Guides

Cloudinary

Watermark Cloudinary uploads automatically. An open-source service receives each upload notification, watermarks the image and stores the copy back.

You run the service on your own server. Cloudinary notifies it of each upload, and it stores the watermarked copy next to the original.

Text
upload to Cloudinary ──notification──► your service ──► Etchv
       ▲                                                 │
       └──────── watermarked copy as <public_id>_etchv ◄─┘

The service is in the Etchv integrations repository under the MIT license.

What you get

  • The original stays as it was. The copy is stored as <public_id>_etchv in the same asset folder and tagged etchv-watermarked.
  • Etchv embeds {"public_id": "<source public ID>", "delivery": "<reference>"}. The copy's contextual metadata records etchv_watermark_id and etchv_source, so you can match a detected watermark to the upload it came from.
  • Replacing the original watermarks the new version and replaces the copy. Deleting the original leaves the copy in place.
  • Public image uploads in JPEG, PNG, TIFF, PSD/PSB, GIF, BMP, PPM and WebP are watermarked, up to 50 MB. Each one uses the same credits as an image embed.

Run the service

You need Node.js 24 (24.2 or later) and a host that can receive HTTPS requests from Cloudinary. The service serves plain HTTP, so put it behind your HTTPS proxy or platform.

Set these environment variables:

VariableValue
ETCHV_API_KEYAn API key with only the watermarks:embed scope
ETCHV_DELIVERYThe delivery reference for uploads without a delivery: tag, such as website
CLOUDINARY_CLOUD_NAMEYour Cloudinary cloud name
CLOUDINARY_API_KEYA Cloudinary API key that can upload
CLOUDINARY_API_SECRETIts API secret
CLOUDINARY_WEBHOOK_SECRETOptional. The secret of the key Cloudinary signs notifications with, if that's a different key (see below)
PORTOptional. The port to listen on (default 8080)
ETCHV_API_KEY
Value
An API key with only the watermarks:embed scope
ETCHV_DELIVERY
Value
The delivery reference for uploads without a delivery: tag, such as website
CLOUDINARY_CLOUD_NAME
Value
Your Cloudinary cloud name
CLOUDINARY_API_KEY
Value
A Cloudinary API key that can upload
CLOUDINARY_API_SECRET
Value
Its API secret
CLOUDINARY_WEBHOOK_SECRET
Value
Optional. The secret of the key Cloudinary signs notifications with, if that's a different key (see below)
PORT
Value
Optional. The port to listen on (default 8080)

Then start it:

Shell
git clone https://github.com/etchv-labs/integrations.git
cd integrations
npm install
cd cloudinary
npm start

The service README lists the other optional variables, such as SHA-256 signatures, a regional Cloudinary API host or another suffix for the copy.

Keep both keys on the server. The service never logs them.

Connect Cloudinary

Point an upload notification at the service's HTTPS URL:

  • For all uploads: in the Cloudinary Console, open Settings → Webhook Notifications and add a notification URL for the Upload event type. Keep the signature scheme at default or legacy HMAC and keep the default payload. The service verifies the X-Cld-Signature header, which the EdDSA-only scheme doesn't send.
  • For some uploads: pass notification_url in the upload call, or set it on an upload preset. Global notifications that aren't additive then don't also fire for that upload.

Cloudinary signs notifications with the key chosen for signature verification on the same settings page, or your oldest active key if none is chosen. If that isn't the key in CLOUDINARY_API_KEY, set CLOUDINARY_WEBHOOK_SECRET to its secret. Otherwise every notification gets a 401.

To choose the delivery reference per upload, add a tag such as delivery:partner-42. Uploads without one use ETCHV_DELIVERY.

Upload an image, then look for <public_id>_etchv in the same folder. To confirm the watermark, detect it in the copy and compare the watermark ID with etchv_watermark_id.

Retries and failures

Cloudinary waits 20 seconds for a response, then retries after about 3, 6 and 9 minutes. Each version of an upload maps to one idempotency key, so a retry replays the same Etchv result instead of using more credits. The service keeps working after Cloudinary stops waiting, so run it where a request can continue for a few minutes.

ResponseWhenCloudinary retries
200Watermarked; skipped (not a supported public image, already a copy, or replaced or deleted since); or failed in a way a retry can't fix, such as no credits leftNo
401Missing, stale or invalid signatureYes
413A notification larger than 1 MBYes
503A rate limit, timeout, network or server error at Etchv or CloudinaryYes
200
When
Watermarked; skipped (not a supported public image, already a copy, or replaced or deleted since); or failed in a way a retry can't fix, such as no credits left
Cloudinary retries
No
401
When
Missing, stale or invalid signature
Cloudinary retries
Yes
413
When
A notification larger than 1 MB
Cloudinary retries
Yes
503
When
A rate limit, timeout, network or server error at Etchv or Cloudinary
Cloudinary retries
Yes

After a 200 failure, fix the cause and upload the image again.

Edge cases
  • Before storing, the service checks the original's current version. If the original was replaced or deleted while Cloudinary was still retrying an older notification, nothing is stored. A replacement in the moment between that check and the upload can still leave a copy of the older version; upload the newer version again.
  • Restoring an earlier version from the Media Library isn't an upload, so the copy isn't updated. Upload the restored version again.
  • Uploads tagged etchv-watermarked, or whose public ID ends with the suffix, are skipped, so the service never watermarks its own copies. Pick a suffix your own public IDs don't use.
  • If your product environment has a default upload preset for API uploads, its incoming transformation also applies to the copy and can re-encode it.
View source on GitHub

Your privacy, your choice

We use essential cookies to keep Etchv working. Optional analytics helps us improve the site. Analytics is on by default; you can turn it off in preferences. PostHog loads only if you accept all. Privacy policy

ETCHV

Privacy preferences

Choose what you allow on this browser. Analytics is enabled by default. You can turn it off, and change your choice at any time.

Essential

Always active

Supports secure sign-in, account sessions, site security, and remembering your privacy choice. These are needed for Etchv to work.

Analytics

Helps us understand visits and improve the website using page-view and device statistics, which do not use cookies. PostHog also measures visits and campaigns and may set cookies; it loads only after you save a choice with analytics on. Turning this off stops future analytics events.

We do not load advertising scripts. Meeting calendars load only when you open them. These preferences do not change your account, watermarking requests, or asset storage.

Your choice is remembered for 180 days on this browser. Privacy policy