Authentication#

Every request to the Verification API is authenticated with an OTP application. Each OTP application has a key and a secret. The authentication mode determines which credentials are sent with a request and how they are used.

The API supports three authentication modes of increasing strength. Each application declares a minimum mode. Requests authenticated below that minimum are rejected.


Credentials#

Important

Create the OTP application and credentials in the same environment as the API endpoint. Sandbox credentials work only with the sandbox API, and production credentials work only with the production API. See Choose an environment.

An OTP application has two credentials:

Credential

Description

key

Public identifier of the OTP application. Sent by itself in public mode, used as the HTTP Basic username, and included in the signed application header.

secret

Private shared secret. Used as the HTTP Basic password and as the HMAC signing key for the application scheme. Keep it confidential.

Every OTP application has both credentials, including applications whose minimum authentication mode is public. Public clients use only the key and must not receive or store the secret.

Both credentials are available on the application’s page in the User Panel.

Authentication modes#

The Authorization header determines which authentication mode a request uses. The minimum mode configured on the OTP application sets the lowest mode that is allowed. Requests using a lower mode are rejected with 401.

For example, an application set to public accepts public, basic, and application requests. An application set to basic accepts basic and application requests. An application set to application accepts only signed application requests.

Mode

Authorization header

When to use it

public

Application <key>

Untrusted clients, such as mobile or web apps, where only the application key is sent with the request. Requires a request callback so your backend can approve each verification. Without a callback URL, public-mode requests are denied.

basic

Basic <base64(key:secret)>

Server-to-server calls where the secret can be kept private. This is the default mode for server-side REST API examples.

application

Application <key>:<signature>

Server-to-server calls that additionally sign each request with the secret, protecting against tampering and replay. The strongest mode.

Note

The public and application modes share the Application scheme prefix. They are distinguished by whether a :<signature> is present.

SDK support#

The REST API supports all three authentication modes. SDK support depends on whether the integration runs on a trusted server or an untrusted mobile device. On-device SDKs omit the signed application mode because it requires a signing secret, which must never be shipped in an application:

SDK

public

basic

application

Ruby

auth_mode: :public

auth_mode: :basic (default)

auth_mode: :application

Python

PublicAuth(key)

BasicAuth(key, secret)

ApplicationAuth(key, secret)

Node.js

publicAuth(key)

basicAuth(key, secret)

applicationAuth({key, secret}), from @didww/verification-node

PHP

new PublicAuth($key)

new BasicAuth($key, $secret)

new ApplicationAuth($key, $secret)

iOS

.public(appKey:)

.basic(appKey:secret:), for local development

Not available.

Android

Auth.Public(applicationKey)

Auth.Basic(key, secret), for local development

Not available.

React Native

publicAuth(key)

basicAuth(key, secret), for local development

Not available.

Dart and Flutter

PublicAuthorization(key)

BasicAuthorization(key:, secret:), where the process is yours

Not available.

Authentication result#

A valid Authorization header allows the API to continue processing the request. It does not guarantee that a verification will be started or delivered. For example, a start request may still be denied by a request callback.

If authentication fails, the API returns 401 Unauthorized with error code unauthorized. This includes missing or invalid credentials, an authentication mode below the application’s configured minimum, an invalid signature, or a stale timestamp. No verification is started. See Errors and Status Codes.

Public (public mode)#

The public mode sends only the application key in the request. The OTP application still has a secret, but public-mode clients do not send or store it. This mode is for untrusted clients that must not embed the secret, such as SDKs running in mobile or web apps.

The application must have a request callback configured so your backend can approve each verification. Without a callback URL, public-mode requests are denied.

Authorization: Application <key>

The following examples start an SMS verification using public authentication:

http

POST /api/v1/verifications HTTP/1.1
Host: verification.didww.com
Content-Type: application/json
Accept: application/json
Authorization: Application your-app-key

{
  "data": {
    "destination": "+4915112345678",
    "delivery_method": "sms"
  }
}

curl

curl -i -X POST https://verification.didww.com/api/v1/verifications -H "Accept: application/json" -H "Content-Type: application/json" -H "Authorization: Application your-app-key" --data-raw '{"data": {"delivery_method": "sms", "destination": "+4915112345678"}}'

Set auth_mode: :public. No secret is required in the SDK client.

client = DIDWW::OTPVerification::Client.new(
  key:       "your-app-key",
  auth_mode: :public
)

client.start_verification(
  destination:     "+4915112345678",
  delivery_method: "sms"
)

