Skip to content
IQ Routing

Compliance

Last reviewed: August 20, 2026.

This page walks the gateway's SOC2 posture: the audit-chain primitive that tamper-evidences every state-changing event, the SOC2 evidence pack composer that bundles a period for an auditor in one ZIP, and the Merkle backup primitive, which is designed to pin a daily root to write-once storage so a database admin who rewrites the on-database chain still cannot rewrite the historical root.

The audit-chain primitive and the SOC2 evidence-pack composer are live. The Merkle backup primitive is built but not enabled in production; the section below states what that means for a reviewer.

IQ Routing is not currently claiming SOC2 Type I or Type II compliance and does not represent that it has completed a SOC2 audit. The tooling on this page produces evidence for your own auditor; a SOC2 report, if one exists, is provided separately under NDA.

The audience is a SOC2 auditor working through the Trust Services Criteria for the gateway. Each section names the surface, the operator endpoint, and the verification step you can run yourself from the Settings → Audit pane.

Audit-chain primitive

Status: Live today. The audit-chain primitive and its verifier endpoint are in force in production. Writing the chain runs on every plan; reading it back is a Team-plan feature (see below).

Every state-changing mutation writes a row into the org audit log, on every plan including Free. Each new row stamps two extra fields:

  • A chain hash: the hex digest of HMAC-SHA256(secret, prev_chain_hash || canonical_row_json).
  • A previous-hash pointer: the chain hash of the prior row for the same org.

The secret is per-org and derived from the master key vault. It never crosses the wire; the verifier endpoint computes hashes server-side and returns only the verdict.

Pre-chain rows (everything written before the chain primitive landed) carry null on both chain fields. The chain is forward-only from the cutover row: the verifier reports the pre-chain epoch as out of scope and verifies every row after it end to end.

The chain shape mirrors a git commit-hash chain: any row that gets edited or deleted breaks the next row's hash, and the verifier surfaces the break with the exact row id where the chain diverges.

The verifier endpoint:

GET https://gateway.iq-routing.com/admin/compliance/audit-chain-verify?org_id=<uuid>
Authorization: Bearer <session token>

It walks the chain forward for one org, recomputes each row's hash, and returns a boolean verdict plus the first divergent row id (null if the chain holds end to end) plus the counts of rows verified and pre-chain rows skipped. A cross-org probe (an org you are not a member of) returns 404. Reading the chain back is a Team-plan feature: a Free or Trial org gets 402 from this endpoint (writing to the chain is unaffected and continues on every plan). The response carries X-Request-Id, like every gateway response, for correlating a specific run. The Settings → Audit pane exposes the verifier as a one-click button so an operator on Team or above can run it before each evidence-pack export.

SOC2 evidence pack

Status: Live today, on the Enterprise plan. The SOC2 evidence-pack composer bundles one period's audit history into a single ZIP for an auditor. Access to the SOC2 and HIPAA packs is limited to Enterprise organisations; the GDPR DSAR export below is open to every plan. The ZIP carries audit-log.json with the chain columns intact so the auditor re-runs the chain verifier offline. The full ZIP carries:

  • audit-log.json: every audit row in the period with the chain columns intact so the auditor can re-run the chain verifier offline.
  • key-vault-access.csv: the four reveal-action audit terms (api_key_revealed, api_key_reveal_blocked, api_key_reveal_ip_blocked, api_key_reveal_hardware_token_blocked) joined to a normalised result column.
  • webhook-deliveries.csv: every webhook delivery with its delivery status, attempt number, latency, and a dead-letter flag, with the destination URL SHA-256 hashed rather than shown.
  • anomaly-events.csv: every anomaly the detector surfaced.
  • role-assignments.csv: every role grant and revocation.
  • chain-verify.py: a self-contained pure-stdlib script that re-runs the chain verifier against audit-log.json so the auditor does not need network access to the gateway.
  • manifest.json: pack metadata plus row counts per file.
  • README.md: an auditor-facing walk-through naming each file plus the offline verifier invocation.

The bundled files run through the gateway's offline redactor before they are written, so emails, phone numbers, national identifiers, and card numbers in audit metadata, anomaly reasons, and key-vault user-agent strings arrive as [REDACTED:TYPE] markers. Source IPs are deliberately left intact because an access-trail review needs the literal value. The chain hash on each row was committed against the un-redacted canonical form, so an offline recompute reads clean only when the operator supplies the un-redacted view.

The endpoint

