# OTP Email Verification

Banxa Native supports headless OTP: your app collects the customer's email address, triggers the verification flow server-side, and handles the code entry UI natively. No Banxa-hosted screens, no redirects.

If your app has a login mechanism, the OTP requirement can be disabled entirely — contact your Banxa integration manager. If your app does not have a login mechanism, headless OTP is required before you can process a transaction.

## When to use OTP verification

OTP is used to verify a customer's email address as part of identity establishment. It is relevant when:

- Your app does not have its own login or authentication mechanism and you need Banxa to verify the customer's email access
- A customer's identity was created without an email address (sparse identity) and you want to collect and verify email via OTP — if you only need to add an email without verification, use the identity patch endpoint instead


If OTP is not enabled on your account, the endpoints return `403 OtpFeatureNotEnabled`.

## Endpoints

Both endpoints use HMAC authentication. See [Authentication](/products/native-api/docs/getting-started/authentication) for signing details.

| Action | Endpoint |
|  --- | --- |
| Send OTP code | `POST /eapi/v1/verifications/otp` |
| Verify OTP code | `POST /eapi/v1/verifications/otp/verify` |


## The verification flow


```mermaid
sequenceDiagram
    participant App
    participant YourBackend as Your backend
    participant Banxa

    App->>YourBackend: Customer submits email
    YourBackend->>Banxa: POST /eapi/v1/verifications/otp
    Banxa-->>YourBackend: 200 { message: "OTP sent successfully" }
    Banxa->>App: Email with OTP code (via customer's inbox)
    App->>App: Customer enters OTP code
    App->>YourBackend: Code submitted
    YourBackend->>Banxa: POST /eapi/v1/verifications/otp/verify
    Banxa-->>YourBackend: 200 { message: "Success" }
    YourBackend-->>App: Email verified — proceed
```

## Step 1 — Send the OTP code

**`POST /eapi/v1/verifications/otp`**


```json
{
  "identityReference": "customer-12345",
  "email": "user@example.com"
}
```

| Field | Type | Description |
|  --- | --- | --- |
| `identityReference` | string | Your stable identifier for this customer |
| `email` | string | The email address to verify |


**Success response (200):**


```json
{
  "message": "OTP sent successfully"
}
```

Banxa sends a short-lived 4-digit code to the provided email address. Your app should prompt the customer to check their inbox and enter the code promptly.

**Rate limit:** 3 requests per minute per customer.

## Step 2 — Verify the OTP code

**`POST /eapi/v1/verifications/otp/verify`**


```json
{
  "identityReference": "customer-12345",
  "email": "user@example.com",
  "code": "1234"
}
```

| Field | Type | Description |
|  --- | --- | --- |
| `identityReference` | string | Same value used in the send request |
| `email` | string | Same email address used in the send request |
| `code` | string | 4-character code from the email |


**Success response (200):**


```json
{
  "message": "Success"
}
```

On success, the email address is recorded as verified on the customer's identity. You can proceed with eligibility checks and ramp creation.

**Rate limit:** 4 verification attempts per minute per customer.

## Sparse identity and email collection

OTP can be used to collect and verify a customer's email address after identity creation. An identity created without an email address is valid — the OTP send call writes the email onto the identity record on successful verification.

This pattern is useful when your onboarding flow collects email separately from identity creation:

1. Create identity via `POST /eapi/v0/identities/basic` — omit the `email` field
2. When ready to verify the customer's email, call `POST /eapi/v1/verifications/otp` with the `identityReference` and `email`
3. The email is written onto the identity record on successful verification


## Email merging

If the email address supplied in the OTP flow already exists on a different Banxa identity record, Banxa merges the two records. The `identityReference` from the newer identity is moved onto the existing account; the newer identity and its associated `externalCustomerId` are removed.

**Practical effect:** after a merge, the `identityReference` you supplied continues to work, but it now refers to the older account. Any KYC state from the older account is carried over.

**What to watch for:** if a customer uses an email address that belongs to an existing Banxa account (e.g. they previously transacted via a different partner), the merge will silently absorb their new identity into the existing one. The customer's KYC history is preserved, but be aware that the older account's state governs their compliance tier.

## Error handling

### Wrong OTP code

A `422` response indicates the code is incorrect:


```json
{
    "message": "Code does not match, please try again",
    "code": 180,
    "traceId": "2f92b534-55a0-4340-9e2a-2575913ff792"
}
```

An incorrect code cannot be corrected — the OTP flow must restart from the beginning. Call `POST /eapi/v1/verifications/otp` again to issue a new code before prompting the customer to re-enter.

### Rate limit exceeded

A `429` response is returned when the per-minute limit is reached:


```json
{
  "message": "Too many OTP requests. Please try again later.",
  "code": 429,
  "traceId": "9fafad58-f1eb-44db-a77e-329610b1b755"
}
```

| Limit | Threshold |
|  --- | --- |
| OTP send | 3 per minute per customer |
| OTP verify | 4 per minute per customer |


Present a clear wait-state to the customer rather than retrying silently.

### Feature not enabled

A `403` response means OTP is not enabled on your merchant account. Contact your Banxa integration manager.

### Invalid identity reference

A `422` response with `"The selected identity reference is invalid."` means the `identityReference` does not exist in Banxa's system. Ensure identity creation has completed successfully before triggering OTP.

## Interaction with Banxa Hosted Checkout

If your integration also uses Banxa Hosted Checkout, headless OTP suppresses the email verification screen that Banxa Hosted Checkout would otherwise show to the customer.

## Summary

| Step | Endpoint | Rate limit |
|  --- | --- | --- |
| 1. Send OTP | `POST /eapi/v1/verifications/otp` | 3/min |
| 2. Verify code | `POST /eapi/v1/verifications/otp/verify` | 4/min |


On wrong code: restart from step 1. On rate limit: wait and retry. On success: email is verified on the identity record and the customer can proceed.