JPG · PNG · WebP
MP4 · MOV
API files are uploaded privately to temporary storage and deleted after processing.
Clean image and video metadata through a private asynchronous API. Upload a file, create a job, poll to a terminal state, then download the cleaned result.
MP4 · MOV
API files are uploaded privately to temporary storage and deleted after processing.
API uses purchased credits. Advanced pack credits can be used with the API; Pro does not include API credits.
Browser cleaning stays on the device and never uploads files.
3 active jobs per account
Results are retained for about 24 hours
Keep API keys secret and use them on your server only. API files are uploaded privately; browser cleaning never uploads files.
POST /api/v1/uploadsPOST /api/v1/jobsGET /api/v1/jobs/{id}GET /api/v1/jobs/{id}/resultGET /api/v1/usage# Bash + curl + jq. Stop on HTTP errors; do not save error JSON as an image.
set -euo pipefail
BASE='https://clearaimetadata.com'
UPLOAD=$(curl --fail-with-body -sS --max-time 120 -X POST "$BASE/api/v1/uploads" -H "Authorization: Bearer $CLEAR_AI_METADATA_API_KEY" -H 'Content-Type: image/png' --data-binary @photo.png | jq -er .uploadId)
# Keep this key and uploadId when retrying an uncertain create request.
REQUEST_ID=$(uuidgen)
JOB=$(curl --fail-with-body -sS --max-time 30 -X POST "$BASE/api/v1/jobs" -H "Authorization: Bearer $CLEAR_AI_METADATA_API_KEY" -H 'Content-Type: application/json' -H "Idempotency-Key: $REQUEST_ID" -d "$(jq -nc --arg uploadId "$UPLOAD" '{uploadId:$uploadId}')" | jq -er .id)
STATE='ready'
for attempt in $(seq 1 120); do
STATE=$(curl --fail-with-body -sS --max-time 30 "$BASE/api/v1/jobs/$JOB" -H "Authorization: Bearer $CLEAR_AI_METADATA_API_KEY" | jq -er .status)
case "$STATE" in
succeeded) break ;;
failed|canceled) echo "Job failed: $JOB" >&2; exit 1 ;;
esac
sleep 2
done
[ "$STATE" = 'succeeded' ] || { echo "Still pending: $JOB; query this job again, do not create another." >&2; exit 1; }
curl --fail -sS --max-time 300 "$BASE/api/v1/jobs/$JOB/result" -H "Authorization: Bearer $CLEAR_AI_METADATA_API_KEY" -o cleaned.pngimport { readFile, writeFile } from 'node:fs/promises';
const base = 'https://clearaimetadata.com';
const key = process.env.CLEAR_AI_METADATA_API_KEY;
const headers = { Authorization: 'Bearer ' + key };
const upload = await fetch(base + '/api/v1/uploads', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'image/png' },
body: await readFile('photo.png'),
});
if (!upload.ok) throw new Error('Upload failed: ' + upload.status);
const { uploadId } = await upload.json();
const requestId = crypto.randomUUID(); // Reuse for retries with this uploadId.
const created = await fetch(base + '/api/v1/jobs', {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json',
'Idempotency-Key': requestId,
},
body: JSON.stringify({ uploadId }),
});
if (!created.ok) throw new Error('Job failed: ' + created.status);
const { id } = await created.json();
let current;
for (let attempt = 0; attempt < 120; attempt += 1) {
const response = await fetch(base + '/api/v1/jobs/' + id, { headers });
if (response.status === 429 || response.status === 503) {
await new Promise(resolve => setTimeout(resolve, Math.min(60, Number(response.headers.get('Retry-After')) || 5) * 1000));
continue;
}
if (!response.ok) throw new Error('Status failed: ' + response.status);
current = await response.json();
if (current.status === 'succeeded' || current.status === 'failed') break;
await new Promise((resolve) => setTimeout(resolve, 2000));
}
if (current?.status !== 'succeeded') throw new Error('Job did not succeed');
const result = await fetch(base + '/api/v1/jobs/' + id + '/result', { headers });
if (!result.ok) throw new Error('Download failed: ' + result.status);
await writeFile('cleaned.png', Buffer.from(await result.arrayBuffer()));import os, time, uuid, requests
base = 'https://clearaimetadata.com'
headers = {'Authorization': 'Bearer ' + os.environ['CLEAR_AI_METADATA_API_KEY']}
with open('photo.png', 'rb') as image:
upload = requests.post(base + '/api/v1/uploads', headers={**headers, 'Content-Type': 'image/png'}, data=image, timeout=120)
upload.raise_for_status()
upload_id = upload.json()['uploadId']
created = requests.post(
base + '/api/v1/jobs',
headers={**headers, 'Content-Type': 'application/json', 'Idempotency-Key': str(uuid.uuid4())},
json={'uploadId': upload_id}, timeout=30,
)
created.raise_for_status()
job_id = created.json()['id']
current = {'status': 'ready'}
for _ in range(120):
response = requests.get(base + '/api/v1/jobs/' + job_id, headers=headers, timeout=30)
if response.status_code in (429, 503):
time.sleep(min(60, int(response.headers.get('Retry-After', '5'))))
continue
response.raise_for_status()
current = response.json()
if current['status'] in ('succeeded', 'failed'):
break
time.sleep(2)
if current['status'] != 'succeeded':
raise RuntimeError('Job did not succeed')
result = requests.get(base + '/api/v1/jobs/' + job_id + '/result', headers=headers, timeout=300)
result.raise_for_status()
with open('cleaned.png', 'wb') as output:
output.write(result.content){"uploadId":"7a6e...","credits":1,"expiresAt":"..."}
{"id":"job_...","status":"ready","credits":1}
{"id":"job_...","status":"succeeded","progress":100}
{"balance":499,"apiUsesTotal":12,"apiAvailable":true}| 400 | INVALID_REQUEST | Check the JSON body, UUID and required headers. |
| 401 | INVALID_API_KEY | Use an active Bearer key from /account/api. |
| 402 | INSUFFICIENT_CREDITS | Buy any credit pack; Pro does not include API credits. |
| 404 | UPLOAD_NOT_FOUND / NOT_FOUND | The upload or job does not belong to this account, or expired. |
| 409 | IDEMPOTENCY_CONFLICT | The idempotency key was already used with another upload. |
| 409 | UPLOAD_ALREADY_USED | Each upload belongs to one job. Retry that job with its original idempotency key, or upload a new file. |
| 409 | RESULT_NOT_READY | The job has not succeeded yet; poll the status endpoint. |
| 410 | RESULT_EXPIRED | The download window has expired; submit a new job. |
| 413 | FILE_TOO_LARGE | Images are limited to 10 MiB and videos to 25 MiB. |
| 415 | UNSUPPORTED_FORMAT | Use JPEG, PNG, WebP, MP4 or MOV. |
| 429 | RATE_LIMITED / UPLOAD_LIMIT / CONCURRENCY_LIMIT | Respect Retry-After and reduce concurrent work. |
| 503 | API_UNAVAILABLE / PROCESSOR_OFFLINE | The API or the local processor is temporarily unavailable. |