Getting Started#

Create an OTP application, obtain its API credentials, and complete your first verification through the REST API or an SDK. Start in the sandbox environment to test the integration before moving it to production.


Choose an environment#

Create the OTP application and credentials in the same environment as the Verification API endpoint you plan to use:

Environment

User Panel

Verification API base URL

Use

Sandbox

DIDWW Sandbox User Panel

https://verification-sandbox.didww.com

Integration testing

Production

DIDWW Production User Panel

https://verification.didww.com

Live verifications

Important

Credentials are environment-specific. Use credentials from a sandbox OTP application with the sandbox API, and credentials from a production OTP application with the production API.

The sandbox sends no real SMS or calls. Use its test numbers to get a fixed code or a chosen outcome. See Testing in sandbox.


Before you begin#

  • An account in the selected environment is required. For testing, open the DIDWW Sandbox User Panel. For production, sign in to the DIDWW Production User Panel or create a DIDWW account. You create the OTP application and copy its credentials in the User Panel of that environment.

  • A positive account balance is required for production. The verification fee is billed when a verification reaches verified, in addition to the delivery cost. SMS delivery is billed per message sent and voice delivery per minute, at the rate for the destination country.

  • A phone number you control is required when testing delivery through the production environment.


Step 1: Create an OTP application#

An OTP application groups the credentials and settings used by your verification requests. You need one before you can start verifications, because each request is tied to an OTP application key, secret, callback URL, and minimum authentication mode. An account can have up to 50 OTP applications.

Create the application in one of two ways. Both create an OTP application that you can use in the next steps:

  • Quick setup with Try it out: create an application with the default settings on the Get Started page and send a test verification right away.

  • Advanced configuration: create an application on the Applications page and configure all its settings.

Quick setup with Try it out#

Try it out on the OTP Verify > Get Started page creates a simplified OTP application with the default settings: minimum authentication mode Public, code length 6, code timeout 5 minutes, and DIDWW’s shared SMS senders. It then walks you through one complete verification: it sends a code to your phone, shows the API responses, and reports the code you received.

  1. In the DIDWW User Panel, go to OTP Verify > Get Started. Use the DIDWW Sandbox User Panel for testing or the DIDWW Production User Panel for live verifications.

  2. In Try it out, enter an Application name.

  3. Optionally, enter a Callback URL. If it is set, DIDWW calls this URL before every verification and sends the code only if your server allows it. Leave it empty to test right away. See Callbacks.

  4. Click Create. You can use the application in the next steps or change its settings later. See Edit an OTP application.

Try it out section on the OTP Verify Get Started page with the Application name and Callback URL fields

Fig. 1. Create an application with Try it out#

  1. In the Send verification request tab, select the Application, the Method (SMS or Voice call), the Phone number of a phone you control, and the Language of the message. The Request panel shows the matching cURL request. Click Send.

Send verification request tab of Try it out with the Application, Method, Phone number, and Language fields and the cURL request

Fig. 2. Send a test verification#

  1. In the See verification response tab, review the API response with the pending status in the Response panel. When the code arrives on your device, click Next.

See verification response tab of Try it out with the pending verification response

Fig. 3. Review the verification response#

  1. In the Verify code tab, enter the code you received in OTP code and click Verify code. The Request panel shows the matching cURL request.

Verify code tab of Try it out with the OTP code field and the cURL request

Fig. 4. Enter the received code#

  1. In the Report Response tab, the Report response panel shows the verified status, which confirms that the test succeeded. Details shows the application name, the API key, the API secret key, and the authentication mode. Click Restart to send another test verification.

Report Response tab of Try it out with a verified response and the application details

Fig. 5. A successful test verification#

Note

Try it out authenticates its requests with basic authentication, so the test works without a callback URL even though the application’s minimum authentication mode is public. Each test verification is a real verification and is billed like any other verification.

Advanced configuration#

Create an application on the Applications page to configure all its settings when you create it.

  1. In the DIDWW User Panel, go to OTP Verify > Applications. Use the DIDWW Sandbox User Panel for testing or the DIDWW Production User Panel for live verifications.

  2. Click Create application.

Create application button on the OTP Verification Applications page

