## Before you start

Make sure your RiskOS™ environment is provisioned with:

[A workflow configured](https://help.socure.com/riskos/docs/otp-riskos-setup#/) 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:
>
> - **Force Delivery Failure**: Use phone number `+11234567890`.
> - **Force Invalid Code**: Enter `000000` during the verification step. (Any other 6 digits will succeed).

### Choose your environment

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

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

- **No real customer data**
- **Free testing environment**
- **Unlimited API calls**

```undefined
  https://riskos.socure.com/api/evaluation
```

- **Real customer data**
- **API usage charges apply**
- **Connects to live data services**

### Get an API key

1. In the Sandbox RiskOS™ Dashboard, go to [**Developer Workbench > API Keys**](https://riskos.sandbox.socure.com/developers/api-keys).
2. Copy your API key securely.

1. In the Production RiskOS™ Dashboard, go to [**Developer Workbench > API Keys**](https://riskos.socure.com/developers/api-keys).
2. 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.

- **The "Wait" State**: Because OTP is an asynchronous process, RiskOS™ pauses the evaluation. You receive a `REVIEW` decision and an `ON_HOLD` status.

#### Key requirements

- **SMS/Voice OTP**: Include `phone_number` in **E.164 format** (for example, +14155550100).
- **Email OTP**: Include `email` address in valid **RFC-5322 format** (for example, [name@example.com](mailto:name@example.com)).

#### Example request

cURL

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

- **UI Action**: When your frontend sees the "sub_status": "Pending OTP Code", transition to the 6-digit code entry screen.

#### Example response

JSON

```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:**
>
> - Use `otp.resend` to re-deliver the **same code** on the same channel. It doesn't skip the OTP step.
> - Use `actions.resume` to **skip** the paused OTP step (for example, to switch delivery channels). It doesn't send a code.

#### 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**.

- **Same-Code Guarantee**: Re-sends the existing code to ensure both the original and new messages are valid if they arrive out of order.
- **Persistence**: Re-sending the code does **not** reset the 5-attempt verification limit.
- **TTL Constraint**: This does **not** extend the 10-minute session expiration. If the original session has 2 minutes remaining, the resent code will also expire in 2 minutes.

cURL

```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
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
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
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

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

- **API Message**: "Max verify attempts reached"
- **Cooling Period**: A 10-minute lockout is enforced per destination (phone or email).
- **UI Action**: Disable input fields/buttons, display a 10-minute countdown timer, and prevent new `POST` requests for that specific destination until the timer expires.

#### Example error response

JSON

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