Phone number verification (v1)
Verifies that an end-user owns a given phone number by sending an SMS OTP
Use when you need to confirm that a user owns a specific phone number as part of an onboarding or authentication flow. The Trust Platform sends a one-time code by SMS; your backend collects the code from the user and submits it via the sessions API. The step resolves once the code is validated, or when all retry attempts are exhausted.
PHONE_VERIFY:v1 has no Player UI. Your backend is responsible for presenting the OTP input screen to the end-user and submitting their response through the sessions API. See Mid-workflow actions below.
Key features
- SMS OTP delivery — Sends a one-time code to the phone number provided in the
UserContactinput datablock. - Configurable retries — Control how many times the user can attempt validation (
maxValidationRetries) and how many new codes can be requested (maxGenerationRetries). - Resend support — The end-user can request a new code at any time while retries remain. Resending resets the validation retry counter.
- Mid-workflow input — The step waits for your backend to submit an action via
POST /v1/{environment}/sessions/{sessionId}/actions, keeping the Temporal workflow paused until you respond.
Configuration
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
sender | string | No | "IDnow" | SMS sender name or number. Maximum 14 characters. |
messageTemplate | string | No | "Your code is {{otp}}. Do not share it." | SMS message body. Must contain the {{otp}} placeholder exactly once. Maximum 160 characters. |
maxValidationRetries | integer | No | 3 | Maximum number of OTP validation attempts per generated code. Accepted values: 3 or 5. |
maxGenerationRetries | integer | No | 5 | Maximum number of new codes that can be generated for the session. This value is stored in the workflow DSL and forwarded to the OTP service, which enforces the cap server-side. |
Input datablocks
| Datablock | Required | Description |
|---|---|---|
UserContact | Yes | Contains the phoneNumber field in E.164 format (e.g. +33612345678). Provided at session creation time. |
Pass userContact in the input object when creating the session:
POST /api/v1/flows/{flowId}/{environment}/sessions
{
"input": {
"userContact": {
"phoneNumber": "+33612345678"
}
},
"metadata": {
"subjectId": "your-internal-user-id"
}
}
Mid-workflow actions
While PHONE_VERIFY:v1 is running, poll GET /v1/{environment}/sessions/{sessionId}/step to observe the current state and discover which actions are available.
The actions array in the /step response lists only the actions available in the current state. Each entry carries a schema field — a JSON Schema object that describes the exact payload your backend must send to POST /actions. Use it to validate the request body before submitting. Never hardcode the list of accepted actions; always derive it from the actions array of the latest /step response.
GET /step returns { stepType: 'START', ... } in two situations:
- The brief window between session start and the first OTP being sent
- A step that is not API-exposed is running before or after
PHONE_VERIFY:v1(e.g. a Player-driven or headless step) — keep polling untilstepTypeisPHONE_VERIFY:v1orEND
A session with status CREATED (not yet started by the engine) returns 400.
GET /v1/{environment}/sessions/{sessionId}/step
Returns the current step directive. Poll until stepType is "END".
Response shape:
{
"stepType": "PHONE_VERIFY:v1",
"ticketId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"data": {
"validationRetriesLeft": 3,
"generationRetriesLeft": 4,
"otpExpiresAt": "2026-05-26T10:15:00Z",
"lastValidationResult": null
},
"actions": [
{
"type": "validateOtp",
"schema": {
"type": "object",
"properties": { "otp": { "type": "string", "pattern": "^[0-9]{6}$" } },
"required": ["otp"]
}
},
{
"type": "resendOtp",
"schema": { "type": "object", "properties": {} }
}
]
}
Step states
stepType is the only reliable signalThe stepType field is the authoritative signal for polling logic — branch on it, not on data fields. data is state-specific and its structure varies by step type; it is not present for START or END responses. The "Machine state" column below describes the internal state for documentation purposes; it is not a field in the API response.
stepType | Machine state | ticketId | Meaning |
|---|---|---|---|
START | — | absent | Session created, Temporal not yet started |
START | — | absent | Workflow running but no API-actionable step is active (Player step or transitional window) |
PHONE_VERIFY:v1 | otpSent | UUID | OTP sent; awaiting submission |
PHONE_VERIFY:v1 | otpInvalid | UUID | Last OTP was wrong; retries remaining |
PHONE_VERIFY:v1 | otpExpired | UUID | OTP expired; user must request a new code |
PHONE_VERIFY:v1 | generationLimitReached | UUID | All generation attempts exhausted; validateOtp still accepted if the user has a code in hand |
END | — | absent | Workflow finished; read output datablocks for the result |
data fields (PHONE_VERIFY:v1 states)
| Field | Type | Description |
|---|---|---|
validationRetriesLeft | integer | Number of remaining OTP submission attempts for the current code. Present on otpSent, otpInvalid, and generationLimitReached. |
generationRetriesLeft | integer | Number of new codes that can still be generated. 0 in generationLimitReached. |
otpExpiresAt | string (ISO 8601) | Expiry timestamp of the current OTP. Present on otpSent, otpInvalid, and generationLimitReached; absent on otpExpired. |
lastValidationResult | string | null | Outcome of the last validation attempt. null on first poll or after a successful resend. See values below. |
lastValidationResult values:
| Value | Description |
|---|---|
null | First poll after OTP sent, or after a resend |
invalid | OTP submitted but incorrect |
expired | OTP was submitted after it expired |
validation_limit_exceeded | All validation attempts exhausted |
generation_limit_exceeded | All generation attempts exhausted |
Available actions by state
| State | validateOtp | resendOtp |
|---|---|---|
otpSent | ✅ | ✅ (only if generationRetriesLeft > 0) |
otpInvalid | ✅ | ✅ (only if generationRetriesLeft > 0) |
otpExpired | ❌ | ✅ (only if generationRetriesLeft > 0) |
generationLimitReached | ✅ | ❌ |
POST /v1/{environment}/sessions/{sessionId}/actions
Submit an action for the current step. Pass the ticketId returned by GET /step in the X-Ticket-Id request header. A mismatched or stale ticketId returns 409 Conflict.
Headers:
| Header | Required | Description |
|---|---|---|
X-Ticket-Id | Yes | Value of ticketId from the last GET /step response |
Response: 202 Accepted — the action is queued. Poll GET /step to observe the updated state.
validateOtp
{ "type": "validateOtp", "otp": "123456" }
| Field | Type | Validation | Description |
|---|---|---|---|
otp | string | 6 digits ([0-9]{6}) | The code from the SMS |
resendOtp
{ "type": "resendOtp" }
Requests a new code. The validation retry counter resets to maxValidationRetries. Only available when generationRetriesLeft > 0.
Polling lifecycle
Session created → GET /step → START ← 400 if CREATED; { stepType: 'START' } if no active step yet
→ GET /step → PHONE_VERIFY:v1 (otpSent)
→ POST /actions { validateOtp }
→ GET /step → PHONE_VERIFY:v1 (otpInvalid)
or PHONE_VERIFY:v1 (otpExpired) ← resendOtp to get a new code
or PHONE_VERIFY:v1 (generationLimitReached) ← no more codes; validateOtp still accepted
or END (completed)
→ ...
→ GET /step → END (completed) → read flow execution result
Stop polling when stepType === "END". Check the flow execution result via GET /v1/{environment}/sessions/{sessionId} to retrieve the workflow outcome (accepted / rejected).
Poll every 2–3 seconds. There is no server-push mechanism for this step — polling is the intended pattern. Avoid polling faster than once per second to stay within rate limits.
Verdicts
| Verdict | Condition |
|---|---|
verified | OTP submitted by the user matched the generated code |
not_verified | All validation attempts exhausted, or all generation attempts exhausted |
error | The OTP service returned an unexpected error (4xx/5xx) |
Output datablocks
PHONE_VERIFY:v1 does not produce output datablocks. The workflow outcome (accepted / rejected) is determined by how the flow definition routes the step's verdict ports.
Error responses (actions endpoint)
| Status | Condition |
|---|---|
400 | Request body failed schema validation (e.g. otp is not 6 digits) |
401 | Missing or invalid Bearer token |
403 | Token belongs to a different client than the session owner |
404 | Session not found |
409 | X-Ticket-Id mismatch (stale request), action not valid in current state, or generationRetriesLeft === 0 for resendOtp |