Pass PublicAuth. No secret is required in the SDK client.

from didww_verification import PublicAuth, VerificationClient

with VerificationClient(PublicAuth("your-app-key")) as client:
    client.start_verification(
        destination="+4915112345678",
        delivery_method="sms",
    )

Pass publicAuth(key). It is exported from @didww/verification-core, so a client that only starts verifications needs no other package.

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

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

await client.startVerification({
  destination: '+4915112345678',
  deliveryMethod: 'sms',
});

Pass PublicAuth. No secret is required in the SDK client.

use Didww\Verification\Auth\PublicAuth;
use Didww\Verification\Model\DeliveryMethod;
use Didww\Verification\VerificationClient;

$client = new VerificationClient(new PublicAuth('your-app-key'));

$client->startVerification('+4915112345678', DeliveryMethod::Sms);

Use .public(appKey:) in production mobile apps. The client sends only the application key.

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

let verification = try await client.start(
    destination: "+4915112345678",
    method: .sms
)

Use Auth.Public in production mobile apps. The request is sent when the returned handle’s states flow is first collected.

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

val handle = didww.start(
    destination = "+4915112345678",
    method = DeliveryMethod.SMS,
)

viewModelScope.launch {
    handle.states.collect { state -> println(state) }
}

Build the client once at module scope with publicAuth(key), then drive the verification from useVerification().

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

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

const controller = useVerification({ client });

controller.start({
  destination: '+4915112345678',
  deliveryMethod: 'sms',
});

Use PublicAuthorization. It is the mode a Flutter application should ship with.

final client = VerificationClient(
  auth: const PublicAuthorization('your-application-key'),
);

final started = await client.startVerification(
  destination: '+4915112345678',
  deliveryMethod: DeliveryMethod.sms,
);

After authenticating the key, the API sends a request to the application’s callback URL. The callback result determines whether the verification proceeds or is denied. Without a configured callback URL, the request is denied with denied_missing_callback_url.

Calling from the browser#

/api/v1 answers cross-origin requests from any origin: every response, including error responses, carries Access-Control-Allow-Origin: *, and a preflight OPTIONS request is answered without an Authorization header. Allowed request methods are GET, POST, PUT, PATCH, OPTIONS; allowed request headers are Authorization, Content-Type, Accept, x-timestamp, and X-User-Agent. X-Request-Id and Retry-After are readable from JavaScript on the response.

There is no credentialed (cookie-based) mode, so no Access-Control-Allow-Credentials header is ever sent; authentication is always the Authorization header. Use public mode for browser calls — it is the only mode that does not require the application secret. Never ship the secret in browser-side code: both basic and application mode need it. A public-mode application must have a request callback: your backend approves each verification there. Without a callback URL, every start is denied with denied_missing_callback_url.

HTTP Basic (basic mode)#

Use basic mode for server-side integrations where the OTP application secret can be kept private. HTTP Basic uses the key as the username and the secret as the password:

Authorization: Basic <base64(key:secret)>

Where <base64(key:secret)> is the Base64 encoding of the key and secret joined by a colon. Most HTTP clients construct this header automatically when you supply a username and password.

Warning

Use basic authentication in the iOS and Android SDKs only for local development. A secret included in a mobile app can be extracted from the app binary. Use public authentication with a request callback in production mobile apps.

The following examples start an SMS verification using HTTP Basic authentication:

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"
  }
}

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"}}' --user your_app_key:your_app_secret

The Ruby SDK builds the Basic header for you. :basic is the default auth mode.

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

client.start_verification(
  destination:     "+4915112345678",
  delivery_method: "sms"
)

The Python SDK builds the Basic header from BasicAuth.

from didww_verification import BasicAuth, VerificationClient

with VerificationClient(BasicAuth("your-app-key", "your-app-secret")) as client:
    client.start_verification(
        destination="+4915112345678",
        delivery_method="sms",
    )

The Node.js SDK builds the Basic header from basicAuth(key, secret). Server-side only: the secret is sent on every request.

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

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

await client.startVerification({
  destination: '+4915112345678',
  deliveryMethod: 'sms',
});

The PHP SDK builds the Basic header from BasicAuth.

use Didww\Verification\Auth\BasicAuth;
use Didww\Verification\Model\DeliveryMethod;
use Didww\Verification\VerificationClient;

$client = new VerificationClient(new BasicAuth('your-app-key', 'your-app-secret'));