Fig. 6. Open the Create Application form#

  1. Under General, enter the Application name. Optionally, enter a Description.

  2. Under Configuration, set the Callback URL if you plan to use public authentication, including the iOS and Android SDK examples in this guide. The Verification API calls this URL so your backend can approve or deny each start request. See Callback URL.

  3. Select the Minimum authentication mode: Public (default), Basic, or Application. It is the weakest authentication mode that callers may use. See Minimum authentication mode values.

General and Configuration sections of the Create Application form

Fig. 7. General and Configuration settings of an OTP application#

  1. Optionally, adjust the Code length (default 6) and the Code timeout (default 5 minutes) under Verification settings. See Verification settings.

  2. Optionally, under SMS Settings, search for and select dedicated senders in Sender IDs. Only senders verified on your account are listed, and each sender is used only for the destination countries where it is approved. If none is selected, DIDWW’s shared senders are used. See SMS Settings.

  3. Click Create.

Verification settings and SMS Settings sections of the Create Application form

Fig. 8. Verification settings and SMS Settings of an OTP application#

Step 2: Get your API credentials#

Each OTP application has a key and a secret. See Credentials.

Copy the credentials from the OTP application in your selected environment. This walkthrough uses credentials created in the DIDWW Sandbox User Panel.

The REST API and Ruby SDK examples use both credentials with basic authentication. The iOS and Android SDK examples use only the key with public authentication.

  1. In the DIDWW User Panel, go to OTP Verify > Applications and locate the application you created.

  2. In the Credentials column, copy the KEY and SECRET for that application. Use the copy icon to copy each value without revealing it on screen.

  3. If you need to view the actual value before copying it, click the eye icon.

Credentials column of an OTP application on the OTP Verification Applications page

Fig. 9. Copy the key and secret from the Credentials column#

You can also copy them from the application page. See View application details.

Warning

Treat the secret like a password. Store it securely and never expose it in client-side code or public repositories. If it is leaked, rotate it. The key does not change, and you choose how long the previous secret keeps working, from revoking it immediately to a grace period of up to 7 days. See Rotating the secret.

Step 3: Start a verification#

Call the start endpoint with the destination number and delivery method. The examples use the sandbox environment and send an SMS code to +12015550100, a sandbox test number whose code is 123456 for the default code length.

Note

The REST API and Ruby SDK examples use basic authentication. The iOS and Android SDK examples use public authentication, which requires the callback URL configured in Step 1. An OTP application with minimum auth mode public accepts both authentication modes. All examples use credentials created in the DIDWW Sandbox User Panel.

http

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

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

curl

curl -i -X POST https://verification-sandbox.didww.com/api/v1/verifications -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"data": {"delivery_method": "sms", "destination": "+12015550100", "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": "12015550100",
    "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
    }
  }
}

Note

The Authorization header contains the Base64 encoding of key:secret. Most HTTP clients build this header when you supply the key as the username and the secret as the password.

Install the Ruby SDK, then create a sandbox client and start the verification:

require "didww/otp_verification"

client = DIDWW::OTPVerification::Client.new(
  key:    "your-app-key",
  secret: "your-app-secret",
  env:    :sandbox
)

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

verification.id       # => "0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21"
verification.status   # => "pending"
verification.pending? # => true

Install the Python SDK, then create a sandbox client and start the verification:

from didww_verification import (
    BasicAuth,
    Environment,
    SmsOptions,
    VerificationClient,
)

client = VerificationClient(
    BasicAuth("your-app-key", "your-app-secret"),
    environment=Environment.SANDBOX,
)

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

verification.id           # "0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21"
verification.status       # "pending"
verification.is_finished  # False

AsyncVerificationClient is the same surface with await.

Install the Node.js SDK, then create a sandbox client and start the verification:

import { VerificationClient, basicAuth } from '@didww/verification-core';

const client = new VerificationClient({
  auth: basicAuth('your-app-key', 'your-app-secret'),
  environment: 'sandbox',
});

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

verification.id;     // '0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21'
verification.status; // 'pending'

Install the PHP SDK, then create a sandbox client and start the verification:

use Didww\Verification\Auth\BasicAuth;
use Didww\Verification\ClientOptions;
use Didww\Verification\Environment;
use Didww\Verification\Model\DeliveryMethod;
use Didww\Verification\Request\SmsOptions;
use Didww\Verification\VerificationClient;

$client = new VerificationClient(
    new BasicAuth('your-app-key', 'your-app-secret'),
    new ClientOptions(environment: Environment::Sandbox),
);

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

