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.
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>_etchvin the same asset folder and taggedetchv-watermarked. - Etchv embeds
{"public_id": "<source public ID>", "delivery": "<reference>"}. The copy's contextual metadata recordsetchv_watermark_idandetchv_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:
| Variable | Value |
|---|---|
ETCHV_API_KEY | An API key with only the watermarks:embed scope |
ETCHV_DELIVERY | The delivery reference for uploads without a delivery: tag, such as website |
CLOUDINARY_CLOUD_NAME | Your Cloudinary cloud name |
CLOUDINARY_API_KEY | A Cloudinary API key that can upload |
CLOUDINARY_API_SECRET | Its API secret |
CLOUDINARY_WEBHOOK_SECRET | Optional. The secret of the key Cloudinary signs notifications with, if that's a different key (see below) |
PORT | Optional. The port to listen on (default 8080) |
ETCHV_API_KEY- Value
- An API key with only the
watermarks:embedscope
ETCHV_DELIVERY- Value
- The delivery reference for uploads without a
delivery:tag, such aswebsite
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:
git clone https://github.com/etchv-labs/integrations.git
cd integrations
npm install
cd cloudinary
npm startThe 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-Signatureheader, which the EdDSA-only scheme doesn't send. - For some uploads: pass
notification_urlin 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.
| Response | When | Cloudinary retries |
|---|---|---|
200 | 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 | No |
401 | Missing, stale or invalid signature | Yes |
413 | A notification larger than 1 MB | Yes |
503 | A rate limit, timeout, network or server error at Etchv or Cloudinary | Yes |
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.