export const actionCode = `exports.onExecuteCustomPhoneProvider = async (event, api) => {
  if (!event.notification.message_type.startsWith('otp')) {
    api.notification.drop(\`MiniMoth only delivers OTP messages, not '\${event.notification.message_type}'\`)
    return
  }
  if (event.notification.delivery_method !== 'text') {
    api.notification.drop('MiniMoth does not support voice delivery')
    return
  }

  let response
  try {
    response = await fetch(event.secrets.SERVICE_URL, {
      method: 'POST',
      headers: { 'content-type': 'application/json', authorization: \`Bearer \${event.secrets.TOKEN}\` },
      body: JSON.stringify({ phone: event.notification.recipient, code: event.notification.code }),
    })
  } catch (err) {
    api.notification.retry(\`Network error calling MiniMoth: \${err.message}\`)
    return
  }

  if (response.status >= 500) {
    api.notification.retry(\`MiniMoth returned \${response.status}\`)
    return
  }
  if (!response.ok) {
    const body = await response.text()
    api.notification.drop(\`MiniMoth rejected the request (\${response.status}): \${body}\`)
  }
}`

export const terraformCode = `resource "auth0_action" "minimoth_custom_phone_provider" {
  name    = "MiniMoth Custom Phone Provider"
  runtime = "node22"
  deploy  = true
  code    = <<-EOT
    exports.onExecuteCustomPhoneProvider = async (event, api) => {
      if (!event.notification.message_type.startsWith('otp')) {
        api.notification.drop(\`MiniMoth only delivers OTP messages, not '\${event.notification.message_type}'\`)
        return
      }
      if (event.notification.delivery_method !== 'text') {
        api.notification.drop('MiniMoth does not support voice delivery')
        return
      }

      let response
      try {
        response = await fetch(event.secrets.SERVICE_URL, {
          method: 'POST',
          headers: { 'content-type': 'application/json', authorization: \`Bearer \${event.secrets.TOKEN}\` },
          body: JSON.stringify({ phone: event.notification.recipient, code: event.notification.code }),
        })
      } catch (err) {
        api.notification.retry(\`Network error calling MiniMoth: \${err.message}\`)
        return
      }

      if (response.status >= 500) {
        api.notification.retry(\`MiniMoth returned \${response.status}\`)
        return
      }
      if (!response.ok) {
        const body = await response.text()
        api.notification.drop(\`MiniMoth rejected the request (\${response.status}): \${body}\`)
      }
    };
  EOT
  supported_triggers {
    id      = "custom-phone-provider"
    version = "v1"
  }
}

resource "auth0_trigger_action" "minimoth_custom_phone_provider" {
  trigger = "custom-phone-provider"
  actions {
    id           = auth0_action.minimoth_custom_phone_provider.id
    display_name = auth0_action.minimoth_custom_phone_provider.name
  }
  depends_on = [
    auth0_action.minimoth_custom_phone_provider
  ]
}

resource "auth0_phone_provider" "minimoth_custom_phone_provider" {
  depends_on = [auth0_trigger_action.minimoth_custom_phone_provider]
  name       = "custom"
  disabled   = false
  configuration {
    delivery_methods = ["text"]
  }
  credentials {}
}`

Deliver Auth0's phone OTPs over WhatsApp first, SMS fallback, for Indian numbers — at MiniMoth's rate, via a small Action you paste into your Auth0 tenant.

<Callout variant="accent">
  <p className="font-medium mb-1">Before you enable this</p>
  <p className="text-ink/60">
    This is tenant-wide, not opt-in per notification type — it takes over <em>every</em> Auth0 phone notification, including
    account-security SMS like blocked-account and password-change. MiniMoth's Action only delivers OTPs; anything else is
    rejected, not delivered. Make sure you have another channel for those alerts first.
  </p>
</Callout>

## How it fits together

