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 method | Description | Default |
|---|---|---|
session(serverURL:token:) | Profile session used for identity answers and upload | — |
documentType(_:) | Document type | XXX_ST_0 |
timeout(_:) | Verification time window in seconds | 30 |
videoRecording(_:) | Enables/disables video evidence recording | true |
verificationSteps(_:) | Ordered passphrase / identity-question steps | One default passphrase |
identityAnswers(...) | Manual identity answers | Automatic lookup when omitted |
prompts(_:) | Custom identity/spoken-text prompts | SDK defaults |
uiTexts(_:) | Runtime UI text overrides | SDK defaults |
appearance(_:) | Colors and exempt words | SpeechVerifierAppearance() |
voiceProvider(_:) | Optional voice guidance, see Voice Assistant | No voice |
onSuccess(_:) | Local verification success callback | — |
onFailure(_:) | Failure reason + attempt count | — |
build() | Returns the SpeechVerifier; can throw | — |
Build Errors
build() throws SpeechVerifierBuilderError:
| Error | Meaning |
|---|---|
invalidProfileToken | The serverURL / token passed to session is invalid. |
missingSession | Identity 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
spokenTextstep randomly selects one text from its array. - An
identityQuestionstep 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.
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 value | Swift value | Expected answer |
|---|---|---|
ID_NUMBER | .idNumber | Complete national identity number |
MOTHER_NAME | .motherName | Mother's given name |
FATHER_NAME | .fatherName | Father's given name |
DOCUMENT_NUMBER | .documentNumber | Document 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"
)
)
| Property | Purpose |
|---|---|
retry | Retry button title |
failed | Verification mismatch message |
verified | Successful local verification state |
listening | Active listening state |
verifying | Verification/finalization state |
instruction | Main instruction label |
recognizerNotAvailable | Speech-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"]
)
)
| Property | Description |
|---|---|
instructionTextColor | Instruction label color |
visibleTextColor | Unmatched passphrase color |
highlightedTextColor | Matched passphrase-word color |
statusTextColor | Listening/verifying/result text color |
progressTintColor | Progress completed color |
progressTrackTintColor | Progress track color |
overlayBackgroundColor | Overlay/background color |
retryButtonTextColor | Retry button title color |
retryButtonBackgroundColor | Retry button background color |
listeningIconColor | Active microphone icon color |
successIconColor | Success icon color |
failureIconColor | Failure icon color |
exemptWords | Visible 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
| Reason | Meaning |
|---|---|
verificationFailed | Spoken value did not match the current step; Retry is shown. |
timeout | Configured verification time elapsed; Retry is shown. |
speechRecognitionUnavailable | Apple Speech is unavailable or the recognition pipeline cannot continue. |
cameraPermissionDenied | Camera authorization was denied. |
microphonePermissionDenied | Microphone authorization was denied. |
speechRecognitionPermissionDenied | Speech-recognition authorization was denied. |
cameraUnavailable | A suitable camera could not be found. |
microphoneUnavailable | A suitable audio input could not be found. |
cameraInputFailed | Camera input could not be attached to the capture session. |
microphoneInputFailed | Microphone input could not be attached to the capture session. |
audioSessionFailed | The application audio session could not be configured. |
identityAnswerNotFound | No usable answer was available for the configured identity question. |
identityAnswerMismatch | Spoken identity answer did not match the expected value. |
unknown | Unclassified 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
sessionwas 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.
}
}
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.