Remove watermarks with one API call
Send an image, get a clean one back — visible AI logos and hidden tags removed. Not a developer? Use the free web tool.
Easy to integrate
One endpoint, no SDK required. Here's what to know:
Source image
Upload the image as multipart form-data (PNG, JPG or WebP, up to 25 MB).
Result
The cleaned image bytes are returned directly, plus an X-Unmarkr-Removed header listing what was removed.
Resolution
Full resolution up to 24 megapixels — no recompression on the API.
What it removes
Visible logos (Gemini, Doubao…) and hidden AI tags. Invisible/SynthID is coming soon.
Get started
- 1
Get your API key
Create a key in your dashboard. API calls spend credits — subscribe (Lite/Pro) or buy pay-as-you-go credits.
- 2
Use a code sample
Copy one of the samples below and drop in your key.
- 3
Read the reference
Check the parameters, response and limits in the reference section.
Sample code
Send an image file (replace unmarkr_live_xxx with your key):
curl -X POST https://unmarkr.io/api/remove \
-H "Authorization: Bearer unmarkr_live_xxx" \
-F "image=@photo.png" \
-F "mode=standard" \
-o clean.pngOutput & resolution
The cleaned image is returned in the same format you sent. Resolution depends on your access:
| Tier | Format | Max resolution |
|---|---|---|
| Free web tool | PNG / JPG / WebP | Preview (~0.25 MP) |
| API / HD (credits) | PNG / JPG / WebP | Full, up to 24 MP |
PNG keeps transparency and is lossless; JPG/WebP are smaller. Output format matches the input.
API reference
One endpoint cleans an image. Send it as multipart form-data, get the cleaned bytes back.
Endpoint
POST https://unmarkr.io/api/removeAuthentication
Send your secret key as a bearer token. Create one in your dashboard and keep it server-side.
Authorization: Bearer unmarkr_live_xxxRequest parameters
Body is multipart/form-data.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| image | file | Required | — | The image to clean — PNG, JPG or WebP. |
| mode | string | Optional | standard | Which removal to run — standard or full. See Modes below. |
Modes
Two modes, depending on whether you need invisible-watermark removal — which has to run on a GPU:
| mode | GPU | Removes | Credits | Status |
|---|---|---|---|---|
| standard | No | Visible logos & badges + hidden AI tags (C2PA, “Made with AI”, EXIF/XMP). | 1 | Available |
| full | Yes | Everything in standard, plus invisible watermarks woven into the pixels (e.g. SynthID). | 3 | Coming soon |
standard is the default. full currently returns 501 until GPU removal goes live.
Response
200 OK returns the cleaned image bytes directly, in the same format you sent (no JSON wrapper). On any error, the body is a JSON object instead — see Errors & retries.
Response headers
| Header | Description |
|---|---|
| Content-Type | Format of the cleaned image, matching your input (e.g. image/png). |
| X-Unmarkr-Removed | Comma-separated list of what was removed, e.g. Gemini sparkle, metadata:c2pa. |
Credits
Each successful removal costs 1 credit. Subscription credits refill monthly; pay-as-you-go credits never expire. A 402means you're out of credits.
Error format
Errors return a JSON body with a human-readable error message (and sometimes a machine-readable code):
{
"error": "Out of credits. Buy credits or subscribe to Lite/Pro.",
"code": "no_credits"
}Rate limit
Requests are limited per API key (default 60 / minute). Exceeding it returns 429 Too Many Requests with a Retry-After header (seconds to wait). Need higher limits? Contact us.
Errors & retries
| Status | Meaning |
|---|---|
| 401 | Missing or invalid API key. |
| 402 | Out of credits — buy credits or subscribe. |
| 413 | Image exceeds the 25 MB limit. |
| 429 | Rate limit exceeded — wait Retry-After seconds. |
| 501 | Full (GPU) mode isn’t available yet — use standard. |
| 5xx | Temporary engine error — retry with backoff. |
Exponential backoff
For 5xx errors, retry with increasing delays (don't retry 429 immediately — honour Retry-After):
import time, requests
def remove(path, key, attempts=4):
for i in range(attempts):
r = requests.post(
"https://unmarkr.io/api/remove",
headers={"Authorization": f"Bearer {key}"},
files={"image": open(path, "rb")},
)
if r.status_code < 500:
return r
time.sleep(2 ** i) # 1s, 2s, 4s, 8s
r.raise_for_status()