# Authentication

Every request to the Verification API is authenticated with an
[OTP application](getting-started.html#otp-verification-getting-started). 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](getting-started.html#otp-verification-environments).

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](callbacks.html#otp-verification-callbacks) 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](sdks/ruby.html#otp-verification-sdk-ruby) | `auth_mode: :public` | `auth_mode: :basic` (default) | `auth_mode: :application` |
| [Python](sdks/python.html#otp-verification-sdk-python) | `PublicAuth(key)` | `BasicAuth(key, secret)` | `ApplicationAuth(key, secret)` |
| [Node.js](sdks/node.html#otp-verification-sdk-node) | `publicAuth(key)` | `basicAuth(key, secret)` | `applicationAuth({key, secret})`, from `@didww/verification-node` |
| [PHP](sdks/php.html#otp-verification-sdk-php) | `new PublicAuth($key)` | `new BasicAuth($key, $secret)` | `new ApplicationAuth($key, $secret)` |
| [iOS](sdks/ios.html#otp-verification-sdk-ios) | `.public(appKey:)` | `.basic(appKey:secret:)`, for local development | Not available. |
| [Android](sdks/android.html#otp-verification-sdk-android) | `Auth.Public(applicationKey)` | `Auth.Basic(key, secret)`, for local development | Not available. |
| [React Native](sdks/react-native.html#otp-verification-sdk-react-native) | `publicAuth(key)` | `basicAuth(key, secret)`, for local development | Not available. |
| [Dart and Flutter](sdks/dart.html#otp-verification-sdk-dart) | `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](callbacks.html#otp-verification-callbacks).

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](api-reference/errors.html#otp-verification-api-errors).

## 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](callbacks.html#otp-verification-callbacks) 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:

REST API

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


Ruby SDK

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


Python SDK

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


Node.js SDK

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',
});
```


PHP SDK

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);
```


iOS SDK

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


Android SDK

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


React Native SDK

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',
});
```


Dart SDK

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](callbacks.html#otp-verification-callbacks):
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:

REST API

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
```


Ruby SDK

The [Ruby SDK](sdks/ruby.html#otp-verification-sdk-ruby) 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"
)
```


Python SDK

The [Python SDK](sdks/python.html#otp-verification-sdk-python) 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",
    )
```


Node.js SDK

The [Node.js SDK](sdks/node.html#otp-verification-sdk-node) 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',
});
```


PHP SDK

The [PHP SDK](sdks/php.html#otp-verification-sdk-php) 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);
```


iOS SDK

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


Android SDK

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


React Native SDK

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',
});
```


Dart SDK

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.

REST API

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


Ruby SDK

The [Ruby SDK](sdks/ruby.html#otp-verification-sdk-ruby) 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"
)
```


Python SDK

The [Python SDK](sdks/python.html#otp-verification-sdk-python) 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",
    )
```


Node.js SDK

`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!,
  }),
});
```


PHP SDK

The [PHP SDK](sdks/php.html#otp-verification-sdk-php) 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);
```


iOS SDK

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](callbacks.html#otp-verification-callbacks) for
production iOS integrations.


Android SDK

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](callbacks.html#otp-verification-callbacks) for
production Android integrations.


React Native SDK

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](callbacks.html#otp-verification-callbacks) for production
integrations.


Dart SDK

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](callbacks.html#otp-verification-callbacks) 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](callbacks.html#otp-verification-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](sdks/ruby.html#otp-verification-sdk-ruby), [Python](sdks/python.html#otp-verification-sdk-python),
[Node.js](sdks/node.html#otp-verification-sdk-node), and [PHP](sdks/php.html#otp-verification-sdk-php) SDKs
sign requests automatically in `application` mode. You only need to implement this signing scheme when integrating
without an SDK.

On this page
