Some workflows begin with a collect_user_info step that gathers the applicant’s identity details (their name, and optionally an email, phone, date of birth, or address) before identity verification. The embedded capture modules do not render this step. Instead, collect these fields in your own form and satisfy the step directly with collectUserInfo, then mount the journey so it opens on the verification step.
collectUserInfo is a small standalone function, not a mounted module. It renders no UI, needs no container element, no stylesheet, and no registration import. It reuses the same zero-lifecycle token model as mount(), so you write one fetchToken callback and nothing else.
Two ways to satisfy the step
Section titled “Two ways to satisfy the step”There are two ways to satisfy a collect_user_info step so the embedded journey can advance to verification:
- Seed it when you create the verification (no UI needed). If you already hold the applicant’s details, pass them on the inline
applicantobject of yourPOST /v1/sessionscall. When the seed covers the fields the step requires, a name and a date of birth (and a structured address if Address is enabled), the step is marked satisfied at creation and skipped, and the journey opens on the verification step. The exact key names differ by plane, and both spellings are listed at the end of this item: a rawPOST /v1/sessionstakes the snake-case keys, and thesessions.createhelper takes the camelCase ones. This works only on the inlineapplicantpath, not when you reference an existingapplicant_id. When abackground_*check is in the workflow, thecollect_user_infostep is required with Date of birth enabled. You cannot remove it. Seeding is how you skip an applicant-facing form without changing the workflow template. A complete seed means a name anddate_of_birth, plus a fulladdresswhen Address is enabled: for a US or CA applicant that includesregion(state or province) andpostal_code. For the name, sendfamily_namewithgiven_names. A surname of several words stays ONEfamily_namevalue, and a middle name is simply anothergiven_namesentry:family_name: "Garcia Lopez"withgiven_names: ["Jose", "Maria"]. A person with one name sendsfamily_namewith an emptygiven_namesarray. A whole undivided name is not part of this path’s name shape. The raw REST wire and the SDK helper each refuse it, but in different ways. On the raw REST wirelegal_nameis a retired key, so sending it returns a422 validation_errornaming the replacement and its shape. Thesessions.createSDK helper instead maps a fixed set of named fields onto the wire, andlegalNameis not among them: TypeScript rejects it at compile time when the applicant object is passed inline, and at runtime the helper simply never reads it, so it reaches neither the wire nor an error. Note this helper runs on your server and throws if it detects a browser, so nothing about this path happens client-side. If the seed then carries no name at all, the create still returns 201 with acollect_user_info_not_presatisfiedwarning, described below, and the applicant is prompted during the journey. The applicant request wire takes the name in parts only, because a background check reads the family name and the given names as separate values, and a whole name is never split into them.reference_nameis your own display label and does not count as a name, and neither doesname_suffixon its own. Neither doesbooking_name, which is the name the reservation, booking, or payment holds: it is compared against the ID document by the Name match check when that check is enabled, but it is never the applicant’s display name, it never reaches a background check, and it never pre-satisfies this step. Bothreference_nameandbooking_nameare write-only, so neither is echoed back on a read. On the raw REST wire these keys are snake_case (family_name,given_names,name_suffix,date_of_birth,address.postal_code); thesessions.createSDK helper accepts the camelCase equivalents (familyName,givenNames,nameSuffix,dateOfBirth,address.postalCode) and maps them for you. If the seed is incomplete the create still returns 201 and the response carries awarningsarray naming the missing field (acollect_user_info_not_presatisfiedwarning), so the step is not skipped and the applicant is prompted for it during the journey. - Submit it from your own form with
collectUserInfo(the rest of this page). Use this when the details are entered at check-in rather than known up front.
Both paths satisfy the same step; pick per applicant based on whether you already have the data.
The client token
Section titled “The client token”Pass a fetchToken callback that returns your durable client token, the SAME token you pass to Checktiv.mount(). Your backend mints it once per applicant; the SDK exchanges it for a short-lived working token and runs the collect submit on that token. You never manage token refresh, and the applicant’s client token is the only credential in the browser.
import { collectUserInfo } from '@checktiv/sdk-web/collect-user-info';
const collector = collectUserInfo({ session: { publishableKey: 'ah_pk_us_test_...', fetchToken: () => fetch('/kyc/session', { method: 'POST' }) .then((res) => res.json()) .then((data) => data.clientToken), },});On the first-party path (no publishable key), pass region and mode instead of publishableKey.
Submit collect before you mount the journey
Section titled “Submit collect before you mount the journey”Satisfy the collect step BEFORE you call mount() (or render <ChecktivJourney>). When the collect step is already satisfied, the journey opens on the verification step. If you mount first, the journey lands on a collect_user_info step the embedded modules cannot render, and it never advances.
The recommended flow:
- Render your own form and call
collector.submit(...). - When the result is
ok, mount the journey.
Session-driven fields with describe()
Section titled “Session-driven fields with describe()”Always send a name: either familyName with givenNames, or legalName on its own. Sending both is accepted. A nameSuffix on its own does not count as a name.
Three different things enforce that, and only the third is a server rejection:
- TypeScript, at compile time. A submission carrying neither shape does not typecheck, as long as the object is passed to
submit()inline. A spread or a pre-built variable defeats the check. - Your own form, at runtime. There is no general server-side gate requiring a name, so a JavaScript host that submits without one, but is otherwise complete, gets a success and the applicant is left with no name recorded unless one was seeded at creation. Make your name inputs required.
- The server, but only when a check needs the parts. If the verification runs a background check, a submission without
familyNameandgivenNamesis rejected withname_components_required.describe()reports that up front asnameComponents: 'required';'requested'and'optional'are not enforced.
When you send both, they are stored as two independent answers and are never merged. legalName then takes precedence wherever the applicant’s name is shown or compared: the operator console, the report PDF, sanctions screening, the identity name the document name match compares, and rule fields. So send legalName only when it is the name the applicant gave you for this verification. A stale whole-name string, for example one carried over from a booking record, overrides the parts you collected. The document name match compares every name on file, so an identity name is not the only thing it looks at: a booking_name sent at session create or on the applicant is compared as well, separately, and a mismatch on either one sends the verification to review. That is also why a booking name belongs in booking_name and not in legalName. See Choose the right check types.
Every other field is optional and gated by the operator’s workflow template. Rather than hardcode which fields to show, call describe() and render your form from it:
const config = await collector.describe();
if (config.ok) { // config.fields is a subset of: 'email', 'phone', 'dob', 'address' // (the optional contact fields the template collects). // config.nameComponents is 'required', 'requested', or 'optional'. // 'required' means this verification runs a check that needs the name // in separate parts, so a submission without them is rejected. renderForm(config.fields, config.nameComponents);} else { // config.code is the same reason taxonomy submit() uses (see below). For // example, 'not_collect_step' means the journey is not on a collect step.}nameComponents
Section titled “nameComponents”nameComponents reports what the server enforces for this verification:
| Value | What it means | What to render |
|---|---|---|
'required' |
A check in this verification reads the name in separate parts. A submission without them is rejected with name_components_required. |
Render the family-name and given-names inputs and make them required. |
'requested' |
The workflow template asks for the separate parts. A submission without them is still accepted. | Render the inputs. Do not make them required. |
'optional' |
Neither. A legalName on its own is accepted. |
Render the inputs if you want them. legalName alone is enough. |
Never hide the family-name and given-names inputs. They are always an accepted name shape, and they are the only shape a background check can use.
Why the parts and not a full name: a background check reads the family name and the given names as separate values, and a full name is never split into them. Guessing which words are the surname would screen a different person, so a legalName on its own cannot be used for those checks. A person with one name sends familyName with an empty givenNames array, which is a complete answer.
captureStructuredName is deprecated and is removed in 1.10.0. It is still returned until then, but it reports only what the workflow template declares, not what the server enforces, and its default is false, so it reads false even on a workflow whose background check cannot run without the name components. Read nameComponents instead, and remove any code that branches on captureStructuredName before you upgrade to 1.10.0. Note that this deprecation does not show up in your editor: the published type declarations ship without comments, so this page is the notice. See Versioning for the deprecated-field removal policy.
Render one input for the family name, one or more inputs for the given names, and one input for a suffix. What matters is that every input maps to exactly ONE captured value, and that nothing you render is split or joined on the way to the wire.
Two shapes satisfy that, and both are supported:
- A repeatable given-names input, where the applicant adds one entry per given name. Label it “Given names”.
- Labeled boxes: “First name” for
givenNames[0], “Middle name(s)” for the next entry, and “Suffix” fornameSuffix. This is what the hosted journey renders, because it asks less of the applicant than a repeater does. It is a labeling choice on top of the same array, not a first/middle/last model: whatever the applicant types into one box becomes ONE entry, however many words it holds, so “Mary Ann” in a first-name box is a single given name.
Whichever you render, do not split a box on whitespace, and put a suffix in nameSuffix rather than in a given-name entry or on the family name.
describe() is a pre-render probe, so its failure result carries only a code, not a user-visible message. The server always re-validates the submit body, so if a required field is missed, the applicant sees a validation_failed on submit rather than a dead end.
Submitting the collected fields
Section titled “Submitting the collected fields”submit() posts the fields you collected. It never throws for a server or network error. It resolves a discriminated result you branch on:
const result = await collector.submit({ // The name. Send these two together, or send legalName on its own. familyName: 'Lovelace', givenNames: ['Ada'], // Optional, and never a givenNames entry: it is displayed last. nameSuffix: 'Jr.', email: 'ada@example.com', // phone, dateOfBirth ('YYYY-MM-DD'), and address are also optional and // gated by the template.});
if (result.ok) { // The collect step is satisfied. Mount the journey now. mountJourney();} else { // result.code is a stable, machine-readable reason. // result.message is an actionable, user-visible string. showError(result.message);}Send only the fields you collected. Send a name, as either familyName plus givenNames or legalName alone, per the three enforcement points above. When describe() reports nameComponents: 'required', only the first shape is accepted. Omit any optional field the applicant did not fill. Every name field rejects a value that is blank or only spaces, so omit a field rather than sending an empty string.
submit() forwards only the fields it knows (legalName, familyName, givenNames, nameSuffix, email, phone, dateOfBirth and address). A misspelled or retired field name is discarded in the browser and never reaches the server, and the SDK logs one console warning reporting how many keys it discarded and naming the ones safe to print. Discarding is not the same as succeeding: the call then stands or falls on what remains, so if the discarded key was carrying the name, the server rejects the submission (validation_failed, or name_components_required when a background check needs the parts) rather than storing a nameless applicant. TypeScript hosts that pass the object inline get a compile error instead, though a spread or a pre-built variable defeats that check. Search your integration for stale name keys rather than relying on a failed request to find them.
Dates are ISO YYYY-MM-DD strings with no time component. An address is a structured object (line1, city, country, and country-conditional region / postalCode).
familyName and givenNames are the structured name. familyName is the applicant family name or surname, exactly as printed on their ID; it can hold several words, such as a double surname or a name with a particle. givenNames is an ordered array with one entry per given name, so a name beyond the first is simply another entry and there is no separate middle-name field. One entry can hold more than one word. Send an empty array for a person with one name: that is a complete answer, not a missing one. Entries are stored exactly as supplied, and they are never split apart or joined together.
nameSuffix is a generational or honorific suffix such as Jr., Sr. or III. Send it on its own field, never as a givenNames entry and never appended to familyName. A full name is composed as the given names, then the family name, then the suffix, so a suffix carried inside givenNames reads as Martin Luther Jr. King wherever the applicant’s name is shown. Omit the key when the applicant has none. A suffix on its own does not name anybody, so it never satisfies the name requirement: send it alongside familyName and givenNames, or alongside legalName.
These examples show the shapes that a single name field cannot carry:
// A double surname with two given names.{ legalName: 'Jose Maria Garcia Lopez', familyName: 'Garcia Lopez', givenNames: ['Jose', 'Maria'] }
// A particle stays inside the one entry the applicant typed it in.{ legalName: 'Jan van der Berg', familyName: 'van der Berg', givenNames: ['Jan'] }
// A family name in a non-Latin script, kept exactly as printed.{ legalName: '山田 太郎', familyName: '山田', givenNames: ['太郎'] }
// A mononym: the whole name is the family name, and the list is empty.{ legalName: 'Sukarno', familyName: 'Sukarno', givenNames: [] }
// A generational suffix, on its own field so it is displayed last.{ legalName: 'Martin Luther King Jr.', familyName: 'King', givenNames: ['Martin', 'Luther'], nameSuffix: 'Jr.' }Submit result codes
Section titled “Submit result codes”When result.ok is false, result.code is one of:
| Code | Cause | What to do |
|---|---|---|
not_collect_step |
The current step does not collect user info. There is nothing to submit here. | If the collect step is already satisfied, mount the journey. Otherwise, fix the workflow template: move the collect_user_info step before the current step, or provide the applicant fields when you create the verification (via POST /v1/sessions) so the step is satisfied automatically. Do not mount the journey to reach it. |
validation_failed |
The server rejected the body: a bad shape, or a required field was missing. | Show the message, fix the fields, and retry. Check that the body names the applicant, and that it carries the fields describe() reported. |
name_components_required |
This verification runs a check that reads the name in separate parts, and the body carried neither familyName nor a given name. A legalName on its own does not satisfy it. |
Collect the family name and the given names and retry. Call describe() before you render the form: it reports nameComponents: 'required' for these verifications, so a form built from it never hits this. |
token_expired |
The client token could no longer be exchanged after one automatic retry. | Start a new session so fetchToken returns a fresh client token, then retry. |
origin_not_allowed |
The page origin is not allowed for the publishable key. | Add the origin in the console under Developers, API keys. See API keys. |
rate_limited |
Too many requests in a short window. | Wait a moment and retry. |
service_unavailable |
A temporary server error, including the brief processing race after a submit. | Retry in a few seconds. |
session_expired |
The verification session is no longer valid. | Start a new verification from your backend. |
wrong_token_type |
fetchToken returned the wrong kind of token for the data plane. |
Return the client token, the same one you pass to Checktiv.mount(). |
network_error |
The request could not be sent (offline, DNS, or an aborted request). | Check the connection and retry. |
The ./react subpath exposes the same collector through the useCollectUserInfo hook. Pass the same session (a scope plus a fetchToken) you give <ChecktivJourney>:
import { useCollectUserInfo } from '@checktiv/sdk-web/react';
const collector = useCollectUserInfo({ publishableKey: 'ah_pk_us_test_...', fetchToken });// collector.describe() and collector.submit(...) work exactly as above.The hook builds the collector once per mount and returns a stable reference, so a fresh session object on a later render does not rebuild it. See React for the full example.
Related pages
Section titled “Related pages”- Modules overview - the full public surface and the self-registration model
- React - the
useCollectUserInfohook and the journey wrappers - Quickstart - end-to-end integration walkthrough
- Error reference - error codes and the processing signal