Integrate One-Time Passcode (OTP)

Before you start

Make sure your RiskOS™ environment is provisioned with:

A workflow configured for the One-Time Passcode (OTP) enrichment.

Postman Collection

Use the following Postman collection to test the OTP enrichment with the Evaluation endpoint.

📘

Testing Failure Paths in Sandbox:

Use these "Magic Values" to force specific error responses:

Choose your environment

Start with Sandbox for development and testing, then move to Production for live applications.

  https://riskos.sandbox.socure.com/api/evaluation
  https://riskos.socure.com/api/evaluation

Get an API key

  1. In the Sandbox RiskOS™ Dashboard, go to Developer Workbench > API Keys.

  2. Copy your API key securely.

  3. In the Production RiskOS™ Dashboard, go to Developer Workbench > API Keys.

  4. Copy your API key securely.

How it works

The OTP integration follows an asynchronous pattern. Unlike a standard identity check, the API pauses and waits for the user to enter their code.

  1. Initiate delivery: Send a POST /api/evaluation request with the user’s contact information (phone number and/or email).
  2. Wait State: RiskOS™ creates a verification session and delivers the 6-digit code via SMS, Voice, or Email. The API returns an ON_HOLD status and a unique eval_id.
  3. Verify and Resume: Once the user enters the code into your UI, submit a PATCH /api/evaluation/{eval_id} request to validate the code.
  4. Final Decision: RiskOS™ resumes the workflow, applies final scoring logic, and returns a terminal decision (ACCEPT, REJECT, or REVIEW).

Integration flow

Your integration logic must handle the "In-Between" states, such as a user requesting a code resend or deciding to switch from an SMS delivery to an email delivery if they have poor cell reception.

RiskOS™ClientAppUserRiskOS™ClientAppUserPauses evaluationopt[User requests Resend]opt[User switches Channel (e.g., SMS to Email)]alt[OTP Valid][OTP Invalid/Expired]Enter phone/emailPOST /evaluation200 OK (status: ON_HOLD, sub_status: Pending OTP Code)Deliver OTP (SMS/Email/Voice)Input 6-digit codePATCH /evaluation/{eval_id} (otp.code: "123456")200 OK (status: CLOSED, decision: ACCEPT)400 Bad Request (msg: "Invalid code")PATCH /evaluation/{eval_id} (otp.resend: true)Re-deliver same codePATCH /evaluation/{eval_id} (actions.resume: true)POST /evaluation (new email payload)Deliver new code to Email

Quick response reference

Use this table to map RiskOS™ API responses to your application's routing and logic.

User Action status sub_status decision Your Application Action
Initial POST (Success) ON_HOLD Pending OTP Code REVIEW Transition UI to the 6-digit code entry screen.
Enters correct OTP (KYC Complete) CLOSED Approve ACCEPT Redirect to Success/Onboarding Complete page.
Enters correct OTP (KYC Incomplete) ON_HOLD More information needed REVIEW Prompt user for manual PII (Name, Address, SSN) and PATCH evaluation using eval_id.
Enters incorrect OTP ON_HOLD Pending OTP Code REVIEW Show "Incorrect code" error; display remaining attempts.
5 failed attempts CLOSED Max attempts reached REJECT Disable inputs; start 10-minute cooling period timer.
Requests OTP resend ON_HOLD Pending OTP Code REVIEW Notify user that a new message was sent; keep UI on entry screen.
Switches channel ON_HOLD Pending OTP Code REVIEW Call PATCH (resume: true) to clear the hold, then a new POST for the new channel.
Session expires CLOSED Decline REJECT The 10-minute OTP window has elapsed. The enrichment-level response.status returns "reject" because the verification session was destroyed on expiration. Show a "Session Expired" screen and prompt the user to restart with a new evaluation.

One-Time Passcode implementation timeline

Step 1: Initiate delivery (POST)

Submit the applicant's contact details to start the session. This triggers the initial OTP delivery via SMS, Voice, or Email depending on your workflow configuration.

Key requirements

Example request

cURL

curl -X POST https://riskos.sandbox.socure.com/api/evaluation \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
  "id": "otp_session_123",
  "workflow": "your_otp_workflow",
  "data": {
    "individual": {
      "phone_number": "+14155550100",
      "email": "user@example.com"
    }
  }
}'

Step 2: Handle the "On-Hold" response

Your application must capture the eval_id from the response. This ID is the unique "key" for the user's current session. By default, the OTP remains active for 10 minutes. If the user doesn't authenticate with that OTP within that time frame, you must execute a new OTP request in the workflow and get a new eval_id.

Example response

JSON

{
  "decision": "REVIEW",
  "eval_id": "123e4567-e89b-12d3-a456-426614174000",
  "status": "ON_HOLD",
  "sub_status": "Pending OTP Code"
}

