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 |
secret |
Private shared secret. Used as the HTTP Basic password and as the HMAC signing key
for the |
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 |
|---|---|---|
|
|
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. |
|
|
Server-to-server calls where the secret can be kept private. This is the default mode for server-side REST API examples. |
|
|
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 |
|---|---|---|---|
|
Supported with |
Supported with |
Supported with |
|
Supported and used by default. |
Supported for local development with |
Supported for local development with |
|
Supported with |
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 |
|---|---|
|
|
|
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#
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-METHODRequest method in uppercase, for example
POST.CONTENT-MD5Base64-encoded MD5 digest of the raw request body. Use an empty string when there is no body.
CONTENT-TYPEThe request
Content-Type, for exampleapplication/json.TIMESTAMPThe same value sent in the
x-timestampheader.PATHThe request path, for example
/api/v1/verifications.Derive the signing key by Base64url-decoding the application secret to raw bytes.
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.