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

# POST /v3/send — Deliver a Client-Supplied OTP Code

> POST /v3/send delivers an OTP code you generate yourself. Provide the code in the otp field. Returns otp_id, status, price, and remaining credits.

Use `POST /v3/send` when your system generates the OTP code and you want OTP.ID to handle only delivery. This is identical to `POST /v3/request` in every other respect — same channels, same response shape, same error codes — except you must supply the code in the `otp` field and the `whatsapp_inbound` channel is not supported. The code you supply is delivered to the user and is **never echoed back in any API response**.

## Endpoint

```text theme={null}
POST https://api.otp.id/v3/send
```

## Request Headers

| Header          | Value              | Required |
| --------------- | ------------------ | -------- |
| `Authorization` | `Bearer <api_key>` | ✅ Yes    |
| `Content-Type`  | `application/json` | ✅ Yes    |

## Request Body

<ParamField body="otp" type="string" required>
  The OTP code to deliver. Must contain **digits only** and be between 4 and 8 characters long. Example: `"482913"`.
</ParamField>

<ParamField body="channel" type="string" required>
  The delivery channel for the OTP. Accepted values: `whatsapp`, `sms`, `email`, `misscall`.

  <Warning>
    The `whatsapp_inbound` channel is **not supported** by this endpoint. Passing `whatsapp_inbound` returns `400 INVALID_CHANNEL`. Use `POST /v3/request` for inbound flows.
  </Warning>
</ParamField>

<ParamField body="number" type="string" required>
  The destination phone number or email address.

  * **Phone numbers**: digits only, minimum 10 characters (e.g. `"6281234567890"`)
  * **Email**: a valid email address (e.g. `"user@example.com"`)
</ParamField>

<ParamField body="brand" type="string">
  The brand name included in the OTP message. Defaults to the `brand_name` configured on your merchant account.
</ParamField>

<ParamField body="otp_length" type="integer">
  Ignored when `otp` is provided — the length is inferred from the `otp` value you supply. Included for API compatibility; has no effect.
</ParamField>

<ParamField body="ttl" type="integer">
  Time-to-live in seconds before the OTP expires. Accepted range: `60`–`900`. Defaults to `300` (5 minutes). Values outside this range are **clamped** to the nearest bound — not rejected.
</ParamField>

<ParamField body="external_id" type="string">
  Your own idempotency key for this transaction. If you resubmit the same `external_id`, OTP.ID returns the original `otp_id` without resending the message and without deducting additional credits.
</ParamField>

## Response Fields

<ResponseField name="otp_id" type="string">
  The unique transaction ID assigned by OTP.ID. Use this in `POST /v3/verify` and `GET /v3/otp/{otp_id}`.
</ResponseField>

<ResponseField name="status" type="string">
  Current delivery status: `sent` / `success` when delivered, `failed` if vendor delivery failed.
</ResponseField>

<ResponseField name="channel" type="string">
  The channel used to deliver the OTP, echoed from your request.
</ResponseField>

<ResponseField name="number" type="string">
  The destination address used, echoed from your request.
</ResponseField>

<ResponseField name="price" type="integer">
  The number of credits charged for this transaction. Present even when delivery fails; no credits are deducted on failure.
</ResponseField>

<ResponseField name="last_balance" type="integer">
  Your remaining credits after this transaction. Unchanged if delivery failed or if this was an idempotency replay.
</ResponseField>

<ResponseField name="expires_at" type="string">
  The datetime at which this OTP expires, in `YYYY-MM-DD HH:MM:SS` format (WIB, UTC+7).
</ResponseField>

## Examples

### SMS

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.otp.id/v3/send \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"channel": "sms", "number": "6281234567890", "otp": "482913", "brand": "MyApp", "ttl": 180, "external_id": "order-8822"}'
  ```
</CodeGroup>

**Success response:**

```json theme={null}
{
  "success": true,
  "data": {
    "otp_id": "OTP20260807ABCD000003",
    "status": "sent",
    "channel": "sms",
    "number": "6281234567890",
    "price": 175,
    "last_balance": 99125,
    "expires_at": "2026-08-07 10:02:00"
  },
  "error": null
}
```

### WhatsApp

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.otp.id/v3/send \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"channel": "whatsapp", "number": "6281234567890", "otp": "739012", "brand": "MyApp", "ttl": 300}'
  ```
</CodeGroup>

**Success response:**

```json theme={null}
{
  "success": true,
  "data": {
    "otp_id": "OTP20260807ABCD000004",
    "status": "sent",
    "channel": "whatsapp",
    "number": "6281234567890",
    "price": 350,
    "last_balance": 98775,
    "expires_at": "2026-08-07 10:05:00"
  },
  "error": null
}
```

<Note>
  The OTP code you sent is never returned in the response — you already know it on your side. Store it securely if you need to reference it later (e.g. for audit logs), but never log it in plaintext.
</Note>

## Error Responses

All errors follow the shape `{"success": false, "data": null, "error": {"code": "...", "message": "..."}}`.

| HTTP Status | Error Code                 | Description                                                                                                                                        |
| ----------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400         | `INVALID_CHANNEL`          | The `channel` value is invalid or is `whatsapp_inbound` (not supported by this endpoint).                                                          |
| 400         | `INVALID_NUMBER`           | The `number` format is invalid or missing.                                                                                                         |
| 400         | `VALIDATION_ERROR`         | The `otp` field is missing, contains non-digit characters, or its length is outside 4–8; or another field failed validation.                       |
| 401         | `UNAUTHORIZED`             | The `Authorization` header is missing or the API key is invalid.                                                                                   |
| 402         | `INSUFFICIENT_BALANCE`     | You do not have enough credits to cover this transaction.                                                                                          |
| 409         | `DUPLICATE_EXTERNAL_ID`    | An `external_id` collision with a different request payload was detected. `error.details.existing_otp_id` contains the conflicting transaction ID. |
| 429         | `RATE_LIMITED`             | You have exceeded the API-level request rate limit.                                                                                                |
| 429         | `DESTINATION_RATE_LIMITED` | Too many OTPs have been sent to this destination in a short window.                                                                                |
| 503         | `CHANNEL_UNAVAILABLE`      | The requested channel is temporarily unavailable.                                                                                                  |
| 500         | `INTERNAL_ERROR`           | An unexpected server-side error occurred.                                                                                                          |

**Example — otp validation error:**

```json theme={null}
{
  "success": false,
  "data": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The otp field must contain digits only and be between 4 and 8 characters"
  }
}
```