Step 3: Manage "In-Between" UX logic

While the evaluation is ON_HOLD, use the eval_id to handle non-linear user actions without breaking the session.

📘

resend vs resume — these are different actions:

Scenario: User didn't receive the code (Resend)

If the user requests a resend, do not initiate a new evaluation. Use a PATCH request with the resend flag to deliver the same 6-digit code.

cURL

curl -X PATCH https://riskos.sandbox.socure.com/api/evaluation/{eval_id} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
  "id": "resend_request_001",
  "timestamp": "2026-02-13T16:38:37Z",
  "workflow": "your_otp_workflow",
  "data": {
    "individual": {
      "otp": { "resend": true }
    }
  }
}'

Scenario: User made a typo (change number)

If a user needs to change their destination address (phone or email), follow this two-step sequence to ensure the session updates correctly.

1. Clear the session. Send a PATCH request with "resume": true to tell RiskOS™ to stop waiting for the old code:

cURL

curl -X PATCH https://riskos.sandbox.socure.com/api/evaluation/{eval_id} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
  "id": "clear_session_typo",
  "workflow": "your_otp_workflow",
  "actions": { "resume": true }
}'

2. Initiate new evaluation. Submit a fresh POST with the updated phone_number or email:

cURL

curl -X POST https://riskos.sandbox.socure.com/api/evaluation \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
  "id": "new_otp_attempt_002",
  "workflow": "your_otp_workflow",
  "data": {
    "individual": {
      "phone_number": "+14155559999"
    }
  }
}'

Scenario: Switching channels (SMS to Email)

If a user has poor cell reception and wants an email instead, you must pivot the delivery method mid-stream.

📘

Note: Users can typically only change channels once per session.

1. Resume. Call PATCH with "resume": true to bypass the active SMS step.

2. Initiate Email. Call POST /api/evaluation with the user's email address.

Step 1: Bypass the active SMS step.

curl -X PATCH https://riskos.sandbox.socure.com/api/evaluation/{eval_id} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
  "id": "clear_session_switch",
  "workflow": "your_otp_workflow",
  "actions": { "resume": true }
}'

Scenario: OTP session expires (10-minute window elapses)

Each OTP session has a 10-minute time-to-live (TTL) that starts when the code is first delivered. If the user does not submit a valid code via PATCH within this window, the OTP provider destroys the verification session and the evaluation closes.

What happens when the session expires

  1. The OTP provider destroys the verification session resource after 10 minutes.
  2. If a PATCH with otp.code is submitted after expiration, the enrichment-level response.status returns "reject".
  3. The evaluation closes with `status: "CLOSED", sub_status: "Decline", and decision: "REJECT".

Example expired-session response

JSON

{
  "decision": "REJECT",
  "eval_id": "123e4567-e89b-12d3-a456-426614174000",
  "status": "CLOSED",
  "sub_status": "Decline",
  "data_enrichments": [
    {
      "enrichment_name": "Verify OTP SMS",
      "enrichment_provider": "SocureOTP",
      "status_code": 200,
      "request": {
        "otp": "691573",
        "verificationId": "a8ae056a-497d-41b3-ac43-66613506c9ba"
      },
      "response": {
        "attemptCount": 1,
        "status": "reject",
        "verificationId": "a8ae056a-497d-41b3-ac43-66613506c9ba"
      }
    }
  ]
}

Scenario: Max attempts reached (Lockout)

Users have 5 attempts maximum to enter the correct code. This limit is persistent across the entire session; requesting a code resend does not reset the attempt counter. After the 5th failure, the API returns a 400 Bad Request and terminates the session.

Example error response

JSON

{
  "status": 400,
  "error": "Bad Request",
  "message": "Max verify attempts reached"
}

Step 4: Verify the code (PATCH)

Once the user enters the code into your interface, submit it back to RiskOS™ to resume the evaluation. You must use the eval_id captured in Step 2: Handle the "On-Hold" response within the URL path.

This request validates the code and automatically triggers the next steps in your workflow (for example, final risk scoring or fraud checks).

cURL

curl -X PATCH https://riskos.sandbox.socure.com/api/evaluation/{eval_id} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
  "id": "verify_step_001",
  "timestamp": "2026-02-13T16:38:37Z",
  "workflow": "your_otp_workflow",
  "data": {
    "individual": {
      "otp": { "code": "123456" }
    }
  }
}'

Step 5: Receive final decision

If the code is correct, RiskOS™ automatically resumes the workflow to perform final scoring. The session typically moves from ON_HOLD to a terminal CLOSED status.

A successful OTP verification typically results in an ACCEPT decision, though this depends on your specific workflow rules and other risk signals.