Send Auth0 SMS codes through TextKit
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 and copy one from the console. Make it a restricted key with only themessages:sendscope (scopes): 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
- In the Auth0 dashboard, open Branding > Phone Provider and select Custom.
- Set the delivery method to Text. TextKit sends text messages, not voice calls.
- Under Provider Configuration, add a secret named
TEXTKIT_API_KEYholding your test key, and replace the code with the Action below.
2. The Action
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). 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 endsdelivered, and nothing reaches a phone. Any other real-looking number is simulated too: the message shows in the console's Logs asTest · 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. Answers403 recipient_opted_out, which shows you what your users see when a send is refused.
See sandbox and test numbers for every magic number.
Go live
- Swap in a live key (
ssms_sk_live_...) in theTEXTKIT_API_KEYsecret under Provider Configuration, then save. Keep it restricted tomessages:send. - 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. A registration for two-factor codes fits a sign-in flow best. With one live number, leave
fromout and TextKit sends from it. Once you have a second, addfrom. - Add credits. Free accounts text only numbers they have verified, and your first credit purchase lifts that (credits).
- 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).
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: