EN TR
OZDNA.COM/DOCS/API — ORIGINDNA API REFERENCE · v0 PREVIEW

API Reference

The OriginDNA REST API is content provenance infrastructure for JPG/PNG images: it registers an image's fingerprint, locks it with a timestamp no one can backdate, and verifies the same image later — even after social platforms strip its metadata. JSON requests and responses, Bearer key authentication, OpenAPI 3.1 contract.

Preview — This page is an early contract reference. Access is currently invite-only via the waitlist. Each endpoint is tagged with its status: live implemented and usable, planned in MVP scope, not yet shipped. Fields and responses may change until v1 is stable.

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:

FieldFormatWhat it's for
sha25664 hexThe exact file — changing a single byte breaks the match.
phash16 hex (64 bit)Perceptual hash — survives recompression, resizing and metadata stripping.
pdq25664 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

POST/v1/marksAPI key

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)

FieldTypeDescription
sha256 requiredstring64 hex
phash requiredstring16 hex
file_mime requiredstringimage/jpeg · image/png
pdq256string64 hex, optional
file_bytesintegerFile size in bytes
titlestring≤ 120 characters, shown on the public record
generatorstring≤ 100 characters, e.g. acme-imagegen/2.3. Will be written into the manifest once embedding ships; ignored for now.
is_testbooleanTest 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

POST/v1/registrationsno key

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:

FieldTypeDescription
kind requiredstringai_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

GET/v1/verifyno key

Checks whether an image is in the registry. Two modes:

ParameterDescription
hash64 hex SHA-256 — exact file match
phash16 hex — perceptual match (modified copies)
pdq64 hex, optional — confirms a perceptual match
deep1 = 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

GET/v1/records/{id}no key

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

GET/v1/anchors/{batch_id}/proof/{record_id}no key

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

GET/v1/usageAPI key
{
  "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.

verdictMeaning
EXACT_ANCHOREDExact file is registered; timestamp confirmed
EXACT_PENDINGExact file is registered; timestamp queued
VISUAL_MATCH_HIGHVisually the same (modified copy), high confidence
VISUAL_MATCH_PROBABLEProbable match, medium confidence
NEAR_MISSSimilar but outside the threshold — not a match
NO_RECORDNot found in the registry
SIGNED_BY_OZDNA · THIRD_PARTY_CREDENTIALS · SIGNATURE_BROKEN · SIGNATURE_REVOKEDplanned 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

POST/v1/webhook-endpointsAPI key
GET/v1/webhook-endpointsAPI key
DELETE/v1/webhook-endpoints/{id}API key

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.

PlanRequests/min (per key)Marks + registrations / month
Free1025 (API) + 50 web signs
Starter602,000
Growth12010,000
Scale30050,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.

HTTPerrorWhen
400validation_error · invalid_json · invalid_hash · invalid_phash · invalid_pdqMissing/invalid field, malformed JSON, wrong hex length
401authentication_error (invalid_api_key · revoked_api_key)Missing, invalid or revoked key
404not_foundNo such record, batch or route
413file_too_largeFile larger than 20 MB
415unsupported_media_typeFile other than JPG/PNG
429 plannedrate_limit_exceeded · plan_quota_exceededRate 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.