$client->startVerification('+4915112345678', DeliveryMethod::Sms);

Use .basic(appKey:secret:) for local development only. The SDK builds the Basic header from the supplied credentials.

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

let verification = try await client.start(
    destination: "+4915112345678",
    method: .sms
)

Use Auth.Basic for local development only. The SDK logs a warning when it is used in a build that is not marked debuggable.

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

val handle = didww.start(
    destination = "+4915112345678",
    method = DeliveryMethod.SMS,
)

viewModelScope.launch {
    handle.states.collect { state -> println(state) }
}

Use basicAuth(key, secret) for local development only. A secret in an application bundle is recoverable by anyone who installs it.

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

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

Use BasicAuthorization only where the process is yours, such as a server-side Dart program or local development.

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

If the credentials are valid, the request continues. When the OTP application has a callback URL, the API waits for the callback decision before proceeding. Without a callback URL, the verification proceeds without that approval step.

Signed requests (application mode)#

Use application mode for server-side integrations that need signed requests. This mode uses HMAC-SHA256 to sign each request with the OTP application secret, so DIDWW can verify the caller and reject requests that were altered or replayed.

Send two headers:

Header

Value

Authorization

Application <key>:<signature>

x-timestamp

Unix time in seconds when the request is signed.

Note

The x-timestamp value must be within 5 minutes of the server’s clock. Requests outside that window are rejected as stale.

The REST API and Ruby SDK examples start an SMS verification using a signed application request. The iOS and Android tabs explain the mobile SDK limitation.

http

POST /api/v1/verifications HTTP/1.1
Host: verification.didww.com
Content-Type: application/json
Accept: application/json
Authorization: Application your-app-key:<signature>
x-timestamp: <unix-timestamp>

{
  "data": {
    "destination": "+4915112345678",
    "delivery_method": "sms"
  }
}

curl

curl -i -X POST https://verification.didww.com/api/v1/verifications -H "Accept: application/json" -H "Content-Type: application/json" -H "X-Timestamp: <unix-timestamp>" -H "Authorization: Application your-app-key:<signature>" --data-raw '{"data": {"delivery_method": "sms", "destination": "+4915112345678"}}'

The Ruby SDK computes the signature and sets both headers when auth_mode: :application is used.

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

client.start_verification(
  destination:     "+4915112345678",
  delivery_method: "sms"
)

The Python SDK signs each request and sets both headers when the client is built with ApplicationAuth. The secret is validated when the auth object is constructed, not on the first request.

from didww_verification import ApplicationAuth, VerificationClient

auth = ApplicationAuth("your-app-key", "your-app-secret")

with VerificationClient(auth) as client:
    client.start_verification(
        destination="+4915112345678",
        delivery_method="sms",
    )

applicationAuth ships in @didww/verification-node, not in the core package, because it is server-side only. The secret never goes on the wire.

import { VerificationClient } from '@didww/verification-core';
import { applicationAuth } from '@didww/verification-node';

const client = new VerificationClient({
  auth: applicationAuth({
    key: process.env.DIDWW_APPLICATION_KEY!,
    secret: process.env.DIDWW_APPLICATION_SECRET!,
  }),
});

The PHP SDK signs each request and sets both headers when the client is built with ApplicationAuth. The secret is validated when the auth object is constructed, not on the first request.

use Didww\Verification\Auth\ApplicationAuth;
use Didww\Verification\Model\DeliveryMethod;
use Didww\Verification\VerificationClient;

$auth = new ApplicationAuth('your-app-key', 'your-app-secret');

$client = new VerificationClient($auth);

$client->startVerification('+4915112345678', DeliveryMethod::Sms);

The iOS SDK does not support signed application authentication. This mode requires the OTP application secret, which must not be included in a mobile app. Use .public(appKey:) with a request callback for production iOS integrations.

The Android SDK does not support signed application authentication. This mode requires the OTP application secret, which must not be included in an APK. Use Auth.Public with a request callback for production Android integrations.

The React Native SDK does not support signed application authentication. This mode requires the OTP application secret, which must not be included in an application bundle. Use publicAuth(key) with a request callback for production integrations.

The Dart SDK does not support signed application authentication. This mode requires the OTP application secret, which must not be included in a Flutter application bundle. Use PublicAuthorization with a request callback for production integrations.

The API verifies the signature and timestamp before processing the request. Valid signed requests do not trigger the request callback. An invalid signature or stale timestamp causes the API to reject the request with 401 Unauthorized.

