Unmarkr
Developer API

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. 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. 2

    Use a code sample

    Copy one of the samples below and drop in your key.

  3. 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.png

Output & resolution

The cleaned image is returned in the same format you sent. Resolution depends on your access:

TierFormatMax resolution
Free web toolPNG / JPG / WebPPreview (~0.25 MP)
API / HD (credits)PNG / JPG / WebPFull, 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/remove

Authentication

Send your secret key as a bearer token. Create one in your dashboard and keep it server-side.

Authorization: Bearer unmarkr_live_xxx

Request parameters

Body is multipart/form-data.

ParameterTypeRequiredDefaultDescription
imagefileRequiredThe image to clean — PNG, JPG or WebP.
modestringOptionalstandardWhich 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:

modeGPURemovesCreditsStatus
standardNoVisible logos & badges + hidden AI tags (C2PA, “Made with AI”, EXIF/XMP).1Available
fullYesEverything in standard, plus invisible watermarks woven into the pixels (e.g. SynthID).3Coming 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

HeaderDescription
Content-TypeFormat of the cleaned image, matching your input (e.g. image/png).
X-Unmarkr-RemovedComma-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

StatusMeaning
401Missing or invalid API key.
402Out of credits — buy credits or subscribe.
413Image exceeds the 25 MB limit.
429Rate limit exceeded — wait Retry-After seconds.
501Full (GPU) mode isn’t available yet — use standard.
5xxTemporary 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()

Ready to build?

Grab a key and make your first request in minutes.

Get your API key