Skip to main content
Phone sign-in follows the magic-code flow with SMS as the delivery channel. The user enters a phone number, Pylon sends a numeric code (6 digits by default, set by PYLON_AUTH_CODE_LENGTH), and a successful verification mints a session. Phone numbers are E.164-normalized (+15551234567) before storage or transport.

Endpoints

The user entity needs a phone field and optionally phoneVerified:
phone should be unique so two accounts can’t claim the same number.

Sending a code

Response in production (Twilio configured, send succeeded):
codeLength is the number of digits in the code. Use it to size the code input. Response in dev mode, to a request from the same machine:
dev_code is returned only in dev mode. Outside dev mode, if the SMS does not go out, the response is 503 SMS_NOT_SENT and the code is never returned. Errors: The code is 6 digits by default, expires in 10 minutes, and is rate-limited per number to prevent SMS spam.

SMS text

The default text is:
The app name comes from PYLON_APP_NAME, then PYLON_EMAIL_APP_NAME. Without an app name, the text is Your sign-in code is 0427. It expires in 10 minutes. US carriers reviewing an A2P 10DLC campaign expect each text to name the sender, so set PYLON_APP_NAME. To change the text, set PYLON_SMS_TEMPLATE_SIGN_IN_CODE. It can use {{code}} and {{app_name}}. Any other {{...}} is removed.
The text in your A2P campaign’s sample messages must match what Pylon sends.

Verifying

Response on success:
displayName is optional. Pylon uses it when it creates a new User row because no existing row has this phone. On later sign-ins for the same number, displayName is ignored (the existing row’s name stays). Errors: On first successful verify Pylon stamps phoneVerified to the current ISO 8601 timestamp.

Twilio transport (built-in)

The default SMS transport is Twilio. Set three env vars:
When all three are set, /phone/send-code sends an SMS via Twilio’s REST API. When any are missing, sent is false and the dev_code is returned in the response.

Custom SMS providers

The pylon-auth crate exposes an SmsSender trait. It is the crate-level extension point for a non-Twilio provider (MessageBird, Vonage, Plivo, AWS SNS, or an internal SMS gateway):
The shipped pylon binary wires only the built-in Twilio transport. The /phone/send-code route handler calls it directly via TwilioSmsTransport::from_env(). The prebuilt binary has no env-var switch or registration hook to swap in a custom SmsSender. (PhoneCodeStore only generates and persists the code. It returns the code to the caller, which delivers it; it does not hold an SmsSender.) To run a different provider today, build on the pylon-auth crate directly in your own Rust binary. Env-configurable third-party transports for the shipped binary are not yet available.

Security guarantees

  • E.164 normalization on phone before storage. (555) 123-4567, 555-123-4567, and +15551234567 collapse to the same canonical form. No two accounts share a number.
  • 6-digit code by default, 10-minute TTL — same as magic codes. PYLON_AUTH_CODE_LENGTH sets 4 to 8 digits.
  • Code burns after wrong attempts — try_verify increments an attempt counter; too many wrong attempts and the code is invalidated server-side. Status 429 INVALID_CODE.
  • Daily wrong-guess limit per number — 1,000 wrong guesses per 24 hours with 6-digit codes, 10 with 4-digit codes. Stored in the auth database, so a restart does not reset it. See Wrong-guess limit.
  • Constant-time code comparison — no timing leak on verify.
  • Per-number rate limit on send-code so a phone-number enumeration attack cannot exhaust the SMS budget.
  • Optional CAPTCHA gate — set PYLON_CAPTCHA_PROVIDER to require a captcha token before send. See CAPTCHA.
  • Twilio credentials never logged. On transport failure only the provider error is logged at warn level ([phone] twilio send failed: <error>). The SMS body and the code are never written to logs.

Where to go next