Skip to content

API documentation

Everything below is generated from the same definitions the engine uses, so it cannot describe a capability the code does not have.

Machine-readable description: /api/v1/openapi.json

Authentication

Send your key in either header. Both are equivalent.

Authorization: Bearer nmd_live_xxxxxxxxxxxxxxxx
X-API-Key: nmd_live_xxxxxxxxxxxxxxxx

Keys are created in your dashboard. A key is shown once and stored only as a hash — if you lose it, revoke it and create another.

POST /api/v1/analyze

Reads every metadata field in a file and returns the normalised model.

curl -X POST https://your-domain/api/v1/analyze \
  -H "Authorization: Bearer $NMD_KEY" \
  -F "file=@photo.jpg"

The response groups fields by category and keeps untranslated values under raw:

{
  "file": { "name": "photo.jpg", "format": "jpeg", "size": 4821904 },
  "counts": { "total": 31, "removable": 26, "sensitive": 16 },
  "blocks": { "exif": { "present": true, "count": 29 }, "gps": { "present": true } },
  "metadata": {
    "camera": {
      "camera.make": { "label": "Make", "value": "Apple", "sensitive": true, "removable": true }
    }
  }
}

POST /api/v1/clean

Removes metadata, then re-reads the produced bytes to verify the result. Returns the cleaned file by default, or the report alone with <code>response=json</code>.

FieldValuesDefault
modequick, fullquick
keepCopyright0, 10
keepColorProfile0, 1quick: 1, full: 0
keepOrientation0, 1quick: 1, full: 0
removeC2pa0, 10
responsefile, jsonfile
curl -X POST https://your-domain/api/v1/clean \
  -H "Authorization: Bearer $NMD_KEY" \
  -F "file=@video.mp4" -F "mode=full" -OJ

removeC2pa is off by default and is not implied by mode=full. A metadata clean never removes C2PA Content Credentials: stripping a file’s record of where it came from is a separate instruction, and the caller has to give it.

When a file is returned, a summary of the verification report travels URL-encoded in the X-NoMoreData-Result header: the scalars verbatim — verified is true only when re-analysis found nothing unexpected surviving — and each list as a count, removedCount, residualCount and so on. It always decodes and parses. For the lists themselves, ask for response=json.

POST /api/v1/edit

Applies field edits. <code>value: null</code> deletes a field. Keys outside the writable registry are reported in <code>warnings</code> rather than silently dropped.

FieldValuesDefault
editsJSON array of {key, value}required
replaceAll0, 10
removeC2pa0, 10
responsefile, jsonfile

replaceAll drops the existing metadata blocks. It does not remove C2PA Content Credentials — that is removeC2pa, off by default. When you replace everything and leave the credential, the response says so.

curl -X POST https://your-domain/api/v1/edit \
  -H "Authorization: Bearer $NMD_KEY" \
  -F "file=@photo.jpg" \
  -F 'edits=[{"key":"camera.make","value":"Canon"},{"key":"location.latitude","value":null}]' \
  -F "response=json"

POST /api/v1/apply-profile

Writes a coherent set of device-style fields. Profile ids come from <code>GET /api/profiles</code>.

curl -X POST https://your-domain/api/v1/apply-profile \
  -H "Authorization: Bearer $NMD_KEY" \
  -F "file=@photo.jpg" -F "profile=canon-eos-r5" -F "iso=400"

Applying a profile writes metadata typical of a device. It does not and cannot establish that a file originated there — metadata is editable by design and is not evidence of provenance. Every response carries this disclaimer.

Other languages

JavaScript

const body = new FormData();
body.append('file', file);           // a File or Blob
body.append('mode', 'full');
body.append('response', 'json');

const res = await fetch('https://your-domain/api/v1/clean', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.NMD_KEY}` },
  body,
});

const report = await res.json();
console.log(report.verified, report.removed.length);

Python

import os, requests

with open("photo.jpg", "rb") as fh:
    response = requests.post(
        "https://your-domain/api/v1/clean",
        headers={"Authorization": f"Bearer {os.environ['NMD_KEY']}"},
        files={"file": fh},
        data={"mode": "full", "response": "json"},
    )

report = response.json()
print(report["verified"], len(report["removed"]))

Errors

Every error has the same shape and never contains internal detail or stack traces.

{ "error": { "code": "unsupported_format", "message": "GIF is not supported.", "hint": "..." } }
CodeStatusMeaning
missing_api_key401No key was supplied.
invalid_api_key401The key is unknown, revoked or malformed.
plan_required403Your plan does not include API access.
missing_file400No file part in the request.
invalid_request400A parameter was missing or malformed.
file_too_large413The file exceeds your plan limit.
unsupported_format415The format is not supported.
corrupt_file422The file could not be parsed.
unsupported_operation422Valid file, but the operation is not implemented for its format.
rate_limited429Too many requests in a short window.
quota_exceeded429The monthly allowance is spent.

Rate limits and quotas

Two limits apply: a short-window per-address rate limit that protects the service, and your plan’s monthly call allowance. Successful responses carry your remaining quota.

X-RateLimit-Limit: 2000
X-RateLimit-Remaining: 1834
X-RateLimit-Reset: 1767225600

A quota refusal returns 429 with quota_exceeded and a Retry-After header. Usage is counted only for requests that actually did work, so a rejected request does not spend your allowance.

Format support

FormatAnalyzeCleanEdit
JPEGyesyesyes
PNGyesyesyes
WebPyesyesyes
HEIC/HEIFyesyesyes
AVIFyesyesyes
MP4yesyesyes
QuickTime MOVyesyesyes
WebMyesyesno
PDFyesyesyes
MP3yesyesyes
WAVyesyesyes
M4Ayesyesyes

An operation that is not implemented for a format returns 422 unsupported_operation rather than returning the input unchanged.

Writable fields (28)

Keys accepted by /api/v1/edit. Each is backed by a real encoder.

KeyField
camera.makeMake
camera.modelModel
camera.lensMakeLens make
camera.lensModelLens model
camera.serialNumberBody serial number
camera.lensSerialNumberLens serial number
capture.isoISO
capture.exposureTimeExposure time
capture.fNumberAperture (f-number)
capture.focalLengthFocal length
capture.focalLength35mmFocal length (35 mm equiv.)
capture.exposureBiasExposure compensation
capture.exposureProgramExposure program
capture.meteringModeMetering mode
capture.whiteBalanceWhite balance
capture.flashFlash
capture.colorSpaceColour space
capture.orientationOrientation
date.originalDate taken
date.digitizedDate digitised
date.modifiedFile change date
general.softwareSoftware
general.artistArtist
general.copyrightCopyright
general.descriptionImage description
location.latitudeLatitude
location.longitudeLongitude
location.altitudeAltitude

How your files are handled

A file sent to the API is held in memory for the duration of one request, used only for the operation requested, and released with the response. Nothing is written to disk, nothing is stored, nothing is logged, and nothing is used for training or analytics.

This differs from the website, which processes files entirely in the browser and uploads nothing at all. If your integration can run client-side, that is the stronger option.