Introduction
The API does three core things:
- Mark (
POST /v1/marks) — registers an image produced by your GenAI app as "AI-generated". This is the primary use case, built for the EU AI Act Article 50 content-marking obligation. - Register (
POST /v1/registrations) — registers hashes computed in the browser or on the client (the free web signing flow uses this). - Verify (
GET /v1/verify) — checks whether an image is in the registry, by exact hash or by perceptual fingerprint (for recompressed or resized copies).
Records are timestamped in batches at regular intervals; no one can backdate a record, and its proof
can be checked independently via GET /v1/anchors/…/proof/…. v1 scope is images only
(JPG/PNG); video and audio come in later releases.
Authentication
Write endpoints (marks, usage, webhook-endpoints,
sign-digest) require an Authorization header:
Authorization: Bearer ozdna_live_…
Keys come with one of two prefixes: ozdna_live_ (production) and ozdna_test_
(test). Records created with a test key are flagged is_test, excluded from timestamping and
quotas, and never appear in public verification. A key is shown once, at creation; the server stores only
its SHA-256 digest, never the raw key. Verification and record-read endpoints work without a key
(IP-based rate limits apply).
Base URL
https://api.ozdna.com # single host; test vs. production is set by the key prefix
There is no separate sandbox host — an ozdna_test_ key uses the same API with isolated test records.
Fingerprints
The API does not store the image itself; it works with three fingerprints:
| Field | Format | What it's for |
|---|---|---|
sha256 | 64 hex | The exact file — changing a single byte breaks the match. |
phash | 16 hex (64 bit) | Perceptual hash — survives recompression, resizing and metadata stripping. |
pdq256 | 64 hex (256 bit) | Optional second perceptual hash; confirms probable matches and cuts false positives. |
Recommended flow: compute the fingerprints on the client (browser or your server) and send only the hashes. The image never crosses the network.
POST /v1/marks live · registry only
Registers an AI-generated image. The record kind is always ai_generated — there is no way
to mark an image as a "camera capture" through this endpoint.
Current mode: registry only. The record, fingerprint and timestamp are created; a C2PA
manifest is not yet embedded in the image. The mode: "registry_only" and
embed: false fields in the response say so explicitly. Once server-side embedding and signing
ship, the same endpoint will return the marked image bytes.
JSON body (recommended)
| Field | Type | Description |
|---|---|---|
sha256 required | string | 64 hex |
phash required | string | 16 hex |
file_mime required | string | image/jpeg · image/png |
pdq256 | string | 64 hex, optional |
file_bytes | integer | File size in bytes |
title | string | ≤ 120 characters, shown on the public record |
generator | string | ≤ 100 characters, e.g. acme-imagegen/2.3. Will be written into the manifest once embedding ships; ignored for now. |
is_test | boolean | Test record (automatically true with a test key) |
Example request
curl -X POST https://api.ozdna.com/v1/marks \
-H "Authorization: Bearer ozdna_live_…" \
-H "Content-Type: application/json" \
-d '{
"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"phash": "c4a2b1d8e0f39a57",
"file_mime": "image/png",
"title": "product hero v1",
"generator": "acme-imagegen/2.3"
}'
Alternative: multipart file upload
If you send multipart/form-data with a file (JPG/PNG, ≤ 20 MB), the server computes
sha256. Because server-side image decoding isn't available yet, the phash form field is
still required in this mode; pdq256, title and is_test are optional.
curl -X POST https://api.ozdna.com/v1/marks \
-H "Authorization: Bearer ozdna_live_…" \
-F "file=@output.png;type=image/png" \
-F "phash=c4a2b1d8e0f39a57"
Example response 201 Created · 200 if the sha256 is already registered
{
"mode": "registry_only",
"embed": false,
"note": "No C2PA bytes embedded yet — record + fingerprint only (Sept spike).",
"record": {
"id": "rec_…",
"kind": "ai_generated",
"status": "registered",
"sha256": "9f86d081…0f00a08",
"phash": "c4a2b1d8e0f39a57",
"pdq256": null,
"created_at": "2026-10-08T14:03:22.113Z",
"url": "https://ozdna.com/r/rec_…"
},
"deduplicated": false
}
If the same sha256 is already registered, no new record is created; the existing record is
returned with "deduplicated": true.
POST /v1/registrations live
Registers hashes computed on the client. The free browser signing flow (the image is signed in the
browser; only hashes reach the server) uses this endpoint. The body is the same as marks, plus:
| Field | Type | Description |
|---|---|---|
kind required | string | ai_generated · claimed_capture · unspecified |
claimed_capture is a claim: it records the registrant's "I took this" statement; it
does not prove the image was actually captured by a camera.
GET /v1/verify live
Checks whether an image is in the registry. Two modes:
| Parameter | Description |
|---|---|
hash | 64 hex SHA-256 — exact file match |
phash | 16 hex — perceptual match (modified copies) |
pdq | 64 hex, optional — confirms a perceptual match |
deep | 1 = wider search (complete up to Hamming distance ≤ 10). The default search is complete up to ≤ 7. |
Example — exact match
curl "https://api.ozdna.com/v1/verify?hash=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
{
"verdict": "EXACT_ANCHORED",
"match_type": "exact",
"confidence": "exact",
"hash": "9f86d081…0f00a08",
"record": {
"id": "rec_…",
"sha256": "9f86d081…0f00a08",
"phash": "c4a2b1d8e0f39a57",
"pdq256": null,
"status": "anchored",
"createdAt": "2026-10-08T14:03:22.113Z",
"anchoredAt": "2026-10-08T18:00:41Z",
"creatorDisplay": "Acme Imagegen"
}
}
Example — perceptual match
curl "https://api.ozdna.com/v1/verify?phash=c4a2b1d8e0f39a51&pdq=…"
{
"verdict": "VISUAL_MATCH_HIGH",
"match_type": "perceptual",
"confidence": "high",
"phash_distance": 2,
"pdq_distance": 14,
"pdq_confirmed": true,
"show_as_match": true,
"record": { "id": "rec_…", "status": "anchored", "…": "…" },
"near_misses": []
}
With no match the response is "verdict": "NO_RECORD"; close candidates outside the threshold
are listed in near_misses (up to 5) — these are not matches.
planned POST /v1/verify — verification by file upload (the server
computes the hashes and checks any embedded manifest) and a ?url= mode are in MVP scope.
GET /v1/records/{id} live
Returns the record's public fields: id, kind, sha256,
phash, pdq256, status (registered → anchored),
created_at, anchored_at, title, file_mime,
anchor_batch_id, creator_display. Public record page: https://ozdna.com/r/{id}.
GET /v1/anchors/{batch_id}/proof/{record_id} partial
The record's timestamp proof: which batch it belongs to (batch_id) and at which position
(leaf_index), the batch's root digest (merkle_root) and the public transaction
reference (tx_hash). The proof is designed to be checked independently, without asking ozDNA.
Currently: the proof array (sibling hashes) is returned empty; it will be filled
once the batching service starts persisting proofs.
GET /v1/usage live
{
"month": "2026-10",
"plan": "starter",
"events": { "mark": 412, "registration": 0, "verify_file": 0, "sign_digest": 0 },
"quotas": { "mark": 2000, "registration": 2000, "verify_file": 0, "sign_digest": 0 }
}
Verification verdicts
The verdict field comes from a fixed vocabulary. match_type and
confidence summarise it for clients that only need the coarse bucket.
| verdict | Meaning |
|---|---|
EXACT_ANCHORED | Exact file is registered; timestamp confirmed |
EXACT_PENDING | Exact file is registered; timestamp queued |
VISUAL_MATCH_HIGH | Visually the same (modified copy), high confidence |
VISUAL_MATCH_PROBABLE | Probable match, medium confidence |
NEAR_MISS | Similar but outside the threshold — not a match |
NO_RECORD | Not found in the registry |
SIGNED_BY_OZDNA · THIRD_PARTY_CREDENTIALS · SIGNATURE_BROKEN · SIGNATURE_REVOKED | planned Arrive with embedded-manifest checks (verification by file upload) |
Important: ozDNA records registration, not creation. A timestamp shows when a
fingerprint entered the registry — not when or by whom the image was made, and not whether it is
AI-generated. NO_RECORD does not mean "this image is fake".
Webhooks endpoint management delivery planned
Create an endpoint with {"url": "https://…", "description": "…"}; the URL must be
https://. The secret in the response is shown only once.
Events (delivery in MVP scope): record.anchored, mark.created,
usage.threshold_reached (at 80% and 100% of quota).
{
"id": "evt_…",
"type": "record.anchored",
"created_at": "2026-10-08T18:00:45Z",
"data": { "record_id": "rec_…", "batch_id": "bat_…" }
}
Signature verification: verify the Ozdna-Signature: t=<unix>,v1=<hex>
header as HMAC-SHA256(secret, t + "." + raw_body). Reject the request if t is more than
5 minutes from the current time. Failed deliveries are retried after +1 min, +10 min and +1 hour.
Until webhook delivery ships, poll GET /v1/records/{id} for timestamp status.
Limits and quotas enforcement planned
Quotas can be tracked with GET /v1/usage; enforcement of the limits below (429) goes live with the MVP.
| Plan | Requests/min (per key) | Marks + registrations / month |
|---|---|---|
| Free | 10 | 25 (API) + 50 web signs |
| Starter | 60 | 2,000 |
| Growth | 120 | 10,000 |
| Scale | 300 | 50,000 |
| Unauthenticated (per IP) | 30 reads/min | — |
When the quota is reached, requests stop (429); there is no automatic overage billing.
Prices: pricing. Free for journalists, fact-checking teams and individual
creators — write to us.
Error codes
Error bodies carry a machine-readable error code and usually a message. A uniform
envelope (type, code, request_id) arrives in v1; branch your code on the
error/code field, not on the message text.
| HTTP | error | When |
|---|---|---|
| 400 | validation_error · invalid_json · invalid_hash · invalid_phash · invalid_pdq | Missing/invalid field, malformed JSON, wrong hex length |
| 401 | authentication_error (invalid_api_key · revoked_api_key) | Missing, invalid or revoked key |
| 404 | not_found | No such record, batch or route |
| 413 | file_too_large | File larger than 20 MB |
| 415 | unsupported_media_type | File other than JPG/PNG |
| 429 planned | rate_limit_exceeded · plan_quota_exceeded | Rate limit (see the Retry-After header) or monthly quota |
| 503 | — | Temporary outage — retry with exponential backoff |
OpenAPI spec
The OpenAPI 3.1 definition of the live routes is served by the API itself and takes precedence over this page:
https://api.ozdna.com/v1/openapi.yaml
What we don't claim
- We don't do AI detection. OriginDNA does not guess whether an image is AI-generated; it records and verifies provenance.
- Not "trusted Content Credentials". Until ozDNA completes the C2PA Conformance Program, its signatures show as "unknown source" in the official C2PA Verify tool. That is why we provide our own verification page and an independent timestamp proof.
- Not legal advice. The API is infrastructure that helps you meet the EU AI Act Article 50 marking obligation; it does not claim to deliver full compliance on its own.