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.

custom

string

No

Up to 4096 characters of your own data, for example a session or order reference. It is passed unchanged to your request callback as data.custom and is never returned by the API. Do not put secrets in it. A longer value returns custom_too_long.

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.autofill

object

No

How the message supports automatic code entry on the user’s device. When omitted, the application default applies: the domain line is added when the OTP application has an SMS domain set, and nothing is added otherwise. Added text counts toward the message length. A malformed object, or one sent together with sms.app_hash, returns autofill_invalid.

sms.autofill.type

string

Yes, in sms.autofill

app_hash: Android SMS Retriever. The message starts with <#> and ends with the hash given in value.

domain: the message ends with a blank line and @<domain> #<code>, where <domain> is the SMS domain of the OTP application. iOS and web browsers use this line to offer the code for the matching site. Returns sms_domain_not_configured when the application has no SMS domain.

none: nothing is added, even when the application has an SMS domain.

sms.autofill.value

string

For app_hash

Android SMS Retriever application hash: exactly 11 characters from A-Z, a-z, 0-9, +, and /. A wrong format returns app_hash_invalid. Do not send value with domain or none.

sms.app_hash

string

No

Deprecated. Use sms.autofill with type app_hash instead. Still accepted when sms.autofill is omitted, with the same effect. A wrong format returns app_hash_invalid.

sms.template_group

string

No

Name of a custom SMS template group of your account. It overrides the group set on the OTP application. An unknown name returns template_group_not_found. When the group has no approved template for the requested languages or its default language, the system template is used, and sms.template_fallback_reason says why.

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.

callout.template_group

string

No

Name of a custom voice template group of your account. It overrides the group set on the OTP application. An unknown name returns template_group_not_found. When the group has no approved recording for the requested languages or its default language, the system announcement is used, and callout.template_fallback_reason says why.

The following request body uses the optional fields:

{
  "data": {
    "destination": "+4915112345678",
    "delivery_method": "sms",
    "custom": "session=7f3c9a",
    "sms": {
      "languages": ["de-DE", "en-US"],
      "autofill": {"type": "app_hash", "value": "FA+9qCX9VSu"},
      "template_group": "checkout"
    }
  }
}

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, denied, or not possible. See Start outcomes.

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, invalid delivery-method options, an unknown template group, a domain autofill without an SMS domain on the application, and a custom value that is too long.

429 Too Many Requests

A limit refused the start: the 30-second destination cooldown, the daily or monthly volume limit, or a per-second rate limit. errors[].code names the limit, and Retry-After gives the seconds to wait. No verification is created. See Limits.

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 start can also be denied or fail before delivery and still return 201 Created, with status denied or failed and a matching error_code. Always inspect both fields.

Start outcomes#

A 201 Created response carries one of the following outcomes:

status

error_code

Meaning

pending

null

The start was approved, and the API is delivering the challenge.

denied

denied_by_callback

Your request callback returned deny, or the start used the sandbox denied-by-callback test number.

denied

denied_invalid_callback_response

Your request callback timed out or returned an unusable response.

denied

denied_missing_callback_url

The start used public authentication, but the OTP application has no callback URL.

denied

application_paused

The OTP application is paused. Resume it in the application settings.

denied

destination_country_disabled

The destination country is on your account’s country blocklist.

failed

destination_not_supported_for_channel

The destination cannot be reached with the selected delivery method. Try another delivery method.

expired

expired

Sandbox only: the expired test number. The verification is created already expired, and nothing is sent.

For every outcome except pending, nothing is sent, no fee is charged, and the start does not count toward the volume limit or start a cooldown; the sandbox expired outcome alone starts a cooldown, like a sent code. Except for denied_by_callback and denied_invalid_callback_response, these outcomes are decided before the request callback, so your callback is not called. The sandbox denied-by-callback test number does not call it either.

Delivery-method fields#

The response contains an sms or a callout object, named after delivery_method. Besides the fields described in the verification object, it contains the following fields:

Field

Type

Description

sms.autofill

object

Present only when an autofill marker was added to the message. type is app_hash or domain, and value is the application hash or the SMS domain.

sms.app_hash

string

Deprecated. Use sms.autofill.value. Present only for type app_hash.

template_group

string

Name of the custom template group that applied, from the request or the OTP application, or null when none did.

template_group_used

boolean

true when the message or the call was produced from template_group.

template_fallback_reason

string

Why a template group was not used, or null when it was used or none applied: language_missing (no template for the requested languages or the group default language), version_pending (the template is awaiting review), version_rejected (the template was rejected), audio_not_ready (phone call only: the recording is not ready yet), or group_missing (the default group set on the OTP application is not available). The default system template is used instead.

The template_group, template_group_used, and template_fallback_reason fields are present in both the sms and the callout object.

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.

Only a start that was approved for delivery, or a sandbox start for the expired test number, starts the cooldown. A denied start, and a failed start for an unsupported destination, do not. The check runs before the request callback, so a rejected start normally does not call it. See Limits for the other limits that return 429.

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,
      "template_group": null,
      "template_group_used": false,
      "template_fallback_reason": null
    }
  }
}

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,
      "template_group": null,
      "template_group_used": false,
      "template_fallback_reason": null
    }
  }
}

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.