The public endpoints
Seven unauthenticated endpoints, the exact fields they return, and the proof endpoints that are not public.
Seven endpoints, no credential
None of these take an authorization header. There is no key to request, no rate-limited developer token, and nothing you have to agree to first.
Every public transparency endpoint, with the cache header it sets.
| Endpoint | Returns | Cache-Control |
|---|---|---|
GET /v1/public/transparency/checkpoints | A page of signed statements, newest first | public, max-age=60 |
GET /v1/public/transparency/checkpoints/latest | The newest statement | public, max-age=60 |
GET /v1/public/transparency/checkpoints/{sequence} | One statement by sequence | public, max-age=3600, immutable |
GET /v1/public/transparency/keys | Every key that ever signed a checkpoint | public, max-age=300 |
GET /.well-known/unifyid-checkpoint-keys.json | The same key set, at a well-known path | public, max-age=300 |
GET /v1/public/transparency/overview | Log id, channel status, recent checkpoints, key rotations, legal notice versions | public, max-age=60, stale-while-revalidate=60 |
GET /v1/public/transparency/anchored-checkpoints/{sequence} | One statement plus both channels' anchor state | public, max-age=60 |
A checkpoint, as recorded
The list endpoint returns { items, nextBefore } at the top level — not wrapped in data. nextBefore is the sequence to pass back as ?before= for the next page, or null at the end of the log. limit defaults to 24 and must be 1 to 200; anything else is AUDIT_PROOF_RANGE_INVALID, status 400.
{
"checkpointId": "bdf7ec15-005a-472b-8bb8-bdc8ba637eb1",
"sequence": "2",
"version": 1,
"logId": "555983597a1cb6517ef9c23949d3370e100c5ca637c833dd37abae204e2d2da6",
"periodStart": "2026-09-18T02:05:00.000Z",
"periodEnd": "2026-09-18T03:05:00.000Z",
"combinedRoot": "4520f8ffeae3e86e37b1b5e5a0a3042e2fa7e745b25de71cf0e7f4b644d710b0",
"previousCombinedRoot": "d2aebc8fb6f20a33a68bdf43ea160b0b88cf92b77136104a9ce75b1d03dabf7a",
"fingerprint": "5d67966ffa2696fc221264bfa3d2efc7ce20603c7f0a2b00d958c41475f33add",
"previousFingerprint": "1ce17299acf64a2f67804267be5877365188e4aae16bd5a2fdc785fd8538aff5",
"statement": "0x00000000000000000000000000000000000000000000000000000000000000015559…",
"signature": "0x024f78483fa196f05c753196a34741b3d7ed200b22d4f46c5156d81c9b5ffc142fa6fabeb1360a4622053aa7b95531bb8d5197de83f10481ee0559d04f7f2b5e",
"signingKeyId": "audit-WvDfp1ygnv93KiceJlwn",
"signingKeyFingerprint": "745896659f3fd078ae663119934bc6e2ec5e1fcfc8cac2ef94e6057836aef27a",
"createdAt": "2026-09-18T03:05:00.111Z"
}Every field, and what you do with it
sequencedecimal string- The checkpoint index. A string, not a number, because it can exceed 2^53.
version1. The statement layout.logId- Which log this is. Every statement in one deployment carries the same value; compare it before you compare anything else.
periodStart / periodEnd- ISO-8601.
periodStartis1970-01-01T00:00:00.000Zfor sequence 1, which is how0renders. combinedRoothex64- What this statement commits to.
previousCombinedRoot- The previous statement's
combinedRoot, ornullfor sequence 1. The JSON saysnull; the statement bytes say 32 zeroes. fingerprintSHA-256(statement). The checkpoint's identity everywhere.previousFingerprint- The previous statement's fingerprint, or
nullfor sequence 1. A second, independent way to walk the chain backwards. statement0x + 512 hex- The 256 bytes themselves. Everything above is decodable from this; the fields are a convenience, not the source.
signature0x + 128 hex- 64 bytes,
r || s, low-S. signingKeyId- The
kidof the key that signed. Look it up in the key set. signingKeyFingerprintSHA-256(0x04 || X || Y)of that key. It is also the eighth word of the statement, so the statement names its own key.createdAt- When the row was written. This is a server clock, not a signed field —
periodEndis the one inside the statement.
The keys
GET /v1/public/transparency/keys and GET /.well-known/unifyid-checkpoint-keys.json return the identical document: every key that ever signed a statement, newest first, as JWKs with extra fields.
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "PQq7G_MrYupj66ozneB85214GOHVUUxd2ac-q4A22vI",
"y": "4KbXeKffwJbN7U8ss__sEnYuhY5jIA5Aq4F-i9GjEkc",
"kid": "audit-WvDfp1ygnv93KiceJlwn",
"alg": "ES256",
"use": "sig",
"fingerprint": "745896659f3fd078ae663119934bc6e2ec5e1fcfc8cac2ef94e6057836aef27a",
"status": "active",
"activatedAt": "2026-09-18T02:05:00.061Z",
"retiredAt": null
}
]
}kid is "audit-" followed by the first 20 characters of the base64url SHA-256 of the canonical JWK. A retired key keeps retiredAt and stays in the list forever, because statements it signed are still in the log and still have to verify.
The overview
GET /v1/public/transparency/overview is the one endpoint wrapped in data. It is the whole public posture in a single request, and it is the endpoint to use when you want to check a claim this documentation makes rather than believe it.
Under `data`
generatedAt- When this view was assembled.
logId- The same log id that is inside every statement.
channels- One entry per anchoring channel —
unifyid_contractwithrole: "authority", andsign_protocolwithrole: "witness"— each withenabled,network,chainId,contractAddress,contractUrl,hookAddress,schemaIdanddeployment. Recorded today: `enabled: false` and every other value `null` on both. From launch,deploymentnames the contract a channel is bound to (chainId,network,contractAddress,contractUrl,hookAddress,hookUrl,schemaId,genesisSequence,boundAt). governanceowner,timelock,minDelaySeconds,pausedandcheckedAt, read from the contract itself, ornullwhile the contract channel is off. When the chain does not answer, the values arenullandcheckedAtis still set. Cached for five minutes.checkpoints- Recent checkpoints in a shorter form than the list endpoint:
checkpointId,sequence,periodStart,periodEnd,combinedRoot,previousCombinedRoot,fingerprint,signingKeyId,signingKeyFingerprint,state,channelsanddownloadPath. Note that it carries nostatementand nosignature— for those, use the checkpoint endpoints above. signingKeyskeys— each withkid,fingerprint,status,activatedAt,retiredAtandcustody— androtations.custodyis"software"on a log whose signer is not KMS.legalNoticesitems: the published versions of the platform's legal notices.verifypackage,keysUrl,checkpointsUrland acommandsobject. See the warning below before you act on any of it.
Each entry in checkpoints[].channels has channel, label, role, status, network, chainId, transactionHash, transactionUrl, attestationId, signscanUrl, confirmations and anchoredAt. The chain, the transaction link and the SignScan link come from the delivery row itself, so a checkpoint always names the chain it was actually published on. Recorded today every one of them is status: "pending" with confirmations: 0 and every other value null, while the checkpoint's own state is "sealed".
The proof endpoints are not public
This is the distinction people get wrong. The statements are public. A proof about one record is not, because it is about somebody's record.
| Endpoint | Who may call it |
|---|---|
GET /v1/admin/audit/proofs/events/{eventId} | UnifyID staff only |
GET /v1/admin/audit/proofs/streams/{stream}/{sequence} | UnifyID staff only |
GET /v1/admin/audit/proofs/consistency | UnifyID staff only |
The one public route that looks like a proof endpoint is /v1/public/transparency/anchored-checkpoints/{sequence}, and it is not about a record: it returns the same checkpoint view as the endpoints above plus an anchors object with unifyid_contract and sign_protocol. With anchoring off there is nothing in that object to show. An unknown sequence, or one that is not a number, is AUDIT_CHECKPOINT_NOT_FOUND, status 404.
What this does not do
- There is no public endpoint that will tell you whether a given person did something. By design. The public surface is roots and keys; everything about a record needs the record's owner.
- `/checkpoints` is not wrapped in `data` and `/overview` is. They were built in different tracks and the envelopes differ. Read what you are parsing.
- These endpoints are unauthenticated, not unlimited. Page with
beforeandnextBeforerather than requestinglimit=200in a loop, and cache what you already hold — a sealed statement never changes.
Verified against API commit 234dba22 on 2026-10-18. The routes, scopes, checklist, events and limits on these pages are read from that export, not typed.