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(...).

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 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 step can have its own threshold.
  • The flow succeeds only after the final resolved step succeeds.
Step-replacing setters

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 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

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.

Android difference

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"
)
)
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​

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"]
)
)
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
) { 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.

Retain the verifier until upload completes

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

Public Configuration Reference​

MethodDescriptionDefault / behavior
setType(type:)Non-chainable document-type setterXXX_ST_0
documentType(_:)Chainable document-type setterXXX_ST_0
setVideoRecording(enabled:)Enables/disables video evidence recordingtrue
setTimeout(seconds:)Verification time window30 seconds
setEnableSpeechVoices(_:)Enables/disables Voice Assistant guidancetrue
setSpeechVoicesURL(_:)Overrides AppConfig voice URLAppConfig when unset
setText(_:_:)One passphrase; replaces existing stepsThreshold 100 when omitted
setTexts(_:_:)Random passphrase pool; replaces existing stepsThreshold 100 when omitted
setTexts([SpeechVerifierTextConfiguration])Random passphrase pool with per-text thresholds—
setIdentityQuestion(_:_:)One identity question / pool; replaces existing stepsThreshold 100 when omitted
setIdentityQuestions(_:_:)Identity-question poolThreshold 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 answersAutomatic profile lookup when omitted
setPrompts(_:)Custom identity/spoken-text promptsSDK/config defaults
setUITexts(_:)Runtime UI text overridesSDK defaults
setAppearance(_:)Colors and exempt wordsSpeechVerifierAppearance()
onSuccess(_:)Local verification success callback—
onFailure(_:)Failure reason + verification-attempt count—
start()Creates and starts module viewReturns UIView?; can throw
upload(location:completion:)Uploads internally tracked evidenceLocation 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.