Send a file, get the clean file back. Photos, PDFs, video and RAW from scripts and services, with the same allowances as the website on every plan.
Base address https://metadatatools.io. One file per request. The clean file is the response body and the outcome travels in headers, so there is nothing to parse.
| Call | Accepts | How to send it |
|---|---|---|
| POST /api/v1/clean/image | JPEG, PNG, WebP | Multipart field file, or the raw body |
| POST /api/v1/clean/pdf | Multipart field file, or the raw body | |
| POST /api/v1/clean/video | MP4, M4V, MOV, MKV, WebM | Raw body plus X-Filename |
| POST /api/v1/clean/raw | CR2, CR3, NEF, ARW, DNG, RAF, ORF, RW2 and more | Raw body plus X-Filename |
| POST /api/v1/inspect | Any file | Raw body; returns JSON, changes nothing |
| GET /api/v1/usage | n/a | No body; costs nothing |
Sign up, then create a key under Dashboard › API keys. It is displayed once. Put it in the Authorization header of every call:
Authorization: Bearer mrt_live_xxxxxxxxxxxxxxxx
Keep keys in environment variables, never in front-end code. A key can be revoked from the same page at any moment.
Post the file as multipart/form-data in a field named file, or as the raw body.
# photo (JPEG, PNG, WebP) curl -X POST https://metadatatools.io/api/v1/clean/image \ -H "Authorization: Bearer $KEY" \ -F "[email protected]" -o holiday-clean.jpg # PDF curl -X POST https://metadatatools.io/api/v1/clean/pdf \ -H "Authorization: Bearer $KEY" \ -F "[email protected]" -o invoice-clean.pdf
// Node.js 18 or newer
import fs from "node:fs";
const form = new FormData();
form.append("file", new Blob([fs.readFileSync("holiday.jpg")]), "holiday.jpg");
const res = await fetch("https://metadatatools.io/api/v1/clean/image", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.KEY}` },
body: form,
});
if (!res.ok) throw new Error(await res.text());
console.log("removed", res.headers.get("x-metadata-removed"), "left", res.headers.get("x-metadata-remaining"));
fs.writeFileSync("holiday-clean.jpg", Buffer.from(await res.arrayBuffer()));# Python
import os, requests
with open("holiday.jpg", "rb") as f:
r = requests.post(
"https://metadatatools.io/api/v1/clean/image",
headers={"Authorization": "Bearer " + os.environ["KEY"]},
files={"file": f},
)
r.raise_for_status()
print(r.headers["X-Metadata-Removed"], "removed;", r.headers["X-Metadata-Remaining"], "left")
open("holiday-clean.jpg", "wb").write(r.content)Pixels are copied, not re-compressed, so quality is exactly what you sent.
These files are large, so send the bytes as the request body and put the file name in the X-Filename header. The call stays open while our server works and then returns the clean file. Video is remuxed without re-encoding, so picture and sound are untouched. Each file spends one of your monthly video or RAW cleanings.
# video: .mp4 .m4v .mov .mkv .webm curl -X POST https://metadatatools.io/api/v1/clean/video \ -H "Authorization: Bearer $KEY" -H "X-Filename: holiday.mov" \ --data-binary @holiday.mov -o holiday-clean.mov # RAW: .cr2 .cr3 .nef .arw .dng .raf .orf .rw2 and more curl -X POST https://metadatatools.io/api/v1/clean/raw \ -H "Authorization: Bearer $KEY" -H "X-Filename: DSC_1024.nef" \ --data-binary @DSC_1024.nef -o DSC_1024-clean.nef
For RAW, location, capture time, owner name, serial numbers and XMP and IPTC blocks are removed, while the image data and the tags a RAW editor depends on stay put. Nikon uses its camera serial number as a key to decode its own data, so it is kept and appears in X-Metadata-Left along with anything else that could not be taken out. Set a generous client timeout for big files.
POST /api/v1/inspect takes any file as the raw body and returns every tag it holds as JSON, without changing the file. It uses one image-and-PDF allowance, like a clean call, and a failed read gives it back.
curl -X POST https://metadatatools.io/api/v1/inspect \ -H "Authorization: Bearer $KEY" \ --data-binary @holiday.jpg
Every clean call reports its outcome in headers:
Treat a file as clean only when X-Metadata-Remaining is 0. To see your balance without spending anything, call GET /api/v1/usage: it returns your plan, what you used and have left of each allowance (a limit of null is unlimited) and the largest file you may send.
Prefer to talk to an assistant instead of writing code? The MCP server wraps this API.
| Plan | Images and PDFs / day | Videos / month | RAW photos / month | Largest file |
|---|---|---|---|---|
| Free | 500 | 10 | 10 | 25 MB |
| Starter | Unlimited | 100 | 100 | 100 MB |
| Pro | Unlimited | 1,000 | 1,000 | 500 MB |
| Business | Unlimited | 10,000 | 10,000 | 1000 MB |
The daily image-and-PDF count is shared by the website, the API and MCP and resets at midnight UTC. Video and RAW counts reset every month. Requests are also limited to 120 a minute per account. More room? Compare plans
Failures come back as JSON in this shape: { "error": { "code": "…", "message": "…" } }
| 401 | invalid_key | No key was sent, or it is wrong or was revoked. |
| 403 | plan_no_api | The plan on this account has no API access. |
| 413 | file_too_large | The file is bigger than your plan allows. |
| 415 | unsupported_type | This endpoint does not take that kind of file. |
| 422 | unreadable_file | The file is damaged, truncated or encrypted. |
| 429 | daily_quota_exceeded, job_quota_exceeded, rate_limited | The day's image and PDF allowance or the month's video or RAW allowance is spent, or you went above 120 requests a minute. |
On a 429 with a Retry-After header, wait that long. 413, 415 and 422 will not succeed on a retry; log them and move on. Bulk recipes are in cleaning files in bulk.