POST https://gateway.iq-routing.com/admin/compliance/evidence-pack?org_id=<uuid>
Authorization: Bearer <session token>
Content-Type: application/json

{
  "period_start": "2026-04-01",
  "period_end":   "2026-04-30"
}

A missing or expired session returns 401. The endpoint enforces the org-access check so an attacker with admin-of-org-A credentials cannot probe org-B's pack (a cross-org probe returns 404), plus the admin-or-owner role gate (a confirmed org member who is not an admin or owner gets 403), plus a 5-per-hour per-(user, org) export rate limit shared with the other pack endpoints below (exceeding it returns 429). The 1-year period cap is enforced server-side: a request with period_end - period_start > 365 days returns a 400 with the inline message "Evidence pack period exceeds 1-year cap". A reversed period returns 400 with the message period_start must be <= period_end. The cap protects the gateway from a runaway request that would otherwise stream a long-lived org's full audit history through one worker. The response carries X-Request-Id for correlating a specific export attempt.

The endpoint writes a soc2_evidence_pack_exported audit row before streaming the ZIP body so the export itself is auditable. The row carries the period bounds and the actor id but not the ZIP contents.

The manifest

manifest.json carries the pack metadata so the auditor can spot a truncated download immediately:

{
  "org_id":               "org_5d9f...",
  "period_start":         "2026-04-01",
  "period_end":           "2026-04-30",
  "generation_timestamp": "2026-05-01T12:30:00+00:00",
  "gateway_version":      "0.13.0",
  "row_counts": {
    "audit-log.json":          1247,
    "key-vault-access.csv":       8,
    "webhook-deliveries.csv":    92,
    "anomaly-events.csv":         3,
    "role-assignments.csv":       6
  }
}

The manifest carries exactly those keys; a caller-supplied key that is not on the whitelist is dropped rather than written through. The exporting actor is recorded on the audit row rather than in the manifest.

The row counts in the manifest match the row counts in each bundled file. A mismatch is the canonical "truncated download" signal and the auditor should re-export.

The README

README.md walks the auditor through the bundle in plain prose. It names each file, the column meanings, and the offline verifier invocation. The verifier runs as:

python chain-verify.py audit-log.json

It prompts for the per-org chain secret on stdin, which the operator supplies out of band; the secret is derived from the master key vault and never ships inside the bundle. The script prints ok on a clean walk and exits zero, or prints inconsistency at row <id> and exits one on the first divergence. It also catches reorder, fork, cycle, and mid-chain deletion when the export runs from genesis. A windowed export proves the linkage and content of the rows it contains but cannot prove the bundle is complete. The Merkle root below is designed to be the separate anchor for that, and it is not enabled in production, so a windowed export carries no completeness anchor today.

Merkle backup

Status: Built, not enabled in production. The audit chain above is tamper-evident inside the gateway's own durable storage, but an operator holding raw write access to that storage could still rewrite both a row and its chain hash together. A Merkle-anchoring primitive is designed to close that gap: a daily root per org, and a gateway-wide root composed from every org's daily root, each pinned to a write-once immutable store that even a privileged storage credential cannot overwrite or shorten the retention on.

No immutable-storage target is provisioned in production today, so no root has ever been sealed there. The audit chain above, not the Merkle root, is the tamper-evidence control in force. Do not record the Merkle primitive as an operating control in an audit.

The verifier endpoint

POST https://gateway.iq-routing.com/admin/compliance/audit-merkle-verify?org_id=<uuid>
Authorization: Bearer <session token>
Content-Type: application/json

{
  "period_start": "2026-04-01",
  "period_end":   "2026-04-30"
}

The endpoint is live and callable today, but because no root has ever been sealed, it reports integrity_status: "pending" for every day in the requested period rather than "verified" or "inconsistent" -- there is nothing yet to compare against. Treat "pending" as "not yet an operating control," not as a passed check. The response carries X-Request-Id for correlating a specific run.

A gateway-wide variant, GET https://gateway.iq-routing.com/admin/compliance/audit-merkle-cross-org-verify, runs the same comparison across every org's daily roots for one day and is reserved for IQ Routing's own platform operators, not tenant roles. A non-operator caller gets 404 rather than 403, so the route reads as not found rather than forbidden. The Settings → Audit pane exposes the per-org verifier as a one-click button parallel to the chain verifier.

GDPR DSAR pack

Status: Live today. The GDPR DSAR composer and its export endpoint are in force in production.

