# Send Auth0 SMS codes through TextKit

Source: https://jointextkit.com/integrations/auth0
Index: https://jointextkit.com/llms.txt

Auth0 lets you replace its phone provider with your own code. Choose the custom phone provider and Auth0 runs an Action each time it needs to send a phone message: a code to verify a phone number, to reset a password, or for SMS multi-factor sign-in. The Action below hands each code to TextKit and tells Auth0 whether to retry or give up when a send fails. Auth0 still creates and checks every code.

## Before you start

- An Auth0 tenant whose phone messages are set under **Branding > Phone Provider**. Availability varies by Auth0 plan, so check yours.
- A TextKit test key (`ssms_sk_test_...`). [Sign up](/signup) and copy one from the console. Make it a restricted key with only the `messages:send` scope ([scopes](/docs/authentication)): the key sits in another service's settings, and a key that leaks can then only send texts.

Auth0 also has an older `send-phone-message` trigger, for multi-factor only. Auth0's docs say not to use it for a custom phone provider, so this guide uses `custom-phone-provider`.

## 1. Choose the custom provider

1. In the Auth0 dashboard, open **Branding > Phone Provider** and select **Custom**.
2. Set the delivery method to **Text**. TextKit sends text messages, not voice calls.
3. Under **Provider Configuration**, add a secret named `TEXTKIT_API_KEY` holding your test key, and replace the code with the Action below.

## 2. The Action

```javascript
exports.onExecuteCustomPhoneProvider = async (event, api) => {
  const n = event.notification;
  if (n.delivery_method !== 'text') {
    api.notification.drop('TextKit sends text messages only');
    return;
  }
  // Codes only (otp_verify, otp_enroll). See "Account notices" below.
  if (!n.message_type.startsWith('otp')) {
    api.notification.drop(`Not a code: ${n.message_type}`);
    return;
  }

  const res = await fetch('https://api.jointextkit.com/v1/messages', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${event.secrets.TEXTKIT_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ to: n.recipient, body: n.as_text, category: 'otp' }),
  });
  if (res.ok) return;

  const { error = {} } = await res.json().catch(() => ({}));
  const reason = `TextKit ${res.status} ${error.code}: ${error.message}`.slice(0, 1024);
  // A rate limit or a server error can pass on a later try; a refusal will not.
  if ([429, 500, 503].includes(res.status)) api.notification.retry(reason);
  else api.notification.drop(reason);
};
```

Auth0 passes the number in `event.notification.recipient`, already in E.164, and the finished text in `as_text`, with the code inside. The Action sends that text as it is. `api.notification.retry` makes Auth0 try again, up to 5 times over the next few minutes; `api.notification.drop` stops it for good. The reason includes TextKit's error code, such as `recipient_opted_out`, so you can tell why a code never came.

TextKit sends one-time codes at any hour, and it decides what is a code from the text's wording, not from `category`. Digits within a few words of a word that names them ("Your Acme code is 482913") count. A text that does not read as a code is held overnight like any other message ([quiet hours](/docs/compliance-checks#quiet-hours)). If you edit the template, keep the word "code" next to the digits.

### Account notices

Auth0 routes more than codes through this provider: `blocked_account`, `change_password` and `password_breach` notices too. They are not codes, so TextKit treats them as ordinary transactional texts: held overnight, and from a registered number they need the person's opt-in on file. The Action sends codes only and drops the rest. Remove that check once your registration and opt-in records cover account notices.

## 3. Save and test in the sandbox

Click **Save**, which deploys the Action. **Send Test Message** works only after a save. The Action above drops anything that is not a code, so if the test message is dropped, test with a real flow instead.

With the test key in place, enroll a test user in SMS multi-factor, or verify a phone number, and check the code in TextKit's Logs.

- **Your own phone.** Verify your cell in the console's settings and use it as the number. The text arrives for real and ends with "- via TextKit sandbox". Real sandbox texts are capped at 10 a day.
- **`+15005550006`.** Always ends `delivered`, and nothing reaches a phone. Any other real-looking number is simulated too: the message shows in the console's Logs as `Test · simulated`.
- **`+15005550015`.** A recipient for whom it is always night. Only a text that reads as a one-time code is delivered, so this is how you check your template's wording.
- **`+15005550011`.** Answers `403 recipient_opted_out`, which shows you what your users see when a send is refused.

See [sandbox and test numbers](/docs/sandbox) for every magic number.

## Go live

1. **Swap in a live key** (`ssms_sk_live_...`) in the `TEXTKIT_API_KEY` secret under Provider Configuration, then save. Keep it restricted to `messages:send`.
2. **Have a number that can text anyone:** a local number on an approved 10DLC registration, or a verified toll-free number. We file the registration for you: see [compliance and registration](/docs/compliance). A registration for two-factor codes fits a sign-in flow best. With one live number, leave `from` out and TextKit sends from it. Once you have a second, add `from`.
3. **Add credits.** Free accounts text only numbers they have verified, and your first credit purchase lifts that ([credits](/docs/credits)).
4. **Send your code template early.** In your first 30 days of live sending, our team reviews each new kind of message before it goes out. Codes from a two-factor registration are exempt. Otherwise, send one code to a real person before launch so the template is approved by the time your users sign in ([review of new kinds of message](/docs/compliance-checks#review-of-new-kinds-of-message)).

TextKit sends to US and Canadian numbers only. A number from anywhere else answers `400 invalid_request`, so limit phone sign-up to those two countries.

## Sources

Provider steps were checked against these pages on 2026-10-28:

- [Auth0: Configure a custom phone provider](https://auth0.com/docs/customize/phone-messages/configure-phone-messaging-providers/configure-a-custom-phone-provider)
- [Auth0: custom-phone-provider event object](https://auth0.com/docs/actions/reference/custom-phone-provider/custom-phone-provider-event-object)
- [Auth0: custom-phone-provider API object](https://auth0.com/docs/actions/reference/custom-phone-provider/custom-phone-provider-api-object)
- [Auth0: Send Phone Message trigger](https://auth0.com/docs/customize/actions/explore-triggers/send-phone-message)
