Skip to content

API reference

Label AI images from your own software. One call in, one finished image out, with the badge drawn in and the metadata written.

This reference is available in English only.

Introduction

The API does what the website does, through the same code. An image goes in, the badge is drawn into one of the four corners, and the IPTC digital source type is written to XMP and IPTC. Calls are synchronous: labelling takes milliseconds, so there is no job id and nothing to poll.

Everything lives under api.imgmarker.net. Download links point at the website, because that is where files are served from.

Base URL
https://api.imgmarker.net/v1
Downloads
https://imgmarker.net/d/…
Request
multipart/form-data
Response
the image itself, errors as JSON
Max file size
25 MB
Max pixels
50 megapixels
Formats
JPEG, PNG, WebP
Authentication
Bearer token

Authentication

Create a key in your account and send it as a bearer token. We keep only a hash of it, the same way we keep passwords, so a key is shown once at creation and can never be looked up again. Lose it and you make a new one.

Authorization: Bearer 7|kJ3fQ2m…
Accept: application/json
  • Keep keys on your server. A key in browser JavaScript is a key you have given away.
  • One key per integration, so revoking one does not stop the others.
  • Revoking takes effect on the next call, with no delay.

Quickstart

Label one image and write the result next to the original.

curl -X POST https://api.imgmarker.net/v1/label \
  -H "Authorization: Bearer YOUR_KEY" \
  -F image=@photo.jpg \
  -F type=generated \
  -F corner=br \
  -o photo-aimarked.jpg
POST

/v1/label

Labels one image and returns it. A successful response carries the image file, not JSON. The filename keeps its stem and gains -aimarked before the extension.

Request fields

Field Type Values Description
image required file
  • image/jpeg .jpg, .jpeg
  • image/png .png
  • image/webp .webp

Up to 25 MB and 50 megapixels. Sent as multipart/form-data.

The image to label. Anything else is refused with unsupported_type.
type required string
  • generated the picture was created by AI
  • modified an existing picture was edited with AI
Decides which DigitalSourceType is written. See "What ends up in the file".
corner required string
  • tl top left
  • tr top right
  • bl bottom left
  • br bottom right
  • none no badge, metadata only
Where the badge is drawn. none leaves the picture untouched — see "Metadata without a badge".
badge_locale optional string
  • en AI generated / AI modified
  • de KI-generiert / KI-bearbeitet
  • es Generada por IA / Modificada por IA
  • it Generata da IA / Modificata da IA
  • fr Généré par IA / Modifié par IA

Default: en

Language of the badge caption. Ignored when corner is none.
creator optional string

Free text, 1 to 200 characters.

Written to XMP-dc:Creator and IPTC By-line.
description optional string

Free text, 1 to 2000 characters.

Written to XMP-dc:Description and IPTC Caption-Abstract.
credit optional string

Free text, 1 to 200 characters.

Written to XMP-photoshop:Credit.
c2pa_acknowledged optional boolean
  • true also 1, "1", yes, on
  • false also 0, "0", no, off

Default: false

Consent that existing Content Credentials are invalidated. Only needed when the image carries them — see "Content Credentials".
response optional string
  • file the finished image in the response body
  • url JSON with a download link

Default: file

What comes back. See "A link instead of the image".

Response headers

Header Type Values Description
X-Imgmarker-Credits-Remaining integer

Zero or more.

What is left after this call: the rest of the monthly allowance plus any bought credits. Sent on every label call, which is why /usage is rarely needed.
Content-Type string
  • image/jpeg
  • image/png
  • image/webp
  • application/json for response=url and for every error
Matches the input format. We never convert between formats.
Content-Disposition string

attachment; filename="yourname-aimarked.jpg"

Only when the body is the image. Your filename with the suffix before the extension.

Full example, with metadata

curl -X POST https://api.imgmarker.net/v1/label \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Idempotency-Key: article-88-hero" \
  -F image=@hero.png \
  -F type=modified \
  -F corner=tl \
  -F badge_locale=de \
  -F "creator=Redaktion Beispielblatt" \
  -F "description=Retuschiert mit generativer KI" \
  -F "credit=Beispielblatt / imgmarker" \
  -D headers.txt \
  -o hero-aimarked.png

A link instead of the image

Send response=url and you get JSON with a download link instead of the file. Useful when the URL is passed on to something else, or when you want the metadata without reading headers.

curl -X POST https://api.imgmarker.net/v1/label \
  -H "Authorization: Bearer YOUR_KEY" \
  -F image=@photo.jpg \
  -F type=generated \
  -F corner=br \
  -F response=url
Property Type Values Description
filename string The name we would have served the file under: yours with -aimarked before the extension.
url string

Absolute URL on imgmarker.net, not on this domain.

Download link. The link is the entire authorisation — treat it as a secret.
expires_at string

