Getting Started#

This guide walks you through creating an OTP application, generating API credentials, and running your first verification from start to finish. The walkthrough uses the sandbox environment so you can 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.


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.

  • Access to the corresponding User Panel is required to create an OTP application and copy its credentials.

  • A positive account balance is required for production. The verification fee is billed when a verification reaches verified. SMS and call delivery costs are billed separately.

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

For this walkthrough, create the application in the DIDWW Sandbox User Panel.

  1. Open the User Panel for your selected environment: use the DIDWW Sandbox User Panel for testing or the DIDWW Production User Panel for live verifications.

  2. Expand OTP Verify.

  3. Select Applications.

  4. Click Create application.

  5. Enter a name for the application.

  6. Optionally, enter a description.

  7. Set a 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 reject each start request. The callback URL is optional for basic and application authentication. See Callbacks.

  8. Select the minimum authentication mode required for requests to this OTP application:

    • Public: anyone with the credential key can call the OTP endpoint.

    • Basic: callers must use HTTP Basic authentication with the credential key as the username and the credential secret as the password.

    • Application: callers must sign each request with the credential secret.

  9. Optionally, adjust the Verification settings:

    • Code length: the number of digits in the code, from 4 to 8. The default is 6.

    • Code timeout: how long the code stays valid, counted from when the verification starts. Choose 1, 2, 5, or 10 minutes, or Custom to enter 60 to 600 seconds. The default is 5 minutes.

    Changes to these settings apply to new verifications only.

  10. Click Create to save the application.

Create OTP application form in the DIDWW Sandbox User Panel

Create an OTP application in the DIDWW Sandbox User Panel.#

Step 2: Get your API credentials#

Each OTP application has a key and a secret. The key identifies the OTP application when you make verification requests. The secret is used for stronger authentication modes, such as HTTP Basic or signed application authentication.

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 OTP Verify > Applications, find 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.

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, use Regenerate secret on the application page in the corresponding DIDWW User Panel: the old secret stops working immediately and the key does not change.

OTP application credentials in the DIDWW Sandbox User Panel

Copy the key and secret from the Credentials column.#

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.

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": "+4915112345678",
    "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": "+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
    }
  }
}

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:     "+4915112345678",
  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="+4915112345678",
    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: '+4915112345678',
  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(
    '+4915112345678',
    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: "+4915112345678",
    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 = "+4915112345678",
    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: '+4915112345678',
  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: '+4915112345678',
  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": "4915112345678",
    "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": "4915112345678",
    "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 and minimum authentication mode.

  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#