Skip to main content

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.

API-only step

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 UserContact input 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

OptionTypeRequiredDefaultDescription
senderstringNo"IDnow"SMS sender name or number. Maximum 14 characters.
messageTemplatestringNo"Your code is {{otp}}. Do not share it."SMS message body. Must contain the {{otp}} placeholder exactly once. Maximum 160 characters.
maxValidationRetriesintegerNo3Maximum number of OTP validation attempts per generated code. Accepted values: 3 or 5.
maxGenerationRetriesintegerNo5Maximum 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

DatablockRequiredDescription
UserContactYesContains 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.

Schema-driven actions

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.

What GET /step returns when PHONE_VERIFY:v1 is not the active step

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 until stepType is PHONE_VERIFY:v1 or END

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 signal

The 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.

stepTypeMachine stateticketIdMeaning
STARTabsentSession created, Temporal not yet started
STARTabsentWorkflow running but no API-actionable step is active (Player step or transitional window)
PHONE_VERIFY:v1otpSentUUIDOTP sent; awaiting submission
PHONE_VERIFY:v1otpInvalidUUIDLast OTP was wrong; retries remaining
PHONE_VERIFY:v1otpExpiredUUIDOTP expired; user must request a new code
PHONE_VERIFY:v1generationLimitReachedUUIDAll generation attempts exhausted; validateOtp still accepted if the user has a code in hand
ENDabsentWorkflow finished; read output datablocks for the result

data fields (PHONE_VERIFY:v1 states)

FieldTypeDescription
validationRetriesLeftintegerNumber of remaining OTP submission attempts for the current code. Present on otpSent, otpInvalid, and generationLimitReached.
generationRetriesLeftintegerNumber of new codes that can still be generated. 0 in generationLimitReached.
otpExpiresAtstring (ISO 8601)Expiry timestamp of the current OTP. Present on otpSent, otpInvalid, and generationLimitReached; absent on otpExpired.
lastValidationResultstring | nullOutcome of the last validation attempt. null on first poll or after a successful resend. See values below.

lastValidationResult values:

ValueDescription
nullFirst poll after OTP sent, or after a resend
invalidOTP submitted but incorrect
expiredOTP was submitted after it expired
validation_limit_exceededAll validation attempts exhausted
generation_limit_exceededAll generation attempts exhausted

Available actions by state

StatevalidateOtpresendOtp
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:

HeaderRequiredDescription
X-Ticket-IdYesValue 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" }
FieldTypeValidationDescription
otpstring6 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).

Polling frequency

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

VerdictCondition
verifiedOTP submitted by the user matched the generated code
not_verifiedAll validation attempts exhausted, or all generation attempts exhausted
errorThe 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)

StatusCondition
400Request body failed schema validation (e.g. otp is not 6 digits)
401Missing or invalid Bearer token
403Token belongs to a different client than the session owner
404Session not found
409X-Ticket-Id mismatch (stale request), action not valid in current state, or generationRetriesLeft === 0 for resendOtp