API reference

The Dokaz REST API is versioned under /v1. The machine-readable OpenAPI 3.1 document is at /openapi.json.

Authentication

Create an API key on the API keys page, then send it as a bearer token. The examples below are identical requests — pick the one for your shell.

macOS / Linux (bash, zsh)
curl https://dokaz.net/v1/drills -H "Authorization: Bearer so_your_key_here"
Windows (PowerShell)
curl.exe https://dokaz.net/v1/drills -H "Authorization: Bearer so_your_key_here"

In PowerShell, curl is an alias for Invoke-WebRequest, which does not accept -H. Call curl.exe to use the real curl, or use the native cmdlet:

Invoke-RestMethod https://dokaz.net/v1/drills -Headers @{ Authorization = "Bearer so_your_key_here" }

Conventions

  • Every response is a { data, meta, errors } envelope.
  • List endpoints paginate with an opaque cursor; follow meta.next_cursor.
  • State-changing requests (POST, PATCH, DELETE) require an Idempotency-Key header; a repeated key replays the original response. test-source changes nothing and needs none, and the streaming PUT …/upload (whose body is the dump) takes none: each PUT is a new upload.
  • Requests are rate-limited per account; over the limit returns 429 with Retry-After.

Connecting a backup

A database's source is either an S3-compatible bucket ("kind": "s3" — AWS S3, Cloudflare R2, Backblaze B2 or MinIO, with a read-only key and either an exact key or the newest backup under a prefix) or "kind": "push", where your backup job uploads to Dokaz. Endpoints must be https://. The secret access key is write-only: it is encrypted per account and never returned — responses show only credentials.access_key_hint.

curl https://dokaz.net/v1/databases \
  -H "Authorization: Bearer so_your_key_here" \
  -H "Idempotency-Key: 6f1c0d2e-prod-postgres" \
  -d '{"name": "prod-postgres",
       "source": {"kind": "s3", "region": "eu-west-1",
                  "bucket": "acme-prod-backups", "prefix": "postgres/prod/",
                  "access_key_id": "AKIA...", "secret_access_key": "..."}}'
  • POST /v1/databases/{id}/test-source probes the saved source (or a candidate source body) and answers { ok, code, message, object }; 10 tests per minute per account.
  • PATCH /v1/databases/{id}/source replaces the source; omit both credential fields to keep the saved key, or send a new pair to rotate it.
  • Drills of s3 and push sources run in an isolated, single-use restore sandbox that receives only a 15-minute presigned link to the one backup object — never your credentials. On a server where the sandbox is not configured, POST /v1/drills answers 409 sandbox_unavailable (a push source on a server without an ingest bucket answers push_unavailable); source.drills_available says which applies.

The key needs s3:GetObject on the backups and s3:ListBucket on the bucket — nothing else.

Pushing backups

