Skip to main content

Usage

The flow presents a camera screen where the user reads a passphrase aloud or answers an identity question. Speech is recognized live, the current step is matched locally, and optional video evidence is recorded. After all resolved steps succeed, onSuccess(true) fires and the host application calls upload(...).

The verifier is created with SpeechVerifier.Builder. Configure it, call build() (which can throw), then call start() on the result.

Quick Start — Single Passphrase​

import UIKit
import AmaniSpeechVerifier

final class VerificationViewController: UIViewController {

private var speechVerifier: SpeechVerifier?
private var speechVerifierView: UIView?

func startSpeechVerifier() {
do {
let verifier = try SpeechVerifier.Builder()
.session(serverURL: "https://server.example", token: "PROFILE_TOKEN")
.documentType("XXX_ST_0")
.videoRecording(true)
.timeout(30)
.verificationSteps([
.spokenText([
SpeechVerifierTextConfiguration(
text: "I accept the identity verification",
matchThresholdPercent: 80
)
])
])
.onSuccess { [weak self] result in
guard result == true else { return }
self?.uploadSpeechVerifierEvidence()
}
.onFailure { reason, currentAttempt in
print(
"Speech Verifier failure:",
reason.rawValue,
"attempt:",
currentAttempt
)
}
.build()

// Keep a strong reference until upload completes.
speechVerifier = verifier

guard let speechView = try verifier.start() else {
print("Speech Verifier could not create its capture view.")
return
}

speechVerifierView = speechView
speechView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(speechView)

NSLayoutConstraint.activate([
speechView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
speechView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
speechView.topAnchor.constraint(equalTo: view.topAnchor),
speechView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])

} catch {
print("Speech Verifier error:", error)
}
}

private func uploadSpeechVerifierEvidence() {
speechVerifier?.upload(location: nil) { [weak self] result in
DispatchQueue.main.async {
print("Speech Verifier upload result:", result as Any)
self?.speechVerifierView?.removeFromSuperview()
self?.speechVerifierView = nil
self?.speechVerifier = nil
}
}
}
}

Builder Reference​

Builder methodDescriptionDefault
session(serverURL:token:)Profile session used for identity answers and upload—
documentType(_:)Document typeXXX_ST_0
timeout(_:)Verification time window in seconds30
videoRecording(_:)Enables/disables video evidence recordingtrue
verificationSteps(_:)Ordered passphrase / identity-question stepsOne default passphrase
identityAnswers(...)Manual identity answersAutomatic lookup when omitted
prompts(_:)Custom identity/spoken-text promptsSDK defaults
uiTexts(_:)Runtime UI text overridesSDK defaults
appearance(_:)Colors and exempt wordsSpeechVerifierAppearance()
voiceProvider(_:)Optional voice guidance, see Voice AssistantNo voice
onSuccess(_:)Local verification success callback—
onFailure(_:)Failure reason + attempt count—
build()Returns the SpeechVerifier; can throw—

Build Errors​

build() throws SpeechVerifierBuilderError:

ErrorMeaning
invalidProfileTokenThe serverURL / token passed to session is invalid.
missingSessionIdentity answers must be fetched from the backend but session was not set.

Start Errors​

start() is throws and returns UIView?. If the configured steps are empty or invalid, it returns nil and calls onFailure with unknown. Each call to start() resets the verification and upload attempt counters.

Flow Lifecycle​

Build SpeechVerifier
↓
Retain SpeechVerifier strongly
↓
start()
↓
Present returned UIView
↓
Permissions + preparation
↓
Listening / matching / retry
↓
onSuccess(true)
↓
upload(location:completion:)
↓
Remove view + release SpeechVerifier

When identity answers must be fetched from the backend, the returned view handles its preparation/loading state internally before listening begins.

Passphrase Steps​

Use a spokenText step for passphrases. The second value of each configuration is matchThresholdPercent:

100 → exact normalized match
90 → high similarity required
80 → more tolerant matching