Auth0 already owns the entire OTP lifecycle — it generates the code, verifies it, and issues the session. MiniMoth becomes the delivery backend an Auth0 <a href="https://auth0.com/docs/customize/phone-messages/configure-phone-messaging-providers/configure-a-custom-phone-provider" target="_blank" rel="noopener noreferrer">custom phone provider Action</a> calls out to. Unlike some integrations, Auth0 doesn't fix the shape of that call — the Action below is MiniMoth's own code for you to paste in as-is.

<div className="bg-white/5 border border-white/10 rounded-lg px-4 py-3 text-xs text-ink/50 leading-relaxed mt-3">
  MiniMoth never sees the Auth0 session or the verification step — only the notification Auth0's Action forwards when it needs an OTP delivered. One consequence: this integration never returns a MiniMoth <a href="/docs/identity" className="text-saffron hover:underline">identity_id</a> — Auth0 already owns that user's identity as its own <code className="font-mono">user_id</code>, so use that instead.
</div>

Every branch of this Action explicitly tells Auth0 whether to retry or give up — nothing is ever silently dropped without Auth0 knowing about it:

<CodeBlock code={actionCode} lang="javascript" />

## Setup

Wire up test mode first — it confirms the connection without sending a real message or touching your balance.

1. **MiniMoth dashboard** — open your project, find the Auth0 Hook card, and click `Enable Auth0 Hook`. Copy the `Test` Hook URL and Token, and copy the Action snippet shown in the card.

   <img
     src="/docs-auth0-hook-card.png"
     alt="The Auth0 Hook card in the MiniMoth dashboard, showing the Hook URL, Token, and Action snippet to paste into Auth0"
     className="w-full max-w-2xl rounded-lg border border-white/10 my-3"
     width="1256"
     height="444"
   />

2. **Auth0 dashboard** — go to `Actions → Library`, create a new Custom action bound to the `Send Phone Message` (custom phone provider) trigger, paste in the Action code, then add two secrets on the Action: `SERVICE_URL` (the Test Hook URL you copied) and `TOKEN` (the Test Token). Deploy the Action.

3. **Verify the connection** — trigger a phone OTP flow from your app (or Auth0's own testing tools), then check your Auth0 tenant's Action logs for a successful call. Test mode sends no real message, so this is the only way to confirm the hook is wired correctly.

4. **Go live** — swap the Action's `SERVICE_URL`/`TOKEN` secrets for the `Live` Hook URL and Token from the same card. This is also the only way to see a real OTP actually delivered, since test mode by design never sends one.

## Setting up via Terraform

Managing your Auth0 tenant as code? The steps above translate to an `auth0_action`, wired to the custom phone provider trigger via `auth0_trigger_action`, with `auth0_phone_provider` enabling it:

<CodeBlock code={terraformCode} lang="terraform" />

This resource doesn't manage the Action's `SERVICE_URL`/`TOKEN` secrets — set those separately, either in the Auth0 dashboard or via your own Terraform handling of Action secrets. Also note that Auth0 requires deleting any custom phone provider already configured manually via the dashboard before one can be created through Terraform.

<div className="bg-white/5 border border-white/10 rounded-lg px-5 py-4 text-sm text-ink/60 leading-relaxed my-10">
  <p className="font-medium text-ink/80 mb-1">Testing with real testers</p>
  <p>
    Testing your integration before going live? MiniMoth's <a href="/docs/integrations/test-group" className="text-saffron hover:underline">Test Group</a> preview feature works with the Auth0 test hook too — up to 5 real testers can see OTPs sent through your Test Hook without a real message going out.
  </p>
</div>

<Callout variant="accent">
  <p className="font-medium mb-1">Requirements</p>
  <p className="text-ink/60">
    Your Auth0 connection's OTP length must stay at Auth0's default of 6 digits — MiniMoth's SMS delivery uses a DLT-approved template that can't accommodate other lengths. A mismatched configuration means every send fails.
  </p>
</Callout>

<div className="mt-6">
  <Callout variant="accent">
    <p className="font-medium mb-1">Billing</p>
    <p className="text-ink/60">
      Real OTPs sent via the live hook are billed to your wallet balance. Test-mode calls cost nothing and consume no credits.
    </p>
  </Callout>
</div>

---

Full documentation index: https://minimoth.dev/llms.txt
