Skip to main content

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

Permission denial

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 SpeechVerifier instance until the flow and upload are complete.
  • Keep the returned UIView in 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 onSuccess before calling upload(...).
  • The Apple Speech recognizer can temporarily become unavailable. Handle speechRecognitionUnavailable as 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 spokenText step randomly selects one phrase from its array.
  • An identityQuestion step 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; 100 requires an exact normalized match, while lower values allow the configured amount of similarity.
Do not combine step-replacing setters

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 valueSwift valueExpected answer
ID_NUMBER.idNumberThe complete national identity number
MOTHER_NAME.motherNameMother's given name
FATHER_NAME.fatherNameFather's given name
DOCUMENT_NUMBER.documentNumberThe 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"]
)
)
PropertyDescriptionDefault
instructionTextColorInstruction label colour
visibleTextColorUnmatched passphrase colour
highlightedTextColorColour applied to matched words
statusTextColorListening/result status text colour
progressTintColorProgress-view completed colour
progressTrackTintColorProgress-view track colour
overlayBackgroundColorBottom overlay/background colour
retryButtonTextColorRetry button title colour
retryButtonBackgroundColorRetry button background colour
listeningIconColorActive microphone icon colour
successIconColorSuccess icon colour
failureIconColorFailure icon colour
exemptWordsVisible words excluded from phrase verification

Public Configuration Methods

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

ReasonMeaning
verificationFailedThe spoken value did not match the current step. The module may restart the verification flow internally.
timeoutThe configured time window elapsed before the flow completed.
speechRecognitionUnavailableApple's speech recognizer is unavailable or returned a terminal recognition error.
cameraPermissionDeniedCamera authorization was denied.
microphonePermissionDeniedMicrophone authorization was denied.
speechRecognitionPermissionDeniedSpeech-recognition authorization was denied.
cameraUnavailableA suitable camera device could not be found.
microphoneUnavailableA suitable audio input could not be found.
cameraInputFailedThe camera input could not be attached to the capture session.
microphoneInputFailedThe microphone 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.
identityAnswerMismatchThe spoken identity answer did not match the expected value.
unknownAn 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.

Retain the verifier until upload completes

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 identityQuestion steps 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.
  • exemptWords remain 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.