API Reference¶
Complete reference for the Diogenes REST API. All endpoints are prefixed with /api/v1.
Interactive API Explorer
Try the API interactively using the Swagger UI or the ReDoc viewer.
Health¶
GET /api/v1/health¶
Check server status.
Response:
Authentication¶
Authentication uses a challenge-response protocol. The client requests a challenge nonce, signs it with their private key, and submits the signature to obtain a JWT.
POST /api/v1/auth/challenge¶
Request an authentication challenge.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
fingerprint |
string | Yes | Key fingerprint to authenticate as |
recovery |
boolean | No | Request a recovery challenge (default: false) |
purpose |
string | No | Challenge purpose: "general" or "login" (default: "general") |
Response:
{
"challenge_id": "abc123...",
"nonce": "random-nonce-value",
"expires_at": "2026-01-15T10:35:00Z"
}
POST /api/v1/auth/login¶
Authenticate and receive a JWT.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
challenge_id |
string | Yes | The challenge ID from /auth/challenge |
challenge_signature |
string | Yes | Signature of the challenge nonce |
password |
string | No | Password if one is registered for this key |
Response:
{
"token": "eyJ...",
"fingerprint": "sha256:...",
"pseudonym": "Alice Scholar",
"expires_at": "2026-01-15T11:00:00Z"
}
Error codes: 404 Challenge not found, 410 Challenge expired, 409 Challenge already used, 403 Invalid signature or password.
POST /api/v1/auth/renew¶
Renew an existing JWT before it expires.
Headers: Authorization: Bearer <token>
Response: Same as /auth/login.
POST /api/v1/auth/password¶
Set or change a password for a key.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
challenge_id |
string | Yes | Auth challenge ID |
challenge_signature |
string | Yes | Signed challenge nonce |
password |
string | Yes | New password to set |
current_password |
string | No | Current password (required when changing) |
Users¶
An account (user) owns one or more credentials, each binding a signing
key to that account. Identity is decoupled from any single key so a device
can be added or retired without losing the account.
POST /api/v1/users¶
Create an account together with its first credential, atomically. Unauthenticated by design — there is no prior credential to authenticate against.
Request:
{
"display_name": "Alice Scholar",
"email": "alice@example.org",
"credential_type": "software",
"key_fingerprint": "sha256:...",
"device_label": "Laptop"
}
Response (201): user, credential, and recovery_key. The recovery
key is returned exactly once and cannot be retrieved again.
POST /api/v1/users/recover¶
Recover an account with pseudonym + recovery_key. Revokes every active
credential, enrolls the supplied replacement, and rotates the recovery key.
Returns 401 on a wrong key or unknown pseudonym.
/api/v1/users/{user_id}/credentials — removed¶
All four verbs (GET, POST, PATCH, DELETE) were deleted in issue #502
(finding C-2 of the FedRAMP High security review, #200). They resolved to a
database session and nothing else. Because a session's identity is resolved
purely by the credential row matching the JWT fingerprint, binding your own
key to another account's user_id made your own valid login resolve as that
account — full takeover, reachable with no credentials at all. The
unauthenticated PATCH renamed another user's device and the DELETE was a
lockout primitive.
They were removed rather than authenticated. Binding a credential belongs to
POST /api/v1/credentials/enroll, which does the job properly: signed
challenge, active non-revoked issuing credential, verified chain signature,
and hardware backing on both sides.
GET /api/v1/credentials/user/{user_id}/fingerprints — removed¶
Deleted in the same issue (finding L-15). It was unauthenticated on the same
surface; its incremental disclosure over the already-public
GET /api/v1/keys was the user-supplied device_label for any user_id.
Removed rather than authenticated because no caller anywhere in the system
used it.
Keys¶
GET /api/v1/keys¶
List all registered keys.
Response:
{
"keys": [
{
"fingerprint": "sha256:...",
"pseudonym": "Alice Scholar",
"algorithm": "ed25519",
"status": "active",
"registered_at": "2026-01-15T10:00:00Z",
"expires_at": null
}
]
}
GET /api/v1/keys/search¶
Search keys by pseudonym.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
pseudonym |
string | Partial pseudonym match (case-insensitive) |
Response: Same format as GET /api/v1/keys.
GET /api/v1/keys/{fingerprint}¶
Get details for a specific key.
Response:
{
"fingerprint": "sha256:...",
"public_key": "-----BEGIN PUBLIC KEY-----\n...",
"pseudonym": "Alice Scholar",
"algorithm": "ed25519",
"status": "active",
"registered_at": "2026-01-15T10:00:00Z",
"expires_at": null,
"log_entry_id": 1
}
GET /api/v1/keys/{fingerprint}/status¶
Quick key validity check (target: less than 500ms).
Response:
POST /api/v1/keys/register¶
Register a new public key on the transparency log.
Request body:
{
"payload": {
"public_key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
"pseudonym": "Alice Scholar",
"key_algorithm": "ed25519",
"expiration_date": "2027-01-15T00:00:00Z"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
payload.public_key |
string | Yes | PEM-encoded public key |
payload.pseudonym |
string | Yes | Display name for the key holder |
payload.key_algorithm |
string | Yes | "ed25519", "ecdsa-p256", or "rsa-2048" |
payload.expiration_date |
string | No | ISO 8601 expiration date |
Response:
{
"fingerprint": "sha256:...",
"pseudonym": "Alice Scholar",
"algorithm": "ed25519",
"log_entry_id": 1,
"leaf_index": 0,
"entry_hash": "sha256:..."
}
| Field | Type | Description |
|---|---|---|
log_entry_id |
integer | Autoincrement primary key of the log entry. Stable identifier for lookups. |
leaf_index |
integer | 0-based chronological position in the Merkle log. Use with GET /api/v1/log/proof/{leaf_index} to fetch an inclusion proof (issue #375). |
POST /api/v1/keys/succeed¶
Replace an active key with a new one (key succession).
Request body:
{
"payload": {
"old_fingerprint": "sha256:...",
"new_public_key": "-----BEGIN PUBLIC KEY-----\n...",
"new_algorithm": "ed25519"
},
"auth": {
"challenge_id": "...",
"challenge_signature": "..."
}
}
Accepts either JWT Bearer auth or legacy auth proof. For recovery succession of expired keys, include "recovery": true in the auth object.
Response:
{
"old_fingerprint": "sha256:...",
"new_fingerprint": "sha256:...",
"log_entry_id": 3,
"leaf_index": 2,
"entry_hash": "sha256:..."
}
leaf_index is the 0-based Merkle position for the new succession entry (issue #375).
POST /api/v1/keys/revoke¶
Revoke an active key.
Request body:
{
"payload": {
"fingerprint": "sha256:..."
},
"auth": {
"challenge_id": "...",
"challenge_signature": "..."
}
}
Response:
{
"fingerprint": "sha256:...",
"status": "revoked",
"log_entry_id": 4,
"leaf_index": 3,
"entry_hash": "sha256:..."
}
leaf_index is the 0-based Merkle position for the revocation entry (issue #375).
Signing¶
POST /api/v1/attestations¶
Submit a client-signed attestation. The private key never leaves the client.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
manifest |
object | Yes | Document manifest with content hash |
public_key_pem |
string | Yes | PEM-encoded public key of the signer |
signature |
string | Yes | Base64-encoded signature of the manifest |
attestation_type |
string | No | "authorship", "editorial_review", "peer_review", "publication" (default: "authorship") |
pseudonym |
string | No | Signer pseudonym (used if key not yet registered) |
intent_statement |
string | No | Free-text statement of signing intent |
password |
string | No | Password if registered for this key |
Response:
{
"attestation": {
"id": "a1b2c3...",
"type": "authorship",
"scope": { "content_hash": "sha256:..." },
"signer_key_fingerprint": "sha256:...",
"signature_algorithm": "ed25519",
"signature": "...",
"timestamp": "2026-01-15T10:30:00Z"
},
"fingerprint": "sha256:...",
"pseudonym": "Alice Scholar",
"log_entry_id": 2,
"leaf_index": 1
}
| Field | Type | Description |
|---|---|---|
log_entry_id |
integer | Autoincrement primary key of the log entry. Stable identifier for lookups. |
leaf_index |
integer | 0-based chronological position in the Merkle log. Pass to GET /api/v1/log/proof/{leaf_index} to fetch an inclusion proof for offline-verification bundles (issue #375). |
Error codes: 422 Invalid public key, attestation type, or manifest; signature verification failed. 403 Password required or invalid. 500 Log entry persisted but leaf_index could not be resolved (log_entry_id is also a 500 in this branch — both fields are required on the receipt).
Verification¶
POST /api/v1/verify¶
Verify a document given its manifest and source hash. Source content is never sent to the server.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
manifest |
object | Yes | The document manifest |
source_hash |
string | Yes | SHA-256 hex hash of the source document |
Response:
{
"layer1": { "status": "valid", "details": "..." },
"layer2": { "status": "valid", "details": "..." },
"overall_status": "valid",
"attestation_graph": { "..." }
}
Error codes: 422 Invalid manifest. 503 Transparency log unreachable.
Transparency Log¶
GET /api/v1/log¶
Paginated log entries with optional filters.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
integer | 1 | Page number (1-indexed) |
per_page |
integer | 50 | Entries per page (max 200) |
event_type |
string | -- | Filter by event type (single, or comma-separated list) |
tags |
string | -- | Comma-separated tags. Entries must carry all of them. |
search |
string | -- | Prefix search across digests, hashes and fingerprints |
Tag matching (changed in #456). A tag matches a whole element of the
entry's tags array, case-insensitively. Previously the filter was a
substring test over the serialised array, so ecosystem:go also matched
entries tagged ecosystem:golang. One divergence from the in-memory log
remains and is deliberate: leading/trailing whitespace inside a stored
tag is not stripped, because payload is an input to the entry hash and
so can never be rewritten by a backfill.
Search matching (changed in #456). Terms are matched as prefixes, not interior substrings:
- A digest prefix matches any entry whose
entry_hashes[*].digest_hexstarts with the term (case-insensitive on input; digests are stored lowercase). - A complete hash term (64 or 128 hex characters, with or without a
sha256:prefix) is matched exactly againstdocument_hash,manifest_hashand the fingerprint payload fields, in both the bare andsha256:-prefixed stored forms. Pasting a bare hash now reliably finds an entry the signing pipeline stored prefixed, and vice versa. - Any other term is matched as a case-insensitive prefix of the same payload fields.
Searching by a hash tail or an interior fragment no longer matches.
The same tags semantics apply to GET /api/v1/log/audit-trail.
Response:
{
"entries": [
{
"id": 1,
"timestamp": "2026-01-15T10:00:00Z",
"event_type": "key_registration",
"payload": { "..." },
"previous_hash": null,
"entry_hash": "sha256:..."
}
],
"page": 1,
"per_page": 50,
"total": 1
}
GET /api/v1/log/head¶
Get the latest log entry and total count.
Response:
{
"head": {
"id": 42,
"entry_hash": "sha256:...",
"event_type": "attestation",
"timestamp": "2026-01-15T10:30:00Z"
},
"entry_count": 42
}
GET /api/v1/log/{entry_id}¶
Get a single log entry by ID.
Response: Same format as entries in GET /api/v1/log.
Error codes: 404 Entry not found.
GET /api/v1/log/by-fingerprint/{fingerprint}¶
Get log entries referencing a specific fingerprint, one page at a time.
Matches six payload fields: fingerprint, old_fingerprint,
signer_fingerprint, signer_key_fingerprint, endorsed_key_fingerprint,
endorser_key_fingerprint. Entries are ordered by ascending log entry ID.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
int | 1 |
1-based page number. |
per_page |
int | 50 |
Entries per page. Maximum 200. |
Response:
Changed in #456. This endpoint previously returned every matching entry in a single unbounded response. It now defaults to 50 entries and reports
page/per_page/total. Callers that relied on receiving the complete history in one response must page through it.
GET /api/v1/log/sth¶
Return the latest persisted Signed Tree Head (STH) for third-party audit. The server signs an STH on every log append (issue #294 Phase 2) and this endpoint returns the most recent row.
Response:
{
"tree_size": 42,
"root_hash": "sha256:...",
"signature": "...",
"timestamp": "2026-01-15T10:30:00Z",
"key_id": "<operator-fingerprint>"
}
Historical STHs for past tree sizes are available at
GET /api/v1/log/sth/{tree_size} (404 if the size predates the STH chain).
GET /api/v1/log/audit-trail¶
Query audit trail with combinable filters.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
fingerprint |
string | Filter by key fingerprint |
document_hash |
string | Filter by document or manifest hash |
start_date |
string | ISO 8601 start date |
end_date |
string | ISO 8601 end date |
At least one parameter is required.
Response: Same format as GET /api/v1/log.
POST /api/v1/log/entries — removed¶
Deleted in issue #500 (finding C-1 of the FedRAMP High security review,
200). The endpoint was unauthenticated and unsigned: anyone could¶
hash-chain arbitrary content into the authoritative log under any
event_type, and because a forged entry keeps the chain internally
valid, POST /api/v1/log/verify could not distinguish it from a genuine
one.
It was removed rather than authenticated. Every hardened writer — key
registration, endorsements, attestations — appends through
core/transparency_log_db.append_chained_log_entry internally, so the
route added nothing but a second, public way into the same code. Use the
domain endpoint for the event you are recording (for example
POST /api/v1/attestations); there is no general-purpose append API.
Operators who need a Signed Tree Head at the current size use the
operator-only POST /api/v1/log/sth/sign.
POST /api/v1/log/verify¶
Verify the integrity of the entire hash chain.
Response:
POST /api/v1/log/anchor¶
Anchor the latest log head via OpenTimestamps. Operator-only.
Requires Authorization: Bearer <token> where the token is a JWT signed by
the operator key whose sub claim is the operator fingerprint. Ordinary
session tokens are also signed by the operator key, so a valid signature is
not sufficient — the sub must be the operator.
This is the only endpoint that updates an existing transparency log row:
it writes ots_proof onto the newest entry. That proof is the temporal
evidence of when the head existed, so overwriting it degrades the log's
core claim; the endpoint also drives an outbound OpenTimestamps calendar
submission. Both are why it is gated (issue #501).
Anchoring is a manual operator action. GET /api/v1/log/anchor-status is
public and its advisory anchor_due flag is what tells a polling operator
that a call to this endpoint is warranted.
The request takes no body.
Response:
{
"anchored_entry_id": 42,
"entry_hashes": [
{
"algorithm": "sha2-256",
"digest_hex": "3f2a...",
"is_primary": true
}
],
"ots_proof": "base64-encoded-proof..."
}
When the log is empty the response is {"detail": "No entries to anchor"}.
Error responses:
| Status | Meaning |
|---|---|
401 |
Missing, malformed or unverifiable operator JWT |
403 |
Token verifies but its sub is not the operator fingerprint |
503 |
The operator key provider is unavailable — the route fails closed |
Attestation Requests¶
POST /api/v1/log/attestation-requests¶
Ask another key for an attestation. Self-requests (requester == target) are valid.
Authentication required (issue #503). Prove control of
requester_fingerprint either way:
- Session — send
Authorization: Bearer <token>. The session's key is authoritative: arequester_fingerprintthat disagrees is a 403, and an absent one is filled in from the session rather than trusted. - Detached proof — send an
authobject ({challenge_id, challenge_signature}) fromPOST /api/v1/auth/challenge, signed byrequester_fingerprint.
Recovery-only sessions are refused.
The route was previously unauthenticated. Every accepted request becomes a permanent, unretractable log entry attributed to a caller-supplied fingerprint, and raises an inbox notification at the target — so it was both an attribution-forgery primitive and an unmetered way to flood a victim's inbox.
Limits. requester_fingerprint and target_fingerprint 256 chars,
attestation_type 512, nonce 128, intent_statement 4096; claims is
capped at 64 KiB of serialized JSON — by size, not key count, since one
large string value defeats any per-key rule.
Quota. At most RATE_LIMIT_ATTESTATION_REQUEST_PER_TARGET (default 50)
requests per target per hour. Keyed on the target, not the requester:
the abuse is flooding one victim, and a per-requester cap is defeated by
registering more keys.
Error responses:
| Status | Meaning |
|---|---|
401 |
No session and no auth proof |
403 |
Proof or session is for a different key, or a recovery-only session |
409 |
Duplicate request nonce |
422 |
A field or the claims object exceeds its cap |
429 |
The target's hourly request quota is exhausted |
POST /api/v1/log/attestation-requests/{request_id}/respond¶
Accept or decline a request.
Authentication required (issue #504). Prove control of
responder_fingerprint with a session token or an auth object, exactly
as for creating a request. The proven fingerprint is what is recorded; an
absent or empty responder_fingerprint is filled in from the session
rather than stored as given.
The route previously copied a caller-supplied responder_fingerprint —
and a caller-supplied attestation_id — into a permanent log entry, so any
anonymous caller could record that an arbitrary key had accepted or
declined an arbitrary request. The forgery also drives an inbox
notification presenting the fabricated acceptance to the real requester as
genuine, which is why the proof is checked before both writes.
reason is capped at 4096 characters.
Error responses:
| Status | Meaning |
|---|---|
401 |
No session and no auth proof |
403 |
Proof or session is for a different key, or a recovery-only session |
404 |
No such request |
410 |
The request's deadline has passed |
422 |
reason exceeds its cap |
Known limit. Proof binds the response to the key it names; it does not require that key to be the request's target. A key holder can therefore record a truthfully-attributed response to a request addressed to someone else. That is a narrower problem than H-3's forgery and is not in this issue's scope.
POST /api/v1/log/request-policies¶
Publish the request-filtering policy for a target key.
Authentication required (issue #505). Prove control of
target_fingerprint with a session token or an auth object. A policy is
self-published: there is no way to publish one for a key you do not hold.
The route previously took a client-chosen target_fingerprint with no
proof at all. Beyond the obvious forgery, the supersession rule means a
publish replaces the target's active policy — so an attacker could
rewrite a victim's filtering rules, and since #483 those rules are
evaluated against inbound requests. A planted policy therefore silently
filtered the legitimate traffic the victim was meant to receive. The proof
is checked before the supersession lookup, so an unproven caller cannot
probe the existing policy either.
predicates is capped at 64 KiB serialized;
default_action_on_failure at 64 characters. At most
RATE_LIMIT_REQUEST_POLICY_PER_TARGET (default 20) publications per target
per hour — self-publishing makes flooding self-harm, but each publish is
still a permanent log entry and a rapid series churns what is enforced.
Error responses:
| Status | Meaning |
|---|---|
401 |
No session and no auth proof |
403 |
Proof or session is for a different key, or a recovery-only session |
409 |
effective_from is not later than the active policy's |
422 |
Invalid predicates, invalid default action, or a field over its cap |
429 |
The target's hourly publication quota is exhausted |
Endorsements¶
POST /api/v1/endorsements/offer¶
Offer an endorsement to another key holder.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
endorser_fingerprint |
string | Yes | Fingerprint of the endorsing key |
endorsed_fingerprint |
string | Yes | Fingerprint of the key being endorsed |
category |
string | Yes | "x-diogenes:human_attestation" or "x-diogenes:institutional_endorsement" |
valid_until |
string | No | Expiry date (ISO 8601) |
institutional_metadata |
object | No | Metadata for institutional endorsements |
external_identifiers |
array | No | External identity links |
auth |
object | Yes | Auth proof (challenge_id + challenge_signature) |
Response: Log entry for the endorsement offer event.
POST /api/v1/endorsements/accept¶
Accept a pending endorsement offer.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
offer_entry_id |
integer | Yes | Log entry ID of the endorsement offer |
endorsed_fingerprint |
string | Yes | Fingerprint of the endorsed key |
auth |
object | Yes | Auth proof for the endorsed key |
POST /api/v1/endorsements/withdraw¶
Withdraw an endorsement (endorser action).
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
endorser_fingerprint |
string | Yes | Endorser's fingerprint |
endorsed_fingerprint |
string | Yes | Endorsed key's fingerprint |
original_offer_entry_id |
integer | Yes | Original offer log entry ID |
note |
string | Yes | Reason for withdrawal |
auth |
object | Yes | Auth proof for the endorser |
POST /api/v1/endorsements/revoke-acceptance¶
Revoke acceptance of an endorsement (endorsed party action).
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
endorsed_fingerprint |
string | Yes | Endorsed key's fingerprint |
original_offer_entry_id |
integer | Yes | Original offer log entry ID |
note |
string | Yes | Reason for revocation |
auth |
object | Yes | Auth proof for the endorsed key |
GET /api/v1/endorsements/{fingerprint}¶
Get endorsements received by a key.
GET /api/v1/endorsements/{fingerprint}/issued¶
Get endorsements issued by a key.
GET /api/v1/endorsements/{fingerprint}/status¶
Get endorsement status summary for a key.
POST /api/v1/endorsements/revocation-alert¶
Publish an endorser revocation alert.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
endorser_fingerprint |
string | Yes | Alerting endorser's fingerprint |
compromised_fingerprint |
string | Yes | Compromised key's fingerprint |
reason |
string | Yes | Alert reason (max 1000 chars) |
evidence |
string | No | Supporting evidence (max 5000 chars) |
auth |
object | Yes | Auth proof for the endorser |
GET /api/v1/endorsements/revocation-alerts/{fingerprint}¶
Get revocation alerts for a key.
Graph Traversal¶
GET /api/v1/graph/{fingerprint}¶
Depth-limited BFS traversal of the endorsement graph.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
max_depth |
integer | 3 | Maximum traversal depth |
type |
string | -- | Filter by endorsement type |
status |
string | -- | Filter by endorsement status |
institution |
string | -- | Filter by institution |
since |
string | -- | Only endorsements after this date |
until |
string | -- | Only endorsements before this date |
Results are cached (default TTL: 60 seconds) and subject to a configurable timeout (default: 10 seconds).
Trust Configurations¶
POST /api/v1/trust-configs¶
Create a trust configuration for a key.
GET /api/v1/trust-configs¶
List trust configurations.
GET /api/v1/trust-configs/{fingerprint}¶
Get trust configuration for a specific key.
POST /api/v1/trust-configs/revoke¶
Revoke a trust configuration.
Encrypted Claims¶
POST /api/v1/claims¶
Post an encrypted claim to the transparency log. Requires a JWT and a hardware-backed signing key.
The payload must declare "commitment_alg": "hmac-sha256-ecdh-v1". A payload
that declares "sha256" — or omits the field, which means the same thing — is
rejected with 400 (#517): an unkeyed commitment over a short structured
plaintext, published beside its ciphertext, is brute-forceable offline, so
minting new ones must fail loudly rather than silently succeed from a stale
SDK. Relays that forward payloads verbatim need to upgrade their sealing side.
Response:
GET /api/v1/claims¶
List encrypted claims by lookup_key. Possession of the token is the
access control.
GET /api/v1/claims/{entry_id}¶
Get a specific encrypted claim. Returns the opaque payload only.
POST /api/v1/claims/{entry_id}/reveal¶
Verify a revealed plaintext against the entry's stored commitment.
Offline verification is the preferred path and does not need this endpoint — a holder of the plaintext and the entry's commitment key checks it in-process and discloses nothing. Calling this route discloses the plaintext and that one entry's commitment key to the log operator.
Auth: requires a JWT (#517). Rate limits: per IP per hour
(DIOGENES_RATE_LIMIT_CLAIM_REVEAL, default 30) and per
(entry, authenticated caller) per hour (30). The per-caller keying is
deliberate: it slows a guesser without giving anyone a lever to deny an
honest auditor access to a specific entry.
Request body:
Response:
Status codes:
| Code | Meaning |
|---|---|
| 200 | Verdict returned in valid |
| 400 | Blinded entry and no commitment_key supplied |
| 401 | No JWT |
| 404 | No such encrypted-claim entry |
| 409 | The entry carries a pre-#517 unsalted commitment. Server-side reveal is withdrawn for those: for such an entry this route's only unique capability is acting as a guess-checker, since anyone holding the plaintext verifies it offline in one line. Those entries stay verifiable offline forever. |
| 422 | commitment_key is not 64 hexadecimal characters |
| 429 | Per-IP or per-(entry, caller) budget spent |
Composite Documents¶
POST /api/v1/composite/assemble¶
Assemble a composite document from multiple parts.
POST /api/v1/composite/compile¶
Compile a composite document into a single manifest.
Audit Export¶
GET /api/v1/audit/export¶
Export audit trail in JSON or CSV format with combinable filters.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
format |
string | "json" or "csv" |
scope |
string | Filter scope |
fingerprint |
string | Filter by fingerprint |
document_hash |
string | Filter by document hash |
start_date |
string | ISO 8601 start date |
end_date |
string | ISO 8601 end date |
page |
integer | Page number |
page_size |
integer | Page size (max 1000, total max 10000) |
Webhooks¶
POST /api/v1/webhooks¶
Register a webhook for event notifications.
GET /api/v1/webhooks¶
List registered webhooks.
DELETE /api/v1/webhooks/{webhook_id}¶
Delete a webhook.
Identifier Types¶
GET /api/v1/identifier-types¶
List supported external identifier types.
Rate Limits¶
The API enforces the following rate limits:
| Resource | Default Limit |
|---|---|
| General API queries | 100 requests/minute |
| Key registrations | 10/hour |
| Endorsement offers | 20/day |
Attestation events (incl. POST /api/v1/documents/sign) |
50/hour |
| Log head anchoring | 12/hour |
| PIN proof checks | 5 per 15 minutes, per fingerprint |
All limits are per client IP except PIN proof checks, which are keyed by fingerprint so a per-user lockout holds regardless of source address.
Rate limit headers are included in all responses.