A database with "source": {"kind": "push"} receives its dumps from your backup job. Use an API key with the uploads:write scope (add drills:write to drill each upload). The simplest way is one curl after the dump is written; it streams the file (up to 5 GiB and your plan's dump size) and Dokaz computes its SHA-256 as it arrives:

pg_dump -Fc -f dump.pgdump "$DATABASE_URL"
curl -fsS -T dump.pgdump \
  -H "Authorization: Bearer so_your_key_here" \
  "https://dokaz.net/v1/databases/<database-id>/upload?drill=1"
  • Content-Length is required (411 without it, so upload a file, not a pipe); over the limit is 413. Optional: X-Content-SHA256 (or ?sha256=) is checked against the bytes received (422 sha256_mismatch deletes the upload), ?declared_backup_at= (RFC 3339) records when the backup was taken for RPO, and ?drill=1 starts a drill.
  • For larger dumps, or to upload straight to storage: POST /v1/databases/{id}/uploads with optional size_bytes, multipart, declared_backup_at and sha256. It returns either put.url (a presigned PUT, valid 15 minutes, up to 5 GiB) or, above 5 GiB or with "multipart": true and size_bytes, multipart.parts (presigned part URLs valid 12 hours, each part_size_bytes long except the last).
  • Then POST /v1/databases/{id}/uploads/{upload_id}/complete, optionally with "drill": true. A multipart upload also sends its parts, with the ETag each part PUT returned: {"parts": [{"part_number": 1, "etag": "…"}, …]}. A dump over your plan's limit is deleted and answers 413. A declared sha256 is recorded and checked against the bytes the restore sandbox downloads.
  • Each upload is deleted once its drill is done, unless the source sets retain_uploads_days (0–7); uploads never outlive 7 days, and unfinished uploads are removed after 24 hours. Uploads are limited to 10 per minute and 20 in progress per account.
  • The upload response's drill is the drill it started, or drill_error says why none was (for example sandbox_unavailable on a server without the restore sandbox); the upload is complete either way.

Endpoints

MethodPathDescription
GET/v1/databasesList database targets
POST/v1/databasesRegister a database target
GET/v1/databases/{id}Get a database target
PATCH/v1/databases/{id}Set a database's RTO / RPO targets
PATCH/v1/databases/{id}/sourceChange (or rotate the key of) a database's backup source
POST/v1/databases/{id}/test-sourceTest a backup source: finds the backup a drill would use
GET/v1/databases/{id}/suggestionsList checks proposed from the restored schema
POST/v1/databases/{id}/suggestions/{sid}/acceptAccept a suggested check (creates the assertion)
POST/v1/databases/{id}/suggestions/{sid}/dismissDismiss a suggested check for good
PUT/v1/databases/{id}/uploadStream a dump to a push database (curl -T, up to 5 GiB)
POST/v1/databases/{id}/uploadsStart a presigned upload (single PUT or multipart)
POST/v1/databases/{id}/uploads/{upload_id}/completeFinish a presigned upload, optionally starting a drill
GET/v1/drillsList drills
POST/v1/drillsStart a drill
GET/v1/drills/{id}Get a drill with steps, assertions + recovery metrics
GET/v1/drills/{id}/evidenceDownload the signed evidence PDF
GET/v1/drills/{id}/signatureGet the detached Ed25519 signature for the evidence PDF
GET/v1/drills/{id}/logsGet captured restore output (snippet + SHA-256 of the full log)
GET/v1/drills/{id}/schema-driftGet schema changes since the previous successful drill
GET/v1/alertsList alerts
GET/v1/heartbeatsList backup check-ins
GET/v1/heartbeats/{id}Get a backup check-in
GET/v1/integrations/complianceList compliance-platform connections (Vanta, Drata — beta)
POST/v1/integrations/complianceConnect Vanta or Drata — beta (credentials are write-only)
GET/v1/integrations/compliance/{id}Get a compliance-platform connection
PATCH/v1/integrations/compliance/{id}Change a connection's target, databases or toggles; rotate its credentials
POST/v1/integrations/compliance/{id}/testTest a connection's credentials and target
DELETE/v1/integrations/compliance/{id}Disconnect (destroys the stored credentials)

Suggested checks and schema drift

After every restore Dokaz records the restored database's schema — tables, columns and types, primary and foreign keys, row counts and the newest value of created/updated-style columns. Names, types, counts and timestamps only, never row data. From it, Dokaz proposes checks by fixed rules, each with its reason: key tables get table_exists, row counts get a row_count floor at about 90% of the rows seen, key columns get column_exists, id- and email-like columns that had no NULLs get no_nulls, live tables get freshness, and foreign keys get orphan checks. Nothing runs until you accept it; dismissed suggestions stay dismissed.

  • freshness takes {"table", "column", "max_age_seconds"} and passes when the column's newest value is at most that far behind the drill's backup time, or its start when the backup time is unknown; the result says which.
  • GET /v1/drills/{id}/schema-drift lists tables and columns added or removed, type changes, row-count drops over the database's threshold (20% unless changed on the database page) and newest timestamps that went backwards, compared with the previous successful drill. Removals and big drops are also sent by email, to Slack and as a drill.schema_drift webhook. Drift is informational: it never changes a verdict.

Compliance platforms (Vanta, Drata)Beta — tell us if anything looks off

Both providers are in beta: the integrations are built and tested against each vendor's published API documentation, not yet proven against live Vanta and Drata accounts. If a push lands differently from what is described here, email support@dokaz.net.

A connection pushes every finished drill's signed evidence into the platform: the PDF, its signature JSON (Vanta receives it as .json.txt, the extension it accepts) and a plain-text summary with the database, verdict, measured RTO / RPO and the framework mapping (SOC 2 A1.2 and A1.3, ISO/IEC 27001:2022 Annex A 8.13, HIPAA 45 CFR 164.308(a)(7)(ii)(D), and optionally DORA Article 12). It supports evidence for those requirements; it is not a certification. Failed drills are pushed too unless push_failed_drills is false. These endpoints need the opt-in integrations:read / integrations:write scopes; the same settings are under Account → Compliance platforms.

curl https://dokaz.net/v1/integrations/compliance   -H "Authorization: Bearer so_your_key_here"   -H "Idempotency-Key: connect-vanta-1"   -d '{"provider": "vanta", "client_id": "vci_...", "client_secret": "...",
       "document_id": "backup-restoration-test", "push_failed_drills": true}'
  • Vanta: create an application in Vanta's Developer console with the vanta-api.all:read, vanta-api.all:write and vanta-api.documents:upload scopes, and pass its client_id / client_secret with the document_id evidence goes to. Files are uploaded to the document and the document is submitted.
  • Drata: pass an api_key with the Evidence Library: Create Evidence permission, the workspace_id, optional region (us, eu, apac) and control_ids. Each drill becomes one evidence item.
  • Secrets are never returned; responses carry credential_hint. When the platform rejects them the connection becomes reconnect_needed and pushes stop until new credentials are sent with PATCH, which also retries the failed pushes.
  • Each push is idempotent per drill and connection: a retried push never uploads a file twice.

