Start a verification#

Create a verification for a destination phone number and select how the verification challenge is delivered. The response contains the verification identifier, initial status, expiration time, and delivery-method details when applicable.

For an approved request, the Verification API attempts to deliver the challenge by SMS or phone call.

Request#

HTTP method: POST

Path: /api/v1/verifications

Request body#

The request body contains a top-level data object. The following table lists the fields accepted inside data and identifies which fields are required:

Field

Type

Required

Description

destination

string

Yes

Phone number to verify in E.164 format. The leading + is optional.

delivery_method

string

Yes

The method used to deliver the verification challenge. Supported values are sms and callout.

sms

object

No

Options for the sms delivery method. Send this object only when delivery_method is sms. The API ignores it when another delivery method is selected.

sms.languages

Array of string

No

Preferred message-template languages as BCP 47 tags, ordered from most to least preferred. Tags are matched exactly, so pl does not match pl-PL. If none of the requested languages has a template, the API uses en-US. Invalid tags return languages_invalid. See Supported languages.

sms.app_hash

string

No

Android SMS Retriever application hash. It must contain exactly 11 characters from A-Z, a-z, 0-9, +, and /. When supplied, the SMS message starts with <#> and ends with the application hash so a compatible Android application can capture the code automatically. Omit this field on other platforms.

callout

object

No

Options for the callout delivery method. Send this object only when delivery_method is callout. The API ignores it when another delivery method is selected.

callout.languages

Array of string

No

Preferred announcement languages as BCP 47 tags, ordered from most to least preferred. These are the same tags with the same semantics as sms.languages, so one language list works for both delivery methods. Tags are matched exactly, so pt does not match pt-PT. If none of the requested languages has a recording, the announcement uses en-US. Invalid tags return languages_invalid. See Supported languages.

Response#

The endpoint returns JSON responses. When a verification is created, its verification object appears under a top-level data key. Errors that prevent a verification from being created appear under a top-level errors array.

The following table lists the HTTP status codes returned by this endpoint:

Status

Meaning

201 Created

The verification record was created. Inspect status and error_code to determine whether delivery was approved or denied.

401 Unauthorized

Authentication failed, credentials are missing or invalid, or the authentication mode is below the minimum configured for the OTP application.

402 Payment Required

The account balance is insufficient to start a verification.

422 Unprocessable Content

Request validation failed. This includes an invalid destination, unsupported delivery method, or invalid delivery-method options.

429 Too Many Requests

A verification for the same destination was started less than 30 seconds ago. The Retry-After header gives the number of seconds to wait.

See Errors and status codes for the error response structure and available error codes.

Note

A 201 Created response confirms that the verification record was created. It does not confirm that the challenge was delivered. An approved start normally has status pending, and delivery can still fail later.

A request callback can deny the start before delivery. A denied start can also return 201 Created with status denied and a matching error_code. Always inspect both fields.

Repeated starts#

An OTP application cannot start a new verification for the same destination within 30 seconds of the previous one, whichever delivery method either request uses. Such a request returns 429 Too Many Requests with the destination_in_cooldown error code and a Retry-After header: the whole number of seconds, at least 1, until a new start is accepted. The earlier verification is not changed and stays active.

A start that was denied does not count. The check runs before the request callback, so a rejected start normally does not call it.

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 18

{
  "errors": [
    {
      "code": "destination_in_cooldown",
      "detail": "verification for this destination was requested too recently"
    }
  ]
}

Do not retry a rejected start automatically. Let the user request a new code once the Retry-After interval has passed.

Examples#

The REST API examples use HTTP Basic authentication. See REST API authentication for credential and header requirements.

The SDK examples assume that the corresponding SDK is installed and initialized. See the Ruby, Python, Node.js, PHP, iOS, Android, React Native, and Dart SDK pages.

The identifiers, expiration times, and fees shown in the examples are illustrative.

REST API#

Send a POST request to create a verification. When the record is created, the response returns a verification object containing its initial state under a top-level data key.

http

POST /api/v1/verifications HTTP/1.1
Host: verification.didww.com
Content-Type: application/json
Accept: application/json
Authorization: Basic eW91cl9hcHBfa2V5OnlvdXJfYXBwX3NlY3JldA==

