Skip to main content

Audit log export, chain verification, and refresh workflow

This page covers the audit-log export flow: a queued job that bundles your workspace’s audit trail over a date range you choose, plus the chain-verify endpoint that replays the tamper-evident hash chain over that same range and returns the daily Merkle roots your auditor cross-checks against the exported bundle. Use this when a regulator, buyer, or internal reviewer needs a downloadable, provable slice of the audit ledger — SOC 2 evidence requests, GDPR supervisory replies, or quarterly internal reviews — rather than a continuous stream.
This page describes Devotel Orbit’s platform controls. It is not legal advice. Your audit and retention obligations depend on your industry, your regulators, and your contracts. Confirm the specifics with qualified counsel.

Export vs stream — pick the right surface

Two surfaces move your audit trail out of Orbit. Choose by lifecycle, not by format:
  • Export (this page) — a bounded date range, bundled into a file you hand to an auditor and keep as evidence. The export is day-aligned on UTC because the daily Merkle roots the chain-verifier returns anchor one digest per UTC day, and a partial day would anchor against an incomplete root.
  • Stream (SIEM sinks) — every audit event pushed continuously to Splunk, Datadog, an S3 bucket, or an HTTPS drain you own. Configure a sink when your SOC policy requires live delivery, and use exports for point-in-time evidence requests.
Both are tenant scope: export jobs and sinks only ever read your workspace’s own audit ledger.

Queue an export

GET /api/v1/compliance/audit-export accepts three query parameters and returns 202:
The response is the job handle. The heavy work (reading rows, encoding, uploading the bundle) runs in the background, so a quarter covering hundreds of thousands of events does not block your request:
Bad parameters return 422 VALIDATION_ERROR with the offending field; a queue-write failure returns 500 AUDIT_EXPORT_QUEUE_FAILED.

Job lifecycle: pending → running → complete | failed

Poll GET /api/v1/compliance/audit-export/:jobId until status settles on complete or failed: The job row is scoped to your workspace — a job id from another organization returns 404 NOT_FOUND.

Signed-URL expiry and refresh

The signed download URL has a 7-day TTL. Poll the job inside that window and mirror the bundle into your own evidence store instead of bookmarking the link. If you click a stale link, storage refuses it with a 403. The status response handles this explicitly: once the URL lapses, the job returns download_url: null and download_url_expired: true rather than handing you a link that will fail. The recovery path is to queue a fresh export over the same range — the queue call is cheap, the export is deterministic against the ledger, and two runs over the same UTC days produce the same rows.

Chain-verify: replay the tamper-evident chain

GET /api/v1/compliance/audit-export/:jobId/verify replays the hash chain over the job’s exact range and returns the verdict plus every daily root that anchors it. This is the endpoint auditors hit to prove the exported rows match the tamper-evident chain Orbit persists. Each audit event carries the hash of the event before it, so inserting, removing, or reordering a row breaks the link at that position. The verifier responds with: Issue reason codes flag exactly which link broke: Each issue carries the affected row id, its created_at, and expected / actual hashes, so a break narrows to one event rather than invalidating the range.

How an auditor cross-checks the bundle

  1. Run verify against the job id. chain_valid: true with an empty issues list means every link in the range held at verification time.
  2. Compare rows_checked against the total_rows on the export job — the counts must agree, proving the bundle contains every row the chain covers.
  3. Compare first_hash / last_hash against the first and last rows of the exported bundle.
  4. For each entry in daily_roots, the merkle_root commits to the set of rows Orbit recorded on that UTC day, and anchor_url points to the persisted root record the auditor can pin independently. Date-aligned exports make this comparison exact.
Treat chain_valid: false as a signal to hold the export internally and contact security@devotel.io with the job id and the issues entries — do not hand a broken-chain bundle to an external party without the caveat agreed first. Orbit records every verification call in your audit log with the range, row count, verdict, and issue count.

Worked example — queue → poll → download → verify

Worked example — SOC 2 evidence request

An auditor asks for your access-control audit trail for Q1 with proof the records are complete and untampered:
  1. Queue the export with from=2026-01-01&to=2026-03-31. Choose format=json for the raw ledger, or format=scim when the recipient’s evidence tooling ingests SCIM Event Token (RFC 8417) envelopes.
  2. Poll to complete, download inside the 7-day window, and mirror the file into your own store so the evidence survives the URL TTL.
  3. Run verify and capture rows_checked, chain_valid, first_hash, last_hash, and the daily_roots list. Attach that JSON alongside the bundle.
  4. Hand the auditor both files plus the cross-check steps above. They confirm the row counts agree and the chain endpoints match before reading a single event.
  5. If the signed URL lapses before the auditor fetches from your mirror, queue the same range again — do not forward the expired link.
For a broader framework-mapped pack that assembles these rows into SOC 2, ISO 27001, GDPR, or HIPAA chapters, use the evidence binder; the export flow here is for handing over the raw ledger itself.

Rate limits and roles

  • All three endpoints are gated to the workspace owner or admin role (API keys follow the same gate), because the payload exposes every audit event on the workspace — user ids, IP addresses, and event details.
  • The queue endpoint (GET /audit-export) is write-weighted: 5 per minute per workspace, so scripted evidence pulls should queue a handful of ranges, not hundreds.
  • Status-poll and chain-verify are read-weighted: 120 per minute per workspace, so a 1–2 second poll loop from a dashboard or script stays well under the ceiling.
  • Queueing writes a compliance.audit_export_requested audit entry; each verification call writes a compliance.audit_export_verified entry with the verdict and issue count.

Tenant-owned posture

Audit export and chain verification are tenant-owned controls: Orbit produces the bundle and anchors the chain and Merkle roots; you retain the evidence. Mirror downloads into your own store within the 7-day window, keep the verify response alongside the bundle, and treat the export’s integrity as yours to check — the endpoint gives you the verdict, the retention decision is yours.