The GDPR DSAR (Data Subject Access Request) composer mirrors the SOC2 evidence-pack primitive but scopes the export to one subject (a single user) inside one org for one period. The pack covers Articles 15, 17, and 30 of the GDPR: Article 15's right of access, Article 17's right to erasure, and Article 30's records of processing activities.

The composer assembles nine files into one ZIP:

  • subject-access-log.json: every audit row tagged with the subject id for the period, ordered by (created_at, id). Carries the audit chain columns intact so the offline verifier still works.
  • subject-identity.json: the subject's own identity record (id, email, name, creation date) plus their org membership role, per Article 15(3). It is left un-redacted, because this is the subject's own access copy.
  • subject-held-data.json: the held processing content the subject's requests fall under, meaning stored prompt and response bodies plus agent-session usage, scoped to the requested org. Also un-redacted, and not bounded by the one-year audit window, because Article 15(3) is about the data held rather than a one-year slice. A row limit bounds the file so a very long processing history cannot stream unbounded.
  • data-processing-register.json: an Article 30 manifest naming every distinct purpose, data category, retention window, and lawful basis declared for the subject within the period.
  • deletion-log.csv: every erasure event per Article 17. The gateway emits a subject_data_deleted_account event on account erasure, and those rows land in this file.
  • data-transfer-log.csv: every cross-border-transfer event per Chapter V records. The CSV carries the source region, the destination region, an explicit cross_border flag, and the transfer mechanism, so an auditor can spot a cross-border transfer at row granularity.
  • retention-policy-snapshot.json: a JSON snapshot of the org's current retention policy by data class.
  • manifest.json: pack metadata plus row counts per file plus the subject_id and the period bounds. The exporting actor lives on the audit row rather than in the manifest.
  • README.md: an auditor-facing walk-through naming each file plus the offline chain-verifier invocation.

The retention-policy snapshot reports what your org's configuration actually is at export time, and it is a snapshot rather than a statement of policy. The authoritative description of what the gateway retains, in particular the Zero Data Retention boundary and what stays recorded even with it on, lives on the security page and this page defers to it rather than restating it in slightly different words. If the two ever read differently, the security page is the one to hand your reviewer.

The endpoint

POST https://gateway.iq-routing.com/admin/compliance/gdpr-dsar-pack?org_id=<uuid>
Authorization: Bearer <session token>
Content-Type: application/json

{
  "subject_id":   "user_5d9f...",
  "period_start": "2026-04-01",
  "period_end":   "2026-04-30"
}

The endpoint enforces org-access plus the admin-or-owner role gate (403 for a confirmed member who is not an admin or owner), then rate-limits (the same 5-per-hour cap that gates the SOC2 endpoint, 429 when exceeded), then walks the subject-belongs-to-org check before the composer fires. A subject_id whose user or audit-event trail does not appear under the requested org returns 404 so a cross-org subject probe stays indistinguishable from a typo. The role and rate-limit checks run first, so an unprivileged or rate-limited caller sees 403 or 429 rather than the 404 subject mask, even against a bad subject_id.

The 1-year cap is enforced server-side: a request with `period_end

  • period_start > 365 daysreturns400with the inline message "GDPR DSAR pack period exceeds 1-year cap". The cap protects the gateway from a runaway request that would otherwise stream a subject's full audit history through one worker. The response carriesX-Request-Id` for correlating a specific export attempt.

The endpoint is synchronous. The composer returns the full ZIP bytes, the endpoint writes the gdpr_dsar_pack_exported audit row before constructing the StreamingResponse, and the response yields the bytes as a single chunk. There is no background-job queue; an operator hitting the endpoint sees either the ZIP download or the 4xx error in one round trip.

The audit row

Every successful export writes one audit row tagged action = 'gdpr_dsar_pack_exported'. The row carries the requesting admin's user id, the period bounds, the subject_id, plus the row counts read from the manifest so the auditor can re-correlate the download to the pack contents.

HIPAA audit-control export

Status: Not a HIPAA business associate today. IQ Routing does not currently offer or sign a Business Associate Agreement and is not a HIPAA business associate. The tooling described here is HIPAA-oriented audit-control tooling for your own review, not a signed BAA or a HIPAA-readiness attestation. A per-org PII redaction setting can mask four kinds of identifier in message text -- email addresses, phone numbers, US Social Security numbers, and payment card numbers -- before a request reaches the model. It does not detect other PHI categories (names, addresses, and the rest pass through unredacted), it is not a HIPAA Safe Harbor de-identification control, and it is off by default for every org: turning it on is not a self-serve dashboard toggle today, contact us to enable it for your organization. Do not route protected health information (PHI) through the gateway.

