> ## Documentation Index
> Fetch the complete documentation index at: https://docs.senderz.app/llms.txt
> Use this file to discover all available pages before exploring further.

# OTP API

> Send and verify one-time passcodes over SMS or voice.

The OTP API sends one-time passcodes to a phone number over SMS or voice, and verifies them. It's transactional — no marketing consent, quiet hours or frequency caps — but still wallet-gated and sender-ID checked.

Both endpoints authenticate with the [`X-Sender-Tenant` + `X-API-Key`](/en/api-reference/authentication) headers.

## Send a code

```http theme={null}
POST /api/v1/otp/send
```

### Body

<ParamField body="phone" type="string" required>
  Recipient phone number (max 32). Israeli-local or E.164 is normalised to
  `+972…`.
</ParamField>

<ParamField body="channel" type="string" default="sms">
  `sms` or `voice`. Voice speaks the code digit by digit.
</ParamField>

<ParamField body="message" type="string">
  Custom message template (max 320). Use `{{code}}` where the code should
  appear.
</ParamField>

<ParamField body="senderId" type="string">
  SMS sender ID / caller ID (max 11). Must be an approved sender ID; defaults
  to the platform pool.
</ParamField>

### Response

<ResponseField name="verificationId" type="string">
  Pass this back to `/otp/verify`.
</ResponseField>

<ResponseField name="channel" type="string">
  The channel used (`sms` or `voice`).
</ResponseField>

<ResponseField name="expiresInSeconds" type="integer">
  Code lifetime — `300` (5 minutes).
</ResponseField>

<ResponseField name="segments" type="integer">
  SMS segment count for the message (wallet debit unit).
</ResponseField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.senderz.app/api/v1/otp/send \
    -H "X-Sender-Tenant: your-workspace-slug" \
    -H "X-API-Key: your-api-key" \
    -H "Content-Type: application/json" \
    -d '{ "phone": "0501234567", "channel": "sms", "message": "Your code is {{code}}" }'
  ```

  ```json Response theme={null}
  {
    "success": true,
    "data": {
      "verificationId": "otp_9f3c…",
      "channel": "sms",
      "expiresInSeconds": 300,
      "segments": 1
    }
  }
  ```
</CodeGroup>

<Warning>
  Send is wallet-gated. If your SMS credit balance is empty it returns
  `402 Insufficient SMS credits`. There's also a per-phone cap of 5 sends per
  10 minutes (`429`).
</Warning>

## Verify a code

```http theme={null}
POST /api/v1/otp/verify
```

### Body

<ParamField body="verificationId" type="string" required>
  The ID returned by `/otp/send` (max 64).
</ParamField>

<ParamField body="code" type="string" required>
  The code the user entered (max 12).
</ParamField>

### Response

<ResponseField name="verified" type="boolean">
  `true` if the code matched and was within the window.
</ResponseField>

<ResponseField name="reason" type="string">
  Present when `verified` is false: `expired_or_unknown`, `code_mismatch`, or
  `too_many_attempts`.
</ResponseField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.senderz.app/api/v1/otp/verify \
    -H "X-Sender-Tenant: your-workspace-slug" \
    -H "X-API-Key: your-api-key" \
    -H "Content-Type: application/json" \
    -d '{ "verificationId": "otp_9f3c…", "code": "482913" }'
  ```

  ```json Response theme={null}
  {
    "success": true,
    "data": { "verified": true }
  }
  ```
</CodeGroup>

<Note>
  The code is stored only as a hash with a 5-minute TTL and a maximum of 5
  verify attempts — it's never returned or logged.
</Note>
