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:
| Tier | Auth | What it is for |
|---|---|---|
| Public verification | none | Re-checking evidence you were handed. Anyone, no account. |
| Tenant-scoped reads | JWT bearer token | Your own organization’s proofs and certificates. |
| Server-to-server submission | possession of the signing secret | Submitting 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_hashis 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 normal200with 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=Ndeliberately 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_indexnames it (currently always0). 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
0is an empty slot, so an all-zero path means the batch held this proof alone. Batch sizes are also public viaproof_counton 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_hashis 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.
5. Look Up a Consent Certificate (public)
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.
statusisverifiedonce the certificate’s batch is confirmed on-chain, andpendinguntil then. The batch fields (merkle_root,zkverify_*,horizen_*,anchor_block) arenulluntil the certificate has been batched and anchored; once it is, the response also carriesleaf,leaf_index,merkle_pathandmerkle_path_indicesso 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 returns410 Gonerather than404— 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_idand the numericidappear here because you are authenticated and it is your own data — neither is ever returned by a public endpoint. proof_idis the opaque signature. Use it to deep-link a public receipt (endpoint 1); never build a public link from the numericid.statsis computed over the whole filtered set, not the current page.- Requires an organization context; a token with none returns
403 NO_ORG.
7. List a Subject’s Consent Certificates
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_refis required and must be 255 characters or fewer; omitting it returns400.- The listing omits per-item Merkle paths — re-fetch any single certificate from endpoint 5 for full offline reverification.
Server-to-server submission
8. Submit a Sealed Consent Certificate
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
Authorizationheader and no API key. A tampered seal is rejected401. PROOF_HMAC_SECRETmust 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_idfields it signed, were removed.
The canonical field list, the exact byte layout the seal is computed over, and worked examples live
in AdPriva/server → docs/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 identical404for 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 returnsproofs,statsandpaginationas siblings ofok, andPOST /auth/loginreturnstoken,userandexpiresAtat the top level. - SDKs — there are no official SDKs yet. Every endpoint here is plain HTTPS and JSON; the
examples use
curldeliberately. - 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.