A spokenText step with several texts is a pool: one text is randomly selected for that step.

.verificationSteps([
.spokenText([
SpeechVerifierTextConfiguration(
text: "I approve this verification",
matchThresholdPercent: 95
),
SpeechVerifierTextConfiguration(
text: "I confirm my identity verification",
matchThresholdPercent: 85
)
])
])

Shortcut Setters​

After build(), the returned SpeechVerifier instance exposes the same options as chainable setters (setTimeout(seconds:), setVideoRecording(enabled:), setUITexts(_:), setAppearance(_:), setPrompts(_:), setVoiceProvider(_:), identityAnswers(...), onSuccess, onFailure) plus shortcuts for simple flows:

verifier.setText("I approve this verification", 90)

verifier.setTexts(
["I approve this verification", "I confirm my identity verification"],
90
)

If the threshold is omitted, the default is 100.

Multi-Step Verification​

Use verificationSteps(...) for an ordered flow that mixes passphrases and identity questions:

.verificationSteps([
.spokenText([
SpeechVerifierTextConfiguration(
text: "I approve the identity verification",
matchThresholdPercent: 90
)
]),
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(
type: .documentNumber,
matchThresholdPercent: 100
),
SpeechVerifierIdentityQuestionConfiguration(
type: .motherName,
matchThresholdPercent: 90
)
])
])

The rules are:

  • The array order is the order experienced by the user.
  • A spokenText step randomly selects one text from its array.
  • An identityQuestion step randomly selects one valid question from its array.
  • Repeating a step creates another sequential step.
  • Every configured item can have its own threshold.
  • The flow succeeds only after the final resolved step succeeds.
Step-replacing setters

setText, setTexts, setIdentityQuestion, setIdentityQuestions, and verificationSteps each replace the current configured step list.

This does not create a two-step flow:

verifier
.setText("I accept the identity verification", 80)
.setIdentityQuestions([.motherName, .fatherName], 100)

The second call replaces the first. Use verificationSteps(...) when passphrase and identity-question steps must run together.

Running Every Identity Question​

Passing multiple questions into one identityQuestion step creates a question pool and selects one valid question. To run every question, add each as a separate step:

.verificationSteps([
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(type: .documentNumber, matchThresholdPercent: 100)
]),
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(type: .motherName, matchThresholdPercent: 100)
]),
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(type: .fatherName, matchThresholdPercent: 90)
]),
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(type: .idNumber, matchThresholdPercent: 80)
])
])

Identity Question Types​

Remote/config valueSwift valueExpected answer
ID_NUMBER.idNumberComplete national identity number
MOTHER_NAME.motherNameMother's given name
FATHER_NAME.fatherNameFather's given name
DOCUMENT_NUMBER.documentNumberDocument number; matching uses its digit stream

Instance shortcuts are also available after build():

verifier.setIdentityQuestion(.documentNumber, 100)
verifier.setIdentityQuestions([.motherName, .fatherName], 90)

Providing Identity Answers​

Identity-question steps require expected-answer data.

Manual Answers​

let verifier = try SpeechVerifier.Builder()
.verificationSteps([
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(
type: .idNumber,
matchThresholdPercent: 100
)
])
])
.identityAnswers(
idNumber: "12345678901",
motherName: "AYSE",
fatherName: "MEHMET",
documentNumber: "A03T12456"
)
.build()

All parameters are optional. Expected answers are used for matching and are not displayed as the visible question text. When every answer is provided manually, session is not required.

Automatic Customer/Profile Lookup​

If an identity-question step exists and identityAnswers(...) has not been called, the SDK obtains answers from the customer's document/profile data using the session passed to the builder. build() throws missingSession if no session was set.

If no usable answer can be resolved, the module reports identityAnswerNotFound.

Custom Identity Prompts​

Use prompts(_:) to customize the text shown for identity-question steps and the spoken-text instruction.

