Verifying phone number - API only (v1)
This is the current stable version of this step type.
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.
This step 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 data block. - 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 /action, keeping the flow paused until you respond.
Configuration
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
sender | string | No | — | Sender ID shown on the SMS (max 14 characters). Optional — defaults to the provider default when omitted. |
messageTemplate | string | No | — | Custom SMS message template. Must contain the {{otp}} placeholder exactly once. Maximum 160 characters. When omitted, the provider default template applies. |
maxValidationRetries | literal | No | 3 | Maximum number of OTP submission attempts per generated code. Accepted values: 3 or 5. |
maxGenerationRetries | integer | No | 5 | Maximum number of new OTP codes that can be generated per session. Once this limit is reached, further resendOtp requests are rejected. |
Example configuration
{
"maxValidationRetries": 3,
"maxGenerationRetries": 5
}
Input data blocks
| Data block | Required | Description |
|---|---|---|
UserContact | Yes | Contains the phoneNumber field in E.164 format (e.g. +33612345678). Provided at session creation time. |
Mid-workflow actions
Session created
→ GET /step → START ← 400 if CREATED; { stepType: 'START' } if no active step yet
→ GET /step → PHONE_VERIFY:v1
→ POST /actions { validateOtp }
→ GET /step → PHONE_VERIFY:v1
or PHONE_VERIFY:v1 ← resendOtp if generationRetriesLeft > 0; else submit OTP to exit as not_verified
or PHONE_VERIFY:v1 ← no more codes; validateOtp accepted (non-expired code only)
or END (not_verified) ← if OTP expires while in generationLimitReached
or END (completed)
→ ...
→ GET /step → END (completed) → read flow execution result
GET /step
While you have a session for a flow running that features this step, poll GET /step every 2-3 seconds to observe when the enduser reaches this step and discover which actions are available when they do.
Depending on the length & complexity of your flow, the enduser might be going through various steps, so keep polling this endpoint until stepType === PHONE_VERIFY:v1
stepType === PHONE_VERIFY:v1The stepType field being PHONE_VERIFY:v1 is the authoritative signal for polling logic — branch on it.
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": {
"type": { "const": "validateOtp" },
"otp": { "type": "string", "pattern": "^[0-9]{6}$" }
},
"required": ["type", "otp"]
}
},
{
"type": "resendOtp",
"schema": {
"type": "object",
"properties": {
"type": { "const": "resendOtp" }
},
"required": ["type"]
}
}
]
}
Response: data-object fields
| Field | Type | Description |
|---|---|---|
validationRetriesLeft | integer | Number of remaining OTP submission attempts for the current code. Present on all four states (otpSent, otpInvalid, otpExpired, generationLimitReached). In otpExpired it carries over the value from before expiry. |
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 all four states (otpSent, otpInvalid, otpExpired, generationLimitReached). In otpExpired it reflects the already-elapsed expiry time of the expired OTP. |
lastValidationResult | string | null | Outcome of the last validation attempt. Possible values of this field can be: - null -> on first poll or after a successful resend. - invalid -> OTP submitted but incorrect- expired -> OTP was submitted after it expired- generation_limit_exceeded -> All generation attempts exhausted |
When all validation attempts for a given code are exhausted, the workflow immediately routes to not_verified and the session ends (stepType: 'END')
POST /action
actions-array from GET /stepThe actions array in the GET /step response 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.
Request headers
| Header | Required | Description |
|---|---|---|
x-ticket-id | Yes | Value of ticketId from the last GET /step response |
The OTP expires after 5 minutes
Request for Action validateOtp
{ "type": "validateOtp", "otp": "123456" }
| Field | Type | Validation | Description |
|---|---|---|---|
otp | string | 6 digits ([0-9]{6}) | The code from the SMS |
Request for Action resendOtp
{ "type": "resendOtp" }
Requests a new code. The validation retry counter resets to maxValidationRetries. Only available when generationRetriesLeft > 0.
Response: Success
202 Accepted — the action is queued. Poll GET /step to observe the updated state.
Response: Error
| 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 |
Routes
| Route | Description |
|---|---|
verified | Phone number successfully verified. |
not_verified | Phone number could not be verified (OTP limit exceeded). |
Output data blocks
| Route | Data blocks produced |
|---|---|
verified | — |
not_verified | — |