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:

Mode

Ruby SDK

iOS SDK

Android SDK

public

Supported with auth_mode: :public.

Supported with .public(appKey:).

Supported with Auth.Public(applicationKey).

basic

Supported and used by default.

Supported for local development with .basic(appKey:secret:).

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

application

Supported with auth_mode: :application.

Not available. It requires a signing secret on the device.

Not available. It requires a signing secret on the device.

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

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

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.

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

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

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 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 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 SDK signs requests automatically in application mode. You only need to implement this signing scheme when integrating without an SDK.