$verification->id;           // "0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21"
$verification->status;       // "pending"
$verification->isFinished(); // false

Install the iOS SDK, then create a sandbox client with public authentication and start the verification:

import DIDWWVerification

let client = VerificationClient(
    environment: .sandbox,
    auth: .public(appKey: "your-app-key")
)

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

verification.id     // "0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21"
verification.status // .pending

Install the Android SDK. Inside an AndroidViewModel, create a sandbox client with public authentication and start collecting the verification states. Calling start(...) creates a handle. The request is sent when handle.states is first collected.

import androidx.lifecycle.viewModelScope
import com.didww.android.sdk.verification.Auth
import com.didww.android.sdk.verification.DeliveryMethod
import com.didww.android.sdk.verification.Environment
import com.didww.android.sdk.verification.SmsOptions
import com.didww.android.sdk.verification.VerificationState
import com.didww.android.sdk.verification.all.DidwwVerification
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch

val didww = DidwwVerification(
    context = application,
    auth = Auth.Public("your-app-key"),
    environment = Environment.Sandbox,
)

val verificationState = MutableStateFlow<VerificationState>(
    VerificationState.Starting,
)

val handle = didww.start(
    destination = "+12015550100",
    method = DeliveryMethod.SMS,
    sms = SmsOptions(languages = listOf("en-US")),
)

viewModelScope.launch {
    handle.states.collect { verificationState.value = it }
}

Install the React Native SDK. Build the client once at module scope, then start the verification from the hook. The hook holds the state your screen renders from.

import { VerificationClient, publicAuth } from '@didww/verification-core';
import { useVerification } from '@didww/verification-react-native';

const client = new VerificationClient({
  auth: publicAuth('your-app-key'),
  environment: 'sandbox',
});

const controller = useVerification({ client });

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

controller.state.kind; // 'starting', then 'awaitingInput'

Install the Dart SDK, then create a sandbox client and start the verification:

import 'package:didww_verification/didww_verification.dart';

final client = VerificationClient(
  auth: const PublicAuthorization('your-app-key'),
  environment: VerificationEnvironment.sandbox,
);

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

started.id;          // '0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21'
started.knownStatus; // VerificationStatus.pending

A Flutter screen usually wraps this in VerificationSession instead, which exposes the same flow as a stream of states.

Keep the returned id, verification, handle, or controller, depending on your integration. You need it to report the code and read the verification status in the next steps.

Step 4: Report the code#

When the user enters the code they received, submit it to the report endpoint:

http

PATCH /api/v1/verifications/0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21 HTTP/1.1
Host: verification-sandbox.didww.com
Content-Type: application/json
Accept: application/json
Authorization: Basic eW91cl9hcHBfa2V5OnlvdXJfYXBwX3NlY3JldA==

{
  "data": {
    "delivery_method": "sms",
    "code": "123456"
  }
}

curl

curl -i -X PATCH https://verification-sandbox.didww.com/api/v1/verifications/0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21 -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"data": {"code": "123456", "delivery_method": "sms"}}' --user your_app_key:your_app_secret

response

HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": {
    "id": "0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21",
    "destination": "12015550100",
    "delivery_method": "sms",
    "fee": "0.06",
    "status": "verified",
    "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
    }
  }
}
verification = client.report_verification(
  "0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21",
  delivery_method: "sms",
  code:            "123456"
)

verification.status    # => "verified"
verification.verified? # => true
verification = client.report_verification(
    "0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21",
    delivery_method="sms",
    code="123456",
)

verification.status  # "verified"

An incorrect code raises DidwwValidationError with code_invalid (422), and the verification stays pending for another attempt. Once three attempts are used, the next report returns status failed.

const result = await client.reportVerification(
  '0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21',
  { deliveryMethod: 'sms', code: '123456' },
);

result.status; // 'verified'
$verification = $client->reportVerification(
    '0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21',
    DeliveryMethod::Sms,
    '123456',
);

$verification->status;       // "verified"
$verification->isVerified(); // true

An incorrect code throws ValidationException with code_invalid (422), and the verification stays pending for another attempt. Once three attempts are used, the next report returns status failed.

let result = try await client.verify(
    verification,
    code: "123456"
)

result.status // .verified

Submit the code through the handle created in Step 3. The existing state collector receives the result.

handle.submit("123456")

