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 |
|
Integration testing |
|
Production |
|
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.
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.
Expand OTP Verify.
Select Applications.
Click Create application.
Enter a name for the application.
Optionally, enter a description.
Set a callback URL if you plan to use
publicauthentication, 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 forbasicandapplicationauthentication. See Callbacks.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.
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.
Click Create to save the application.
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.
In OTP Verify > Applications, find the application you created.
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.
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.
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:
Sign in to the DIDWW Production User Panel.
Create a new OTP application in the production account and configure its callback URL and minimum authentication mode.
Copy the key and secret from the production OTP application. Store them separately from the sandbox credentials.
Change the API or SDK environment from sandbox to production:
Integration
Sandbox
Production
REST API
https://verification-sandbox.didww.comhttps://verification.didww.comRuby SDK
env: :sandboxenv: :productionor omitenviOS SDK
environment: .sandboxenvironment: .productionAndroid SDK
environment = Environment.Sandboxenvironment = Environment.ProductionRun 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#
Compare the delivery channels in Verification Methods.
Review the available authentication modes and choose the one that fits your integration in Authentication.
Gate verifications in real time with a request callback.
Review every field and status code in the API Reference.