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(...).
Quick Start — Single Passphrase
The example below assumes amani is already initialized.
import UIKit
import AmaniSDK
final class VerificationViewController: UIViewController {
private var speechVerifier: SpeechVerifier?
private var speechVerifierView: UIView?
func startSpeechVerifier() {
do {
let verifier = amani.speechVerifier()
.documentType("XXX_ST_0")
.setVideoRecording(enabled: true)
.setTimeout(seconds: 30)
.setText(
"I accept the identity verification",
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
)
}
// 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)
view.bringSubviewToFront(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 start error:", error)
}
}
private func uploadSpeechVerifierEvidence() {
speechVerifier?.upload(location: nil) { [weak self] result in
DispatchQueue.main.async {
if result == true {
print("Speech Verifier upload succeeded.")
} else {
print("Speech Verifier upload failed.")
}
self?.finishSpeechVerifier()
}
}
}
private func finishSpeechVerifier() {
speechVerifierView?.removeFromSuperview()
speechVerifierView = nil
speechVerifier = nil
}
}
Flow Lifecycle
Create SpeechVerifier
↓
Configure steps / UI / voice
↓
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 current customer context, the returned view handles its preparation/loading state internally before listening begins.
Single Passphrase
Use setText for one passphrase:
let verifier = amani.speechVerifier()
.setText(
"I approve this verification",
90
)
The second argument is matchThresholdPercent.
100 → exact normalized match
90 → high similarity required
80 → more tolerant matching
If the threshold is omitted, the default is 100.
Random Passphrase Pool
Use setTexts when one phrase should be selected from a pool:
let verifier = amani.speechVerifier()
.setTexts(
[
"I approve this verification",
"I confirm my identity verification"
],
90
)
For per-text thresholds:
let verifier = amani.speechVerifier()
.setTexts([
SpeechVerifierTextConfiguration(
text: "I approve this verification",
matchThresholdPercent: 95
),
SpeechVerifierTextConfiguration(
text: "I confirm my identity verification",
matchThresholdPercent: 85
)
])
A spokenText pool resolves to one randomly selected text for that step.
Multi-Step Verification
Use verificationSteps(...) for an ordered flow that mixes passphrases and identity questions:
let verifier = amani.speechVerifier()
.documentType("XXX_ST_0")
.verificationSteps([
.spokenText([
SpeechVerifierTextConfiguration(
text: "I approve the identity verification",
matchThresholdPercent: 90
),
SpeechVerifierTextConfiguration(
text: "I am completing Amani's identity verification steps",
matchThresholdPercent: 85
)
]),
.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 step can have its own threshold.
- The flow succeeds only after the final resolved step succeeds.
setText, setTexts, setIdentityQuestion, and setIdentityQuestions 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 configuration. Use verificationSteps(...) when passphrase and identity-question steps must run together.
Running Every Identity Question
Passing multiple values into one identity-question step creates a question pool and selects one valid question.
To run every question sequentially, add each question as a separate step:
let verifier = amani.speechVerifier()
.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 |
One Identity Question
let verifier = amani.speechVerifier()
.setIdentityQuestion(
.documentNumber,
100
)
Random Identity Question Pool
let verifier = amani.speechVerifier()
.setIdentityQuestions(
[.motherName, .fatherName],
90
)
Per-Question Thresholds
let verifier = amani.speechVerifier()
.setIdentityQuestions([
SpeechVerifierIdentityQuestionConfiguration(
type: .documentNumber,
matchThresholdPercent: 100
),
SpeechVerifierIdentityQuestionConfiguration(
type: .motherName,
matchThresholdPercent: 90
)
])
Providing Identity Answers
Identity-question steps require expected-answer data.
Manual Answers
let verifier = amani.speechVerifier()
.verificationSteps([
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(
type: .idNumber,
matchThresholdPercent: 100
)
])
])
.identityAnswers(
idNumber: "12345678901"
)
All manual parameters are optional:
.identityAnswers(
idNumber: "12345678901",
motherName: "AYSE",
fatherName: "MEHMET",
documentNumber: "A03T12456"
)
Expected answers are used for matching and are not displayed as the visible question text.
Automatic Customer/Profile Lookup
If an identity-question step exists and identityAnswers(...) has not been called, the Core SDK attempts to obtain answers from the current customer's document/profile data.
let verifier = amani.speechVerifier()
.setIdentityQuestions(
[.idNumber, .motherName],
100
)
The Amani Core SDK must already have a valid authenticated customer context. If no usable answer can be resolved, the module reports identityAnswerNotFound.
The Android standalone module exposes session(serverURL, token). The iOS Core SDK integration does not use that API; customer/profile context comes from the initialized Amani Core SDK.
Custom Identity Prompts
Use setPrompts(...) to customize the text shown for identity-question steps and the spoken-text instruction.
let identityPrompts: [SpeechVerifierIdentityQuestionType: SpeechVerifierStepPrompt] = [
.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"
),
.fatherName: SpeechVerifierStepPrompt(
visibleText: "Say your father's name",
instructionText: "Answer the question aloud"
),
.documentNumber: SpeechVerifierStepPrompt(
visibleText: "Say your document number",
instructionText: "Answer the question aloud"
)
]
let prompts = SpeechVerifierPrompts(
identityPrompts: identityPrompts,
spokenTextInstruction: "Read the displayed text aloud"
)
verifier.setPrompts(prompts)
When AmaniUI is used, remote values such as speechVerifierIdentityPrompts can be mapped into this Core SDK model.
Custom UI Texts
Use setUITexts(...) to override runtime text:
verifier.setUITexts(
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
verifier.setAppearance(
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
) { 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.
A valid customer ID must exist in the active Amani Core SDK session. Backend upload errors may also be forwarded through the standard Amani SDK error delegate.
upload(...) is an instance method. Do not set the SpeechVerifier reference to nil in onSuccess before upload finishes.
Public Configuration Reference
| Method | Description | Default / behavior |
|---|---|---|
setType(type:) | Non-chainable document-type setter | XXX_ST_0 |
documentType(_:) | Chainable document-type setter | XXX_ST_0 |
setVideoRecording(enabled:) | Enables/disables video evidence recording | true |
setTimeout(seconds:) | Verification time window | 30 seconds |
setEnableSpeechVoices(_:) | Enables/disables Voice Assistant guidance | true |
setSpeechVoicesURL(_:) | Overrides AppConfig voice URL | AppConfig when unset |
setText(_:_:) | One passphrase; replaces existing steps | Threshold 100 when omitted |
setTexts(_:_:) | Random passphrase pool; replaces existing steps | Threshold 100 when omitted |
setTexts([SpeechVerifierTextConfiguration]) | Random passphrase pool with per-text thresholds | — |
setIdentityQuestion(_:_:) | One identity question / pool; replaces existing steps | Threshold 100 when omitted |
setIdentityQuestions(_:_:) | Identity-question pool | Threshold 100 when omitted |
setIdentityQuestions([SpeechVerifierIdentityQuestionConfiguration]) | Identity pool with per-question thresholds | — |
verificationSteps([SpeechVerifierStepConfiguration]) | Ordered mixed flow with per-item thresholds | — |
verificationSteps([SpeechVerifierStep]) | Ordered flow using threshold 100 | — |
setVerificationSteps(_:) | Alias of verificationSteps(_:) | — |
identityAnswers(...) | Manual identity answers | Automatic profile lookup when omitted |
setPrompts(_:) | Custom identity/spoken-text prompts | SDK/config defaults |
setUITexts(_:) | Runtime UI text overrides | SDK defaults |
setAppearance(_:) | Colors and exempt words | SpeechVerifierAppearance() |
onSuccess(_:) | Local verification success callback | — |
onFailure(_:) | Failure reason + verification-attempt count | — |
start() | Creates and starts module view | Returns UIView?; can throw |
upload(location:completion:) | Uploads internally tracked evidence | Location optional |
Complete Example
import UIKit
import AmaniSDK
import CoreLocation
final class SpeechVerifierViewController: UIViewController {
var amani: Amani!
private var speechVerifier: SpeechVerifier?
private var speechVerifierView: UIView?
private var currentLocation: CLLocation?
func startSpeechVerifier() {
do {
let uiTexts = SpeechVerifierUITexts(
retry: "Try again",
failed: "Didn't match, please try again",
verified: "Verified",
listening: "Listening…",
verifying: "Verifying…",
instruction: "Please answer aloud",
recognizerNotAvailable: "Speech recognition is unavailable"
)
let appearance = SpeechVerifierAppearance(
highlightedTextColor: .systemGreen,
retryButtonBackgroundColor: .systemGreen,
successIconColor: .systemGreen,
failureIconColor: .systemRed,
exemptWords: ["Amani"]
)
let verifier = amani.speechVerifier()
.documentType("XXX_ST_0")
.setVideoRecording(enabled: true)
.setTimeout(seconds: 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"
)
.setUITexts(uiTexts)
.setAppearance(appearance)
.onSuccess { [weak self] result in
guard result == true else { return }
self?.uploadEvidence()
}
.onFailure { reason, currentAttempt in
print(reason.rawValue, currentAttempt)
}
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 start 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.