{
  "data": {
    "destination": "+4915112345678",
    "delivery_method": "sms",
    "sms": {
      "languages": ["en-US"]
    }
  }
}

curl

curl -i -X POST https://verification.didww.com/api/v1/verifications -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"data": {"delivery_method": "sms", "destination": "+4915112345678", "sms": {"languages": ["en-US"]}}}' --user your_app_key:your_app_secret

response

HTTP/1.1 201 Created
Content-Type: application/json

{
  "data": {
    "id": "0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21",
    "destination": "4915112345678",
    "delivery_method": "sms",
    "fee": "0.06",
    "status": "pending",
    "error_code": null,
    "error_detail": null,
    "expires_at": "2026-07-15T10:05:00.000Z",
    "sms": {
      "template": "Your code is {{CODE}}",
      "language": "en-US",
      "interception_timeout": 300,
      "code_length": 6
    }
  }
}

http

POST /api/v1/verifications HTTP/1.1
Host: verification.didww.com
Content-Type: application/json
Accept: application/json
Authorization: Basic eW91cl9hcHBfa2V5OnlvdXJfYXBwX3NlY3JldA==

{
  "data": {
    "destination": "+4915112345678",
    "delivery_method": "callout",
    "callout": {
      "languages": ["de-DE"]
    }
  }
}

curl

curl -i -X POST https://verification.didww.com/api/v1/verifications -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"data": {"callout": {"languages": ["de-DE"]}, "delivery_method": "callout", "destination": "+4915112345678"}}' --user your_app_key:your_app_secret

response

HTTP/1.1 201 Created
Content-Type: application/json

{
  "data": {
    "id": "2b3c4d5e-6f70-4b3c-9d0e-1f2a3b4c5d6e",
    "destination": "4915112345678",
    "delivery_method": "callout",
    "fee": "0.08",
    "status": "pending",
    "error_code": null,
    "error_detail": null,
    "expires_at": "2026-07-15T10:05:00.000Z",
    "callout": {
      "language": "de-DE",
      "code_length": 6
    }
  }
}

Ruby SDK#

The Ruby SDK sends the request when start_verification(...) is called and returns a verification object containing the initial state.

verification = client.start_verification(
  destination:     "+4915112345678",
  delivery_method: "sms",
  sms:             {languages: ["en-US"]}
)

verification.id           # => "0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21"
verification.status       # => "pending"
verification.pending?     # => true
verification.sms_language # => "en-US"
verification = client.start_verification(
  destination:     "+4915112345678",
  delivery_method: "callout",
  callout:         {languages: ["de-DE"]}
)

verification.id               # => "2b3c4d5e-6f70-4b3c-9d0e-1f2a3b4c5d6e"
verification.status           # => "pending"
verification.callout_language # => "de-DE"

Python SDK#

The Python SDK sends the request when start_verification(...) is called and returns a Verification containing the initial state.

verification = client.start_verification(
    destination="+4915112345678",
    delivery_method="sms",
    sms=SmsOptions(languages=["en-US"]),
)

verification.id            # "0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21"
verification.status        # "pending"
verification.is_finished   # False
verification.sms.language  # "en-US"
verification = client.start_verification(
    destination="+4915112345678",
    delivery_method="callout",
    callout=CalloutOptions(languages=["de-DE"]),
)

verification.id                # "2b3c4d5e-6f70-4b3c-9d0e-1f2a3b4c5d6e"
verification.status            # "pending"
verification.callout.language  # "de-DE"

A denied start returns a Verification with status denied rather than raising. AsyncVerificationClient is the same call with await.

Node.js SDK#

The Node.js SDK sends the request when startVerification(...) is called and returns the decoded verification.

const verification = await client.startVerification({
  destination: '+4915112345678',
  deliveryMethod: 'sms',
  sms: { languages: ['en-US'] },
});

verification.id;            // '0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21'
verification.status;        // 'pending'
verification.sms?.language; // 'en-US'
const verification = await client.startVerification({
  destination: '+4915112345678',
  deliveryMethod: 'callout',
  callout: { languages: ['de-DE'] },
});

verification.id;                // '2b3c4d5e-6f70-4b3c-9d0e-1f2a3b4c5d6e'
verification.status;            // 'pending'
verification.callout?.language; // 'de-DE'

