Voice Agent Simulation Call ID Mapping Template

Sumanyu Sharma
Sumanyu Sharma
Founder & CEO
, Voice AI QA Pioneer

Hamming has 10M+ mins protected across voice-agent QA workflows.

September 25, 2026•Updated September 25, 2026•15 min read
Voice Agent Simulation Call ID Mapping Template

Voice agent simulation call ID mapping sounds like a one-column join. It rarely stays that simple.

A simulator creates a test-run ID. The voice platform creates a call ID. The telephony provider may create separate call-leg and conversation IDs. Your tracing stack creates a trace ID, while your application already has an internal call record. If those identifiers are joined after the call ends, the first transfer or retried webhook can attach evidence to the wrong record.

Voice agent simulation call ID mapping is the process of assigning one internal interaction ID before a test starts, then storing simulator, provider, call-leg, conversation, trace, recording, and evaluation IDs as aliases beneath it.

This template is unnecessary if you run a handful of manual tests and inspect each result immediately. It is for teams running automated simulations, ingesting provider callbacks, replaying failed calls, or joining evaluations to internal QA and CI records.

TL;DR: Create the internal ID first. Pass it to the simulator or provider when possible. Store every external ID as an append-only alias, deduplicate callbacks by provider plus event ID, and model transfers as separate legs under one root interaction. Never use phone number plus timestamp as an automatic primary join.

Methodology: This template proposes a correlation and recovery contract using the public Vonage, Telnyx, Twilio, and W3C documentation cited below, checked on September 25, 2026. The record, pseudocode, and CI scenarios are illustrative engineering recommendations.

Last Updated: September 2026

Related Guides:

What Is Voice Agent Simulation Call ID Mapping?

The mapping is a durable identity record for one simulated user interaction. It connects the test definition, the simulator run, every provider call leg, lifecycle callbacks, traces, recordings, transcripts, tool evidence, and final evaluation.

The common mistake is what we call the provider-key trap: choosing the first external call ID you see and treating it as the primary key for the entire interaction. That works on a happy-path call. It breaks when a transfer creates another leg, a provider retries a callback, or the simulator and monitoring system ingest the same call at different times.

Include multi-leg calls and duplicate callbacks in the first mapping tests. Those cases can expose transcripts, recordings, and evaluations that disagree about which call they describe.

Official provider contracts show why one key is not enough. Vonage Voice webhooks expose a call uuid, a conversation_uuid, custom data in some initiation paths, and lifecycle events. Telnyx Voice webhooks distinguish call_leg_id, call_session_id, webhook event id, and delivery attempt. Twilio's Call resource gives each call a CallSid and reports lifecycle states such as queued, ringing, in progress, completed, busy, no-answer, and failed.

Keep those IDs. Just do not promote any one of them to the root key for your internal record.

Which ID Should Be Canonical?

Create the root interactionId before the first simulation request leaves your system. Give each provider leg its own callLegId. Everything else is an alias or evidence pointer.

IdentifierOwnerScopeUse It ForDo Not Use It For
interactionIdYour applicationWhole simulated interactionCanonical join across tests, legs, evidence, and evaluationProvider API calls
callLegIdYour applicationOne inbound, outbound, or transfer legLeg ordering and per-leg stateWhole transfer chain
simulationRunIdSimulator or test runnerOne scenario executionLink to fixture, prompt version, and expected resultProvider webhook deduplication
providerCallIdVoice or telephony providerUsually one provider call or legFetch provider events, recordings, and statusCross-provider identity
providerConversationIdProviderRelated legs or conversation sessionGroup provider-native legs when documentedUniversal interaction key
traceIdTracing systemOne distributed traceJoin service spans and latency evidenceDurable business identity
webhookEventIdProviderOne callback eventIdempotency and auditCall identity
evaluationIdQA systemOne scoring resultVersioned evaluation evidenceRuntime event correlation

Canonical-ID rule: one internal interaction ID owns the simulated call story. Provider IDs, trace IDs, and evaluation IDs remain typed aliases with narrower scopes.

This separation matters for privacy too. The W3C Trace Context specification standardizes trace propagation and warns against putting personally identifiable or sensitive information in trace context. Use opaque identifiers. Do not encode a phone number, customer email, account number, or patient identifier into interactionId or traceparent.

What Should the Correlation Record Contain?

Start with one compact record. Keep large evidence objects elsewhere and store pointers here.