The HIPAA audit-control export composer lands as a sibling instance of the SOC2 plus GDPR primitives. The pack covers §164.312(b) audit controls plus the Breach Notification Rule under 45 CFR §164.400-414 plus the workforce-training requirement, packaged for a covered-entity customer's own HIPAA review. It is not evidence of a Business Associate Agreement, because IQ Routing has none with any organization.

The composer assembles seven files into one ZIP:

  • phi-access-log.json: audit events tagged as PHI access per §164.312(b); empty in every export -- see "Empty by construction" below.
  • baa-metadata-snapshot.json: the org's plan tier and data-residency region at generation time. Carries baa_in_place: false rather than any BAA terms, since none exist. The file keeps its historical name.
  • breach-notification-log.csv: breach-notification events for the period; empty in every export, for the same reason as the PHI-access log.
  • audit-control-report.json: an aggregate report counting total events and distinct actors for the period; its PHI breakdown is always zero, though its other totals are real.
  • workforce-training-log.csv: workforce-training events for the period; empty in every export, for the same reason as the PHI-access log.
  • manifest.json: pack metadata plus row counts plus period plus the exporting org id.
  • README.md: an auditor-facing guide. It opens by stating plainly that IQ Routing is not a HIPAA business associate and signs no BAA, before the file roster, and explains why the three always-empty logs are empty by construction rather than by finding.

Empty by construction

phi-access-log.json, breach-notification-log.csv, and workforce-training-log.csv are empty in every export IQ Routing produces, and empty by construction rather than by finding: nothing in the gateway currently emits the PHI-access, breach-notification, or workforce-training events these files are composed from. A zero-row file under a HIPAA heading would otherwise read as a finding that nothing happened, rather than what it actually is. audit-control-report.json is not one of these three -- its event and actor totals count real audit rows, and only its PHI breakdown is structurally zero.

The endpoint

POST https://gateway.iq-routing.com/admin/compliance/hipaa-baa-pack?org_id=<uuid>
Authorization: Bearer <session token>
Content-Type: application/json

{
  "period_start": "2024-01-01",
  "period_end":   "2026-01-01"
}

The endpoint mirrors the GDPR endpoint's access plus rate-limit shape (403 for a confirmed member who is not an admin or owner, 404 for a cross-org probe, 429 past the 5-per-hour cap). No subject_id parameter because the export is org-scoped (the §164.312(b) audit-control requirement targets the covered entity's full PHI-touch history, not a single individual's record).

The 6-year cap matches the HIPAA §164.316(b)(2)(i) retention floor: policies, procedures, and documentation must be retained for six years from creation or the last date in effect, whichever is later. The composer pins the ceiling at 2192 days; a longer window returns 400 with "HIPAA audit-control export period exceeds 6-year cap". The response carries X-Request-Id for correlating a specific export attempt.

The endpoint is synchronous on the same shape as the GDPR endpoint: the composer returns the full ZIP bytes, named hipaa-audit-control-export-{org-slug}-{period_start}-{period_end}.zip, the hipaa_baa_pack_exported audit row writes before the StreamingResponse, and the response yields the bytes as a single chunk. No background-job queue; an operator sees either the ZIP or the 4xx error in one round trip.

The audit row

Every successful export writes one audit row tagged action = 'hipaa_baa_pack_exported'. The row carries the requesting admin's user id, the period bounds, plus the row counts read from the manifest. The audit row is the canonical signal a downstream SOC2 evidence pack (which carries the audit rows verbatim) surfaces to confirm a HIPAA export landed.

Cross-org Merkle

Status: Built, not enabled in production. The cross-org reduction composes every org's daily Merkle root into one cross-org root for tamper detection across the whole platform, on top of the per-org Merkle backup covered above -- it inherits that primitive's status and has composed no root in production. The verifier endpoint is still callable and currently returns integrity_status: "pending" for every day, since no root has ever been sealed.

The cross-org verifier is reserved for IQ Routing platform operators, not for tenant roles; your own owners and admins run the per-org verifier instead. A non-operator caller is masked with a 404 rather than a 403 so the route is indistinguishable from one that does not exist.

See also

  • /docs/security -- the Zero Data Retention boundary and what stays recorded even with it on.
  • /docs/auth -- API keys, scopes, and the gw_live_ prefix used on the routing API, as distinct from the dashboard session tokens this page's endpoints use.
  • /docs/webhooks -- wiring an audit event like api_key_revealed into an external destination.