Speech Verifier
Preparation
The Speech Verifier is included in the Amani Core SDK for iOS. It is not distributed as a separate SDK or framework.
The module presents a camera screen where the user reads a passphrase and, optionally, answers identity questions. Speech is evaluated live, matched words are highlighted on screen, and a continuous video can be recorded for upload to the Amani backend.
let verifier = amani.speechVerifier()
Requirements
- iOS 13.0 or later
- Amani Core SDK integrated and initialized
- A valid customer/profile session for evidence upload
- A valid customer/profile session when identity answers will be fetched automatically
- A physical device is recommended for camera, microphone, and speech-recognition testing
Required Permissions
Add the following usage-description keys to the host application's Info.plist:
<key>NSCameraUsageDescription</key>
<string>Camera access is required to record the speech verification session.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required to verify the spoken text.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Speech recognition is required to verify the spoken text.</string>
The module requests camera, microphone, and speech-recognition authorization when the flow starts. Missing Info.plist usage descriptions may terminate the application when iOS attempts to show the permission prompt.
Location is optional. Add NSLocationWhenInUseUsageDescription only when the host application obtains a CLLocation and passes it to upload(location:completion:).
When camera, microphone, or speech-recognition permission is denied, the module reports the corresponding SpeechVerifierFailureReason through onFailure.
Important Integration Notes
- Keep a strong reference to the
SpeechVerifierinstance until the flow and upload are complete. - Keep the returned
UIViewin the view hierarchy while the flow is active. start()returns a view; the host application is responsible for presenting and constraining it.- Keep video recording enabled for production evidence collection unless the Amani team specifies otherwise.
- Do not release the verifier inside
onSuccessbefore callingupload(...). - The Apple Speech recognizer can temporarily become unavailable. Handle
speechRecognitionUnavailableas a recoverable or terminal state according to your application's flow. - For remote-configured KYC journeys, the Speech Verifier step and document type must also be configured for the customer profile on the Amani backend.
Usage
Quick Start — Single Passphrase
The example below assumes amani is an already initialized Amani Core SDK instance.
import UIKit
import AmaniSDK
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)
.setAppearance(
SpeechVerifierAppearance(
highlightedTextColor: .systemGreen
)
)
.onSuccess { [weak self] result in
guard result == true else { return }
self?.uploadSpeechVerifierEvidence()
}
.onFailure { reason, currentAttempt in
print(
"Speech Verifier failure:",
reason.rawValue,
"attempt:",
currentAttempt
)
}
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?.removeSpeechVerifier()
}
}
}
private func removeSpeechVerifier() {
speechVerifierView?.removeFromSuperview()
speechVerifierView = nil
speechVerifier = nil
}
Verification Steps
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 flow rules are:
- The array order is the order shown to the user.
- A
spokenTextstep randomly selects one phrase from its array. - An
identityQuestionstep randomly selects one valid question from its array. - Repeating a step creates another step in the flow.
- All resolved steps share the same active recording.
- The flow succeeds only after the final resolved step succeeds.
- Every text and identity question carries its own
matchThresholdPercent. - Threshold values are clamped to
1...100;100requires an exact normalized match, while lower values allow the configured amount of similarity.
setText, setTexts, setIdentityQuestion, and setIdentityQuestions each replace the currently 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 passphrase step. Use verificationSteps(...) when passphrase and identity-question steps must run together.
Running Every Identity Question
Passing multiple values to one identityQuestion step creates a question pool and selects one valid question from it.
To run every question sequentially, add each type as a separate step:
let verifier = amani.speechVerifier()
.documentType("XXX_ST_0")
.verificationSteps([
.spokenText([
SpeechVerifierTextConfiguration(
text: "I accept the identity verification",
matchThresholdPercent: 90
)
]),
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(
type: .documentNumber,
matchThresholdPercent: 100
)
]),
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(
type: .motherName,
matchThresholdPercent: 100
)
]),
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(
type: .fatherName,
matchThresholdPercent: 90
)
]),
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(
type: .idNumber,
matchThresholdPercent: 80
)
])
])
.identityAnswers(
idNumber: "IDNumber",
motherName: "MotherName",
fatherName: "FatherName",
documentNumber: "DocumentNumber"
)
Identity Question Types
Use the Swift enum values in application code. Uppercase values such as MOTHER_NAME are remote-configuration values, not Swift enum cases.
| Remote/config value | Swift value | Expected answer |
|---|---|---|
ID_NUMBER | .idNumber | The complete national identity number |
MOTHER_NAME | .motherName | Mother's given name |
FATHER_NAME | .fatherName | Father's given name |
DOCUMENT_NUMBER | .documentNumber | The document number; verification uses its digit stream |
For one identity question, pass the threshold directly with the question:
.setIdentityQuestion(
[.documentNumber],
100
)
The single-value overload is also available:
.setIdentityQuestion(
.documentNumber,
100
)
When every question in a pool uses the same threshold:
.setIdentityQuestions(
[
.motherName,
.fatherName
],
90
)
When each question needs a different threshold, use
SpeechVerifierIdentityQuestionConfiguration:
.setIdentityQuestions([
SpeechVerifierIdentityQuestionConfiguration(
type: .documentNumber,
matchThresholdPercent: 100
),
SpeechVerifierIdentityQuestionConfiguration(
type: .motherName,
matchThresholdPercent: 100
),
SpeechVerifierIdentityQuestionConfiguration(
type: .fatherName,
matchThresholdPercent: 90
),
SpeechVerifierIdentityQuestionConfiguration(
type: .idNumber,
matchThresholdPercent: 80
)
])
Each call above creates one identity-question step and randomly selects one
valid question from its configured pool. The selected question keeps its own
matchThresholdPercent.
Providing Identity Answers
Identity-question steps require answer data. Choose one source.
Manual answers
Use identityAnswers(...) to provide values directly:
.identityAnswers(
idNumber: "IDNumber",
motherName: "MotherName",
fatherName: "FatherName",
documentNumber: "DocumentNumber"
)
Only provide values required by the configured question steps. The answers are used for matching and are not displayed as the question text.
Automatic profile lookup
When an identity-question step exists and identityAnswers(...) has not been called, the Core SDK attempts to obtain the answers from the current customer's document/profile data.
The Amani Core SDK must already have a valid authenticated customer context. If no usable answer exists for the configured question pool, the module reports identityAnswerNotFound.
Appearance
Use SpeechVerifierAppearance to customise module colours and exempt words:
.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 | Default |
|---|---|---|
instructionTextColor | Instruction label colour | |
visibleTextColor | Unmatched passphrase colour | |
highlightedTextColor | Colour applied to matched words | |
statusTextColor | Listening/result status text colour | |
progressTintColor | Progress-view completed colour | |
progressTrackTintColor | Progress-view track colour | |
overlayBackgroundColor | Bottom overlay/background colour | |
retryButtonTextColor | Retry button title colour | |
retryButtonBackgroundColor | Retry button background colour | |
listeningIconColor | Active microphone icon colour | |
successIconColor | Success icon colour | |
failureIconColor | Failure icon colour | |
exemptWords | Visible words excluded from phrase verification |
Public Configuration Methods
| Method | Description | Default / behaviour |
|---|---|---|
setType(type:) | Non-chainable equivalent of documentType(_:). | XXX_ST_0 |
setVideoRecording(enabled:) | Enables or disables video evidence recording. | true |
setTimeout(seconds:) | Sets the verification time window. Values must be greater than zero. | 30 seconds |
setText(_:_:) | Configures one passphrase with its own match threshold and replaces existing steps. | Threshold 100 when omitted |
setTexts(_:_:) | Configures a random passphrase pool with one shared threshold and replaces existing steps. | Threshold 100 when omitted |
setTexts([SpeechVerifierTextConfiguration]) | Configures a random passphrase pool where every text can have a different threshold. | — |
setIdentityQuestion(_:_:) | Configures one identity question or a question pool with one shared threshold and replaces existing steps. | Threshold 100 when omitted |
setIdentityQuestions(_:_:) | Alias for configuring an identity-question pool with one shared threshold. | Threshold 100 when omitted |
setIdentityQuestions([SpeechVerifierIdentityQuestionConfiguration]) | Configures a random identity-question pool where every question can have a different threshold. | — |
verificationSteps([SpeechVerifierStepConfiguration]) | Configures an ordered mixed flow with per-text and per-question thresholds. | — |
verificationSteps([SpeechVerifierStep]) | Backward-compatible ordered flow; every step uses threshold 100. | — |
setVerificationSteps(_:) | Alias of verificationSteps(_:). | — |
identityAnswers(...) | Supplies identity answers manually. | Automatic profile lookup when omitted |
setAppearance(_:) | Sets colours and exempt words. | SpeechVerifierAppearance() |
onSuccess(_:) | Receives the completed-flow result. | — |
onFailure(_:) | Receives a failure reason and current attempt count. | — |
start() | Creates and starts the module view. | Returns UIView?; can throw |
upload(location:completion:) | Uploads the internally recorded evidence. | Location is optional |
Callbacks
.onSuccess { result in
// result == true when every resolved verification step has succeeded.
}
.onFailure { reason, currentAttempt in
// Handle a recoverable mismatch, timeout, permission problem, or setup error.
}
onSuccess indicates that all configured runtime steps have passed and the recording has been finalised. It does not upload the evidence automatically.
onFailure may be called for recoverable and non-recoverable conditions. The module handles its own retry UI for supported retry cases. Avoid immediately removing the module view for every failure reason.
UI updates made by the host application should be dispatched to the main queue.
Failure Reasons
| Reason | Meaning |
|---|---|
verificationFailed | The spoken value did not match the current step. The module may restart the verification flow internally. |
timeout | The configured time window elapsed before the flow completed. |
speechRecognitionUnavailable | Apple's speech recognizer is unavailable or returned a terminal recognition error. |
cameraPermissionDenied | Camera authorization was denied. |
microphonePermissionDenied | Microphone authorization was denied. |
speechRecognitionPermissionDenied | Speech-recognition authorization was denied. |
cameraUnavailable | A suitable camera device could not be found. |
microphoneUnavailable | A suitable audio input could not be found. |
cameraInputFailed | The camera input could not be attached to the capture session. |
microphoneInputFailed | The 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 | The spoken identity answer did not match the expected value. |
unknown | An unclassified preparation or runtime error occurred. |
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 video and generated evidence internally. Do not pass a file URL.
A valid customer ID must exist in the active Amani Core SDK session. Upload failures are also forwarded through the standard Amani SDK error delegate when backend error details are available.
upload(...) is an instance method. Releasing speechVerifier in onSuccess prevents the host application from starting the upload.
Complete Example
The following example runs one passphrase followed by all four identity questions, assigns a separate match threshold to every step, uses manual answers, embeds the returned view, and uploads the evidence after success.
import UIKit
import AmaniSDK
import CoreLocation
final class SpeechVerifierViewController: UIViewController {
// `amani` must already be initialized by the host application.
var amani: Amani!
private var speechVerifier: SpeechVerifier?
private var speechVerifierView: UIView?
private var currentLocation: CLLocation?
func startSpeechVerifier() {
do {
let appearance = SpeechVerifierAppearance(
highlightedTextColor: .systemGreen,
retryButtonBackgroundColor: .systemGreen
)
let verifier = amani.speechVerifier()
.documentType("XXX_ST_0")
.setVideoRecording(enabled: true)
.setTimeout(seconds: 30)
.verificationSteps([
.spokenText([
SpeechVerifierTextConfiguration(
text: "I accept the identity verification",
matchThresholdPercent: 90
)
]),
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(
type: .documentNumber,
matchThresholdPercent: 100
)
]),
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(
type: .motherName,
matchThresholdPercent: 100
)
]),
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(
type: .fatherName,
matchThresholdPercent: 90
)
]),
.identityQuestion([
SpeechVerifierIdentityQuestionConfiguration(
type: .idNumber,
matchThresholdPercent: 80
)
])
])
.identityAnswers(
idNumber: "IDNumber",
motherName: "MotherName",
fatherName: "FatherName",
documentNumber: "DocumentNumber"
)
.setAppearance(appearance)
.onSuccess { [weak self] result in
guard result == true else { return }
self?.uploadEvidence()
}
.onFailure { reason, currentAttempt in
print(
"Speech Verifier failure:",
reason.rawValue,
"attempt:",
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)
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 uploadEvidence() {
speechVerifier?.upload(
location: currentLocation
) { [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
}
}
Notes
- The iOS Speech Verifier is part of the Amani Core SDK; do not add a second Speech Verifier framework.
- The module requests camera, microphone, and speech-recognition permissions itself.
- A single
setIdentityQuestions([...])call selects one valid question from the provided pool. - Use separate
identityQuestionsteps to run several identity questions sequentially. - Use
verificationSteps(...)to combine passphrases and identity questions. - Match thresholds belong to the selected text or identity question; there is no global
matchThresholdPercent(...)setter. - Matched passphrase words are highlighted using
highlightedTextColor. exemptWordsremain visible but are excluded from required phrase matching.- When a reading fails, the current recording is discarded and the configured flow restarts from its first step with a fresh recording.
- Diagnostic speech-recognition output should be kept out of production logs.