API Reference

The AdPriva API lets anyone verify cryptographic proofs and consent certificates, and lets authenticated customers read their own organization’s records.

Access falls into three tiers, and which one you are in decides how you authenticate:

TierAuthWhat it is for
Public verificationnoneRe-checking evidence you were handed. Anyone, no account.
Tenant-scoped readsJWT bearer tokenYour own organization’s proofs and certificates.
Server-to-server submissionpossession of the signing secretSubmitting sealed certificates from your backend.

Base URL

https://api.adpriva.com

Endpoints are served at the app root. There is no /v1 path prefix. Server-to-server submission is a different host — see endpoint 8.

Authentication

Public endpoints (1-5) take no credentials at all; that is the point of them.

Tenant-scoped endpoints (6-7) take a JWT bearer token, obtained by logging in:

TOKEN=$(curl -s -X POST https://api.adpriva.com/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"…"}' | jq -r '.token')

curl -H "Authorization: Bearer $TOKEN" https://api.adpriva.com/explorer/proofs

There is no general-purpose API key and no API-key dashboard. (Two narrow internal integrations authenticate with an X-API-Key header, but they are not part of the customer-facing surface.)

Endpoints

Public — no authentication

1. Look Up a Proof (public)

Fetch a single proof by its signature. Like endpoint 5 this is a public verification endpoint: it takes no credentials of any kind. Once the proof has been batched it returns a full Merkle inclusion receipt, so a third party can recompute the anchored root themselves rather than trusting the status field.

Request

GET https://api.adpriva.com/public/proofs/{proof_id}

Response

{
  "ok": true,
  "data": {
    "proof_id": "666959fe0c24984a6aec482e4d2835b847341577cb303b34183aa0c6290d6897",
    "type": "view",
    "status": "pending",
    "created_at": "2026-08-18T10:04:32Z",
    "batch_id_hash": "00000000000007f7",
    "merkle_root": "0x0365496d53b1437195520f69d21296efd113ed17f2c334484b83faed856f5805",
    "leaf": "2895",
    "leaf_index": 0,
    "merkle_path": ["0", "…", "0"],
    "merkle_path_indices": [0, "…", 0],
    "hash_version": "poseidon",
    "zk_proven_leaf_index": 0,
    "zkverify_status": "FINALIZED",
    "zkverify_url": "https://zkverify.subscan.io/extrinsic/0xe3ceb653…",
    "horizen_status": "CONFIRMED",
    "horizen_tx": "0x7fa649ce95ed242b75c5457265c826ab687421ca14a4f12ec3d617df55a08e1a",
    "horizen_url": "https://explorer.horizen.io/tx/0x7fa649ce…",
    "anchor_block": null,
    "validity_score": 55,
    "verdict": "uncertain"
  }
}
  • The record carries no publisher, site, IP, session or raw event data — the proof id is an opaque HMAC signature, and batch_id_hash is a derived handle rather than a database key.
  • The receipt fields (leaf, leaf_index, merkle_path, merkle_path_indices, hash_version, zk_proven_leaf_index) are absent until the proof has been batched. An unbatched proof is a normal 200 with those keys simply missing — not an error.
  • Rate limit: 60 requests per minute. Unknown and malformed ids both return an identical 404, so the endpoint cannot be used to discover which proofs exist.
  • The companion feed GET https://api.adpriva.com/public/proofs/recent?limit=N deliberately omits per-item receipts — re-fetch any single proof here for full offline reverification.
Recomputing the root offline

Fold leaf up the path and compare the result to merkle_root. Which algorithm to use is given by hash_version — there are two, and they are not interchangeable.

hash_version: "sha256" — leaves are proof signatures; hex strings are concatenated, not bytes:

let hash = sha256(leaf);
for (const [i, sibling] of merkle_path.entries()) {
  hash = merkle_path_indices[i] === 0 ? sha256(hash + sibling) : sha256(sibling + hash);
}
assert(hash === merkle_root);

hash_version: "poseidon" — leaves are decimal proof ids over a BN254 Poseidon tree (circomlib) of fixed depth 20. The tree is sparse: an absent sibling is the literal field zero, not the hash of an empty subtree, so a batch holding one leaf has a path of twenty zeros:

let node = poseidon([BigInt(leaf)]);
for (const [i, sibling] of merkle_path.entries()) {
  node = merkle_path_indices[i] === 1
    ? poseidon([BigInt(sibling), node])
    : poseidon([node, BigInt(sibling)]);
}
assert("0x" + node.toString(16).padStart(64, "0") === merkle_root);

merkle_path_indices[i] is 1 when the node being folded is a right child, 0 when it is a left child. The recomputed root must equal the value the Anchored(merkleRoot) event carries in horizen_tx — that on-chain value, not our API, is the authority.

What the receipt does and does not prove

Two limits, stated plainly so the receipt is not read as a stronger claim than it is:

  • Only one leaf per batch is zero-knowledge proven. zk_proven_leaf_index names it (currently always 0). Every other leaf’s inclusion is established by the Merkle path plus the anchored root — sound, but a different claim from “a zk-SNARK attests to this proof”.
  • Batches are small, so the anonymity set is small. Inclusion proves membership of a batch, and a batch currently holds only one or two proofs — so it reveals more than membership of a large set would. Do not read a receipt as anonymity. You can size the set from the receipt itself: a Poseidon sibling of 0 is an empty slot, so an all-zero path means the batch held this proof alone. Batch sizes are also public via proof_count on endpoint 3.

For poseidon batches leaf is the raw numeric proof id, because it is the preimage the circuit hashes and you must be able to hash it yourself. It is not an addressable identifier: nothing can be looked up by it.

2. Recent Proofs (public)

The newest signed and scored proofs across the network. Powers the public activity feed.

Request

GET https://api.adpriva.com/public/proofs/recent?limit=10

Response

{
  "ok": true,
  "data": [
    {
      "proof_id": "f20a0d49826b6e7db2a83afcae262a3b5db6a7cfc28604f4f1dc777c95eb5507",
      "type": "view",
      "status": "pending",
      "created_at": "2026-08-20T13:48:21Z",
      "batch_id_hash": "0000000000000847",
      "merkle_root": "0x0deccff4d5d28c630b80261ed7bd5bf93e2971e8fb75897c515907b21d0d7d1d",
      "zkverify_status": "FINALIZED",
      "zkverify_url": "https://zkverify.subscan.io/extrinsic/0x97e75236…",
      "horizen_status": "CONFIRMED",
      "horizen_tx": "0xe5a0a19e1fe0798151a618ae5849d2b33a055a885ccd6fa921b802ef205ba8d7",
      "horizen_url": "https://explorer.horizen.io/tx/0xe5a0a19e…",
      "anchor_block": null,
      "validity_score": 0,
      "verdict": "abusive_bot"
    }
  ]
}
  • Carries no publisher, site or org dimension — it is a network-wide feed, not a per-customer one. For your own organization’s proofs use endpoint 6.
  • The listing omits per-item Merkle receipts; re-fetch any single proof from endpoint 1 for full offline reverification.
  • Rate limit: 60 requests per minute.

3. Recent Anchors (public)

The most recently anchored batches, newest first.

Request

GET https://api.adpriva.com/public/anchors/recent?limit=10

Response

{
  "ok": true,
  "data": [
    {
      "batch_id_hash": "0000000000000847",
      "merkle_root": "0x0deccff4d5d28c630b80261ed7bd5bf93e2971e8fb75897c515907b21d0d7d1d",
      "proof_count": 2,
      "anchor_tx": "0xe5a0a19e1fe0798151a618ae5849d2b33a055a885ccd6fa921b802ef205ba8d7",
      "anchor_block": null,
      "anchored_at": "2026-08-20T13:55:29Z",
      "zkverify_status": "FINALIZED"
    }
  ]
}
  • batch_id_hash is a derived handle, not a database key. Batches carry no organization dimension — Merkle trees deliberately span customers, which is why this is publishable at all.
  • Rate limit: 60 requests per minute.

4. Network Statistics (public)

Aggregate counters for the network as a whole.

Request

GET https://api.adpriva.com/public/stats/summary

Response

{
  "ok": true,
  "data": {
    "proofs_sealed_24h": 9,
    "batches_anchored_24h": 9,
    "latest_anchor_block": null,
    "latest_anchor_tx": "0xe5a0a19e1fe0798151a618ae5849d2b33a055a885ccd6fa921b802ef205ba8d7",
    "anchored_pct": 6.5
  }
}
  • Counts only, with no publisher, site or organization breakdown.
  • Rate limit: 60 requests per minute.

Fetch a single consent certificate by its signature. This is the public verification endpoint: it takes no credentials of any kind, and is what you give an auditor, a client or a lead buyer so they can check the evidence without an AdPriva account.

Request

GET https://api.adpriva.com/public/certificates/{signature}

Response

{
  "ok": true,
  "data": {
    "signature": "a3f9...",
    "site_id": "acme-blog",
    "form_url": "https://acme-blog.example/devis",
    "consent_text_hash": "9b74c9...",
    "consent_checkbox_state": true,
    "ts": 1754179200,
    "validity_score": 92,
    "verdict": "human",
    "status": "pending",
    "merkle_root": null,
    "zkverify_status": null,
    "zkverify_url": null,
    "horizen_status": "pending",
    "horizen_tx": null,
    "horizen_url": null,
    "anchor_block": null,
    "retention_until": null,
    "created_at": "2026-08-03T09:20:00Z"
  }
}
  • The record is PII-free by construction — the consent wording is returned only as a SHA-256 hash, and the subject reference, nonce and field values are never exposed.
  • status is verified once the certificate’s batch is confirmed on-chain, and pending until then. The batch fields (merkle_root, zkverify_*, horizen_*, anchor_block) are null until the certificate has been batched and anchored; once it is, the response also carries leaf, leaf_index, merkle_path and merkle_path_indices so a third party can recompute the published Merkle root offline.
  • Rate limit: 60 requests per minute. Unknown and malformed signatures both return an identical 404, so the endpoint cannot be used to discover which certificates exist. A certificate whose retention period has ended returns 410 Gone rather than 404 — the record existed and was retired, which is a materially different answer for an auditor.

Authenticated — scoped to your organization

6. List Your Proofs

Every proof belonging to your organization, with its anchoring state. This is the tenant-scoped counterpart to the public feed in endpoint 2.

Request

GET https://api.adpriva.com/explorer/proofs?q=&from=2026-08-01&to=2026-08-20&page=1&size=50
Authorization: Bearer YOUR_JWT

Response

{
  "ok": true,
  "proofs": [
    {
      "id": 2895,
      "site_id": "acme-blog",
      "site_name": "Acme Blog",
      "has_consent": true,
      "has_signature": true,
      "anchored": true,
      "created_at": "2026-08-18T10:04:32Z",
      "proof_id": "666959fe0c24984a6aec482e4d2835b847341577cb303b34183aa0c6290d6897",
      "anchor_status": "CONFIRMED",
      "merkle_root": "0x0365496d53b1437195520f69d21296efd113ed17f2c334484b83faed856f5805",
      "anchor_tx": "0x7fa649ce95ed242b75c5457265c826ab687421ca14a4f12ec3d617df55a08e1a",
      "anchor_chain": "horizen-mainnet"
    }
  ],
  "stats": { "total": 1284, "anchored": 1187 },
  "pagination": { "page": 1, "size": 50, "total": 1284, "total_pages": 26 }
}
  • Results are restricted to sites your organization owns. site_id and the numeric id appear here because you are authenticated and it is your own data — neither is ever returned by a public endpoint.
  • proof_id is the opaque signature. Use it to deep-link a public receipt (endpoint 1); never build a public link from the numeric id.
  • stats is computed over the whole filtered set, not the current page.
  • Requires an organization context; a token with none returns 403 NO_ORG.

Retrieve every consent certificate held for one pseudonymous subject_ref — the controller-mediated GDPR Art. 15 subject-access path. Unlike endpoint 4 this requires authentication and is tenant-scoped: results are limited to sites your organization owns.

Request

GET /certificates?subject_ref=APS_7f21c4

Response

{
  "ok": true,
  "data": {
    "subject_ref": "APS_7f21c4",
    "count": 2,
    "certificates": [
      { "signature": "a3f9...", "form_url": "https://acme-blog.example/devis", "status": "pending" },
      { "signature": "c81e...", "form_url": "https://acme-blog.example/contact", "status": "verified" }
    ]
  }
}
  • Each item uses the same PII-free projection as endpoint 5 (abbreviated above).
  • subject_ref is required and must be 255 characters or fewer; omitting it returns 400.
  • The listing omits per-item Merkle paths — re-fetch any single certificate from endpoint 5 for full offline reverification.

Server-to-server submission

Publishers whose own backend holds the signing secret can submit certificates directly — no browser, no AdPriva tag. Use it for direct integrations or server-side consent events.

This endpoint is on the ingest host, not api.adpriva.com:

POST /proofs
Content-Type: application/json
  • Authentication is possession of the signing secret, not a token: the caller proves itself by presenting an HMAC seal that verifies. There is no Authorization header and no API key. A tampered seal is rejected 401.
  • PROOF_HMAC_SECRET must never reach a browser. Any integration where a browser can read it is broken by design — whoever holds the secret can mint certificates for any site.
  • The current sealed event type is the consent certificate. The ad-era post-click conversion flow, and the campaign_id/creative_id fields it signed, were removed.

The canonical field list, the exact byte layout the seal is computed over, and worked examples live in AdPriva/serverdocs/server-to-server-submission.md. Follow that document rather than reimplementing from this summary: the seal is byte-exact, and a canonicalization mistake produces a 401 that looks like an auth problem.

Notes

  • Rate limits — public endpoints allow 60 requests per minute each, keyed per endpoint. Some authenticated endpoints are tighter where the work is expensive (for example the subject-access listing in endpoint 7).
  • Error handling — errors return { "ok": false, "error": { "code", "message" } } with a matching HTTP status. Public lookups return an identical 404 for unknown and malformed ids, so they cannot be used to discover which records exist.
  • Envelope — successful responses are wrapped in { "ok": true, "data": … }. Two exceptions: endpoint 6 returns proofs, stats and pagination as siblings of ok, and POST /auth/login returns token, user and expiresAt at the top level.
  • SDKs — there are no official SDKs yet. Every endpoint here is plain HTTPS and JSON; the examples use curl deliberately.
  • Sandbox — there is no sandbox host yet. The public endpoints are read-only and safe to call against production.

With the AdPriva API, evidence can be fetched, re-verified and traced on-chain by anyone who holds it — no account, and no need to trust us.