Authentication

Verdifax authenticates API callers via a single header. Keys are provisioned out-of-band and shown to the operator exactly once.

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.json served the 44 runs published under an earlier rule, which made any run whose route_id began with buyer-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.json serves runs published with POST /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.

Continue