A successful submission emits VerificationState.Submitting followed by VerificationState.Verified. If the code is rejected, the flow returns to VerificationState.AwaitingInput with lastError set.

Submit through the controller from Step 3. Nothing throws and nothing returns a promise: the outcome arrives as a new state.

controller.submit('123456');

// state.kind moves to 'submitting', then 'verified' or back to
// 'awaitingInput' with the error attached.
final finished = await client.reportVerification(
  started.id,
  deliveryMethod: started.deliveryMethod,
  value: const ReportValue.code('123456'),
);

finished.knownStatus; // VerificationStatus.verified

With VerificationSession, call session.submit(code) instead and read the result from the state stream.

A status of verified confirms the user controls the number. If the code is incorrect, the report is rejected and the verification remains available for another attempt until the attempt limit is reached. See Errors and status codes for the exact limit and other status values.

Step 5: Check the status (optional)#

Read the current status of a verification at any time. The REST API and the server-side SDKs make a status request. The on-device SDKs that drive a state machine — Android, React Native, and Dart with VerificationSession — read the latest state they already hold rather than issuing a second request.

http

GET /api/v1/verifications/0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21 HTTP/1.1
Host: verification-sandbox.didww.com
Accept: application/json
Authorization: Basic eW91cl9hcHBfa2V5OnlvdXJfYXBwX3NlY3JldA==

curl

curl -i -X GET https://verification-sandbox.didww.com/api/v1/verifications/0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21 -H "Accept: application/json" --user your_app_key:your_app_secret

response

HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": {
    "id": "0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21",
    "destination": "12015550100",
    "delivery_method": "sms",
    "fee": "0.06",
    "status": "verified",
    "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
    }
  }
}
verification = client.get_verification("0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21")

verification.status    # => "verified"
verification.verified? # => true
verification = client.get_verification("0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21")

verification.status       # "verified"
verification.is_finished  # True

is_finished is the signal to stop polling. It is derived from status, so a status added after the SDK was released is treated as finished rather than polled forever.

const current = await client.getVerification(
  '0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21',
);

current.status; // 'verified'
$verification = $client->getVerification('0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21');

$verification->status;       // "verified"
$verification->isFinished(); // true

isFinished() is the signal to stop polling. It is true for every status other than pending, so a status added after the SDK was released is treated as finished rather than polled forever.

let current = try await client.status(verification)

current.status // .verified

Do not collect handle.states a second time. Read the latest value mirrored by the collector created in Step 3:

when (val current = verificationState.value) {
    is VerificationState.AwaitingInput -> println("pending")
    is VerificationState.Verified -> println("verified")
    is VerificationState.Denied -> println("denied")
    is VerificationState.Failed -> println("failed")
    is VerificationState.SetupError -> println("configuration error")
    VerificationState.Expired -> println("expired")
    else -> println("in progress")
}

The controller from Step 3 already holds the current state. Render from it rather than issuing a status request:

switch (controller.state.kind) {
  case 'awaitingInput':
    return <Text>Waiting for the code</Text>;
  case 'verified':
    return <Text>Verified</Text>;
  case 'denied':
  case 'expired':
  case 'failed':
    return <Text>Not verified</Text>;
  default:
    return <Text>Please wait...</Text>;
}

With the client, read the status directly:

final current = await client.getVerification(started.id);

current.knownStatus; // VerificationStatus.verified
current.isFinished;  // true

With VerificationSession, read session.state or listen to session.states instead of making a second request.

Move to production#

After the sandbox flow works as expected, configure the integration separately for production:

  1. Sign in to the DIDWW Production User Panel.

  2. Create a new OTP application in the production account and configure its callback URL, minimum authentication mode, verification settings, and SMS settings. See OTP application settings reference.

  3. Copy the key and secret from the production OTP application. Store them separately from the sandbox credentials.

  4. Change the API or SDK environment from sandbox to production:

    Integration

    Sandbox

    Production

    REST API

    https://verification-sandbox.didww.com

    https://verification.didww.com

    Ruby SDK

    env: :sandbox

    env: :production or omit env

    iOS SDK

    environment: .sandbox

    environment: .production

    Android SDK

    environment = Environment.Sandbox

    environment = Environment.Production

  5. Run a controlled verification using a phone number you can access.

Warning

Production requests can contact real destinations and incur verification and delivery charges. Confirm that the production OTP application, credentials, callback URL, and API environment are configured together before sending requests.

Next steps#