ISO 8601 with offset, e.g. 2026-08-05T09:14:02+00:00

When the file and the link disappear, 60 minutes after the call.
type string
  • generated
  • modified
What you asked for, echoed back.
source_type string
  • http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia for generated
  • http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia for modified
The IPTC value written into the file. Note the http, not https — that is the identifier, not a link.
width integer

Pixels.

Of the finished image. Unchanged from the original.
height integer

Pixels.

Of the finished image. Unchanged from the original.
had_content_credentials boolean
  • true credentials were present and were removed
  • false there were none
Whether the image arrived carrying C2PA Content Credentials.
credits_remaining integer

Zero or more.

Left after this call: the monthly allowance plus any credits bought on top.

Metadata without a badge

corner=none writes the declaration and draws nothing. Meant for callers who put their own visible label on the picture before sending it — a second badge on top of theirs is not a service. Everything else is unchanged: the same IPTC and XMP fields are written, and the call costs the same one credit.

One difference worth knowing: without a badge nothing is decoded and re-encoded, so the image comes back byte for byte as it was sent, with its EXIF intact and no second round of JPEG compression. Only the metadata is touched.

It also moves a responsibility. The machine-readable half is still there, but nothing on the picture tells a person — providing that is then yours. Whether a visible label is required depends on the case.

# The declaration goes into the file; nothing is drawn on the picture
curl -X POST https://api.imgmarker.net/v1/label \
  -H "Authorization: Bearer YOUR_KEY" \
  -F image=@already-labelled.jpg \
  -F type=generated \
  -F corner=none \
  -o out.jpg

The trade is storage. With the default the image passes through and nothing is kept; with a link it sits on our disk until expires_at, 60 minutes after the call. The link is the entire authorisation, so treat it as a secret. Download once, store the file yourself, and use the default when you have nowhere to pass a URL.

POST

/v1/label/batch

Several images in one call, answered with links. Included in the plans that list the batch endpoint; the others get 403 batch_not_available.

Parallel single calls are usually faster

Twenty concurrent calls to /label finish sooner than one batch worked through in sequence, and a failure costs you one image rather than the wait for the whole set. Use the batch endpoint when fanning out is awkward on your side.

Fields are the same as for a single call, with images[] instead of image. One type and one corner apply to the whole batch. How many images fit in one call comes from your plan.

Request fields

Field Type Values Description
images[] required file[]
  • image/jpeg .jpg, .jpeg
  • image/png .png
  • image/webp .webp

Repeat the field once per image. How many fit in one call comes from your plan; each up to 25 MB.

The images to label. Everything else is exactly as for /label.
type required string
  • generated the picture was created by AI
  • modified an existing picture was edited with AI
One value for the whole batch. Mixed batches need separate calls.
corner required string
  • tl top left
  • tr top right
  • bl bottom left
  • br bottom right
  • none no badge, metadata only
One value for the whole batch.
badge_locale, creator, description, credit, c2pa_acknowledged optional string

Same values as for /label.

Apply to every image in the batch. There is no response field here: the answer is always JSON with links.
curl -X POST https://api.imgmarker.net/v1/label/batch \
  -H "Authorization: Bearer YOUR_KEY" \
  -F "images[]=@one.jpg" \
  -F "images[]=@two.png" \
  -F "images[]=@three.webp" \
  -F type=generated \
  -F corner=br
  • Each image costs one credit, so ten in a batch cost ten. The endpoint saves round trips, not money.
  • Only images that came out the other end are charged. A rejected one costs nothing.
  • If your remaining allowance is smaller than the batch, nothing is processed and the call answers 402. Half a batch you cannot account for is worse than none.
  • zip_url hands you everything that worked as one archive.
  • Every link expires with the batch, 60 minutes after the call.
GET

/v1/usage

What is left, and when it refills. Cheap to call before a large run, though the credits header on every label call usually makes it unnecessary.

curl https://api.imgmarker.net/v1/usage \
  -H "Authorization: Bearer YOUR_KEY"
Property Type Values Description
plan string

The key of your plan, as shown on the pricing page.

Which plan the account is on.
quota integer

Images per month.

What the plan includes, before anything is used.
used integer

Zero or more.

Of the monthly allowance, in this period. Credits bought on top are not counted here.
remaining integer

Zero or more.

What the next call is measured against: the rest of the allowance plus any bought credits.
credits integer

Zero or more.

Bought on top. Spent only once the monthly allowance is gone, and never cleared by the reset.
resets_at string | null

ISO 8601 with offset, or null on a plan that counts per day.

When used goes back to zero — your billing date, not the first of the month.
GET

/v1/health

Reachability, no key required. Meant for uptime monitoring, so it never touches your allowance.

curl https://api.imgmarker.net/v1/health

Idempotency

