Identity records
Decide how a verified identity is saved in the vault with your own IdentityRecordMapper.
Preview
This page describes a pre-release version of the SDK. Names, versions and APIs on this page can change before the release.
When a government id verification succeeds during an inquiry, the SDK saves the result in the
user's vault. Your app decides what the saved record looks like: you pass an
IdentityRecordMapper to FolioSdk.create, and the SDK calls it with the verified identity and
writes the RecordContent it returns. The payload is your own JSON document, so your app reads it
back with its own decoder.
An identity record is not a Folio document. A document an organization issues to the person
arrives as a separate record with the schema digital, which the SDK receives and shows through
its own stores; your mapper never sees it. See Folio documents.
VerifiedIdentity
The result of a verification, as the mapper receives it. The inquiry store also reports it in the
identity field of its state; see Inquiry store.
| Field | Type | Meaning |
|---|---|---|
inquiryId | String | The inquiry that produced the result. |
selectedDocumentType | IdentityDocumentType (optional) | The type of the document the step verified. See Selected document type. |
output | VerifierOutput | The fields the verifier extracted. |
issuedAt | i64 | When the verification outcome was issued, milliseconds since the Unix epoch. |
expiresAt | i64 | When the verification outcome expires, milliseconds since the Unix epoch. |
files | [IdentityFile] | The stored verification artifacts, each with the id of the attachment that holds it. |
issuedAt and expiresAt come from the signed verification outcome. They are Int64 on iOS,
Long on Android and bigint on the web. JSON.stringify cannot encode a bigint, so on the web
convert them with Number before you encode the identity, as the web mapper below does.
Selected document type
selectedDocumentType is the document type of the government id step: the type the person picked
in a GovId step, or the chip document of a GovIdNfc step. It is empty when the step names none.
Folio sets Passport, DrivingLicense, NationalIdentityCard or ResidencePermit for a GovId
step, and Passport or NationalIdentityCard for a GovIdNfc step. IdentityDocumentType is a
plain enumeration, so match every case:
| Case | iOS | Android | Web |
|---|---|---|---|
Passport | .passport | IdentityDocumentType.PASSPORT | 'PASSPORT' |
DiplomaticPassport | .diplomaticPassport | IdentityDocumentType.DIPLOMATIC_PASSPORT | 'DIPLOMATIC_PASSPORT' |
ServicePassport | .servicePassport | IdentityDocumentType.SERVICE_PASSPORT | 'SERVICE_PASSPORT' |
EmergencyPassport | .emergencyPassport | IdentityDocumentType.EMERGENCY_PASSPORT | 'EMERGENCY_PASSPORT' |
PassportCard | .passportCard | IdentityDocumentType.PASSPORT_CARD | 'PASSPORT_CARD' |
NationalIdentityCard | .nationalIdentityCard | IdentityDocumentType.NATIONAL_IDENTITY_CARD | 'NATIONAL_IDENTITY_CARD' |
EnhancedIdentityCard | .enhancedIdentityCard | IdentityDocumentType.ENHANCED_IDENTITY_CARD | 'ENHANCED_IDENTITY_CARD' |
DrivingLicense | .drivingLicense | IdentityDocumentType.DRIVING_LICENSE | 'DRIVING_LICENSE' |
ProvisionalDrivingLicense | .provisionalDrivingLicense | IdentityDocumentType.PROVISIONAL_DRIVING_LICENSE | 'PROVISIONAL_DRIVING_LICENSE' |
ChauffeurLicense | .chauffeurLicense | IdentityDocumentType.CHAUFFEUR_LICENSE | 'CHAUFFEUR_LICENSE' |
ResidencePermit | .residencePermit | IdentityDocumentType.RESIDENCE_PERMIT | 'RESIDENCE_PERMIT' |
ResidentIdCard | .residentIdCard | IdentityDocumentType.RESIDENT_ID_CARD | 'RESIDENT_ID_CARD' |
WorkPermit | .workPermit | IdentityDocumentType.WORK_PERMIT | 'WORK_PERMIT' |
Visa | .visa | IdentityDocumentType.VISA | 'VISA' |
VoterId | .voterId | IdentityDocumentType.VOTER_ID | 'VOTER_ID' |
TribalId | .tribalId | IdentityDocumentType.TRIBAL_ID | 'TRIBAL_ID' |
MunicipalId | .municipalId | IdentityDocumentType.MUNICIPAL_ID | 'MUNICIPAL_ID' |
ConsularId | .consularId | IdentityDocumentType.CONSULAR_ID | 'CONSULAR_ID' |
HealthCard | .healthCard | IdentityDocumentType.HEALTH_CARD | 'HEALTH_CARD' |
Other | .other | IdentityDocumentType.OTHER | 'OTHER' |
On the web the constant IdentityDocumentType holds the values, for example
IdentityDocumentType.PASSPORT. The same enumeration is the type of documentType in
GovIdOutput.
VerifierOutput
output names the verifier and carries its fields. Only the two government id verifiers reach the
mapper:
| Case | Payload | Verifier | Fields the SDK uses to recognize a document |
|---|---|---|---|
GovId | GovIdOutput | Document capture and recognition | documentNumber, birthdate |
GovIdNfc | GovIdNfcOutput | Reading the chip of a passport or id card | documentNumber, birthdate |
The other cases, Document, Selfie, OtpEmail and OtpSms, never reach the mapper and never
appear in InquiryUiModel.identity. They exist because VerifierOutput is the type of every
Folio verifier; a switch in Swift or a when in Kotlin still has to cover them, for example with
a default or else branch. Document (.document(value:) in Swift,
VerifierOutput.Document(value) in Kotlin, { type: 'DOCUMENT', value } in TypeScript) carries a
DocumentOutput, the result of a general document verification: the optional files
capturedDocumentFront and capturedDocumentBack, the optional strings documentGroup and
documentClass, and extracted, a list of DocumentExtractedValue, each a key and a value
string.
Every field of the two government id outputs is optional:
| Output | Fields |
|---|---|
GovIdOutput | givenName, surname, middleName, birthdate, sex, nationality, countryCode, documentNumber, documentType, cardAccessNumber, documentSubType, issueDate, expirationDate, issuingAuthority, issuingSubdivision, mrzLine1, mrzLine2, mrzLine3, and the files capturedDocumentFront, capturedDocumentBack and capturedPortraitOcr |
GovIdNfcOutput | documentNumber, birthdate, expirationDate, surname, givenName, nationality, and the file capturedPortraitNfc |
birthdate, issueDate and expirationDate are strings of the type IsoDate. A file is a
FileRef with a reference and an optional mimeType. documentType is an
IdentityDocumentType and documentSubType a DocumentSubType.
switch identity.output {
case .govId(let value):
print(value.documentNumber ?? "")
case .govIdNfc(let value):
print(value.documentNumber ?? "")
default:
break
}IdentityFile
| Field | Type | Meaning |
|---|---|---|
artifact | ArtifactField | Which verification artifact the file holds. |
attachmentId | String | The id of the vault attachment that holds the artifact's bytes. |
image | ImageMeta (optional) | The image metadata of a captured image, such as the document front or a portrait. Empty for the other artifacts, such as outcome.jwt. |
The first file is always the signed verification outcome, outcome.jwt. The files that follow are
the other artifacts the verifier returned for the result, in the order of the
ArtifactField table. The same ids appear in header.attachments of the saved
record.
ImageMeta describes the image the SDK read from the artifact's bytes:
| Field | Type | Meaning |
|---|---|---|
file | FileMeta | The file of the image. |
width | u32 | The width in pixels. |
height | u32 | The height in pixels. |
orientation | Orientation | The orientation the image declares, Up when it declares none. |
FileMeta field | Type | Meaning |
|---|---|---|
id | String | The attachment id, the same as attachmentId. |
name | String | The artifact's wire name with the extension of the image format, such as captured_portrait.jpg. |
format | BinaryFormat | Image with the ImageFormat of the bytes. |
sizeBytes | u32 | The size of the image in bytes. |
checksum | String (optional) | Empty for an identity file. |
width, height and sizeBytes are numbers on every platform: UInt32 on iOS, Long on Android
and number on the web, so JSON.stringify encodes them as they are. An image artifact whose bytes
are not an image the SDK can read fails the save with RecordMapping.
| Type | iOS | Android | Web |
|---|---|---|---|
ImageFormat | .png, .jpeg, .webP, .heic | ImageFormat.Png, ImageFormat.Jpeg, ImageFormat.WebP, ImageFormat.Heic | { type: 'PNG' }, { type: 'JPEG' }, { type: 'WEB_P' }, { type: 'HEIC' } |
BinaryFormat | .image(format:) for an image | BinaryFormat.Image(format) for an image | { type: 'IMAGE', value: { format } } for an image |
Orientation | .up, .upMirrored, .down, .downMirrored, .left, .leftMirrored, .right, .rightMirrored | Orientation.Up, Orientation.UpMirrored, and so on | { type: 'UP' }, { type: 'UP_MIRRORED' }, and so on |
BinaryFormat has more cases for other files, Pdf, Json, Enc, Txt, Pkpass, Bytes and
Other with a mediaType; an identity file always has Image.
ArtifactField
ArtifactField names a verification artifact. It has no payloads.
| iOS | Android | Web |
|---|---|---|
.outcomeJwt | ArtifactField.OUTCOME_JWT | 'outcome.jwt' |
.verifications | ArtifactField.VERIFICATIONS | 'verifications' |
.data | ArtifactField.DATA | 'data' |
.context | ArtifactField.CONTEXT | 'context' |
.claims | ArtifactField.CLAIMS | 'claims' |
.config | ArtifactField.CONFIG | 'config' |
.output | ArtifactField.OUTPUT | 'output' |
.nfcScalar | ArtifactField.NFC_SCALAR | 'nfc_scalar' |
.prevVrf | ArtifactField.PREV_VRF | 'prev_vrf' |
.capturedDocumentFront | ArtifactField.CAPTURED_DOCUMENT_FRONT | 'captured_document_front' |
.capturedDocumentBack | ArtifactField.CAPTURED_DOCUMENT_BACK | 'captured_document_back' |
.capturedPortraitOcr | ArtifactField.CAPTURED_PORTRAIT_OCR | 'captured_portrait_ocr' |
.capturedSignature | ArtifactField.CAPTURED_SIGNATURE | 'captured_signature' |
.capturedPortraitGhost | ArtifactField.CAPTURED_PORTRAIT_GHOST | 'captured_portrait_ghost' |
.capturedBarcode | ArtifactField.CAPTURED_BARCODE | 'captured_barcode' |
.capturedColorDynamic | ArtifactField.CAPTURED_COLOR_DYNAMIC | 'captured_color_dynamic' |
.capturedStamp | ArtifactField.CAPTURED_STAMP | 'captured_stamp' |
.capturedPortrait | ArtifactField.CAPTURED_PORTRAIT | 'captured_portrait' |
.capturedPortraitNfc | ArtifactField.CAPTURED_PORTRAIT_NFC | 'captured_portrait_nfc' |
.capturedNfcBundle | ArtifactField.CAPTURED_NFC_BUNDLE | 'captured_nfc_bundle' |
The web values are the artifact wire names, such as outcome.jwt; on the web the constant
ArtifactField holds them, for example ArtifactField.OUTCOME_JWT. When you encode an
IdentityFile into your record, ArtifactField is written as that same wire name on every
platform: by JSONEncoder on iOS, by kotlinx.serialization on Android and by JSON.stringify on
the web, and decoding accepts only that wire name. A record your mapper writes on one platform
therefore decodes on the others.
The mapper interface
IdentityRecordMapper has one method. It receives the identity and, when the SDK has saved the
same document before, the body of that record. It returns the body to save.
protocol IdentityRecordMapper {
func map(identity: VerifiedIdentity, existing: RecordContent?) throws -> RecordContent
}The returned RecordContent follows the rules of every vault body: schema is your name for the
document without the organization, version is your format version, and payload is a JSON
string. See Client-shaped bodies.
Build the body from the arguments only. When the SDK resumes a save that an interruption stopped, it
calls map again with the same identity and existing and checks that the body is the same JSON;
the order of object keys does not matter. A different body, for example one that carries the current
time, fails the verification with InvalidState. This error appears in InquiryUiModel.error as a
banner and the inquiry stays open: unlike a mapping error, it sets no Failed
lifecycle and no failure view.
The runtime keeps the mapper for its whole lifetime. Pass it when you create the runtime:
let sdk = try await FolioSdk.create(config: config, mapper: SavedIdentityMapper())On iOS, implement the mapper as a class: the SDK holds it as an object, and FolioSdk.create
expects a class instance. On Android, rememberFolioSdk creates the runtime again when it receives
a different mapper instance, so pass the same object every time, such as a Kotlin object.
A complete mapper
The mappers below keep one identity document per physical document and append every new
verification of it to a list. Each one rejects an existing record it does not recognize, which
ends the inquiry with a mapping error instead of overwriting data.
import FolioSDK
struct SavedIdentity: Codable {
let identities: [VerifiedIdentity]
}
struct NotASavedIdentity: LocalizedError {
let schema: String
var errorDescription: String? { "a \(schema) record is no saved identity" }
}
final class SavedIdentityMapper: IdentityRecordMapper {
func map(identity: VerifiedIdentity, existing: RecordContent?) throws -> RecordContent {
var earlier: [VerifiedIdentity] = []
if let existing {
guard existing.schema == "identity", existing.version == 1 else {
throw NotASavedIdentity(schema: existing.schema)
}
earlier = try JSONDecoder()
.decode(SavedIdentity.self, from: Data(existing.payload.utf8))
.identities
}
let document = SavedIdentity(identities: earlier + [identity])
let payload = String(decoding: try JSONEncoder().encode(document), as: UTF8.self)
return RecordContent(schema: "identity", version: 1, payload: payload)
}
}When the mapper runs
The SDK calls the mapper for every successful government id verification (GovId or GovIdNfc)
in an inquiry, before the inquiry moves on, and again only when it resumes an interrupted save. It
does not call it for document, selfie, email OTP and SMS OTP results: the SDK keeps those results
for the inquiry itself and writes no record for them.
A government id verification needs a session that holds a vault. Without one the SDK saves nothing,
does not call the mapper, and the verification fails with InvalidState, shown in
InquiryUiModel.error while the inquiry stays open.
One record per document
The SDK keeps one record per physical document. It recognizes a document by a fingerprint of two fields of the verifier output:
documentNumber, with whitespace and hyphens removed and letters uppercased;birthdate, with surrounding whitespace removed.
What happens next depends on the fingerprint:
| Situation | existing | What the SDK writes |
|---|---|---|
| The SDK saved a record for this fingerprint before, and that record is active and owned by the account | That record's body | Updates that record with the returned body and adds the new artifacts as attachments. |
| The fingerprint is new, or its record was deleted or is not owned by the account | none | Creates a record with the returned body and the artifacts as attachments, and remembers the fingerprint for it. |
documentNumber or birthdate is missing or empty | none | Creates a record. No fingerprint is remembered, so the next verification of the same document creates another one. |
On an update, return a body with the same schema as the existing record: the vault rejects an
update that changes the schema.
Mapping errors
Throw from map when you cannot build a record, for example when existing holds a document your
app does not recognize. The SDK then writes nothing, neither the record nor its attachments, and
the inquiry ends with lifecycle Failed and error RecordMapping, whose message is the message
of your error:
| Platform | What you throw | What the inquiry reports |
|---|---|---|
| iOS | Any Error | InquiryLifecycle.failed(error: .recordMapping(message:)) |
| Android | Any exception | InquiryLifecycle.Failed(InquiryError.RecordMapping(message)) |
| Web | Any Error | { type: 'FAILED', value: { error: { type: 'RECORD_MAPPING', value: { message } } } } |
On iOS the message is the errorDescription of an error that conforms to LocalizedError, and
String(describing:) of any other error. On the web it is the error's message; a thrown value
that is not an Error is converted with String.
The failed inquiry stays failed: InquiryIntent.Retry has no effect on it. InquiryUiModel.failure
holds the generic failure copy with no retry; show it, and send Close from its close button. The
lifecycle then becomes Closed with no outcome.
See Inquiry store for the lifecycle and Errors for the other inquiry errors.
Read the records back
The saved records are ordinary vault records. Mount VaultStore, pick your schema
and decode the payload with the same types the mapper used.
let vault = try VaultStore.mount(runtime: sdk)
let identities = try vault.state.records
.filter { $0.header.schema == "identity" }
.map { try JSONDecoder().decode(SavedIdentity.self, from: Data($0.payload.utf8)) }Filter on header.state as well if you want to leave out records the user has deleted. The ids in
files of each saved identity match header.attachments of its record.