The rest of this section explains how to build the signature yourself when integrating without an SDK.

Building the signature#

  1. Build the string to sign by joining these five components with newline (\n) characters, in this order:

    <HTTP-METHOD>
    <CONTENT-MD5>
    <CONTENT-TYPE>
    x-timestamp:<TIMESTAMP>
    <PATH>
    

    Component

    Value

    HTTP-METHOD

    Request method in uppercase, for example POST.

    CONTENT-MD5

    Base64-encoded MD5 digest of the raw request body. Use an empty string when there is no body.

    CONTENT-TYPE

    The request Content-Type, for example application/json.

    TIMESTAMP

    The same value sent in the x-timestamp header.

    PATH

    The request path, for example /api/v1/verifications.

  2. Derive the signing key by Base64url-decoding the application secret to raw bytes.

  3. Compute HMAC-SHA256(signing_key, string_to_sign) and Base64-encode the result. This value is the <signature>.

Important

Calculate CONTENT-MD5 and the signature from the exact request body bytes sent to the API. Changing whitespace, field ordering, content type, timestamp, or request path after signing causes signature verification to fail.

Authorization: Application 3f1c...e9:Base64(HMAC-SHA256(Base64urlDecode(secret), string_to_sign))
x-timestamp: 1752573720

Note

The same signing scheme applies to request callbacks sent to your server. You can use the same HMAC calculation to sign outgoing requests and verify incoming callbacks.

Reference implementation (bash)#

Use this self-contained bash script to compute a signature from the request parts. It requires openssl and can be used to check your own implementation against a known input.

Assign the request values at the top of the script. The script prints the <signature>.

#!/bin/bash
set -euo pipefail

# --- Request parts (assign these from your actual request) ---
SECRET='c2FtcGxlLWFwcGxpY2F0aW9uLXNlY3JldC0wMTIzNDU2Nzg5'   # application secret (URL-safe base64)
HTTP_METHOD='POST'
CONTENT_TYPE='application/json'
REQUEST_PATH='/api/v1/verifications'
TIMESTAMP='1752573720'   # seconds since epoch, e.g. from `date +%s` or `time.time()`
BODY='{"data":{"destination":"+4915112345678","delivery_method":"sms","sms":{"languages":["en-US"]}}}'

# CONTENT-MD5: Base64(MD5(body)), empty string when the body is empty.
if [ -n "$BODY" ]; then
  CONTENT_MD5="$(printf '%s' "$BODY" | openssl dgst -md5 -binary | openssl base64 -A)"
else
  CONTENT_MD5=''
fi

# String to sign: METHOD, CONTENT-MD5, CONTENT-TYPE, x-timestamp:TS, PATH (newline-joined).
STRING_TO_SIGN="$(printf '%s\n%s\n%s\nx-timestamp:%s\n%s' \
  "$HTTP_METHOD" "$CONTENT_MD5" "$CONTENT_TYPE" "$TIMESTAMP" "$REQUEST_PATH")"

# Signing key = raw bytes of the URL-safe base64 secret. Convert the URL-safe
# alphabet (-_) to standard (+/) and restore '=' padding so `openssl base64 -d` accepts it.
STD_SECRET="$(printf '%s' "$SECRET" | tr '_-' '/+')"
case $(( ${#STD_SECRET} % 4 )) in
  2) STD_SECRET="${STD_SECRET}==" ;;
  3) STD_SECRET="${STD_SECRET}=" ;;
esac
SIGNING_KEY_HEX="$(printf '%s' "$STD_SECRET" | openssl base64 -d -A | od -An -v -tx1 | tr -d ' \n')"

# HMAC-SHA256 over the string to sign, base64-encoded.
printf '%s' "$STRING_TO_SIGN" \
  | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$SIGNING_KEY_HEX" -binary \
  | openssl base64 -A
echo

Note

PATH is a reserved shell variable. The script uses REQUEST_PATH so it does not overwrite the executable search path.

With the sample values above, the script prints:

BY/LCMM2R4eDkONcS4DyssymoWKAYXgUoxTc50Yssf8=

If your implementation produces the same signature for these input values, it matches this test vector. The signed request would include:

Authorization: Application <your-application-key>:BY/LCMM2R4eDkONcS4DyssymoWKAYXgUoxTc50Yssf8=
x-timestamp: 1752573720

Tip

The Ruby, Python, Node.js, and PHP SDKs sign requests automatically in application mode. You only need to implement this signing scheme when integrating without an SDK.