> For the complete documentation index, see [llms.txt](https://docs.openiam.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openiam.com/administration-guide/platform/sms-and-otp.md).

# SMS and one-time passwords

Configure a Twilio, SMSGlobal, email or Groovy OTP provider with the real attribute names and defaults, and test it with a disposable identity.

## What this is, and when you need it

An **OTP provider** (`OTP_PROVIDER`) is a delivery channel for a generated one-time code: an SMS, a voice call, an email, or a Groovy class you supply. It is a standalone object with a name, a type and a free-form set of name/value attributes. It does nothing until an authentication provider references it, and it never decides code lifetime, retry count or lockout — those are policy parameters.

This page covers the provider object: attribute names, transport defaults, the vault, failover, and how to prove delivery. Attaching a provider to an authentication provider, the SMS/EMAIL/TOTP auth levels and the `SUPPORTS_*` capability flags are in [Authentication providers and MFA](/administration-guide/security/authentication-providers-and-mfa.md). TOTP is *not* an OTP provider — it is generated on the device and validated locally, so it has no provider row.

## At a glance

|                          |                                                                                                                                                                 |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **You will end up with** | An `OTP_PROVIDER` that delivers a one-time code to a test identity, with its gateway secret in the vault and a `SEND_OTP_TOKEN` audit event proving the send    |
| **Where**                | **Access Control → OTP Providers** — `/webconsole/otp-provider-search`, editing at `/webconsole/otp-provider-edit`                                              |
| **You need first**       | A gateway account and an approved sender; a disposable identity with an OTP phone or an active email address; the authentication provider you will attach it to |
| **Takes**                | 20 to 30 minutes, plus whatever your gateway takes to approve a sender number                                                                                   |
| **Reversible**           | Partly. Detaching and deleting the provider is clean, but deleting it also deletes its in-vault attribute values, which cannot be recovered                     |

## Before you begin

* **A gateway account and an approved sender.** For Twilio that is an Account SID, an auth token and a `From` number; for SMSGlobal an API key and secret; for email a working mailbox configuration ([Mail and notifications](/administration-guide/platform/mail-and-notifications.md)).
* **A disposable test identity with a usable destination.** OpenIAM takes the destination from the user's OTP phone for SMS and voice, and from their default email address for email; it never asks the user where to send the code. No usable destination is the single most common cause of "nothing was sent".
* **The authentication provider you will attach it to**, and one narrow URI pattern you can point at it.
* **Choose the channel deliberately.** There are exactly five provider types and each one fixes its channel — see [The five provider types](#the-five-provider-types). Prefer `TWILIO_TEXT` for SMS: the shipped `SMS_GLOBAL_TEXT` channel addresses the message to `FROM_ACCOUNT` rather than to the user's phone, and reports success while doing it. That defect is described under [SMSGlobal transport defaults](#smsglobal-transport-defaults).

## Configure an OTP provider and test it

This example uses Twilio SMS, which addresses the user's phone correctly. Every attribute name below is recognised exactly as written and nowhere validated, so type them character for character — the full set, and which type reads which, is in [The attribute names](#the-attribute-names).

{% stepper %}
{% step %}

### Create the provider before attaching it to anything

**Access Control → OTP Providers → New** (`/webconsole/otp-provider-edit`). Set **Name** to `Twilio SMS test` and **Type** to `Twilio SMS Provider`. Leave **Failover OTP Provider** empty. Save once so the row gets an id before you add attributes.
{% endstep %}

{% step %}

### Add the five attributes, exactly named

In **Attributes**, add one row per line below. Tick **Store in Secret Vault** only on `ACCOUNT_AUTH_TOKEN`.

| Name                  | Value                                                       | In vault |
| --------------------- | ----------------------------------------------------------- | -------- |
| `ACCOUNT_ID`          | your Twilio Account SID                                     | no       |
| `ACCOUNT_AUTH_TOKEN`  | your Twilio auth token                                      | **yes**  |
| `FROM_ACCOUNT`        | `+15550000000` — your approved Twilio sender number         | no       |
| `TEXT_MESSAGE_FORMAT` | `Your <YOUR_ORG> verification code is %s. Do not share it.` | no       |
| `TOKEN_LEN`           | `6`                                                         | no       |

`ACCOUNT_ID`, `ACCOUNT_AUTH_TOKEN` and `FROM_ACCOUNT` are the three Twilio SMS requires; a missing one fails every send with `RESULT_INVALID_CONFIGURATION`. The other two are optional: a blank `TEXT_MESSAGE_FORMAT` falls back to the literal `Your code is %s`, and an absent `TOKEN_LEN` gives six digits.

Save, reopen the record, and confirm `ACCOUNT_AUTH_TOKEN` now shows a UUID in a database dump rather than your token — that is the vault indirection working. The screen still shows the real value, in plain text.

{% hint style="warning" %}
**Twilio Voice reads a different attribute name for the same token.** `TWILIO_VOICE` requires `ACCOUNT_PASSWORD`, not `ACCOUNT_AUTH_TOKEN`. Copying attributes from an SMS provider to a voice provider therefore does not work, and the symptom is an authentication failure against Twilio rather than a configuration error from OpenIAM.

The value must be the account's primary **Auth Token**, not an API key secret. The IDP uses it to check the signature on Twilio's callback, and Twilio always signs with the Auth Token. With an API key secret the call is placed, then the callback is refused and the call says nothing.
{% endhint %}
{% endstep %}

{% step %}

### Create the disposable identity and give it an OTP phone

Create `<TEST_USER>` in a test organization. Add a phone whose metadata type is the one flagged for SMS OTP — `org.openiam.ui.otp.sms.phone.require.on.create` ships **`true`**, so the phone is required on create anyway. Store it in international format: OpenIAM prefixes a `+` when one is missing and does nothing else, so a national-format number reaches the gateway as a malformed international one and is rejected there rather than here.
{% endstep %}

{% step %}

### Attach it to one authentication provider and one URI pattern

On the authentication provider, add `Twilio SMS test` under **Supported OTP Providers** — the join is `OTP_TO_AUTH_PROVIDER_XREF` — and confirm the provider's `SUPPORTS_SMS_OTP` flag is set, or the IdP will not route to `/idp/auth-otp` at all. Point one narrow test URI pattern at it. The full procedure is in [Authentication providers and MFA](/administration-guide/security/authentication-providers-and-mfa.md).
{% endstep %}

{% step %}

### Send one code and read the audit event, not the screen

Sign in as `<TEST_USER>` against that URI pattern so a code is actually sent, then look for the `SEND_OTP_TOKEN` audit event: it carries the `OTP_TYPE` attribute, the URI pattern id, the authentication provider id and the managed system, with `<TEST_USER>` as the target and the resolved principal alongside. Count the digits in the code that arrives: `TOKEN_LEN` is clamped to 4 to 9 and a value outside that range is silently corrected, with only a `WARN` line in the ESB log to say so.

A successful `SEND_OTP_TOKEN` means only that the gateway accepted the request — it does not mean a handset received anything — so reconcile it against the gateway's own message log by timestamp before you call the channel working. Twilio does report a delivery outcome: a message coming back `FAILED`, `UNDELIVERED`, `PARTIALLY_DELIVERED` or `CANCELED` raises `SMS_TOKEN_GENERATE_ERROR` with Twilio's own error message attached, so that response code carrying a Twilio message is a carrier problem rather than a configuration one.
{% endstep %}

{% step %}

### Test the negative case before you widen scope

Enter a wrong code and confirm `CONFIRM_SMS_OTP_TOKEN` records the failure. Then wait past `OTP_SMS_LIFETIME` and confirm the correct code is refused too — the expiry is written to `LOGIN.SMS_CODE_EXPIRATION` before the send and is also the moving factor of the code generator, so an expired code is not merely rejected, it is a different code.
{% endstep %}
{% endstepper %}

## Verification

| Evidence                                                | Meaning                                                                                                                                                      |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `SEND_OTP_TOKEN` succeeded                              | The provider module returned without throwing. `OTP_TYPE`, auth provider id, URI pattern id and managed system are on the event                              |
| `SEND_OTP_TOKEN` failed with `SMS_TOKEN_GENERATE_ERROR` | The gateway rejected or the HTTP call failed. The cause is attached                                                                                          |
| `SEND_OTP_TOKEN` failed with `ACCESS_DENIED`            | Anonymous rate limit hit on that ESB node                                                                                                                    |
| `SEND_OTP_TOKEN` failed with `PRINCIPAL_NOT_FOUND`      | The user has no login on the authentication provider's managed system                                                                                        |
| `CONFIRM_SMS_OTP_TOKEN`                                 | A code was submitted. Success or failure is on the event                                                                                                     |
| `RESET_OTP_STATUS`                                      | An administrator cleared a blocked OTP method. `CLEAR_SMS_OTP_STATUS` is its deprecated predecessor, so search for both if you are reading historical events |
| `ADD_OTP_PROVIDER`                                      | The provider was created **or** edited. In-vault values appear as `[ SECRET ]`                                                                               |
| `SEND_EMAIL_NOTIFICATION` (source `EMAIL_MANAGER`)      | For `EMAIL_TEXT` only — the `OTP_CODE` mail actually reached SMTP                                                                                            |

Retention for each is a `SystemAuditLog` property and every one ships `-1`; see [Audit actions and retention](/administration-guide/reference/audit-actions-and-retention.md).

## When it goes wrong

| Symptom                                                                                                                       | Inspect                                                                                          | Fix                                                                                                                                                                                                 |
| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RESULT_INVALID_CONFIGURATION` on every send                                                                                  | `OTP_PROVIDER_ATTR.NAME` for the provider                                                        | An attribute name is misspelled or missing. Compare against the required column in [The attribute names](#the-attribute-names) — the field is free text                                             |
| Twilio SMS works, Twilio Voice does not authenticate                                                                          | `ACCOUNT_PASSWORD` on the voice provider                                                         | Voice reads `ACCOUNT_PASSWORD`, SMS reads `ACCOUNT_AUTH_TOKEN`. Add the second name                                                                                                                 |
| `VOICE_CALL_ERROR` with valid Twilio credentials                                                                              | The content provider in the request context, and its `DOMAIN_PATTERN`                            | The TwiML callback URL is built from the content provider. No content provider, or an unroutable domain, and the call cannot be placed                                                              |
| Voice call connects, then says nothing; IDP log shows *"Rejected a Twilio callback: the signature does not match https\://…"* | The scheme and host in that log line, and `ACCOUNT_PASSWORD`                                     | If the scheme or host is not the public one Twilio called, the proxy is not passing `X-Forwarded-Proto` / `X-Forwarded-Host`. If they are right, `ACCOUNT_PASSWORD` is not the account's Auth Token |
| Voice call says nothing; IDP log shows *"it is for a different Twilio account"*                                               | `ACCOUNT_ID` on the voice provider                                                               | It must be the SID of the account whose Auth Token is in `ACCOUNT_PASSWORD`                                                                                                                         |
| Voice call says nothing, the callback returns 404                                                                             | The time between the send and Twilio's fetch                                                     | The code is fetchable once, for `org.openiam.otp.voice.call.ttl.seconds` (120 by default). A slow answer or a Twilio retry after the first fetch finds nothing                                      |
| SMS sent, `SEND_OTP_TOKEN` succeeded, nothing arrives, and the gateway log shows the message going to your own sender number  | The gateway's message log, not OpenIAM's — nothing on this side records the destination          | The destination is taken from `FROM_ACCOUNT` on the shipped SMSGlobal channel. Switch to `TWILIO_TEXT` or a `GROOVY` provider                                                                       |
| SMSGlobal returns 401                                                                                                         | `HOSTNAME`, `URL`, and whether your endpoint is on 443                                           | Port `443` is baked into the MAC signature string; the signed path must equal the `URL` attribute exactly                                                                                           |
| Code is 4 digits when you asked for 3, or 9 when you asked for 12                                                             | ESB log for *"Length of token is less than 4 digits"* / *"more than 9 digits"*                   | The clamp is 4–9. Set `TOKEN_LEN` inside that range                                                                                                                                                 |
| Message arrives with no code, or the send throws a format exception                                                           | `TEXT_MESSAGE_FORMAT`                                                                            | The only placeholder is `%s`. A bare `%` or a `{0}` breaks `String.format`                                                                                                                          |
| Email OTP never arrives                                                                                                       | `MAIL_TEMPLATE.IS_ENABLED` and `TEMPLATE_NAME='OTP_CODE'`, then the active mailbox configuration | A disabled or renamed template logs *"is disabled, email will not be sent"* / *"invalid notificationType"* and returns `false`                                                                      |
| Email OTP arrives with a blank closing line                                                                                   | `org.openiam.ui.page.title.organization.name`                                                    | Ships empty; `companyName` renders as nothing                                                                                                                                                       |
| Primary gateway down and no failover delivery                                                                                 | Whether any second `SEND_OTP_TOKEN` event exists for that attempt — there will not be            | Failover is validated on save but not used on the send path in this release. Treat the configuration as inert                                                                                       |
| Save rejected: *"its type differs from the OTP Provider type"*                                                                | The two `TYPE` values, and the channel each one implies                                          | Email pairs only with email; SMS and voice pair with each other                                                                                                                                     |
| Save rejected: *"this would create a circular dependency"*                                                                    | The whole failover chain                                                                         | Some provider downstream points back at the one you are saving                                                                                                                                      |
| Rotated the gateway secret in the vault, old one still used                                                                   | `OTP_PROVIDER_ATTR.VALUE` for that attribute                                                     | The vault key is a UUID regenerated on each save. Change the value on the screen and save; do not write to the vault directly                                                                       |

## Roll it back

Detach the provider from the authentication provider first and confirm the previous sign-in path still works — sign in once as a real user before going further. Then deactivate the gateway credential at the gateway, and only then delete the OTP provider row. Deleting the provider also deletes its in-vault attribute values, which is irreversible: if you may want the provider back, copy the secrets somewhere safe first. Order matters in the other direction too — a `GROOVY` provider whose script you delete from Groovy Manager fails at send time with no message naming the missing script.

## The five provider types

The **Type** drop-down offers exactly five values, and each one fixes the channel — the channel is not a separate setting you can change.

| `TYPE` value      | Screen label               | Channel           | Name in the log when a send fails          |
| ----------------- | -------------------------- | ----------------- | ------------------------------------------ |
| `TWILIO_TEXT`     | Twilio SMS Provider        | `SMS`             | `TwilioSMSOTPModule`                       |
| `TWILIO_VOICE`    | Twilio Voice Call Provider | `VOICE`           | `TwilioVoiceOTPModule`                     |
| `SMS_GLOBAL_TEXT` | SMSGlobal SMS Provider     | `SMS`             | `SMSGlobalSMSOTPModule`                    |
| `EMAIL_TEXT`      | Email OTP Provider         | `EMAIL`           | `EmailOTPModule`                           |
| `GROOVY`          | Custom OTP Provider        | declared as `SMS` | your own script, loaded from `GROOVY_PATH` |

The last column is there because a failed send does not name the provider you configured — the ESB log names the delivery module instead, and this is the table that maps one to the other. There is no plug-in registry, so these five are the only channels available; anything else has to be a `GROOVY` provider.

{% hint style="warning" %}
`GROOVY` declares channel `SMS`. Whatever your script actually does — push, WhatsApp, a bespoke gateway — the product treats it as an SMS provider for destination resolution and for failover compatibility. If your script does not want a phone number it will still be handed the user's OTP phone, and a user with no OTP phone will fail before your script runs.
{% endhint %}

## The attribute names

Attributes are rows in `OTP_PROVIDER_ATTR` (`ATTRIBUTE_ID`, `PROVIDER_ID`, `NAME`, `VALUE`, `METADATA_ID`, `IN_VAULT`), and OpenIAM recognises exactly twelve names:

`ACCOUNT_ID` `ACCOUNT_NAME` `ACCOUNT_AUTH_TOKEN` `ACCOUNT_PASSWORD` `FROM_ACCOUNT` `TOKEN_LEN` `GROOVY_PATH` `LANGUAGE` `HOSTNAME` `URL` `TEXT_MESSAGE_FORMAT` `MANAGED_SYSTEM_ID`

{% hint style="danger" %}
**The Name column is a free-text box, not a drop-down.** It is a plain input with the placeholder *Attribute Name*, and it accepts anything. A misspelling saves cleanly, and the provider then fails on every send with `RESULT_INVALID_CONFIGURATION` — OpenIAM only ever looks for the exact name, so a near-miss is indistinguishable from a missing attribute. Type the names above character for character, in upper case, and confirm by reopening the saved record and reading them back.
{% endhint %}

Which names each type actually reads, and what happens when one is missing:

| Type              | Required — missing means `RESULT_INVALID_CONFIGURATION` on every send   | Also read                          | Stored but ignored                                |
| ----------------- | ----------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------- |
| `TWILIO_TEXT`     | `ACCOUNT_ID`, `ACCOUNT_AUTH_TOKEN`, `FROM_ACCOUNT`                      | `TEXT_MESSAGE_FORMAT`, `TOKEN_LEN` | `ACCOUNT_PASSWORD`, `HOSTNAME`, `URL`, `LANGUAGE` |
| `TWILIO_VOICE`    | `ACCOUNT_ID`, `ACCOUNT_PASSWORD`, `FROM_ACCOUNT`, `TEXT_MESSAGE_FORMAT` | `LANGUAGE`, `TOKEN_LEN`            | `ACCOUNT_AUTH_TOKEN`, `HOSTNAME`, `URL`           |
| `SMS_GLOBAL_TEXT` | `ACCOUNT_ID`, `ACCOUNT_PASSWORD`, `FROM_ACCOUNT`, `TEXT_MESSAGE_FORMAT` | `HOSTNAME`, `URL`, `TOKEN_LEN`     | `ACCOUNT_AUTH_TOKEN`, `LANGUAGE`                  |
| `EMAIL_TEXT`      | *(none — only a non-blank destination)*                                 | `TOKEN_LEN`                        | everything else                                   |
| `GROOVY`          | `GROOVY_PATH` (else the factory returns `null`)                         | whatever your class reads          | —                                                 |

Notes that matter in practice:

* **Twilio SMS uses `ACCOUNT_AUTH_TOKEN`; Twilio Voice uses `ACCOUNT_PASSWORD`.** They are two different attribute names for the same Twilio auth token, one per channel.
* **`ACCOUNT_NAME` is read by nothing** in this release. It is accepted, stored and never used, so an otherwise correct provider that relies on it will fail as though the attribute were absent.
* **`MANAGED_SYSTEM_ID` does not redirect a delivery.** It is read only on the path that resolves the user's principal on another managed system and sends the code to *that* account name, and that path is not one of the five built-in types. Adding `MANAGED_SYSTEM_ID` to a Twilio, SMSGlobal or email provider changes nothing at all — it is stored and ignored. The one case where it does apply is a `GROOVY` provider whose script deliberately implements that behaviour.
* **`GROOVY_PATH` gets a script picker.** Type `GROOVY_PATH` into the Name column and the Value cell changes from a text box to the Groovy script autocomplete; what gets stored is the script's id, not its path, which is why the value you see afterwards does not look like the name you chose. Your script must extend `AbstractOTPModule`. One that does not is loaded and then rejected, and the send fails with no message naming the script — so if a `GROOVY` provider fails on its first send, check the superclass before anything else.

### Code length: the digit clamp

`TOKEN_LEN` is the number of digits the user receives, and the number you type is the number of digits sent. When the attribute is absent, blank or not a number, the length is **6**.

**Effective range is 4 to 9 digits.** A value outside it is silently corrected rather than rejected: you will not see an error on save and the provider will not fail. The only evidence is a `WARN` line in the ESB log, one of *"Length of token is less than 4 digits, unsecured. defaulting to 4"* or *"Length of token is more than 9 digits, Too much. defaulting to 9"*. If you care what length is actually in force, count the digits in a code you send yourself — that is the only check that does not require the log.

### The message format and its `%s`

`TEXT_MESSAGE_FORMAT` is the whole message, with the code substituted into it. The placeholder is `%s` — not `{0}`, not `${code}`.

{% code overflow="wrap" %}

```
Your <YOUR_ORG> verification code is %s. It expires in 30 minutes. Do not share it.
```

{% endcode %}

When the attribute is blank, Twilio SMS and SMSGlobal both fall back to the literal `Your code is %s`. Any other `%` in the string — a percent sign in your own wording, for instance — fails the send rather than being sent literally, so write *per cent* if you need the word. `TWILIO_VOICE` and `SMS_GLOBAL_TEXT` require the attribute to be present at all, and a blank one fails validation before the fallback can apply.

`TWILIO_VOICE` sends no text at all. OpenIAM puts the code in Redis under a random, single-use call token and hands Twilio a callback URL of the form `<content provider scheme>://<domainPattern>/idp/twilio-ml/<OTP_PROVIDER_ID>/<CALL_TOKEN>`, which Twilio then fetches (by `POST`) to be told what to say; the `LANGUAGE` attribute selects the voice. The IDP answers only a request carrying a valid `X-Twilio-Signature` for the provider's account, and only then looks the code up. The lookup spends the code, and an unfetched code expires after `org.openiam.otp.voice.call.ttl.seconds` (ESB property, default `120`).

The consequence is that the voice flow depends on a content provider whose `DOMAIN_PATTERN` is reachable **from Twilio's network**, not just from your users — and with no content provider in the request context the send fails with `VOICE_CALL_ERROR`. The signature covers the URL, so the proxy in front of the IDP must also pass the public scheme and host through (`X-Forwarded-Proto`, `X-Forwarded-Host`). Confirm the URL is reachable from outside before blaming the credentials.

### SMSGlobal transport defaults

These are fixed in the release rather than settings on the screen, and each applies only when the corresponding attribute is left blank:

| What it sets | Value when the attribute is blank | Attribute that overrides it |
| ------------ | --------------------------------- | --------------------------- |
| Gateway host | `api.smsglobal.com`               | `HOSTNAME`                  |
| URL path     | `/v1/sms/`                        | `URL`                       |
| Message text | `Your code is %s`                 | `TEXT_MESSAGE_FORMAT`       |

The request is `POST https://<HOSTNAME><URL>` with an SMSGlobal MAC authorization header — `MAC id="<ACCOUNT_ID>", ts="<epoch seconds>", nonce="<md5>", mac="<HmacSHA256 base64>"` — where the signed string is `timestamp\nnonce\nPOST\n<URL>\n<HOSTNAME>\n443\n\n` and the secret is `ACCOUNT_PASSWORD`. Port `443` is hard-coded into the signature, so a non-443 endpoint will not authenticate. Any status outside 200-299 raises `SMS_TOKEN_GENERATE_ERROR`.

{% hint style="danger" %}
**The shipped SMSGlobal channel sends every code to the `FROM_ACCOUNT` number, not to the user's phone.** The destination on the outgoing request is populated from the sender number; the user's OTP phone is resolved, validated, and then not used, and the sender name on the message is the fixed string `DefaultOTP`. The send succeeds, `SEND_OTP_TOKEN` is audited as a success, and no code reaches the user — which is why this cannot be diagnosed from OpenIAM's own evidence. **Verified only by reading the gateway's message log**, so confirm it on your build before choosing `SMS_GLOBAL_TEXT`: send one code to a test identity and read which number SMSGlobal says it delivered to. If it reproduces, use `TWILIO_TEXT`, which addresses the user's phone correctly, or a `GROOVY` provider.
{% endhint %}

## Secrets

Tick **Store in Secret Vault** on any attribute holding a token or password. On save, OpenIAM replaces the attribute's stored `VALUE` with a fresh random UUID and writes the real value to the secret vault under `vault.secret.otp.<that UUID>`, below the configured vault path — `/secret/openiam/` by default. Reading the provider back refills the value from the vault; clearing the checkbox or deleting the attribute deletes the vault entry with it. Confirm it worked by reopening the record: the stored value you see in a database dump is the UUID, while the screen shows the real value.

Two consequences: the vault key is opaque and rotates on every save, so never hand-edit it; and the `OTP_PROVIDER_ATTR.VALUE` column is safe to read in a database dump for in-vault attributes, because it contains only the UUID.

{% hint style="warning" %}
The **Value** cell is not masked, even for an attribute you have flagged as in-vault: the input falls back to plain text, so the secret is readable on screen and in a screenshot. Treat the OTP provider edit screen as one that displays secrets, and do not demonstrate it on a shared screen.
{% endhint %}

## Failover

**Failover OTP Provider** (`FAILOVER_CONFIG_ID` → another `OTP_PROVIDER`) is checked when you save, in two ways:

* The nominated provider must be channel-compatible, or the save is refused with `FAILOVER_DIFFERENT_TYPE` and the screen shows *"Cannot add Failover Provider — its type differs from the OTP Provider type"*.
* The chain must not lead back to the provider you are saving, however many hops away, or the save is refused with `CIRCULAR_DEPENDENCY` and *"Cannot add a Failover Provider — this would create a circular dependency"*.

Compatibility follows the channel, not the type: the same channel is always allowed, `SMS` accepts `VOICE`, `VOICE` accepts `SMS`, and `EMAIL` accepts nothing but `EMAIL`.

| Primary                                                | May fail over to                    |
| ------------------------------------------------------ | ----------------------------------- |
| `TWILIO_TEXT`, `SMS_GLOBAL_TEXT`, `GROOVY` (all `SMS`) | any `SMS` type, plus `TWILIO_VOICE` |
| `TWILIO_VOICE`                                         | `TWILIO_VOICE`, plus any `SMS` type |
| `EMAIL_TEXT`                                           | `EMAIL_TEXT` only                   |

{% hint style="danger" %}
**Failover is validated on save but not exercised when a send fails, in this release.** OpenIAM sends through the primary provider and stops there: a failure surfaces as `SMS_TOKEN_GENERATE_ERROR` with no second attempt against the nominated failover, and no audit event for one. Configure failover if you like — it is harmless and it is ready for the release that uses it — but **do not count it as an availability control**. Prove it before you rely on it: take the primary gateway's credential out of service on a test provider, send one code, and confirm whether a second delivery happens. On this release, expect that it does not.
{% endhint %}

## The email OTP template

`EMAIL_TEXT` does not talk to SMTP itself. It queues a notification of type `OTP_CODE`, which the mail service then renders and sends, and it passes exactly four parameters:

| Parameter           | What it holds                                              |
| ------------------- | ---------------------------------------------------------- |
| `EMAIL_TOKEN`       | the generated code                                         |
| `identity`          | the user's login name on the relevant managed system       |
| `companyName`       | the value of `org.openiam.ui.page.title.organization.name` |
| `managedSystemName` | the name of the managed system on the user's login row     |

The template is the `MAIL_TEMPLATE` row whose `TEMPLATE_NAME` is `OTP_CODE`, and its declared parameter row is `OTP_CODE_EMAIL_TOKEN` with `REFERENCE_ID = 'EMAIL_TOKEN'`. The template model — Freemarker body, `MAIL_TEMPLATE_PROPERTIES`, what happens to an undeclared parameter, and why `companyName` renders as a blank line out of the box — is covered in [Mail and notifications](/administration-guide/platform/mail-and-notifications.md#the-otp-email-template). Two failure modes belong to both pages: `IS_ENABLED = 'N'` on the template makes the send a silent no-op, and if the login row points at a managed system that no longer exists the module throws `MANAGED_SYS_NOT_FOUND` before any mail is queued.

## Where it lives

| Screen              | Route                             |
| ------------------- | --------------------------------- |
| OTP provider search | `/webconsole/otp-provider-search` |
| OTP provider edit   | `/webconsole/otp-provider-edit`   |

The menu resources are `OTP_PROVIDERS` (under `ACC_CONTROL`, display order 14), `OTP_PROV_SEARCH`, `OTP_PROV_EDIT`, `OTP_PROV_NEW` and `OTP_PROV_BACK_TO_SEARCH`, shipped since 4.2.1.0. They originally pointed at `/webconsole/otp-providers/` and `/webconsole/otp-providers/edit`, and as of 4.2.2.0 both were changed to the routes above. If a runbook sends you to `/webconsole/otp-providers/` it predates that change; confirm which your deployment has by opening the menu and reading the address bar.

Two things about the audit trail on that screen are worth knowing before you go looking for evidence. Creating a provider and editing one are the same save, and both record the same action, so **an edit is audited as `ADD_OTP_PROVIDER`** — there is no audit event that distinguishes the two, and none at all for a delete. `UPDATE_OTP_PROVIDER` and `DELETE_OTP_PROVIDER` exist in the audit action catalogue and their retention properties ship, but nothing writes them, so do not build a report that expects them. What the event records of the save is redacted: an attribute flagged in-vault is stored as the literal `[ SECRET ]`, which you can confirm by editing a provider and reading the event it produced.

## What sends a code, and what the send path is exposed to

The only screen involved is the challenge page the user lands on, `/idp/auth-otp`. Everything else on this path is driven by the sign-in flow rather than by you: the code is sent when a user authenticates against a URI pattern whose authentication provider has the provider attached, which is why the test in the procedure above is a sign-in and not something you trigger directly. There is no screen that sends a test code on demand.

**Twilio voice needs an inbound URL.** For a voice provider, OpenIAM tells Twilio to fetch the spoken message from `<content provider base URL>/idp/twilio-ml/<OTP_PROVIDER_ID>/<CALL_TOKEN>` — so that path has to be reachable from Twilio's network, and a voice call that connects and then says nothing is usually that reachability, or a callback refused for its signature, rather than the provider configuration. The path needs no session, but it answers only requests signed by Twilio for the provider's account, and the call token in it is random, single use and short-lived, so the URL is no use to anyone but Twilio.

**The send path can be reached without a session,** and requests made that way are rate limited per user over a rolling window: once the ceiling is reached the request is refused with `ACCESS_DENIED` and the ESB log records *"Cannot anonymous send an OTP Token to user: … due to rate limiting constraints"*. The count is held in each service's own memory rather than centrally, so the effective ceiling multiplies by the number of ESB nodes you run — a limit of three across four nodes admits twelve. Neither the ceiling nor the window is a property on **Administration → System Configuration**; both are fixed for the service, so changing them is a question for whoever owns the deployment.

Lifetime and attempt budget come from the authentication policy, not the provider: `OTP_SMS_LIFETIME` (**`30`** minutes on shipped policy `4001`, and 30 minutes again if the parameter is absent altogether), `MAX_OTP_RESEND_ALLOWED` (**`3`**) and `OTP_MAX_FAIL_ATTEMPT`. See [Password and authentication policy parameters](/administration-guide/reference/policy-parameters.md). The expiry is written to `LOGIN.SMS_CODE_EXPIRATION` before the send, and that timestamp is also the moving factor of the RFC 4226 generator — so the code and its expiry are one value.

Three more fields live on the authentication provider rather than on the OTP provider, so look for them there and not on this screen: `OTP_RESEND_INTERVAL`, which ships **`30`** seconds; `OTP_SUPPRESSING_PERIOD`; and `OTP_DEDICATED_IP_SUBNET`, which is `NOT NULL` and ships **`0.0.0.0`** — meaning no subnet restriction, not a blocked one.

## Related

* [Authentication providers and MFA](/administration-guide/security/authentication-providers-and-mfa.md) — attaching a provider, the `SUPPORTS_*` flags, TOTP
* [Mail and notifications](/administration-guide/platform/mail-and-notifications.md) — the `OTP_CODE` template and mailbox configurations
* [Password and authentication policy parameters](/administration-guide/reference/policy-parameters.md) — `OTP_SMS_LIFETIME`, `MAX_OTP_RESEND_ALLOWED`, `OTP_MAX_FAIL_ATTEMPT`
* [Enumerated value reference](/administration-guide/reference/enums.md#one-time-password-provider-types) — the five provider types

*Applies to OpenIAM 2026.8.1. Last reviewed 7 August 2026.*


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.openiam.com/administration-guide/platform/sms-and-otp.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