let prompts = SpeechVerifierPrompts(
identityPrompts: [
.idNumber: SpeechVerifierStepPrompt(
visibleText: "Say your national ID number",
instructionText: "Answer the question aloud"
),
.motherName: SpeechVerifierStepPrompt(
visibleText: "Say your mother's name",
instructionText: "Answer the question aloud"
)
],
spokenTextInstruction: "Read the displayed text aloud"
)

// on the builder
.prompts(prompts)

When AmaniUI is used, remote values such as speechVerifierIdentityPrompts are mapped into this model.

Custom UI Texts​

.uiTexts(
SpeechVerifierUITexts(
retry: "Try again",
failed: "Didn't match, please try again",
verified: "Verified",
listening: "Listening…",
verifying: "Verifying…",
instruction: "Answer the question shown on screen aloud",
recognizerNotAvailable: "Speech recognition is unavailable on this device"
)
)
PropertyPurpose
retryRetry button title
failedVerification mismatch message
verifiedSuccessful local verification state
listeningActive listening state
verifyingVerification/finalization state
instructionMain instruction label
recognizerNotAvailableSpeech-recognizer unavailable state

A nil or blank value falls back to the SDK default.

Appearance​

.appearance(
SpeechVerifierAppearance(
instructionTextColor: .white,
visibleTextColor: .white,
highlightedTextColor: .systemGreen,
statusTextColor: UIColor.white.withAlphaComponent(0.90),
progressTintColor: .systemGreen,
progressTrackTintColor: UIColor.white.withAlphaComponent(0.25),
overlayBackgroundColor: UIColor.black.withAlphaComponent(0.80),
retryButtonTextColor: .white,
retryButtonBackgroundColor: .systemGreen,
listeningIconColor: .white,
successIconColor: .systemGreen,
failureIconColor: .systemRed,
exemptWords: ["Amani"]
)
)
PropertyDescription
instructionTextColorInstruction label color
visibleTextColorUnmatched passphrase color
highlightedTextColorMatched passphrase-word color
statusTextColorListening/verifying/result text color
progressTintColorProgress completed color
progressTrackTintColorProgress track color
overlayBackgroundColorOverlay/background color
retryButtonTextColorRetry button title color
retryButtonBackgroundColorRetry button background color
listeningIconColorActive microphone icon color
successIconColorSuccess icon color
failureIconColorFailure icon color
exemptWordsVisible words excluded from phrase verification

exemptWords affects phrase matching only. The words remain visible on screen.

Callbacks​

.onSuccess { result in
guard result == true else { return }
// All resolved local verification steps succeeded.
}
.onFailure { reason, currentAttempt in
// Handle/report the failure.
// Keep the module view visible for supported retry cases.
}

onSuccess(true) does not upload evidence automatically.

For supported retry conditions, Speech Verifier displays its own Retry button. The host application should not recreate the module for a normal retry.

Retry Behavior​

For a mismatch or timeout:

current attempt stops
↓
recognition + recording stop
↓
failure state is shown
↓
Retry button is shown
↓
user taps Retry
↓
flow returns to the first configured step
↓
fresh attempt / recording begins

Failure Reasons​

ReasonMeaning
verificationFailedSpoken value did not match the current step; Retry is shown.
timeoutConfigured verification time elapsed; Retry is shown.
speechRecognitionUnavailableApple Speech is unavailable or the recognition pipeline cannot continue.
cameraPermissionDeniedCamera authorization was denied.
microphonePermissionDeniedMicrophone authorization was denied.
speechRecognitionPermissionDeniedSpeech-recognition authorization was denied.
cameraUnavailableA suitable camera could not be found.
microphoneUnavailableA suitable audio input could not be found.
cameraInputFailedCamera input could not be attached to the capture session.
microphoneInputFailedMicrophone input could not be attached to the capture session.
audioSessionFailedThe application audio session could not be configured.
identityAnswerNotFoundNo usable answer was available for the configured identity question.
identityAnswerMismatchSpoken identity answer did not match the expected value.
unknownUnclassified preparation/runtime failure.

