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

# Send Phone Code

> Send a one-time passcode (OTP) to a phone number over WhatsApp, SMS, or another supported channel, then verify it with [`POST /v3/phone/check/`](/standalone-apis/phone-check).

**How send and check pair up.** Verification state is keyed by your application plus the E.164 `phone_number` — the check call does not take `request_id`. Each new send creates a pending verification that lives for **5 minutes**: call the check with the same number and the code the user received before the window closes. Calling send again for the same number while a verification is pending re-sends a code for that same verification (`status: "Retry"`, same `request_id`). At most one retry is attached this way (two sends total); a further send starts a fresh verification with a new `request_id`. The 5-minute window is measured from the first send and is not extended by retries.

**Delivery channels.** `options.preferred_channel` selects the channel (`whatsapp` by default; `sms`, `telegram`, `voice`, `rcs`, `viber`, and `zalo` are also supported). When the preferred channel is unavailable for the destination country or number, delivery automatically falls back to SMS — the send still reports `Success`, and the channel actually used is reported by the check response (`phone.verification_method` and the `lifecycle` events).

**Send statuses.** `Success` — OTP sent for a new verification; `Retry` — OTP re-sent for the pending verification; `Blocked` — the anti-fraud layer refused to send (see `reason`, e.g. `repeated_attempts`, `suspicious`, `spam`). A `Blocked` send immediately finalizes the verification as `Declined` with a high-risk warning, so a follow-up check returns `Expired or Not Found`.

**Billing.** The price depends on the destination country and the channel that actually delivers the message — see [Phone Verification Pricing](/getting-started/phone-verification-pricing). Only `Blocked` sends are guaranteed free: any other unbilled send attempt is charged at verification finalization or expiration, even if delivery was never confirmed or the delivery provider reported the message undeliverable. Checks are always free. As an anti-abuse measure, phone verification is disabled until your organization's first top-up (`403`).

**Session persistence.** Every new verification is persisted as an API-type session: `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the verification appears in the Business Console, and `status.updated` webhooks fire as it progresses.

**Sandbox.** Sandbox API keys skip delivery and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Success` payload with a random `request_id`; no message is sent and nothing is persisted. Use code `123456` on the sandbox check.

**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) — this API never returns `401`.

**Rate limits.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints, plus an anti-abuse cap of 4 send attempts per phone number per hour; exceeding either returns `429`.



## OpenAPI

````yaml POST /v3/phone/send/
openapi: 3.0.0
info:
  version: 3.0.0
  title: Didit Verification API
  description: Identity verification API. Authenticate with x-api-key header.
servers:
  - url: https://verification.didit.me
