Official Sinch API skills for AI coding agents — SMS, Voice, Verification, Numbers, Mailgun email, and more.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Minimum supported deployment target: iOS 12.0.
You can include the Sinch SDK in several ways. Pick the one that fits your setup.
https://github.com/sinch/sinch-ios-sdk-spmdynamic (or main) for the latest dynamic xcframeworkstatic for the latest static xcframeworkx.y.z for a pinned dynamic releaseFor dynamic/main: in your app target > General > Frameworks, Libraries, and Embedded Content, set SinchRTC to Embed & Sign.
For static: set SinchRTC to Do Not Embed.
platform :ios, '12.0'
target 'YourApp' do
pod 'SinchRTC', 'x.y.z'
workspace './YourApp.xcworkspace'
endThen run pod install.
Drag SinchRTC.xcframework (Swift) or Sinch.xcframework (Objective-C) into the Frameworks section in Xcode Project Navigator and set it to Embed & Sign.
If you integrate manually, link these system frameworks/libraries: libc++.tbd, libz.tbd, Security.framework, AVFoundation.framework, AudioToolbox.framework, VideoToolbox.framework, CoreMedia.framework, CoreVideo.framework, CoreImage.framework, GLKit.framework, OpenGLES.framework, QuartzCore.framework, Metal.framework, MetalKit.framework, PushKit.framework, SystemConfiguration.framework.
Before writing any code, configure your Xcode project. iOS handles permission prompts automatically at runtime, but you must declare the required entries or your app will crash or be rejected.
Enable Push Notifications in your app target's Signing & Capabilities. This adds aps-environment to your entitlements.
Add the following keys to your Info.plist:
Required background modes (UIBackgroundModes):
audio - App plays audio or streams audio/video using AirPlayvoip - App provides Voice over IP servicesPrivacy - Microphone Usage Description (NSMicrophoneUsageDescription):
A string explaining why the app needs microphone access. Example: "Application wants to use your microphone to be able to capture your voice in a call."
Privacy - Camera Usage Description (NSCameraUsageDescription) - only if you enable video:
A string explaining why the app needs camera access. Example: "Application wants to use your camera to be able to make a video call."
Note: iOS will present a system permission dialog to the user the first time the microphone or camera is activated. You do not request these permissions manually through Sinch, but you must declare the usage descriptions or iOS will terminate your app.
Every integration must follow these steps in order. Do not skip or reorder them.
audio, voip), microphone/camera usage descriptions, and enable Push Notifications capability.SinchClientDelegate to handle clientDidStart, clientDidFail, and clientRequiresRegistrationCredentials.clientRequiresRegistrationCredentials to supply a signed JWT. Decide whether to sign locally (prototyping only) or fetch from a backend.sinchClient.enableManagedPushNotifications() before start(). Create SinchManagedPush early in your app lifecycle (typically in AppDelegate). Required for app-to-app calls, even as the caller.CXProvider/ConversationManager.sinchClient.start(). Wait for clientDidStart before proceeding.sinchClient.callClient to detect incoming calls via client(_:didReceiveIncomingCall:).SinchCallDelegate to handle callDidProgress, callDidRing, callDidAnswer, callDidEstablish, and callDidEnd.When helping a user integrate, walk through these steps one at a time. Confirm each step is in place before moving to the next.
The SinchClient is the Sinch SDK entry point. It manages the client lifecycle and capabilities, and exposes feature APIs such as callClient (calling), audioController (audio), and videoController (video).
import SinchRTC
// Keep a strong reference to sinchClient
private(set) var sinchClient: SinchClient?
do {
self.sinchClient = try SinchRTC.client(withApplicationKey: "<application key>",
environmentHost: "ocra.api.sinch.com",
userId: "<user id>")
} catch {
// Handle error
}ocra.api.sinch.com is the name of the Sinch API that SDK clients target.Before starting, assign a delegate conforming to SinchClientDelegate:
sinchClient.delegate = self
sinchClient.start()Delegate methods:
// SinchClientDelegate
func clientDidStart(_ client: SinchRTC.SinchClient) {
// Sinch client started successfully
}
func clientDidFail(_ client: SinchRTC.SinchClient, error: Error) {
// Sinch client start failed
}
func clientRequiresRegistrationCredentials(_ client: SinchRTC.SinchClient,
withCallback callback: SinchRTC.SinchClientRegistration) {
// Registration required, get JWT token for user and register
}When SinchClient starts with a given user ID, you must provide an authorization token (JWT) to register with Sinch.
Implement clientRequiresRegistrationCredentials(_:withCallback:) and supply a JWT signed with a key derived from the Application Secret.
Read https://developers.sinch.com/docs/in-app-calling/ios/auth.md
In general it is not suggested to embed the Application Secret in a production application.
Ask the user whether it can be embedded.
If it can be embedded:
The Swift reference application on GitHub includes a SinchJWT.swift helper that demonstrates how to create and sign the JWT locally. I can be also found in assets/SinchJWT.swift Read and adapt it.
func clientRequiresRegistrationCredentials(_ client: SinchRTC.SinchClient,
withCallback callback: SinchRTC.SinchClientRegistration) {
do {
// WARNING: Development example only. In production, fetch a JWT from your backend.
let jwt = try SinchJWT.sinchJWTForUserRegistration(withApplicationKey: "<application key>",
applicationSecret: "<application secret>",
userId: client.userId)
callback.register(withJWT: jwt)
} catch {
callback.registerDidFail(error: error)
}
}If it can not be embedded: Implement the required functionality on your backend and fetch a signed registration token when required.
func clientRequiresRegistrationCredentials(_ client: SinchRTC.SinchClient,
withCallback callback: SinchRTC.SinchClientRegistration) {
authServer.fetchRegistrationToken(for: client.userId) { result in
switch result {
case .success(let token):
callback.register(withJWT: token)
case .failure(let error):
callback.registerDidFail(error: error)
}
}
}Create and start a single SinchClient and keep it alive for the lifetime of your application. Retain a strong reference. The client uses little memory once started.
To temporarily stop incoming calls without disposing the client:
sinchClient.unregisterPushNotificationDeviceToken()To completely stop and dispose of the client:
sinchClient.terminateGracefully()
sinchClient = nilTo receive incoming calls via Apple VoIP push notifications, enable managed push on the client and set up SinchManagedPush.
Enable managed push on the client:
sinchClient.enableManagedPushNotifications()Create SinchManagedPush early in the app lifecycle (typically in AppDelegate.application(_:didFinishLaunchingWithOptions:)):
class AppDelegate: UIResponder, UIApplicationDelegate {
private var sinchPush: SinchManagedPush?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
sinchPush = SinchRTC.managedPush(forAPSEnvironment: .development)
sinchPush?.delegate = self
sinchPush?.setDesiredPushType(SinchManagedPush.TypeVoIP)
return true
}
}The APS environment you pass (.development or .production) must match your app's provisioning profile. A Debug build signed with a Development profile uses .development; a Release build signed with a Distribution profile uses .production.
Upload your APNs Signing Key:
.p8 key file to your Sinch Developer Account.SinchManagedPush is lightweight and can live independently of a SinchClient. It acquires the push device token via PushKit and automatically registers it with any SinchClient created later.
Note:
For use cases requiring only outgoing App-to-Phone, App-to-SIP, or Conference calls, calling sinchClient.enableManagedPushNotifications() is not required. You can place these calls directly once the Sinch client is started.
Apple requires that every incoming VoIP push notification is reported to CallKit (or LiveCommunicationKit on iOS 17.4+) before your push delegate returns. Failing to do so will cause iOS to terminate your app, and repeated failures may stop VoIP push delivery entirely.
Handling incoming pushes with CallKit:
func managedPush(_ managedPush: SinchRTC.SinchManagedPush,
didReceiveIncomingPushWithPayload payload: [AnyHashable: Any],
for type: String) {
let notification = queryPushNotificationPayload(payload)
guard notification.isCall, notification.isValid else { return }
let callNotification = notification.callResult
let uuid = // Get or create a UUID mapped to callNotification.callId
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: callNotification.remoteUserId)
self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
// Handle error and hangup call if needed
}
}If you do not relay the payload to a SinchClient, call SinchManagedPush.didCompleteProcessingPushPayload(_:) so PushKit's completion handler is invoked.
Reporting outgoing calls to CallKit: While not strictly required by Apple for outgoing calls, reporting them to CallKit is necessary for audio to work when the caller app is in the background or the device is locked.
func call(userId: String, uuid: UUID, with completion: @escaping (Error?) -> Void) {
let handle = CXHandle(type: .generic, value: userId)
let startCallAction = CXStartCallAction(call: uuid, handle: handle)
let transaction = CXTransaction(action: startCallAction)
self.callController.request(transaction, completion: completion)
}Implement CXProviderDelegate to start the Sinch call when CallKit requests it:
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
let recipientIdentifier = action.handle.value
let callResult = callClient.callUser(withId: recipientIdentifier)
switch callResult {
case .success(let call):
call.delegate = self
action.fulfill()
case .failure(let error):
action.fail()
}
}Audio session with CallKit: When using CallKit, the system manages audio session activation. Forward these events to the SDK:
// In your CXProviderDelegate
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
sinchClient?.callClient.didActivate(audioSession: audioSession)
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
sinchClient?.callClient.didDeactivate(audioSession: audioSession)
}See the full reference app for a complete CallKit implementation: Swift reference application on GitHub.
LiveCommunicationKit is an alternative to CallKit. The same rules apply: report every incoming VoIP push before your delegate returns.
func managedPush(_ managedPush: SinchRTC.SinchManagedPush,
didReceiveIncomingPushWithPayload payload: [AnyHashable: Any],
for type: String) {
let notification = queryPushNotificationPayload(payload)
guard notification.isCall, notification.isValid else { return }
let callNotification = notification.callResult
let uuid = // Get or create a UUID mapped to callNotification.callId
let localHandle = Handle(type: .generic, value: "localUserId")
let remoteHandle = Handle(type: .generic, value: callNotification.remoteUserId)
let update = Conversation.Update(localMember: localHandle,
activeRemoteMembers: [remoteHandle],
capabilities: nil)
Task {
do {
try await self.conversationManager.reportNewIncomingConversation(uuid: uuid, update: update)
} catch {
// Handle error and hangup call if needed
}
}
}The Sinch SDK supports four types of calls: app-to-app (audio or video), app-to-phone, app-to-sip, and conference calls. The SinchCallClient is the entry point for calling functionality.
Calls are placed through SinchCallClient and events are received via SinchCallClientDelegate. The call client is owned by SinchClient and accessed using sinchClient.callClient.
guard let callClient = sinchClient?.callClient else { return }
let callResult = callClient.callUser(withId: "<remote user id>")
switch callResult {
case .success(let call):
call.delegate = self
case .failure(let error):
// Handle error
}The returned call object includes participant details, start time, state, and possible errors.
Assuming the callee's device is available, callDidProgress(_:) is invoked. If you play a progress tone, start it here.
When the callee's phone is ringing, callDidRing(_:) fires.
When the callee answers, callDidAnswer(_:) fires. Stop any progress tone.
When full audio connectivity is established, callDidEstablish(_:) is called. Users can now talk.
Typically, connectivity is already established when the call is answered, so callDidEstablish may follow immediately after callDidAnswer. On poor networks, it can take longer; consider showing a "connecting" indicator.
Important:
For App-to-App calls, you must enable managed push by calling sinchClient.enableManagedPushNotifications(), even if you are the caller. See the full setup in Push notifications.
An app-to-phone call is a call made to a phone on the regular telephone network. Use callPhoneNumber(_:) with an E.164-formatted number prefixed with +.
guard let callClient = sinchClient?.callClient else { return }
let callResult = callClient.callPhoneNumber("+14155550101")
switch callResult {
case .success(let call):
call.delegate = self
case .failure(let error):
// Handle error
}Mandatory step! You must provide a CLI (Calling Line Identifier) or your call will fail. You need a number from Sinch so you can provide a valid CLI to the handset you are calling. Specify your CLI when creating the SinchClient:
do {
self.sinchClient = try SinchRTC.client(withApplicationKey: "<application key>",
environmentHost: "ocra.api.sinch.com",
userId: "<user id>",
cli: "<Your Purchased Sinch Number>")
} catch {
// Handle error
}Note: When your account is in trial mode, you can only call your verified numbers. If you want to call any number, you need to upgrade your account!
An app-to-sip call is made to a SIP server. Use callSIP(_:) or callSIP(_:headers:). The SIP identity should be in the form user@server. When passing custom headers, prefix them with x-.
guard let callClient = sinchClient?.callClient else { return }
let callResult = callClient.callSIP("<SIP identity>")
switch callResult {
case .success(let call):
call.delegate = self
case .failure(let error):
// Handle error
}A conference call connects a user to a room where multiple users can participate. The identifier may not be longer than 64 characters.
guard let callClient = sinchClient?.callClient else { return }
let callResult = callClient.callConference(withId: "<conference id>")
switch callResult {
case .success(let call):
call.delegate = self
case .failure(let error):
// Handle error
}Add a SinchCallClientDelegate to SinchCallClient to act on incoming calls. When a call arrives, client(_:didReceiveIncomingCall:) is executed.
With CallKit/LiveCommunicationKit (typical production setup):
Use client(_:didReceiveIncomingCall:) primarily to associate the SinchCall with the system call. Keep a mapping between system UUIDs and callId.
extension SinchClientMediator: SinchCallClientDelegate {
func client(_ client: SinchRTC.SinchCallClient,
didReceiveIncomingCall call: SinchRTC.SinchCall) {
call.delegate = self
// Store/match call.callId with your CallKit UUID mapping
}
}Without CallKit (e.g. testing or custom UI):
extension SinchClientMediator: SinchCallClientDelegate {
func client(_ client: SinchRTC.SinchCallClient,
didReceiveIncomingCall call: SinchRTC.SinchCall) {
call.delegate = self
// Present UI for call
}
}The Sinch SDK supports receiving incoming calls that originate from the PSTN (regular phone network) or from SIP endpoints.
When a call arrives at a Sinch voice number or via SIP origination, the Sinch platform triggers an Incoming Call Event (ICE) callback to your backend.
The platform can then route this call to an in-app user by responding with the connectMxp SVAML action.
When a PSTN or SIP call comes in, respond to the ICE callback with a connectMxp action to route the call to an app user:
{
"action": {
"name": "connectMxp",
"destination": {
"type": "username",
"endpoint": "target-user-id"
}
}
}To answer the call, use call.answer():
call.answer()If the call shouldn't be answered, use call.hangup() to decline. The caller is notified that the incoming call was denied.
call.hangup()When the user wants to disconnect an ongoing call, use call.hangup(). Either party can disconnect.
call.hangup()When either party disconnects, callDidEnd(_:) is called on the delegate:
func callDidEnd(_ call: SinchCall) {
// Update UI, e.g., dismiss the call screen
}A call can be disconnected before it has been completely established.
Video calls follow the same flow as audio calls. Use callClient.callUserVideo(withId:) to start a video call. You receive the same callbacks: callDidProgress, callDidAnswer, callDidEstablish.
Assuming a view controller with two UIView outlets for remote and local video:
Local video preview:
override func viewDidLoad() {
super.viewDidLoad()
guard let videoController = sinchClient?.videoController else { return }
localVideoView.addSubview(videoController.localView)
}Remote video stream (attach when the remote track arrives):
func callDidAddVideoTrack(_ call: SinchCall) {
guard let videoController = sinchClient?.videoController else { return }
remoteVideoView.addSubview(videoController.remoteView)
}Use call.pauseVideo() to temporarily stop sending local video and call.resumeVideo() to resume. Audio continues unless you mute it separately.
call.pauseVideo()
call.resumeVideo()The call delegate is notified via callDidPauseVideoTrack(_:) and callDidResumeVideoTrack(_:).
guard let videoController = sinchClient?.videoController else { return }
videoController.captureDevicePosition.toggle()Control how rendered video fits a view via UIView.contentMode. Only .scaleAspectFit and .scaleAspectFill are respected.
videoController.remoteView.contentMode = .scaleAspectFillThe SDK provides UIView extension methods for fullscreen transitions:
if view.sinIsFullscreen() {
view.contentMode = .scaleAspectFit
view.sinDisableFullscreen(true)
} else {
view.contentMode = .scaleAspectFill
view.sinEnableFullscreen(true)
}An incoming video call triggers client(_:didReceiveIncomingCall:) like a voice call. Check call.details.isVideoOffered to determine if video is included.
iOS 12.0 minimum deployment target.
User IDs must not be longer than 255 bytes and must only contain URL-safe characters: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghjiklmnopqrstuvwxyz0123456789-_=. Consider base64url-encoding IDs that contain other characters.
The SinchCallDetails class holds metadata about a call including timestamps (startedTime, progressedTime, rungTime, answeredTime, establishedTime, endedTime), end cause (.timeout, .denied, .noAnswer, .error, .hungUp, .canceled, .otherDeviceAnswered, etc.), and error information.
func callDidEnd(_ call: SinchCall) {
if call.details.endCause == .error {
if let error = call.details.error {
print("Call failed: \(error.localizedDescription)")
}
}
}During calls, the SDK manages AVAudioSession. It sets the category to .playAndRecord with mode .voiceChat at the start of a call and restores original settings when the call ends.
If using CallKit/LiveCommunicationKit, forward audio session activation events to the SDK (see CallKit integration section above).
To override audio session category options:
sinchClient?.audioController.setAudioSessionCategoryOptions([.allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker])skills
sinch-10dlc
references
sinch-authentication
sinch-conversation-api
sinch-elastic-sip-trunking
references
sinch-fax-api
sinch-imported-numbers-hosting-orders
references
sinch-in-app-calling
sinch-mailgun
references
sinch-mailgun-inspect
references
sinch-mailgun-optimize
references
sinch-mailgun-validate
sinch-number-lookup-api
sinch-number-order-api
sinch-numbers-api
references
sinch-porting-api
sinch-provisioning-api
sinch-sdks
sinch-verification-api