Call lifecycle
This document describes the complete lifecycle of a single AI-assisted phone call through the Audact platform, from pre-dial compliance check through post-call verification.
Phase 1: Pre-Dial (Policy Engine)
POST /v1/evaluate — Stateless compliance check. No evidence events are written.
The Policy Engine evaluates country-specific rules (consent requirements, DNC registry, quiet hours) and returns ALLOW or BLOCK with reasons. If BLOCK, the call may not proceed.
GET /v1/policies/{country} — Look up the active policy ruleset for a country (ISO 3166-1 alpha-2). Returns the full rule schema including quiet hours, consent requirements, and DNC registry configuration.
Phase 2: Call Start
POST /v1/events with event_type: CALL_START
The platform posts a CALL_START event. The Evidence Layer auto-creates a calls row with status = 'active' if one does not already exist.
Phase 3: First Turn — Disclosure Required
POST /v1/validate (turn 1)
On the first turn, the disclosure gate fires because call_state.disclosure_delivered == false. The validator returns verdict: BLOCK with the original text in the content field. The remaining 6 checks are not executed (short-circuit). The platform must speak its own disclosure text and set disclosure_delivered = true in call state for subsequent turns.
Phase 4: Conversation — Validator Active
POST /v1/validate (turn N, repeats per turn)
The Validator runs 7 checks within a 40ms hard deadline:
| # | Check | Mode |
|---|---|---|
| 0 | Disclosure gate | Always enforce; short-circuits on BLOCK |
| 1 | Blocklist scan | Shadow or enforce (Aho-Corasick pattern matching) |
| 2 | Pricing band | Shadow or enforce (regex price detection + range check) |
| 3 | PII scan | Shadow or enforce (BSN, IBAN, credit card via Luhn) |
| 4 | Escalation detection | Shadow or enforce (keyword + confidence scoring) |
| 5 | Consent gating | Shadow or enforce (action-consent ledger mapping) |
| 6 | Cross-turn consistency | Enforce (price/fact/commitment consistency) |
Verdicts
Only three verdicts exist: ALLOW, BLOCK, MODIFY. There is no ESCALATE verdict.
- ALLOW — Original text returned in
content. Receipt:input_hash == output_hash. - MODIFY — Modified text returned in
content. Receipt:input_hash(original),output_hash(modified). - BLOCK — Original text in
content; caller substitutes its own fallback response. Receipt:input_hash(original),output_hash(fallback).
Response Shape (ACVFResponse)
json
{
"call_id": "uuid",
"turn": 1,
"verdict": "ALLOW | BLOCK | MODIFY",
"content": "validated or modified text",
"fail_open": false,
"receipt": { "..." },
"compliance_score": { "..." },
"checks": [
{
"check_id": "pii_scan",
"verdict": "ALLOW",
"reason": "",
"modified_content": "",
"duration_ms": 2
}
]
}The LLM Proxy writes a VALIDATION_RESULT event to the Evidence Layer (via POST /v1/events) after each validation. A compliance receipt is attached to all non-fail-open responses.
Fail-Open (Validator Timeout > 40ms)
If the validator exceeds the 40ms deadline: verdict: ALLOW, fail_open: true, content: original text, receipt: nil. A FAIL_OPEN event is written immediately for alerting and durably stored asynchronously. The fail_open_meta field includes reason, deadline_ms, elapsed_ms, and checks_completed.
PII Detection (Check 4)
Detects BSN (Dutch 9-digit), IBAN, and credit card numbers (Luhn algorithm). Returns MODIFY or BLOCK per tenant configuration.
Phase 4b: Audio-Native Validation (Alternative Path)
POST /v1/validate-audio — Audio Validator
For voice calls where the platform sends raw audio instead of pre-transcribed text. The Audio Validator:
- Accepts base64-encoded PCM audio (16-bit LE, 16 kHz, mono).
- Performs ASR transcription (Deepgram, Whisper, or AssemblyAI — configurable per tenant via BYOK).
- Segments the audio using one of two strategies:
asr_first(default) — Streaming ASR drives sentence boundary detection.prosodic_first— VAD silence detection creates segments, then batch ASR transcribes each.
- Runs the same 7-check pipeline on the transcription text (delegates to the Validator).
- Determines audio routing: forward (ALLOW), substitute (MODIFY — replaces with pre-generated TTS), or block.
- Returns per-segment results with dual hashes (
audio_hash+text_hash) and a compliance receipt carrying an HMAC integrity tag.
Parallel ASR
When parallel_asr: true, the Audio Validator submits audio to multiple ASR models simultaneously, records all results in the evidence chain, and selects the highest-confidence transcription.
Request Shape
json
{
"call_id": "uuid",
"audio_data": "base64-encoded PCM bytes",
"audio_format": "pcm_s16le_16000",
"turn_number": 1,
"configuration_id": "uuid",
"call_context": {
"language": "nl",
"disclosure_delivered": true
},
"segmentation_method": "asr_first",
"parallel_asr": false
}Response Shape
json
{
"verdict": "ALLOW",
"segments": [
{
"segment_index": 0,
"audio_hash": "sha256hex",
"transcribed_text": "ASR output",
"text_hash": "sha256hex",
"verdict": "ALLOW",
"check_results": [...],
"segmentation_method": "asr_first",
"asr_confidence": 0.97,
"asr_model_id": "deepgram:nova-2",
"action": "forward"
}
],
"fail_open": false,
"evidence_event_id": "uuid",
"evidence_hash": "sha256hex",
"receipt": {
"input_hash": "sha256hex (original audio)",
"output_hash": "sha256hex (forwarded/substituted audio)",
"text_hash": "sha256hex (transcription)",
"verdict": "ALLOW",
"signature": "HMAC-SHA256"
}
}Phase 5: Mid-Call Events
POST /v1/events — The Evidence Layer accepts any event_type string. Common types:
| Event Type | Description |
|---|---|
CALL_START | Marks call beginning (auto-creates calls row) |
VALIDATION_RESULT | Written by LLM Proxy after each /v1/validate call |
AUDIO_VALIDATION | Written by Audio Validator after each /v1/validate-audio call |
CHECK_RESULT | Individual check outcome |
FAIL_OPEN | Validator timeout event |
DELIVERY_ATTESTATION | Delivery confirmation linked to validation event |
TRANSCRIPT_RECONCILIATION | Post-call transcript comparison result |
CALL_END | Marks call completion |
CALL_FAILED | Marks call failure |
All events are appended to the cryptographic hash chain regardless of type. Custom event types (e.g. tool_call, consent_granted) can be sent freely.
Phase 5b: LLM Proxy
POST /v1/proxy/openai — Non-streaming compliance-aware LLM proxy.
POST /v1/proxy/openai/stream — Streaming compliance-aware LLM proxy.
The LLM Proxy sits between the platform and the upstream LLM provider. For each request:
- Validates the request through the Validator.
- Forwards allowed requests to the upstream LLM (OpenAI-compatible API).
- Validates the LLM response before returning it to the platform.
- Writes evidence events for both request and response validation.
- Supports multi-candidate generation: sends the same prompt to N endpoints in parallel, validates all candidates, and returns the best compliant response.
Phase 6: Delivery Attestation
POST /v1/delivery-confirmation — Submit delivery confirmation.
After the validated response is delivered to the human interlocutor, the platform (or Audact's own audio monitor) sends a delivery confirmation. This creates a tamper-evident link between the validated content and what was actually delivered — any mismatch is detectable by recalculation against the stored commitment:
- DC-1 (webhook): Platform sends a webhook with the delivered content hash.
- DC-2 (api_query): Audact polls the platform API for delivery status.
- DC-3 (tts_completion): TTS engine signals completion with content hash.
- DC-4 (audio_monitoring): Audact's own ASR captures what was spoken (independent of the platform being audited).
Each confirmation includes:
linking_hash= SHA-256(validation_event_hash || confirmation_hash) — tamper-evident evidence chain link.confirmation_grade:independent(DC-4),platform_sourced(DC-1/2/3), orunconfirmed(timeout).content_match: whether delivered content matches validated content.
Delivery confirmations that don't arrive within 30 seconds are automatically marked unconfirmed.
GET /v1/calls/{call_id}/delivery — Get per-turn delivery attestation status for a call.
Phase 7: Call Termination
The platform sends a terminal event via POST /v1/events:
CALL_END— Setscalls.status = 'completed'andcalls.ended_atatomically in the same transaction as the evidence event write.CALL_FAILED— Setscalls.status = 'failed'andcalls.ended_atatomically in the same transaction as the evidence event write.
Important: There is NO background sweep job. Calls that never receive a CALL_END or CALL_FAILED event remain as status = 'active' indefinitely. The platform is responsible for sending terminal events.
Phase 7b: C2PA Content Credentials
POST /v1/calls/{call_id}/c2pa — Generate C2PA manifest for a completed call.
After a call completes, tenants can request a C2PA-compatible content credential manifest. The manifest contains:
- Claim: call metadata, evidence chain summary, config seal hash.
- Assertions: per-event hash references (no raw event data), chain integrity proof.
- Signature: HMAC-SHA256 over canonical JSON (RFC 8785) of claim + assertions using the tenant's KMS-managed key.
- Merkle proof reference: if the call's evidence was included in a cross-tenant aggregation, the manifest references the branch proof and Merkle root.
The endpoint verifies call ownership before generating the manifest. HMAC key material is never included in the response.
Phase 7c: Cross-Tenant Aggregation
The Aggregator service periodically collects evidence hashes across all tenants and builds a Merkle tree for tamper detection and third-party auditability.
Aggregation Pipeline (automatic or manual)
POST /v1/admin/aggregate — Admin-only endpoint to manually trigger aggregation for a time window. Requires owner role. If no window is specified, aggregates the previous completed interval.
The pipeline:
- Collects per-tenant aggregate hashes (SHA-256 of all evidence hashes in the window).
- Builds a balanced binary Merkle tree with minimum 16-leaf padding.
- Requests an RFC 3161 timestamp token from a TSA for the Merkle root.
- Appends the root to the meta-chain (chain-of-chains).
Proof Retrieval (authenticated)
| Method | Path | Description |
|---|---|---|
| GET | /v1/proofs | List aggregation windows in which the tenant participated (newest-first) |
| GET | /v1/proofs/{window_id} | Retrieve Merkle branch proof + RFC 3161 timestamp token for a specific window |
Branch proofs are reconstructed on-demand from stored metadata. Proof steps contain only opaque sibling hashes and position — no tenant identifiers are leaked.
Public Verification (no authentication)
| Method | Path | Description |
|---|---|---|
| GET | /v1/metachain | Complete meta-chain (chain-of-chains) of Merkle roots in ascending order |
| POST | /v1/verify-inclusion | Offline cryptographic verification of a Merkle branch proof |
The meta-chain enables anyone to verify cross-aggregation integrity:
chain_hash = SHA-256(sequence || merkle_root || previous_hash)- Genesis entry:
previous_hash= 64 zeros - The chain enforces integrity on every append
The verify-inclusion endpoint is fully offline — no database access, no authentication. Feed the branch_proof object from GET /v1/proofs/{window_id} directly into it.
Phase 8: Post-Call Verification
| Endpoint | Purpose |
|---|---|
GET /v1/verify/{call_id} | Walks the evidence chain, recomputes SHA-256 hash for each event, verifies previous_hash links. Returns VALID or INVALID with broken_at_sequence. |
GET /v1/chains/{call_id} | Paginated evidence chain retrieval. Params: after_sequence, limit. |
GET /v1/reconstruct/{call_id} | Replays evidence chain to reconstruct the exact policy and validator configuration active during each event (config seal hash resolution). |
GET /v1/receipts/{call_id} | Retrieves and HMAC-verifies the compliance receipt for a call. |
Transcript Reconciliation
POST /v1/transcript-reconciliation — Post-call transcript reconciliation.
After a call completes, the platform can submit the full post-call transcript for reconciliation against the evidence chain. The reconciler compares each transcript segment against validated output events:
- MATCHED: Transcript segment hash matches validated output hash — confirmed delivery.
- MISMATCHED: Transcript segment hash differs — a
transcript_mismatchevidence event is generated. - UNVALIDATED: Transcript segment has no corresponding validation event — an
unvalidated_contentevent is generated (detects platform insertions that bypassed Audact).
Audio Reconciliation
POST /v1/audio-reconciliation — Post-call audio reconciliation.
Combines ASR transcription and transcript reconciliation in a single synchronous endpoint. The caller sends the full call recording; the service transcribes it via ASR and then reconciles the transcription against the evidence chain. This is the preferred endpoint when the platform has raw audio but no text transcript.
This is a synchronous endpoint — the caller blocks until transcription and reconciliation are complete. For long recordings this may take several seconds.
Request Shape
json
{
"call_id": "uuid",
"audio_data": "base64-encoded audio bytes",
"audio_format": "pcm_s16le_16000 | wav | mp3",
"language": "nl",
"asr_provider": "deepgram"
}audio_data: Standard base64-encoded audio. Body limit is 64 MiB (~34 min PCM at 16 kHz mono).audio_format: One ofpcm_s16le_16000(raw PCM 16-bit LE, 16 kHz, mono),wav, ormp3.language: BCP-47 tag (optional, defaults tonl).asr_provider: Override the default ASR provider (optional).- No
tenant_idfield — tenant identity comes from auth context.
Response Shape
json
{
"call_id": "uuid",
"asr_model_id": "mock:deterministic-v1",
"asr_confidence": 0.95,
"total_segments": 5,
"matched_segments": 4,
"mismatched_segments": 1,
"unvalidated_segments": 0,
"segment_results": [
{
"sequence_number": 1,
"status": "matched",
"similarity_score": 1.0,
"validated_hash": "sha256hex",
"transcript_hash": "sha256hex"
}
]
}Flow
- Authenticate via API key.
- Verify call ownership.
- Decode base64 audio bytes.
- Transcribe full audio via ASR (same ASR client as
/v1/validate-audio). - Build reconciliation segments from ASR output.
- Reconcile against the evidence chain (same reconciler as
/v1/transcript-reconciliation). - Return segment-by-segment results.
Tenant Management
These endpoints manage tenants, API keys, and configurations. They are not part of the per-call flow but are required for setup.
Tenants
| Method | Path | Role | Description |
|---|---|---|---|
| POST | /v1/tenants | Public (rate-limited) | Create a new tenant |
| GET | /v1/tenants/{tenant_id} | Viewer | Get tenant details |
| PUT | /v1/tenants/{tenant_id} | Owner | Update tenant |
API Keys
| Method | Path | Role | Description |
|---|---|---|---|
| POST | /v1/tenants/{tenant_id}/keys | Owner | Generate a new API key |
| GET | /v1/tenants/{tenant_id}/keys | Developer | List API keys (prefix only) |
| DELETE | /v1/tenants/{tenant_id}/keys/{key_id} | Owner | Revoke an API key |
Configurations
| Method | Path | Role | Description |
|---|---|---|---|
| POST | /v1/configurations | Developer | Create configuration |
| GET | /v1/configurations/{config_id} | Viewer | Get configuration |
| PUT | /v1/configurations/{config_id} | Developer | Update configuration |
| DELETE | /v1/configurations/{config_id} | Owner | Soft-delete configuration |
| GET | /v1/tenants/{tenant_id}/configurations | Viewer | List tenant configurations |
Configurations include:
- Policy rules (blocklist patterns, PII detection, pricing bands, escalation keywords, consent gates)
- Multi-candidate LLM endpoints (BYOK: URL + API key per provider, stored securely)
- Output constraints (response length limits with compliance-critical content preservation)
- ASR provider keys (BYOK: Deepgram, Whisper, AssemblyAI — API keys stored securely)
Authentication
| Method | Path | Description |
|---|---|---|
| POST | /v1/auth/exchange-key | Exchange API key for JWT access + refresh tokens |
| POST | /v1/auth/refresh | Refresh an expired JWT using the refresh token cookie |
Usage
| Method | Path | Role | Description |
|---|---|---|---|
| GET | /v1/tenants/{tenant_id}/usage | Viewer | Get tenant API usage statistics |
Complete Endpoint Summary
| Method | Path | Description |
|---|---|---|
| POST | /v1/evaluate | "May this call happen?" (pre-dial, stateless) |
| GET | /v1/policies/{country} | Get country policy ruleset |
| POST | /v1/validate | "May the AI say this?" (per turn, 40ms deadline) |
| GET | /v1/receipts/{call_id} | Retrieve compliance receipt (post-call) |
| POST | /v1/events | Append event to chain (during call) |
| GET | /v1/calls | List calls (filtered by tenant) |
| GET | /v1/chains/{call_id} | Retrieve event chain (post-call, paginated) |
| GET | /v1/verify/{call_id} | Chain integrity check (post-call) |
| GET | /v1/reconstruct/{call_id} | Rebuild config history (post-call / audit) |
| POST | /v1/delivery-confirmation | Submit delivery confirmation |
| GET | /v1/calls/{call_id}/delivery | Get delivery attestation status |
| POST | /v1/transcript-reconciliation | Post-call transcript reconciliation |
| POST | /v1/proxy/openai | Compliance-aware LLM proxy (non-streaming) |
| POST | /v1/proxy/openai/stream | Compliance-aware LLM proxy (streaming) |
| POST | /v1/tenants | Create tenant |
| GET | /v1/tenants/{tenant_id} | Get tenant |
| PUT | /v1/tenants/{tenant_id} | Update tenant |
| POST | /v1/tenants/{tenant_id}/keys | Generate API key |
| GET | /v1/tenants/{tenant_id}/keys | List API keys |
| DELETE | /v1/tenants/{tenant_id}/keys/{key_id} | Revoke API key |
| POST | /v1/configurations | Create configuration |
| GET | /v1/configurations/{config_id} | Get configuration |
| PUT | /v1/configurations/{config_id} | Update configuration |
| DELETE | /v1/configurations/{config_id} | Soft-delete configuration |
| GET | /v1/tenants/{tenant_id}/configurations | List configurations |
| GET | /v1/tenants/{tenant_id}/usage | Get usage statistics |
| POST | /v1/auth/exchange-key | Exchange API key for JWT |
| POST | /v1/auth/refresh | Refresh JWT |
| POST | /v1/validate-audio | Validate audio segment (ASR + compliance) |
| POST | /v1/audio-reconciliation | Post-call audio reconciliation (ASR + reconciliation, synchronous) |
| POST | /v1/calls/{call_id}/c2pa | Generate C2PA content credential manifest |
| POST | /v1/admin/aggregate | Trigger manual aggregation (owner only) |
| GET | /v1/proofs | List aggregation windows for tenant |
| GET | /v1/proofs/{window_id} | Get Merkle branch proof + RFC 3161 token |
| GET | /v1/metachain | Get meta-chain of Merkle roots (public) |
| POST | /v1/verify-inclusion | Verify Merkle inclusion proof (public) |
| GET | /v1/health | Health check |
Evidence Properties
All evidence events are:
- Append-only — No updates, no deletes
- Cryptographically chained — SHA-256 over pipe-delimited pre-image:
event_id|call_id|sequence|event_type|timestamp|canonicalJSON(data)|previous_hash - Server-timestamped —
time.Now().UTC(), never from client - Write-protected — Concurrent writes on the same
call_idare serialised - Config-bound — Each event includes
config_seal_hash(SHA-256 of RFC 8785 canonical JSON of tenant config)
The call state cache is derived from the evidence chain, not independent. On cache miss: replay events from the store, reconstruct state, refresh cache.
Security Model
- Tenant isolation: Every request is authenticated via API key → tenant identity extracted from auth context. No tenant_id in request bodies.
- Call ownership: call_id verified to belong to authenticated tenant before processing.
- Evidence integrity: Chain columns are append-only; any tampering is detectable by recalculation against stored commitments.
- Secret management: LLM and ASR API keys stored securely; keys redacted to
has_key: truein GET responses. - No PII in logs: Transcripts, phone numbers, and audio content never logged. Only hashes and metadata in structured logs.
