# Clear AI Metadata API v1

Updated: 2026-11-02. Base URL: https://clearaimetadata.com

The API cleans image and video metadata asynchronously. Files are uploaded privately, processed by a restricted local runner, and returned as a downloadable file. Browser cleaning is separate and never uploads files. Pro does not include API credits.

Limits: images 10 MiB, videos 25 MiB. Upload 10/min, API requests 60/min. Results are retained for about 24 hours.

Create a key at /account/api. Keys are shown once; keep them on your server.

## Workflow

1. `POST /api/v1/uploads` with raw bytes and the media Content-Type.
2. `POST /api/v1/jobs` with `Idempotency-Key` and `{ "uploadId": "..." }`.
3. `GET /api/v1/jobs/{id}` until `succeeded` or `failed`.
4. `GET /api/v1/jobs/{id}/result` to download the cleaned file.
5. `GET /api/v1/usage` for balance, successful calls and API availability.

## Examples

### curl

```curl
# 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.png
```

### node

```node
import { 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()));
```

### python

```python
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)
```

## Errors

- 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.

OpenAPI: https://clearaimetadata.com/openapi.json