When a connection drops mid-call you cannot tell whether the image was labelled and charged. Send an Idempotency-Key header with a value you choose, up to 128 characters, and a repeat of that call is not charged again.

  • A repeat with a used key answers 409 already_processed. The image is not sent a second time, because we no longer hold it.
  • Failed calls are not recorded, so the key stays usable after an error.
  • Keys belong to your account. Two customers may use the same value without colliding.
  • A file hash makes a good key: same input, same key, no accidental double charge.
Header Type Values Description
Idempotency-Key optional string

Any value you choose, 1 to 128 characters. A UUID or a hash of the file both work.

Makes a repeat of the same call free. Scoped to your account and kept for 24 hours.
Authorization required string

Bearer im_live_…

Your API key. Required on every endpoint except /health.
curl -X POST https://api.imgmarker.net/v1/label \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Idempotency-Key: order-4711-image-3" \
  -F image=@photo.jpg -F type=generated -F corner=br

Quota and rate limits

Two separate limits, answering with different codes so you can tell them apart without guessing.

402 quota_exceeded

The monthly allowance is used up. Waiting will not help before the reset date, a larger plan will. We do not bill for overage, so nothing runs up an invoice you did not expect.

429 Too Many Requests

Too many calls per minute for your plan. Read Retry-After and back off. The allowance itself is untouched.

The rate limit counts per key rather than per account, so one runaway script does not throttle your other integrations. The monthly allowance resets on your billing date, not on the first of the month, so a subscription taken out on the 28th still gets a full period.

A rejected image costs nothing. Only calls that return an image are counted.

Content Credentials

Labelling re-encodes the image, which breaks the pixel hash inside an existing C2PA manifest. A broken signature is worse than none, because a verifier reports it as tampered. So when we find credentials, the call is refused rather than quietly destroying them.

HTTP/1.1 409 Conflict

{
  "error": "c2pa_consent_required",
  "message": "This image carries Content Credentials. Labelling invalidates them. Repeat the request with c2pa_acknowledged=true to proceed."
}

Once acknowledged, the invalidated manifest is removed instead of being left in place broken. The same rule applies on the website, so the API cannot be used to sidestep a consent the interface insists on.

What ends up in the file

Two things: the badge drawn into the pixels, and the machine readable declaration. The declaration is the IPTC digital source type, written to XMP and to legacy IPTC so old and new readers agree.

type DigitalSourceType
generated http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia
modified http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia

Note the http: the IPTC vocabulary uses that form, and a reader matching the exact string would miss anything else.

Checking the result

exiftool -G1 -s \
  -XMP-iptcExt:DigitalSourceType \
  -IPTC:Caption-Abstract \
  -XMP-dc:Creator \
  photo-aimarked.jpg

Error reference

Every error is JSON with a stable error field. Branch on that, not on the human readable message, which may be reworded.

{
  "error": "too_many_pixels",
  "message": "The image has too many pixels."
}
Status error What to do
401 Missing, malformed or revoked key. Create a new one in your account.
402 quota_exceeded Wait for the reset date, or move to a larger plan.
403 api_not_available Your plan does not include API access.
403 batch_not_available Your plan does not include the batch endpoint.
409 c2pa_consent_required Repeat with c2pa_acknowledged=true if you accept losing the credentials.
409 already_processed This Idempotency-Key was used before. Use a new one to label again.
400 invalid_idempotency_key Shorten the key to 128 characters or fewer.
422 too_large The file exceeds 25 MB.
422 too_many_pixels More than 50 megapixels. Downscale first.
422 unsupported_type Only JPEG, PNG and WebP are accepted.
422 corrupt The file is not a readable image.
422 Validation failed. The response lists the fields under "errors".
429 Rate limit for your plan. Read Retry-After and back off.
500 processing_failed Something broke on our side. Safe to retry with a new Idempotency-Key.

Retrying sensibly

Retry on 429 and 500, with a growing delay. Never retry 402 or 422: the answer will not change until you change something.

import time

RETRYABLE = {429, 500, 502, 503}


def with_retries(call, attempts=4):
    for attempt in range(attempts):
        response = call()

        if response.status_code not in RETRYABLE:
            return response

        # Honour Retry-After when the server sends one
        wait = float(response.headers.get("Retry-After", 2 ** attempt))
        time.sleep(wait)

    return response

Retention

Nothing is kept. The image exists on our server for the duration of the call and is deleted once the response has gone out. We record the call itself for billing and your usage statistics: timestamp, label type, file format, status. No filename, no image data.

Responsibility for labelling correctly stays with you. We write what you ask us to write. Deciding whether an image needs a label, and which one, is yours.

Versioning

The version sits in the path. Within v1 we add fields and error codes but never remove or rename them, so parsing by field name stays safe. Anything that would break your integration goes into a new version, and v1 keeps running.

Questions about the API: info@fezznrw.de