Skip to content

Backend API

All endpoints are served under /api/ by a single NinjaAPI declared in backend/api/api.py. Authentication is a bearer token (Authorization: Bearer <token>) issued at enrolment. Request and response shapes live in backend/api/schemas.py.

Base URL (production): https://blindproof.co.uk/api/

Endpoints

GET /health

Unauthenticated liveness probe. Returns {"status": "ok"}.

POST /enrol

Create a new user account.

Request (EnrolIn): {"email": "...", "password": "..."}

Response (EnrolOut): {"token": "...", "argon2_salt_hex": "..."}

The salt is generated server-side on enrolment and returned so the client can derive its master key. The password is used for account authentication; it is separate from the author's BlindProof passphrase (which we never see — it is used on the client to derive the encryption keys).

POST /login

Return a fresh bearer token and the user's Argon2 salt for an existing account.

Request (LoginIn): {"email": "...", "password": "..."}

Response (LoginOut): {"token": "...", "argon2_salt_hex": "..."}

Used by the desktop GUI's Sign-in flow when re-linking to an existing account.

GET /whoami — authenticated

Identity probe. Returns {"email": "..."}. Useful for the client to confirm a stored token is still valid.

POST /snapshots — authenticated

Upload a snapshot: ciphertext plus non-sensitive metadata.

Request (SnapshotIn):

{
  "ciphertext_ref": "uuid-hex-string",
  "ciphertext_b64": "base64-of-AES-GCM-ciphertext",
  "nonce_hex": "24-hex-chars",
  "captured_at": "2026-04-22T14:00:00+00:00",
  "file_type": "md",
  "path_ciphertext_b64": "base64-of-AES-GCM-encrypted-path",
  "path_nonce_hex": "24-hex-chars",
  "plaintext_hmac_hex": "64-hex-chars",
  "ciphertext_size": 1234,
  "word_count": 520,
  "char_count": 3102
}

Response (SnapshotOut): the assigned snapshot id and server-stamped uploaded_at.

Notes:

  • The server derives nothing from the manuscript — ciphertext is opaque bytes written to the blob store, plus the HMAC commitment for later Merkle aggregation.
  • path_ciphertext_b64 is the file path encrypted with the client's enc_key, so even path names stay private.
  • ciphertext_ref is a client-chosen UUID-v4; it's used as the blob storage key end-to-end (client filename == server blob key).

GET /snapshots/{snap_id} — authenticated

Fetch a single snapshot: metadata plus the raw ciphertext body. Used for restore flows from another device.

Response (SnapshotFullOut): all metadata fields plus ciphertext_b64.

Only the authenticated user's own snapshots are returned; requesting someone else's snap_id returns 404.

GET /user/metadata — authenticated

List all snapshots for the authenticated user, metadata only — no ciphertext.

Response (UserMetadataOut): {"snapshots": [SnapshotMetadataOut, ...]}.

This is what the dashboard reads.

POST /proof-bundle — authenticated

Assemble and return the signed zip bundle for the authenticated user.

Request (ProofBundleIn): optional filters (from_date, to_date, include_attestation_text).

Response: a Content-Type: application/zip body. The four files inside are described in Proof bundle format.

On first call, the user's authorship attestation text is persisted — subsequent bundles read that stored text to keep bundles stable and comparable.

Authentication

BearerAuth (in backend/api/api.py) looks up the token against the AuthToken model and attaches the user to request.auth for handlers to use. Tokens are long-lived; there is no refresh flow. Rotating a token means calling /login again (the old token remains valid until manually revoked — no automatic expiry in the POC).

Error responses

django-ninja's default error envelope: HTTP status + {"detail": "..."}. The handlers don't customise this; wrong credentials are 401, missing resources are 404, validation errors are 422.

What the API deliberately does not expose

  • No plaintext-echoing endpoints. Nothing accepts, returns, or logs plaintext, file paths, file names, or titles in the clear. If you find a log statement that could leak plaintext, it's a bug.
  • No cross-user reads. Every authenticated handler scopes queries to request.auth.
  • No aggregate analytics endpoints. The dashboard reads per-user data from /user/metadata only.