{  "schemaVersion": "voice-call-correlation.v1",  "interactionId": "int_01J4Q9Y8Q3H6N5X2T7M1P0R4SC",  "environment": "staging",  "createdAt": "2026-08-06T15:04:12.431Z",  "simulation": {    "runId": "sim_28417",    "scenarioId": "refund-transfer-07",    "fixtureVersion": "git:7e3a91c",    "agentVersion": "billing-agent-v42"  },  "legs": [    {      "callLegId": "leg_01",      "sequence": 1,      "direction": "outbound",      "provider": "provider-a",      "providerCallId": "call_a91f",      "providerConversationId": "conversation_77",      "startedAt": "2026-08-06T15:04:14.102Z",      "endedAt": "2026-08-06T15:04:41.887Z",      "terminalState": "transferred",      "evidence": {        "traceIds": ["4bf92f3577b34da6a3ce929d0e0e4736"],        "recordingIds": ["rec_51aa"],        "transcriptIds": ["tr_440"],        "evaluationIds": ["eval_9031"]      }    },    {      "callLegId": "leg_02",      "sequence": 2,      "parentCallLegId": "leg_01",      "direction": "outbound",      "provider": "provider-a",      "providerCallId": "call_b05c",      "providerConversationId": "conversation_77",      "startedAt": "2026-08-06T15:04:39.621Z",      "endedAt": "2026-08-06T15:05:01.204Z",      "terminalState": "completed",      "evidence": {        "traceIds": ["4bf92f3577b34da6a3ce929d0e0e4736"],        "recordingIds": ["rec_51ab"],        "transcriptIds": ["tr_441"],        "evaluationIds": ["eval_9032"]      }    }  ],  "correlation": {    "method": "metadata_echo",    "confidence": "deterministic",    "verifiedAt": "2026-08-06T15:05:03.118Z"  }}

The distinction between interactionId and callLegId is the part that matters most. A transfer should add a leg, not overwrite the first provider call ID. The root interaction remains stable while the leg sequence tells you what happened. Evidence is scoped to each leg; a shared trace can appear under both. If a recording or evaluation spans several legs, store the contributing leg IDs explicitly rather than guessing from timestamps. The overlapping leg times here illustrate a warm transfer.

Required invariants

InvariantPass ConditionFailure SignalAction
Root created firstinteractionId exists before the provider requestFirst callback creates an orphan recordReject or quarantine the callback
Alias uniqueness(provider, providerCallId) maps to one legSame alias points to two interactionsBlock automatic merge
Append-only aliasesNew leg adds an alias without replacing historyTransfer erases the original call IDRestore leg history from events
Event idempotencyOne durable decision and pending-work record per scoped event keyRetry creates duplicate state changes or jobsAtomically commit the receipt, decision, and outbox; recover pending work
Terminal monotonicityA leg does not move from terminal back to activeLate callback reopens a completed legStore as late evidence, do not mutate state
Evidence provenanceTranscript, recording, trace, and score identify their source legEvidence has only a timestampRequire review before attaching
Opaque identifiersIDs contain no customer dataPhone, email, or account number appears in an IDRegenerate and redact

How Should Webhooks Attach to the Right Simulated Call?

Use the strongest available correlation method in a fixed order.

PriorityMethodConfidenceUse When
1Echoed interactionId in supported metadataDeterministicProvider or simulator returns your opaque metadata in callbacks
2Pre-persisted provider call IDDeterministicThe create-call response returns a provider ID before webhooks arrive
3Signed token containing an opaque interaction referenceDeterministic after verificationIntegration supports a state token but not arbitrary metadata
4Provider conversation/session ID plus known leg mapDeterministic within documented provider scopeTransfers share a documented session identifier
5Phone endpoints plus a narrow time windowProbabilisticLegacy integration exposes no stable metadata or early call ID

Time-window matching belongs last. Two automated tests can dial the same number within the same second. If your fallback query returns two candidates, do not pick the nearest one and move on. Mark the callback needs_review and keep it out of automated scoring.

Use a provider adapter so provider-specific fields stop leaking into the core model. Preserve provider call, conversation, event, delivery-attempt, and occurrence-time fields separately. webhookEventId below means a stable event identity, not a new ID generated for every delivery. If a provider has no native event ID, derive an event key from its documented identity fields and test that retries preserve it. Do not use call ID alone or include the delivery attempt in that key.

The following is illustrative transactional pseudocode, not a runnable SDK example. Authenticate and validate the callback first. Derive tenant and provider-account scope from trusted receiver configuration, then apply that scope to every uniqueness constraint and alias lookup.

eventKey = (trustedScope, provider, webhookEventId)BEGIN TRANSACTION  receipt = INSERT immutable_event(eventKey, verifiedEvent)            ON UNIQUE CONFLICT DO NOTHING  if receipt was not inserted:    decision = READ committed_decision(eventKey)    COMMIT    return DUPLICATE with decision  match = resolve_one_known_leg(trustedScope, verifiedEvent)  # Check echoed root ID and provider aliases against existing records.  # A conversation ID alone is insufficient when it identifies several legs.  # Missing or conflicting candidates must not select an arbitrary winner.  if match is missing or ambiguous:    decision = QUARANTINED with reason    SAVE decision for receipt  else:    decision = ATTACHED to match.interactionId and match.callLegId    SAVE decision for receipt    INSERT outbox(eventKey, "process_correlated_event", decision, PENDING)           ON UNIQUE CONFLICT DO NOTHINGCOMMITreturn decision

