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>.
| Field | Values | Default |
|---|---|---|
| mode | quick, full | quick |
| keepCopyright | 0, 1 | 0 |
| keepColorProfile | 0, 1 | quick: 1, full: 0 |
| keepOrientation | 0, 1 | quick: 1, full: 0 |
| removeC2pa | 0, 1 | 0 |
| response | file, json | file |
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.
| Field | Values | Default |
|---|---|---|
| edits | JSON array of {key, value} | required |
| replaceAll | 0, 1 | 0 |
| removeC2pa | 0, 1 | 0 |
| response | file, json | file |
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": "..." } }| Code | Status | Meaning |
|---|---|---|
| missing_api_key | 401 | No key was supplied. |
| invalid_api_key | 401 | The key is unknown, revoked or malformed. |
| plan_required | 403 | Your plan does not include API access. |
| missing_file | 400 | No file part in the request. |
| invalid_request | 400 | A parameter was missing or malformed. |
| file_too_large | 413 | The file exceeds your plan limit. |
| unsupported_format | 415 | The format is not supported. |
| corrupt_file | 422 | The file could not be parsed. |
| unsupported_operation | 422 | Valid file, but the operation is not implemented for its format. |
| rate_limited | 429 | Too many requests in a short window. |
| quota_exceeded | 429 | The 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
| Format | Analyze | Clean | Edit |
|---|---|---|---|
| JPEG | yes | yes | yes |
| PNG | yes | yes | yes |
| WebP | yes | yes | yes |
| HEIC/HEIF | yes | yes | yes |
| AVIF | yes | yes | yes |
| MP4 | yes | yes | yes |
| QuickTime MOV | yes | yes | yes |
| WebM | yes | yes | no |
| yes | yes | yes | |
| MP3 | yes | yes | yes |
| WAV | yes | yes | yes |
| M4A | yes | yes | yes |
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.
| Key | Field |
|---|---|
| camera.make | Make |
| camera.model | Model |
| camera.lensMake | Lens make |
| camera.lensModel | Lens model |
| camera.serialNumber | Body serial number |
| camera.lensSerialNumber | Lens serial number |
| capture.iso | ISO |
| capture.exposureTime | Exposure time |
| capture.fNumber | Aperture (f-number) |
| capture.focalLength | Focal length |
| capture.focalLength35mm | Focal length (35 mm equiv.) |
| capture.exposureBias | Exposure compensation |
| capture.exposureProgram | Exposure program |
| capture.meteringMode | Metering mode |
| capture.whiteBalance | White balance |
| capture.flash | Flash |
| capture.colorSpace | Colour space |
| capture.orientation | Orientation |
| date.original | Date taken |
| date.digitized | Date digitised |
| date.modified | File change date |
| general.software | Software |
| general.artist | Artist |
| general.copyright | Copyright |
| general.description | Image description |
| location.latitude | Latitude |
| location.longitude | Longitude |
| location.altitude | Altitude |
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.