Uploading the Evidence​

Call upload(location:completion:) after onSuccess:

speechVerifier?.upload(
location: currentLocation // optional CLLocation
) { result in
DispatchQueue.main.async {
if result == true {
print("Speech Verifier evidence uploaded.")
} else {
print("Speech Verifier evidence upload failed.")
}
}
}

The module tracks its internally generated video/evidence data. Do not pass a video file URL. The upload uses the session passed to the builder, so the token must still be valid when upload is called.

The completion handler is always called on the main thread. location is optional.

upload returns false when:

  • no session was set on the builder;
  • the recorded video is missing (for example when videoRecording(false) was used, or the evidence was already uploaded);
  • the backend rejected the upload.

Retrying a failed upload​

If the upload fails on the backend side, the recorded video is kept, so you can call upload again on the same SpeechVerifier instance. The recording is deleted only after the server accepts it, so a second upload after a successful one returns false.

speechVerifier?.upload(location: nil) { [weak self] result in
if result == true {
self?.finishSpeechVerifier()
} else {
// Show an error and let the user retry; keep the verifier instance.
}
}
Retain the verifier until upload completes

upload(...) is an instance method. Do not set the SpeechVerifier reference to nil in onSuccess before upload finishes.

Stopping the Flow​

Call stop() to end recognition and recording, for example when the screen is dismissed. It is safe to call more than once, and it is also called automatically when the SpeechVerifier instance is released:

speechVerifier?.stop()
speechVerifierView?.removeFromSuperview()
speechVerifier = nil

Complete Example​

import UIKit
import AmaniSpeechVerifier
import CoreLocation

final class SpeechVerifierViewController: UIViewController {

var serverURL = "https://server.example"
var token = "PROFILE_TOKEN"

private var speechVerifier: SpeechVerifier?
private var speechVerifierView: UIView?
private var currentLocation: CLLocation?

func startSpeechVerifier() {
do {
let verifier = try SpeechVerifier.Builder()
.session(serverURL: serverURL, token: token)
.documentType("XXX_ST_0")
.videoRecording(true)
.timeout(30)
.verificationSteps([
.spokenText([
SpeechVerifierTextConfiguration(
text: "I accept the Amani identity verification",
matchThresholdPercent: 90
)
]),
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(
type: .documentNumber,
matchThresholdPercent: 100
)
]),
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(
type: .motherName,
matchThresholdPercent: 90
)
])
])
.identityAnswers(
motherName: "AYSE",
documentNumber: "A03T12456"
)
.uiTexts(
SpeechVerifierUITexts(
retry: "Try again",
failed: "Didn't match, please try again",
verified: "Verified"
)
)
.appearance(
SpeechVerifierAppearance(
highlightedTextColor: .systemGreen,
retryButtonBackgroundColor: .systemGreen,
successIconColor: .systemGreen,
failureIconColor: .systemRed,
exemptWords: ["Amani"]
)
)
.onSuccess { [weak self] result in
guard result == true else { return }
self?.uploadEvidence()
}
.onFailure { reason, currentAttempt in
print(reason.rawValue, currentAttempt)
}
.build()

speechVerifier = verifier

guard let speechView = try verifier.start() else {
print("Speech Verifier returned no view.")
return
}

speechVerifierView = speechView
speechView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(speechView)

NSLayoutConstraint.activate([
speechView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
speechView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
speechView.topAnchor.constraint(equalTo: view.topAnchor),
speechView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])

} catch {
print("Speech Verifier error:", error)
}
}

private func uploadEvidence() {
speechVerifier?.upload(location: currentLocation) { [weak self] result in
DispatchQueue.main.async {
print("Speech Verifier upload result:", result as Any)
self?.speechVerifierView?.removeFromSuperview()
self?.speechVerifierView = nil
self?.speechVerifier = nil
}
}
}
}

See Voice Assistant for voice guidance configuration.