security: []
tags: []
paths:
  /v3/phone/send/:
    post:
      tags:
        - Standalone APIs
      summary: Send Phone Code
      description: >-
        Send a one-time passcode (OTP) to a phone number over WhatsApp, SMS, or
        another supported channel, then verify it with [`POST
        /v3/phone/check/`](/standalone-apis/phone-check).


        **How send and check pair up.** Verification state is keyed by your
        application plus the E.164 `phone_number` — the check call does not take
        `request_id`. Each new send creates a pending verification that lives
        for **5 minutes**: call the check with the same number and the code the
        user received before the window closes. Calling send again for the same
        number while a verification is pending re-sends a code for that same
        verification (`status: "Retry"`, same `request_id`). At most one retry
        is attached this way (two sends total); a further send starts a fresh
        verification with a new `request_id`. The 5-minute window is measured
        from the first send and is not extended by retries.


        **Delivery channels.** `options.preferred_channel` selects the channel
        (`whatsapp` by default; `sms`, `telegram`, `voice`, `rcs`, `viber`, and
        `zalo` are also supported). When the preferred channel is unavailable
        for the destination country or number, delivery automatically falls back
        to SMS — the send still reports `Success`, and the channel actually used
        is reported by the check response (`phone.verification_method` and the
        `lifecycle` events).


        **Send statuses.** `Success` — OTP sent for a new verification; `Retry`
        — OTP re-sent for the pending verification; `Blocked` — the anti-fraud
        layer refused to send (see `reason`, e.g. `repeated_attempts`,
        `suspicious`, `spam`). A `Blocked` send immediately finalizes the
        verification as `Declined` with a high-risk warning, so a follow-up
        check returns `Expired or Not Found`.


        **Billing.** The price depends on the destination country and the
        channel that actually delivers the message — see [Phone Verification
        Pricing](/getting-started/phone-verification-pricing). Only `Blocked`
        sends are guaranteed free: any other unbilled send attempt is charged at
        verification finalization or expiration, even if delivery was never
        confirmed or the delivery provider reported the message undeliverable.
        Checks are always free. As an anti-abuse measure, phone verification is
        disabled until your organization's first top-up (`403`).


        **Session persistence.** Every new verification is persisted as an
        API-type session: `request_id` is a real session id you can pass to `GET
        /v3/session/{sessionId}/decision/`, the verification appears in the
        Business Console, and `status.updated` webhooks fire as it progresses.


        **Sandbox.** Sandbox API keys skip delivery and billing: after request
        validation (malformed input still returns `400`), the endpoint returns a
        static `Success` payload with a random `request_id`; no message is sent
        and nothing is persisted. Use code `123456` on the sandbox check.


        **Authentication.** Send your application's API key in the `x-api-key`
        header. Missing or invalid credentials return `403` (`{"detail": "You do
        not have permission to perform this action."}`) — this API never returns
        `401`.


        **Rate limits.** Shared write budget of 300 requests/min per API key
        across all POST/PATCH/DELETE endpoints, plus an anti-abuse cap of 4 send
        attempts per phone number per hour; exceeding either returns `429`.
      operationId: post_v3phone_send
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                phone_number:
                  type: string
                  maxLength: 20
                  description: >-
                    Recipient phone number in E.164 format — leading `+` and
                    country code (e.g., `+14155552671`). Numbers are validated
                    and normalized to E.164; unparseable or invalid numbers
                    return `400` with a `phone_number` field error.
                  example: '+14155552671'
                options:
                  type: object
                  description: OTP delivery options. All fields are optional.
                  properties:
                    code_size:
                      type: integer
                      minimum: 4
                      maximum: 8
                      default: 6
                      description: Number of digits in the OTP.
                    locale:
                      type: string
                      maxLength: 5
                      pattern: ^[a-z]{2,3}(-[A-Z]{2,3})?$
                      description: >-
                        BCP-47 locale used to localize the verification message
                        (e.g., `en-US`, `fr`, `es`). Invalid formats return
                        `400`.
                      example: en-US
                    preferred_channel:
                      type: string
                      enum:
                        - whatsapp
                        - sms
                        - telegram
                        - voice
                        - rcs
                        - viber
                        - zalo
                      default: whatsapp
                      description: >-
                        Preferred delivery channel. If the channel is
                        unavailable for the destination country or number, the
                        message automatically falls back to SMS — the send still
                        reports `Success`.
                signals:
                  type: object
                  description: >-
                    Optional device and network signals about the end user,
                    forwarded to the anti-fraud layer to improve detection of
                    abusive or automated traffic. All fields are optional.
                  properties:
                    ip:
                      type: string
                      description: IP address of the end user's device. IPv4 or IPv6.
                      example: 192.0.2.1
                    device_id:
                      type: string
                      maxLength: 255
                      description: >-
                        Unique identifier of the end user's device. Android:
                        `ANDROID_ID`; iOS: `identifierForVendor`.
                      example: 8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2
                    device_platform:
                      type: string
                      enum:
                        - android
                        - ios
                        - ipados
                        - tvos
                        - web
                      description: Platform of the end user's device.
                    device_model:
                      type: string
                      maxLength: 255
                      description: Model of the end user's device.
                      example: iPhone17,2
                    os_version:
                      type: string
                      maxLength: 64
                      description: Operating-system version of the end user's device.
                      example: 18.0.1
                    app_version:
                      type: string
                      maxLength: 64
                      description: Version of your application.
                      example: 1.2.34
                    user_agent:
                      type: string
                      maxLength: 512
                      description: User agent of the end user's browser or app.
                      example: >-
                        Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X)
                        AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3
                        Mobile/15E148 Safari/604.1
                vendor_data:
                  type: string
                  description: >-
                    Optional caller-controlled identifier (your internal user
                    id, an email, a UUID, etc.) persisted on the session and
                    echoed back in the send response, the matching check
                    response, webhooks, and the Business Console. Use it to
                    correlate Didit's `request_id` with your user record.
                metadata:
                  type: object
                  nullable: true
                  description: >-
                    Optional free-form JSON object persisted on the session and
                    echoed back in the send response, the matching check
                    response, webhooks, and the Business Console.
              required:
                - phone_number
            examples:
              WhatsApp (default):
                summary: 6-digit OTP via WhatsApp, falling back to SMS automatically
                value:
                  phone_number: '+14155552671'
                  options:
                    preferred_channel: whatsapp
                    locale: en-US
                  vendor_data: user-1234
              SMS with custom code length:
                summary: Force SMS delivery with a 4-digit code
                value:
                  phone_number: '+34699999999'
                  options:
                    preferred_channel: sms
                    code_size: 4
                    locale: es
              With fraud signals:
                summary: Forward device signals to strengthen anti-fraud screening
                value:
                  phone_number: '+14155552671'
                  signals:
                    ip: 192.0.2.1
                    device_id: 8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2
                    device_platform: ios
                    device_model: iPhone17,2
                    os_version: 18.0.1
                    app_version: 1.2.34
                  vendor_data: user-1234
      responses:
        '200':
          description: >-
            Send acknowledged. Inspect `status`: `Success` and `Retry` mean a
            code is on its way; `Blocked` means the anti-fraud layer refused and
            the verification is already finalized as `Declined`. `request_id` is
            the persisted session id (same id on a `Retry`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneVerificationSendResponse'
              examples:
                Success:
                  summary: OTP sent for a new verification
                  value:
                    request_id: e39cb057-92fc-4b59-b84e-02fec29a0f24
                    status: Success
                    reason: null
                    vendor_data: user-1234
                    metadata: null
                Retry:
                  summary: >-
                    Second send for the same number inside the 5-minute window —
                    same request_id
                  value:
                    request_id: e39cb057-92fc-4b59-b84e-02fec29a0f24
                    status: Retry
                    reason: null
                    vendor_data: user-1234
                    metadata: null
                Blocked:
                  summary: >-
                    Anti-fraud refusal — verification finalized as Declined,
                    nothing billed
                  value:
                    request_id: 0d8af1f4-0c43-46d7-b75c-6e4e22c0a7b3
                    status: Blocked
                    reason: repeated_attempts
                    vendor_data: user-1234
                    metadata: null
        '400':
          description: >-
            Validation error. Malformed input returns DRF's field-error envelope
            (one array of messages per offending field, nested under `options`
            for option errors). A number that parses but is rejected at send
            time (unreachable, or an ineligible line type such as a short code)
            returns a `{"detail": ...}` envelope instead.
          content:
            application/json:
              examples:
                Invalid phone number (field error):
                  summary: Number is not valid E.164
                  value:
                    phone_number:
                      - Invalid phone number provided.
                Missing phone number:
                  summary: '`phone_number` not included in the body'
                  value:
                    phone_number:
                      - This field is required.
                Invalid options:
                  summary: >-
                    Out-of-range `code_size`, malformed `locale`, unknown
                    `preferred_channel`
                  value:
                    options:
                      code_size:
                        - Ensure this value is less than or equal to 8.
                      locale:
                        - Ensure this field has no more than 5 characters.
                      preferred_channel:
                        - '"carrier_pigeon" is not a valid choice.'
                Number rejected at send time:
                  summary: Valid E.164 shape but the number cannot receive an OTP
                  value:
                    detail: Invalid phone number provided.
                Ineligible line type:
                  summary: Line type cannot receive an OTP (e.g., short code)
                  value:
                    detail: Invalid phone line type provided.
        '403':
          description: >-
            Permission denied. Returned when the `x-api-key` header is missing,
            malformed, revoked, or belongs to another environment (`{"detail":
            ...}`) — this API never returns `401`. Also returned before any send
            when the organization has never topped up (anti-abuse gate) or when
            its balance cannot cover the destination's send price (`{"error":
            ...}`).
          content:
            application/json:
              examples:
                Missing or invalid API key:
                  summary: No `x-api-key` header, or the key is invalid/revoked
                  value:
                    detail: You do not have permission to perform this action.
                Top-up required:
                  summary: >-
                    Phone verification stays disabled until the organization's
                    first top-up
                  value:
                    detail: >-
                      To protect against abuse, phone verification is disabled
                      until your first top-up.
                Not enough credits:
                  summary: Organization balance cannot cover the send price
                  value:
                    error: You don't have enough credits to perform this request.
        '429':
          description: >-
            Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget
            of 300 write requests per minute per API key. Additionally, an
            anti-abuse cap of 4 verification attempts per phone number per hour
            applies upstream. The response carries `X-RateLimit-Limit`,
            `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After`
            headers. Note: the `X-RateLimit-*`/`Retry-After` headers accompany
            only the 300/min write-limit 429; the per-number cap 429 carries no
            rate-limit headers.
          content:
            application/json:
              examples:
                Per-number cap:
                  summary: >-
                    More than 4 verification attempts for the same number in the
                    last hour
                  value:
                    detail: >-
                      Maximum verification attempts reached for this phone
                      number. Only 4 authentication attempts are allowed per
                      hour. Try again later or use a different number.
                Write rate limit exceeded:
                  summary: More than 300 write requests in the current minute
                  value:
                    detail: >-
                      Write request rate limit exceeded. You can make up to 300
                      requests per minute.
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '500':
          description: >-
            Unexpected delivery failure while creating the verification (e.g.,
            the delivery provider is unreachable). Safe to retry.
          content:
            application/json:
              examples:
                Provider error:
                  summary: OTP delivery could not be initiated
                  value:
                    detail: Error creating phone verification
              schema:
                type: object
                properties:
                  detail:
                    type: string
      security:
        - ApiKeyAuth: []
      x-codeSamples:
        - lang: curl
          label: curl
          source: |-
            curl -X POST https://verification.didit.me/v3/phone/send/ \
              -H 'x-api-key: YOUR_API_KEY' \
              -H 'Content-Type: application/json' \
              -d '{
                "phone_number": "+14155552671",
                "options": {
                  "code_size": 6,
                  "preferred_channel": "whatsapp",
                  "locale": "en-US"
                },
                "vendor_data": "user-1234"
              }'
        - lang: python
          label: Python
          source: >-
            import os, requests


            resp = requests.post(
                "https://verification.didit.me/v3/phone/send/",
                headers={
                    "x-api-key": os.environ["DIDIT_API_KEY"],
                    "Content-Type": "application/json",
                },
                json={
                    "phone_number": "+14155552671",
                    "options": {
                        "code_size": 6,
                        "preferred_channel": "whatsapp",
                        "locale": "en-US",
                    },
                    "vendor_data": "user-1234",
                },
                timeout=15,
            )

            resp.raise_for_status()

            print(resp.json())  # {request_id, status, reason, vendor_data,
            metadata}

            # Then verify with POST /v3/phone/check/ using the same phone_number
        - lang: javascript
          label: JavaScript
          source: >-
            const res = await
            fetch('https://verification.didit.me/v3/phone/send/', {
              method: 'POST',
              headers: {
                'x-api-key': 'YOUR_API_KEY',
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({
                phone_number: '+14155552671',
                options: { code_size: 6, preferred_channel: 'whatsapp', locale: 'en-US' },
                vendor_data: 'user-1234',
              }),
            });

            if (!res.ok) throw new Error(`Phone send failed: ${res.status}`);

            const data = await res.json();

            console.log(data); // { request_id, status, reason, vendor_data,
            metadata }

            // Then verify with POST /v3/phone/check/ using the same
            phone_number
components:
  schemas:
    PhoneVerificationSendResponse:
      type: object
      properties:
        request_id:
          type: string
          format: uuid
          description: >-
            Session id of the verification. A `Retry` send returns the same
            `request_id` as the original send. This id appears in the Business
            Console, is returned again by a finalized `POST /v3/phone/check/`,
            and can be passed to `GET /v3/session/{sessionId}/decision/`.
        status:
          type: string
          enum:
            - Success
            - Retry
            - Blocked
          description: >-
            `Success` — OTP sent to a new verification. `Retry` — OTP re-sent
            for the pending verification created by a previous send. `Blocked` —
            the anti-fraud layer refused to send; the verification is
            immediately finalized as `Declined` and nothing is billed.
        reason:
          type: string
          nullable: true
          description: >-
            Why a send was `Blocked` (e.g., `repeated_attempts`, `suspicious`,
            `spam`). `null` on `Success` and `Retry`.
        vendor_data:
          type: string
          nullable: true
          description: >-
            Echo of the `vendor_data` stored on the session (from the first
            send).
        metadata:
          type: object
          nullable: true
          description: Echo of the `metadata` stored on the session (from the first send).
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key

````