The insert is the atomic claim: concurrent deliveries contend on the same unique key instead of checking and claiming separately. A conflicting insert must wait for the winning transaction or retry on a serialization conflict before reading its committed decision. A crash before commit rolls back the receipt, decision, and outbox together, so a provider retry can claim the event again. Return success only after commit; a transaction failure must not produce a success acknowledgement.

After commit, a worker scans pending outbox records and recovers expired work leases. A crash before publication cannot lose the job, even when the next webhook is returned as DUPLICATE. That result means the event is durably accepted, not that its business work has finished. An outbox can deliver more than once after a worker crash, so evaluation, CRM, and cleanup consumers also need stable operation keys and idempotent writes or destination-supported idempotency. The receipt alone does not guarantee exactly-once external effects.

Quarantine is a durable decision too. Retries reuse the saved receipt rather than adding another quarantine entry. Reconciliation scans quarantined receipts; once a verified mapping exists, it atomically changes QUARANTINED to ATTACHED and inserts the same uniquely keyed outbox work. Keep that transition guarded and preserve the original receipt. It must not depend on the provider sending the event again.

Webhook correlation rule: authenticate the callback, then atomically commit its unique receipt, correlation decision, and any pending work before acknowledging it. Reject unauthenticated requests; retain authenticated but ambiguous events for reconciliation without mutating a call record.

For mutating tool workflows, apply the same rule in your voice agent sandbox tests: the call correlation key and the tool idempotency key should be related in the audit record, but they should not be the same field.

How Do Transfers, Retries, and Duplicate Callbacks Change the Model?

One simulated interaction can contain more than one call leg and more than one delivery of the same event.

Transfer walkthrough

SequenceEventRoot InteractionCall LegProvider AliasDecision
1Simulation requestedint_01J4...nonenonePersist root before dialing
2Provider accepts callint_01J4...leg_01call_a91fAdd first alias
3Agent starts transferint_01J4...leg_01conversation_77Keep first leg open until terminal evidence
4Transfer leg startsint_01J4...leg_02call_b05cAdd child leg under same root
5Completion callback arrivesint_01J4...leg_02event evt_900Mark second leg complete
6Same callback retriesint_01J4...leg_02event evt_900, attempt 2Record delivery attempt, do not replay work

Telnyx's webhook contract is a useful concrete reference: it exposes a unique webhook event ID, a call-leg ID, a shared call-session ID for related legs, and a delivery attempt number. Other providers use different names. The model stays the same.

Late and out-of-order events

Webhooks do not always arrive in lifecycle order. Store occurredAt from the provider separately from receivedAt in your system. Apply state transitions using the event's meaning and monotonic rules, not arrival order alone.

If a call.completed event arrives before a delayed call.answered event, keep both as evidence. Do not reopen the leg. This is where OpenTelemetry traces help: traces explain service timing, while the correlation record preserves business identity and provider event order.

What Should CI Prove?

Put the mapping contract next to your voice agent tests as code. The test is not complete when the agent says the right words. It is complete when every artifact resolves to the intended interaction and no duplicate delivery replays a side effect.

Test CaseSetupAssertionBlocking Failure
Metadata echoStart one simulation with a known interactionIdCallback resolves without timestamp matchingMissing or changed ID
Concurrent duplicate webhookDeliver the same event ID 3 times concurrentlyOne receipt, one decision, and one outbox recordDuplicate score, CRM write, or cleanup
Crash before commitStop the receiver after inserting the receipt but before commit, then retryTransaction rolls back and retry creates the complete receipt, decision, and outboxOrphan claim suppresses all future work
Crash after commitStop before publishing the outbox, then resend the webhookDuplicate response and eventual processing of the existing pending outboxAccepted event is never processed
Worker retryStop after a destination write but before marking outbox work completeSame operation key is retried without repeating the business effectDuplicate external write
Quarantine recoveryRetry an unmatched event, then add a verified mappingOne quarantine receipt transitions once and schedules recoverable workRepeated quarantine entries or lost event
Out-of-order lifecycleSend completed before answeredLeg remains terminal and both events remain auditableTerminal state reopens
Transfer chainCreate 2 provider legs under one sessionTwo legs share one root and retain separate aliasesFirst alias overwritten
Concurrent same-number testsStart 7 calls to the same endpointEvery callback maps by stable IDTime window selects the wrong run
Missing metadataRemove echoed internal IDEvent is quarantined or matched by pre-persisted provider IDSilent probabilistic merge
Trace samplingOmit a trace from one legBusiness correlation remains intactMissing trace creates a new interaction
ReplayRe-ingest a completed call packetExisting interaction is reused idempotentlyDuplicate interaction created