Recovery metrics (RTO / RPO)

Every drill measures how long recovery took and how old the backup was. Each drill response carries a recovery object:

  • rto_seconds: fetch + restore wall-clock in the sandbox (fetch_ms + restore_ms, rounded up). Null when no restore completed.
  • rto_basis: for drills of s3 and push sources, agent when the times are the sandbox's own, cross-checked against when Dokaz received its progress events, or app_observed when the two disagreed and Dokaz's observation was recorded instead. Null for the built-in sample.
  • rpo_seconds: drill start minus backup_at. backup_at_source says where the backup time came from. A file modification time (file_mtime), S3 Last-Modified or upload-completion time is only the latest the backup could have been taken (backup_at_upper_bound: true), so the true RPO is at least the value shown. The built-in sample dataset has no backup time, so its RPO is null.
  • targets: the database's RTO / RPO targets, copied onto the drill when it starts. within_targets is null when none were set.

Set targets with PATCH /v1/databases/{id} and a body such as {"rto_target_seconds": 3600, "rpo_target_seconds": 86400} (null clears a target), or under Recovery targets when you add or edit a database in the app. A drill that misses a target fails, with rto_max / rpo_max results next to its data assertions. An RPO target fails when the backup time cannot be measured. The signed PDF has a Recovery metrics section, and the current signature version (ed25519-v3) also covers these values: the signature JSON carries the signed recovery object, and dokaz-verify rejects evidence whose recovery figures were edited.

Public transparency log

Every evidence signature is appended to a public, append-only Merkle log (RFC 6962 hashing), so a report cannot be quietly re-signed, backdated or withdrawn: once a signed checkpoint covers it, anyone can prove it was in the log. Each leaf is one line holding only algorithm, public_key_id, signature_sha256 (SHA-256 of the raw signature), pdf_sha256 and signed_at — no account, customer, database or drill identifier:

dokaz-evidence-log/v1 algorithm=ed25519-v3 public_key_id=3b6a…e1 signature_sha256=9f2c…07 pdf_sha256=c41d…5a signed_at=2026-10-12T09:30:00.123456Z

Every 10 minutes, when new leaves exist, the log signs a checkpoint in the C2SP tlog-checkpoint signed-note format — origin dokaz.net/log, tree size, base64 root hash and a timestamp line — with the evidence signing key. The signature JSON (GET /v1/drills/{id}/signature and the evidence bundle) carries a log object with the leaf index, the inclusion proof and that checkpoint; it is null until a checkpoint covers the signature. Check it offline with dokaz-verify --log --sig=signature.json --pubkey=evidence-signing-keys.pem.

What it does and does not prove: the checkpoint timestamp, like signed_at, comes from Dokaz's own clock — it is not an independent timestamp authority. What the log adds is that history cannot be quietly rewritten: anyone who keeps an earlier checkpoint can demand a consistency proof to the current one, and a report removed or re-signed after the fact would not match. Independent witnesses co-signing the checkpoints are not in place yet.

These endpoints need no API key (rate-limited per IP; responses carry cache headers):

GET/log/checkpointLatest signed checkpoint (text/plain signed note)
GET/log/keySigned-note verifier keys the checkpoints are signed with
GET/log/proof/inclusion?index=NInclusion proof for a leaf (or ?pdf_sha256=…; optional &tree_size=M)
GET/log/proof/consistency?old=M&new=NConsistency proof between two tree sizes
GET/log/leaves?start=N&limit=KLeaves covered by the latest checkpoint, 100 per page

Owners and admins can also opt in to a public trust badge under Account → Trust badge: an SVG (/badge/<code>.svg) and page (/trust/<code>) showing recent drill dates, verdicts and their log indices, behind a random, regenerable code. Database names stay hidden unless chosen per database.