Compare the Conceptual Models of Open CTI and Salesforce Voice
Open CTI is a thin bridge: your CTI adapter runs in an iframe and calls opencti JavaScript APIs to invoke the Salesforce UI. All business logic lives in your adapter. Salesforce Voice with Partner Telephony inverts this: Salesforce owns the rep lifecycle through Omni-Channel, and your connector plays two distinct roles simultaneously.
Role 1: Event Publisher (Connector to Salesforce)
Your connector calls publishEvent() to push telephony events into Salesforce. When your telephony system fires a “call ringing” event, you translate it into CALL_STARTED; when the call ends, you publish HANGUP. These events drive everything downstream: VoiceCall record creation, AgentWork routing, screen pop, and presence sync.
Role 2: Method Implementation Target (Salesforce to Connector)
Connector API methods are what you implement to be notified when telephony actions are initiated from within Salesforce. When a rep clicks Accept in the softphone panel, Salesforce calls your acceptCall(). When they click End Call, Salesforce calls your endCall(). When a supervisor initiates listen-in from Command Center for Service (formerly Omni Supervisor), Salesforce calls your superviseCall().
Return a promise with the result; don’t publish the event yourself. When Salesforce invokes one of your connector methods such as acceptCall(), endCall(), or mute(), return a promise that resolves with the appropriate result object (CallResult, MuteToggleResult). The framework publishes the corresponding event automatically based on your return value.
Do not call publishEvent() yourself from inside these methods. That causes duplicate events and breaks the softphone state machine. Reserve publishEvent() for truly asynchronous backend-initiated events such as an inbound call arriving from the carrier.
Important
In Open CTI, the adapter pushes everything from the iframe. With Salesforce Voice with Partner Telephony, there’s a bidirectional contract via the SDK. This bidirectionality is the core architectural shift. Your connector must implement both directions correctly or the integration is incomplete.
Data Model Shift: From Task to VoiceCall
The single biggest data model shift for migration is moving from Task records to VoiceCall records.
Task: The Open CTI Model
In Open CTI, your adapter typically:
Pre-creates a Task record at call start (via opencti.saveLog or custom Apex).
Updates that Task at wrap-up with duration, disposition, and notes.
The Task is the canonical “call happened” record. Reports run on Activity and Task.
Limitations
Task is generic. It’s used for emails, meetings, and all activities. Voice fields are bolted on (CallDurationInSeconds, CallType, etc.) but the schema isn’t voice-native.
The adapter is responsible for record creation. If the adapter crashes mid-call or has a bug, you may have no record.
Single record per call. There’s no clean way to model multiparty, consults, or transfers.
No native recording link. You need a custom field or a parallel ContentDocument.
VoiceCall: The Salesforce Voice with Partner Telephony Model
For outbound calls, the Salesforce platform creates a VoiceCall record automatically when CALL_STARTED is published. For inbound and transfer calls, the telephony partner creates a VoiceCall record by calling the Create a Voice Call Record Telephony Integration API. VoiceCall is the de facto record for the call going forward, including voice-native fields, multiparty support, recording linkage, and supervisor view.
VoiceCall Field
Populated By
Replaces in Task Model
CallerId
CALL_STARTED phoneNumber
Custom field on Task
CallDurationInSeconds
Platform (computed at HANGUP)
Same field on Task, but populated automatically
Status
Platform (Inbound/Outbound/Missed/Declined)
Custom field on Task
StartTime / EndTime
Platform
Activity dates
OwnerId
Platform (assigned rep)
Task.OwnerId, but auto-populated from Omni-Channel assignment
RelatedRecordId
Your Omni-Channel flow (Contact/Case routed to)
Task.WhatId
CallDisposition
Connector or rep ACW input
Custom field
VoiceCallRecordingId
Platform (if recording enabled)
None — required custom field in Open CTI
AdditionalFields
Connector (callInfo.additionalFields JSON)
Custom fields
Multiparty linkage (parentVoiceCallId)
Connector at leg creation
None — not modeled in Open CTI
How VoiceCall Enhances the Experience
Recording is first-class. A linked VoiceCallRecording is automatically created and surfaced on the record page. No custom field, no signed URL configuration.
Multiparty is modeled. Each leg gets its own VoiceCall, linked via parentVoiceCallId. Reports across legs are simple SOQL.
Supervisor view runs on VoiceCall records. With Open CTI, you had to build your own supervisor dashboard.
AI features (transcription, conversation summary, sentiment) attach to VoiceCall natively.
Auditability and durability. The platform creates the record, not your code. If your connector crashes, the call is still logged.
New Behavior for VoiceCall as the De Facto Record
Don’t call saveLog. The log already exists as VoiceCall. The question is no longer “how do I log this call?” The question is “what other records do I want to relate to this call?”
Salesforce admins can choose to relate or create any record on top of VoiceCall, not just Tasks. Common customer patterns include:
Case: Customer support orgs typically relate the VoiceCall to a Case (new or existing). The VoiceCall ↔ Case relationship surfaces calls on the case timeline.
Opportunity: Sales orgs relate VoiceCall to an Opportunity for activity tracking and pipeline reports.
Lead: Inbound prospecting relates VoiceCall to a Lead.
Account or Contact: VoiceCall already exposes related Contact/Account via RelatedRecordId.
Custom object: For industry-specific use cases (Service Order, Patient Visit, Loan Application).
Task: Only when the customer specifically wants Task-based reporting (legacy reports, mobile activity feed, etc.). Not the default recommendation.
This is built declaratively as a record-triggered flow on VoiceCall: same pattern, different target object.
After Conversation Work
Salesforce Voice with Partner Telephony has built-in after conversation work (ACW). When the rep clicks End Wrap-Up, WRAP_UP_ENDED is sent to your connector:
1async wrapUpCall(call){2 await myBackend.endAfterCallWork(call.callId);3 // Return a Promise; framework will publish WRAP_UP_ENDED from your return value.4}