A start is never retried by the SDK. A repeated start supersedes the live verification and sends a new code. The superseded verification is never charged; the fee is charged only if the new verification becomes verified. Within 30 seconds of the previous start, a repeated start is rejected with 429 Too Many Requests instead.

PHP SDK#

The PHP SDK sends the request when startVerification(...) is called and returns a Verification containing the initial state.

$verification = $client->startVerification(
    '+4915112345678',
    DeliveryMethod::Sms,
    sms: new SmsOptions(languages: ['en-US']),
);

$verification->id;             // "0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21"
$verification->status;         // "pending"
$verification->isFinished();   // false
$verification->sms?->language; // "en-US"
$verification = $client->startVerification(
    '+4915112345678',
    DeliveryMethod::Callout,
    callout: new CalloutOptions(languages: ['de-DE']),
);

$verification->id;                 // "2b3c4d5e-6f70-4b3c-9d0e-1f2a3b4c5d6e"
$verification->status;             // "pending"
$verification->callout?->language; // "de-DE"

A denied start returns a Verification with status denied rather than throwing.

iOS SDK#

The iOS SDK sends the request when start(...) is called and returns a Verification containing the initial state.

let verification = try await client.start(
    destination: "+4915112345678",
    method: .sms,
    sms: .init(languages: ["en-US"])
)

verification.id     // "0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21"
verification.status // .pending
let verification = try await client.start(
    destination: "+4915112345678",
    method: .callout,
    callout: .init(languages: ["de-DE"])
)

verification.id     // "2b3c4d5e-6f70-4b3c-9d0e-1f2a3b4c5d6e"
verification.status // .pending

Inspect verification.status before asking the user for a code. A denied start returns a Verification with status .denied instead of throwing an HTTP error.

Android SDK#

The Android SDK returns a VerificationHandle. Calling start(...) does not send the request. Collect handle.states exactly once to send the request and receive verification state changes. After VerificationState.Starting, an approved start emits VerificationState.AwaitingInput, which corresponds to the API status pending.

val handle = didww.start(
    destination = "+4915112345678",
    method = DeliveryMethod.SMS,
    sms = SmsOptions(languages = listOf("en-US")),
)
val handle = didww.start(
    destination = "+4915112345678",
    method = DeliveryMethod.CALLOUT,
    callout = CalloutOptions(languages = listOf("de-DE")),
)

Collect the returned handle from a ViewModel-scoped coroutine:

viewModelScope.launch {
    handle.states.collect { state ->
        when (state) {
            is VerificationState.AwaitingInput ->
                println("Verification ${state.verificationId} is pending")
            is VerificationState.Denied ->
                println(state.error?.detail ?: "Verification denied")
            is VerificationState.SetupError ->
                println("Configuration error: ${state.code}")
            is VerificationState.Failed ->
                println("Start failed: ${state.reason}")
            else -> Unit
        }
    }
}

Dart SDK#

The Dart SDK sends the request when startVerification(...) is called and returns a Verification containing the initial state.

final started = await client.startVerification(
  destination: '+4915112345678',
  deliveryMethod: DeliveryMethod.sms,
  sms: const SmsOptions(languages: ['en-US']),
);

started.id;            // '0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21'
started.knownStatus;   // VerificationStatus.pending
started.sms?.language; // 'en-US'
final started = await client.startVerification(
  destination: '+4915112345678',
  deliveryMethod: DeliveryMethod.callout,
  callout: const CalloutOptions(languages: ['de-DE']),
);

started.id;                // '2b3c4d5e-6f70-4b3c-9d0e-1f2a3b4c5d6e'
started.knownStatus;       // VerificationStatus.pending
started.callout?.language; // 'de-DE'

A Flutter screen usually calls session.start(...) on a VerificationSession instead, which performs the same request and publishes the result as a state.

React Native SDK#

The React Native SDK starts the verification through the useVerification() controller. Nothing throws and nothing returns a promise: the outcome arrives as a new state.

controller.start({
  destination: '+4915112345678',
  deliveryMethod: 'sms',
  sms: { languages: ['en-US'] },
});
controller.start({
  destination: '+4915112345678',
  deliveryMethod: 'callout',
  callout: { languages: ['de-DE'] },
});

state.kind moves to 'starting' and then to 'awaitingInput' for an approved start, or to 'denied' when a request callback refuses it.