The 7 concurrent-call case is intentionally small. It is enough to expose a join that depends on phone number and timestamp without turning a correctness test into a load test.

When a failed production or simulation call becomes a regression test, use the call evidence export runbook to carry the same interaction, leg, transcript, recording, trace, and evaluation references into the fixture. That makes replay evidence comparable instead of merely similar.

Where Does This Template Stop?

This template does not define your storage engine, retention policy, or full event taxonomy. The call logging taxonomy guide owns those decisions. It also does not prove caller identity; use the caller identity testing checklist before trusting account or customer fields.

Three limitations matter:

Some providers cannot echo your metadata. Persist the provider call ID as early as the API allows. If no stable identifier exists until a callback, quarantine ambiguous matches.

Transfers do not share one universal identifier. A provider session or conversation ID is only as broad as that provider documents. Keep your internal root interaction above it.

Correlation does not prove correctness. A perfectly mapped call can still have a wrong transcript, tool result, or evaluation. Pair identity checks with workflow assertions and content-specific QA.

An internal call ID and provider call ID are a useful starting pair. The durable version adds leg scope, event identity, evidence provenance, and an explicit confidence level so ambiguity cannot masquerade as certainty.

Implementation Checklist

  • Generate an opaque interactionId before the simulation starts.
  • Persist the simulation run, scenario, fixture, agent version, and environment.
  • Pass the internal ID through supported metadata or a signed opaque token.
  • Store provider call, leg, conversation, trace, recording, transcript, and evaluation IDs as typed aliases.
  • Enforce uniqueness on provider plus provider call ID.
  • Atomically store the scoped event receipt, correlation decision, and pending outbox work.
  • Recover pending outbox work and quarantined receipts without requiring a provider resend.
  • Give downstream operations stable idempotency keys so worker retries do not repeat business effects.
  • Store provider event time separately from local receive time.
  • Model transfers as child legs under one root interaction.
  • Keep terminal state monotonic when late callbacks arrive.
  • Quarantine ambiguous timestamp matches instead of guessing.
  • Keep PII and sensitive data out of identifiers and trace context.
  • Test concurrent duplicates, crashes before and after commit, worker retries, quarantine recovery, out-of-order events, transfers, concurrent calls, missing metadata, and replay in CI.

The useful outcome is not a prettier ID table. It is a failed simulation that opens to one complete story: fixture, prompt version, provider legs, transcript, recording, trace, tool evidence, and evaluation.

Frequently Asked Questions

Voice agent simulation call ID mapping connects one internal interaction ID to the simulator run, provider call legs, webhooks, traces, recordings, transcripts, and evaluation results. Hamming's template separates the root interaction from each call leg so a transfer does not overwrite earlier evidence.

Hamming's correlation template uses an opaque interaction ID generated by your application before the simulation starts. Keep the simulator run ID, provider call ID, conversation ID, trace ID, webhook event ID, and evaluation ID as typed aliases with narrower scopes.

A provider call ID is a strong alias but a weak universal primary key because transfers, provider changes, and multi-leg calls can create more than 1 external ID. Hamming's template keeps provider plus provider call ID unique while placing every leg under one internal interaction.

Verify the signature, then atomically commit a unique event receipt, its correlation decision, and pending outbox work within trusted tenant and provider-account scope. Concurrent retries reuse the committed decision. Recover pending work after crashes, and give downstream operations stable idempotency keys. Quarantined receipts need a reconciliation path that can attach them and schedule work without waiting for another provider delivery.

Hamming's template creates a new internal call-leg record for every provider leg and attaches each one to the same root interaction ID. Preserve parent leg, sequence, provider call ID, session or conversation ID, timestamps, and terminal state instead of replacing the first leg.

Hamming's template uses a trace ID to join service spans, not as the durable business identity for the simulated interaction. Trace sampling and service boundaries can leave gaps, while the internal interaction ID must remain stable across every leg and evidence system.

Hamming's template persists the provider call ID from the create response as soon as it is available and maps callbacks through that alias. If the provider exposes neither metadata nor a stable early ID, quarantine phone-number and timestamp matches for review rather than treating a probabilistic match as fact.

Sumanyu Sharma

Sumanyu Sharma

Founder & CEO

Previously Head of Data at Citizen, where he helped quadruple the user base. As Senior Staff Data Scientist at Tesla, grew AI-powered sales program to 100s of millions in revenue per year.

Researched AI-powered medical image search at the University of Waterloo, where he graduated with Engineering honors on dean's list.

“At Hamming, we're taking all of our learnings from Tesla and Citizen to build the future of trustworthy, safe and reliable voice AI agents.”