Watermarking a whole product catalogue in one submission
Batch submit takes many items and a webhook instead of you managing ten thousand calls. Position, opacity, and the partial-failure handling nobody writes first.
Watermarking one image on Infrai is POST /v1/image/watermark with a text or watermark_image, a position from a nine-point grid and an opacity. Watermarking ten thousand is POST /v1/image/batch/submit with the items and a webhook_url, because managing ten thousand individual HTTP calls from your own process is a job you don’t need.
The batch endpoint is the easy part. Handling the items that fail is what separates a run you can trust from one you have to redo.
One image first
curl -sS -X POST "https://api.infrai.cc/v1/image/watermark" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"image": "img_2fVc8nRqLmT4xBzY",
"text": "© Northwind 2026",
"position": "bottom_right",
"opacity": 0.35,
"format": "auto"
}'
{
"ok": true,
"data": {
"image_id": "img_9wQ1zV6pLkS3dHyB",
"url": "https://files.infrai.cc/image/9wQ1zV6pLkS3dHyB.webp",
"format": "webp",
"width": 1600,
"height": 1600,
"size_bytes": 284672,
"sha256": "9f2c41bd7a9e8c0b5d3e1f7a2b8d4c6e0a9f3b1d5c7e2a4f6b8d0c2e4a6f8b0d",
"original_sha256": "3a9d545444dc7a02e085889a3f7130789f2c41bd7a9e8c0b5d3e1f7a2b8d4c6e",
"ops_applied": ["watermark"],
"created_at": "2026-09-21T03:50:00Z"
}
}
position is a nine-point grid — top_left through bottom_right, plus the edges and center. opacity between 0 and 1.
Get these right on one image before submitting ten thousand. A watermark at 0.8 opacity in the centre is a watermark that ruins the photo, and discovering that after a full catalogue run means running it again.
| Setting | Effect |
|---|---|
bottom_right, 0.3-0.4 | conventional, readable, unobtrusive |
center, 0.1-0.15 | anti-theft; visible across the whole image |
center, 0.5+ | the image is now unusable, which may be the point |
| Corner, 0.15 or lower | invisible on busy photos; decorative only |
Then the batch
curl -sS -X POST "https://api.infrai.cc/v1/image/batch/submit" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"items": [
{"image": "img_2fVc8nRqLmT4xBzY", "op": "watermark", "text": "© Northwind 2026", "position": "bottom_right", "opacity": 0.35},
{"image": "img_6hJk1pWsQnD9rGtU", "op": "watermark", "text": "© Northwind 2026", "position": "bottom_right", "opacity": 0.35}
],
"webhook_url": "https://ops.example.com/hooks/image-batch",
"store": true,
"idempotency_key": "catalogue-watermark-2026-09-v1"
}'
{
"ok": true,
"data": {
"job_id": "ibj_4kQ9mVzR1sXbNt",
"status": "queued",
"total_count": 2
}
}
The idempotency_key matters more here than on a single call. A retried submit of a ten-thousand-item batch without one is ten thousand duplicate operations and a bill to match.
Read the status, item by item
curl -sS "https://api.infrai.cc/v1/image/batch/status/ibj_4kQ9mVzR1sXbNt" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
The response carries status for the job and items for the individual results — which is the field that matters, because a batch is rarely all-or-nothing. One corrupt source file among ten thousand shouldn’t fail the run, and it shouldn’t silently vanish either.
import os
import requests
API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
CHUNK = 500
def submit(items: list[dict], key: str) -> dict:
resp = SESSION.post(
f"{API}/v1/image/batch/submit",
json={"items": items, "webhook_url": os.environ.get("BATCH_WEBHOOK"),
"store": True, "idempotency_key": key},
timeout=180,
)
body = resp.json()
if not body.get("ok"):
raise RuntimeError(body["error"]["code"])
return body["data"]
def status(job_id: str) -> dict:
resp = SESSION.get(f"{API}/v1/image/batch/status/{job_id}", timeout=60)
resp.raise_for_status()
return resp.json()["data"]
def watermark_catalogue(image_ids: list[str], run: str) -> list[dict]:
"""Chunk the catalogue and key each chunk deterministically, so a resumed run
re-submits the same chunks and gets the same jobs back rather than doubling the
work. The chunk index is part of the key for exactly that reason."""
jobs = []
for index in range(0, len(image_ids), CHUNK):
chunk = image_ids[index:index + CHUNK]
items = [{"image": image_id, "op": "watermark", "text": "© Northwind 2026",
"position": "bottom_right", "opacity": 0.35} for image_id in chunk]
job = submit(items, key=f"catalogue-watermark-{run}-chunk{index // CHUNK}")
jobs.append({"job_id": job["job_id"], "count": job["total_count"],
"first": chunk[0], "chunk": index // CHUNK})
return jobs
def reconcile(job_id: str) -> dict:
"""Separate the failures out so they can be retried on their own. A batch report
that only says '9,987 of 10,000' is a report that starts an investigation."""
state = status(job_id)
items = state.get("items") or []
failed = [i for i in items if (i.get("status") or "").lower() in {"failed", "error"}]
return {"job_id": job_id, "status": state.get("status"),
"total": len(items), "failed": len(failed),
"failed_inputs": [i.get("image") or i.get("input") for i in failed][:20]}
if __name__ == "__main__":
print(watermark_catalogue([os.environ["IMAGE_ID"]], run="2026-09-v1"))
Chunking with a deterministic key per chunk is what makes a ten-thousand-item run resumable. A process that dies at item 6,000 re-submits chunks 0-11 and gets the original jobs back rather than redoing them.
Cancel a run you got wrong
curl -sS -X POST "https://api.infrai.cc/v1/image/batch/cancel/ibj_4kQ9mVzR1sXbNt" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"job_id": "ibj_4kQ9mVzR1sXbNt"}'
Worth wiring into your admin tooling before the first big run rather than after. The moment you need it is the moment you’ve noticed the opacity is wrong on the first two hundred images.
Put a ceiling underneath it too: PUT /v1/account/budget/set bounds a catalogue run that turns out to be larger than anyone thought.
Limitations
A watermark drawn into the pixels is not a robust ownership claim — it can be cropped off, painted over or removed with the same tooling that applies it. There’s no invisible watermarking or forensic marking here, so if provenance matters legally, this isn’t the mechanism.
The batch surface is also submit, status and cancel only: no per-item retry endpoint, so failed items are resubmitted as a new smaller batch, and no priority control between jobs.
Cloudinary’s overlay transformations and imgix’s URL-based rendering both let you apply a watermark at delivery time rather than baking it in, which keeps the clean original and is more flexible. Watermarking bills per call with the live figure in GET /v1/discovery/image.watermark (verified 2026-09-21), and the whole catalogue run — along with the storage it writes and the queue that drives it — shows up in one GET /v1/account/usage on the same credential. Platform rates drift downward as vendor contracts improve.