Authentication
Verdifax authenticates API callers via a single header. Keys are provisioned out-of-band and shown to the operator exactly once.
Header
X-Verdifax-Key: vfx_<24-byte-random-hex>
Every authenticated endpoint accepts the same header. /health and the public bundle endpoints below need no key.
Two modes
Open mode, when no API key has ever been created on the deployment (a fresh deploy, before the first key is provisioned), every authenticated endpoint admits the caller as anonymous (api_key_id = 0). This keeps the docker quickstart trivial.
Locked mode, once any key has been created, revoked or not, every authenticated endpoint requires the X-Verdifax-Key header. Calls without a valid key get 401. Revoking every key does not return the deployment to open mode. Run history is scoped to the key that produced it: /runs only returns runs owned by the calling key, and /runs/{id} 404s if the key didn't own that run.
If every admin key is lost or revoked, the recovery path is the break-glass command verdifax-api --mint-admin-key <name>, run inside the machine that hosts the API server. It mints a new admin key directly against the database, prints the secret once, and exits without starting the server.
Provisioning a key
On a fresh self-hosted deployment, mint the first key with admin scope. A key minted without "scope":"admin" is a customer key, and the listing, revoking and other /admin/* calls below return 403 for customer keys; once that first key exists, open mode is over.
curl -s -X POST http://localhost:9090/admin/keys \
-H "Content-Type: application/json" \
-d '{"name":"ops-admin","scope":"admin","confirm_admin_scope":true}' | python3 -m json.tool
The server answers HTTP 201:
{
"ok": true,
"id": 1,
"name": "ops-admin",
"secret": "vfx_a1b2c3d4...",
"tier": "production",
"hint": "Store this secret now, it will never be shown again."
}
Alternatively, run verdifax-api --mint-admin-key <name> inside the machine to mint the first admin key without using HTTP.
With an admin key, mint keys for the applications that call the API:
curl -s -X POST http://localhost:9090/admin/keys \
-H "Content-Type: application/json" \
-H "X-Verdifax-Key: vfx_..." \
-d '{"name":"compliance-team-2026"}' | python3 -m json.tool
Without "scope":"admin", the key is customer scope: its stored name becomes customer:compliance-team-2026, and its tier defaults to trial (5,000 runs per rolling 30 days; pass "tier":"production" to lift the cap). Customer keys can call /execute, /runs and the other non-admin endpoints. Admin-scope requests without "confirm_admin_scope":true are refused with 400.
The server stores only sha256(secret). Lose the secret and you must revoke + reissue.
Listing keys
curl -s http://localhost:9090/admin/keys \
-H "X-Verdifax-Key: vfx_..." | python3 -m json.tool
Returns names, creation timestamps, last-used timestamps, run counts, and revocation status, never the secrets.
Revoking a key
curl -s -X DELETE http://localhost:9090/admin/keys/1 \
-H "X-Verdifax-Key: vfx_..."
Revocation is immediate. Subsequent calls with that key get 401.
Public bundle endpoints
Two endpoints serve a run's sealed audit bundle, the JSON that
verdifax-verify checks, to anyone without an API key. They serve only
published runs.
GET /runs/{id}/bundle.json
GET /public/runs/{token}/bundle.json
GET /runs/{id}/bundle.jsonserved the 44 runs published under an earlier rule, which made any run whoseroute_idbegan withbuyer-demo-public. All 44 were withdrawn on October 18, 2026, so it now serves none. That prefix no longer makes a run public, and runs published since then are not served at this address.GET /public/runs/{token}/bundle.jsonserves runs published withPOST /admin/runs/{id}/publish.{token}is 32 lowercase hexadecimal characters from a cryptographic random source, so these runs cannot be found by counting run ids.
Responses can be cached for up to one hour, so a run can still be served for up to one hour after it is unpublished. To check a published run without credentials:
verdifax-verify https://api.verdifax.com/public/runs/{token}/bundle.json
Use verdifax-verify 0.14.5 or later. A successful run from run 238 on
should end with VERDICT: ✓ VERIFIED AND ANCHORED, COMPLETE RECORD ANCHORED; see Verify for the other verdicts.
Publishing a run
curl -s -X POST http://localhost:9090/admin/runs/42/publish \
-H "X-Verdifax-Key: vfx_..."
curl -s -X POST http://localhost:9090/admin/runs/42/unpublish \
-H "X-Verdifax-Key: vfx_..."
POST /admin/runs/{id}/publish makes a run's bundle public at a
/public/runs/{token}/bundle.json address, and
POST /admin/runs/{id}/unpublish withdraws it. Both endpoints require
an admin key. An admin key can publish only runs that it created
itself, and can unpublish any run. Customer keys cannot publish any run, and no route_id makes a
run public.
Rate limiting
/execute, /runs, /attestations and the other non-admin authenticated endpoints share one rate limit of 100 requests/minute per key by default (configurable via --rate-limit). POST /attest has its own separate limit of 30 requests/minute per key, because each accepted call is a public-log submission. The /admin/* endpoints are not rate-limited. Anonymous callers in open mode are limited per remote address.
