Phone verification
Phone verification proves that the current account controls a phone number. The account either receives a one-time code over outbound SMS, or texts an issued challenge code to a Fluxer number. Success sets the account’s verified phone state, which the phone required actions and the VERY_HIGH guild verification level consult.
Every route here needs a non-bot user session, and each one admits a session with suspicious account state.
Eligibility
Section titled “Eligibility”Send phone verification and Verify phone code admit only an account that satisfies at least one of four conditions.
- The account already holds a verified phone.
- A TOTP authenticator is enrolled on the account.
- The stored suspicious activity bitfield is non-zero.
- The account belongs to at least one guild whose verification level is
VERY_HIGH.
An account satisfying none of them is refused with 403 PHONE_ADD_NOT_ELIGIBLE.
Both operations run the check before they examine the submitted number. A refused account receives no message and consumes no challenge. Start inbound phone challenge skips the check entirely and issues a challenge for any non-bot session.
Deferred phone requirement
Section titled “Deferred phone requirement”An instance can defer a new account’s phone requirement instead of imposing it at registration. The deferred phone gate policy sets the window after registration in which a qualifying join makes the requirement due, and the member count above which a guild is a qualifying one. A guild with the DISCOVERABLE feature qualifies at any member count.
A deferred requirement is absent from the required actions array and restricts nothing. An attempt to join a qualifying guild inside the window makes it due, and Accept invite refuses that attempt with 403 ACCOUNT_SUSPICIOUS_ACTIVITY. The account then owes REQUIRE_VERIFIED_PHONE or REQUIRE_REVERIFIED_PHONE, and every ordinary user route refuses it with the same code.
Get phone gate escape preview and Run phone gate escape set that requirement aside again in place of verifying a number. Both admit an account the rest of the API refuses. The escape is available only while all six of these hold.
- The instance has the deferred phone gate enabled.
- The account holds no verified phone.
- The account owes
REQUIRE_VERIFIED_PHONEorREQUIRE_REVERIFIED_PHONE. - The account does not owe
REQUIRE_INBOUND_PHONE_VERIFICATION. - The requirement became due through a qualifying join and has not been deferred again.
- The account has at least one visible required action.
Number representation
Section titled “Number representation”The phone field is a number already in canonical E.164 form. Fluxer strips control and formatting characters, trims the value, and then matches it against ^\+[1-9]\d{1,14}$. A value that fails returns 400 INVALID_FORM_BODY with one validation error entry whose path is phone and whose code is PHONE_NUMBER_INVALID_FORMAT.
A number whose leading characters appear on the instance’s banned prefix list is refused with 400 INVALID_PHONE_NUMBER. The list holds a built-in set of country prefixes plus operator additions, and one entry blocks every number that starts with it.
Phone verification channel values
Section titled “Phone verification channel values”| Value | Description |
|---|---|
| sms1 | Fluxer asks the SMS provider to deliver a one-time code to the submitted number |
| inbound_challenge2 | The user texts the issued challenge code to the returned Fluxer number |
1 Requesting this value is equivalent to omitting the field
2 Requesting this value always yields an inbound challenge, and neither the lookup provider nor the outbound SMS provider is contacted
Provider lookup verdicts
Section titled “Provider lookup verdicts”Verify phone code resolves the destination through the configured lookup provider before it checks the code. Send phone verification does the same before it sends a code, except on an attempt that policy routes inbound before the lookup, where no lookup runs. A lookup result is cached for 7 days and reused for later attempts against the same number. The rules below run in order, and the first match decides.
| Order | Condition | Verdict |
|---|---|---|
| 1 | The lookup could not be performed1 | Refused with INVALID_PHONE_NUMBER |
| 2 | The provider reports the number as invalid | Refused with INVALID_PHONE_NUMBER |
| 3 | The line type is fixedVoip or nonFixedVoip | Inbound, reason voip |
| 4 | The number starts with +1 and its numbering plan area is Canadian | Inbound, reason canadian |
| 5 | The line type is absent or unknown | Inbound, reason unknown_line_type |
| 6 | The line type is landline, tollFree, premium, sharedCost, uan, voicemail, or pager | Refused with INVALID_PHONE_NUMBER |
| 7 | The line type is anything other than mobile or personal2 | Refused with INVALID_PHONE_NUMBER |
| 8 | The SMS pumping risk score reaches the threshold for the reported country3 | Refused with INVALID_PHONE_NUMBER |
| 9 | No rule above matched | Accepted for outbound SMS |
1 An instance whose lookup provider is unavailable or unconfigured refuses every number
2 mobile and personal are the only accepted line types
3 The threshold is 100 for US and CA, 70 for GB, DE, FR, IT, ES, NL, SE, NO, DK, FI, AU, NZ, JP, KR, CH, AT, BE, IE, and PT, and 35 for every other country and for a lookup reporting no country
Only Send phone verification converts an inbound verdict into a challenge. Verify phone code refuses the same three verdicts with 400 INVALID_PHONE_NUMBER.
Inbound challenge reason values
Section titled “Inbound challenge reason values”A reason explains why the attempt could not complete over outbound SMS.
| Value | Description |
|---|---|
| voip | Provider lookup classified the destination as a VoIP line |
| canadian1 | The destination is a Canadian numbering plan area |
| unknown_line_type | Provider lookup reported no line type or an unknown line type |
| expensive_destination2 | The configured inbound-required prefix policy matches the destination |
| account_forced3 | The account has the flag that forces inbound verification |
| behavioural_risk | Phone attempt risk controls forced this attempt onto the inbound channel |
1 Determined from the numbering plan area of a +1 number, and evaluated after the VoIP rule, so a Canadian VoIP number reports voip instead
2 Also reported when the caller explicitly requests the inbound_challenge channel. The prefix policy itself applies only while the account holds no verified phone
3 Reported in preference to expensive_destination when both would apply
SMS delivery object
Section titled “SMS delivery object”Confirmation that the SMS provider accepted a one-time code for delivery. Send phone verification returns it on an outbound result, and Verify phone code then takes the number itself.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| channel | string | The delivery channel, always the literal sms |
Inbound challenge object
Section titled “Inbound challenge object”The instructions for texting a code to Fluxer, returned by Send phone verification when policy routed the attempt inbound. channel tells the two response shapes apart.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| channel | string | The delivery channel, always the literal inbound_challenge |
| challenge_code1 | string | The code the user texts to our_number |
| our_number | string | The Fluxer E.164 number that receives the code |
| expires_at2 | ISO8601 timestamp | The moment the challenge stops being redeemable |
| reason | string | The inbound challenge reason that forced this channel |
1 Six decimal digits
2 15 minutes after the challenge was issued
Example
Section titled “Example”{ "channel": "inbound_challenge", "challenge_code": "418207", "our_number": "+15550000000", "expires_at": "2026-03-04T18:15:00.000Z", "reason": "voip"}Direct inbound challenge object
Section titled “Direct inbound challenge object”The code and the Fluxer number for a caller that asked for an inbound challenge directly. Start inbound phone challenge returns it.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| challenge_code1 | string | The code the user texts to our_number |
| our_number | string | The Fluxer E.164 number that receives the code |
| expires_at2 | ISO8601 timestamp | The moment the challenge stops being redeemable |
1 Six decimal digits
2 15 minutes after the challenge was issued
Phone verification result object
Section titled “Phone verification result object”Returned by Verify phone code on a successful verification.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| verified | boolean | Always the literal true |
Phone gate escape guild object
Section titled “Phone gate escape guild object”One guild in a phone gate escape preview.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| id | snowflake | The ID of the guild |
| name | string | The name the guild is listed under |
Phone gate escape preview object
Section titled “Phone gate escape preview object”What Run phone gate escape would do for the current account.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| available | boolean | Whether the escape is open to this account right now |
| guilds | array[phone gate escape guild object] | The qualifying guilds the escape leaves, empty while available is false |
| owned_guilds | array[phone gate escape guild object] | The qualifying guilds the account owns, which the escape keeps |
Send phone verification
Section titled “Send phone verification”POST/v1/users/@me/phone/send-verificationStarts verification of the submitted number. Requires an account satisfying eligibility. Returns an SMS delivery object or an inbound challenge object on success.
Several independent policies can force the inbound channel, and each one overrides a requested sms. Fluxer routes the attempt inbound before it contacts the lookup and SMS providers in three cases. The caller asks for inbound_challenge. The account has the flag that forces inbound verification. The configured inbound-required prefix policy matches the number while the account holds no verified phone.
An attempt that survives that step passes through phone attempt risk controls keyed by account, by client IP address, and by /24 network. A hard block returns 429 PHONE_RATE_LIMIT_EXCEEDED with a Retry-After of 86400. A captcha decision returns 400 CAPTCHA_REQUIRED, and this route reads no CAPTCHA solution header, so the attempt cannot be retried with a solution. An inbound decision returns a challenge with the behavioural_risk reason.
An earlier attempt against the same account or number can record a provider cooldown, and a later request under that cooldown returns 429 PHONE_RATE_LIMIT_EXCEEDED. The destination is then resolved through provider lookup. A number that has already completed verification twice is refused with 400 PHONE_ALREADY_USED.
Two further controls bound outbound delivery: 3 sends per 6 hours for each account, and 3 sends per 5 days for each number. Either denial returns 429 PHONE_RATE_LIMIT_EXCEEDED. The provider can also refuse the send: an invalid destination returns 400 INVALID_PHONE_NUMBER, a throttle returns 429 PHONE_RATE_LIMIT_EXCEEDED, and every other refusal, an unreachable provider included, returns 400 SMS_VERIFICATION_UNAVAILABLE. An attempt routed inbound on an instance where the inbound flow or its receiving number is unconfigured returns the same code.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| phone | string | The number in canonical E.164 form, matching ^\+[1-9]\d{1,14}$ |
| channel?1 | string | The preferred verification channel |
1 Omitting the field requests outbound SMS. Requesting inbound_challenge always yields a challenge reported with the expensive_destination reason, unless the account flag independently forces the channel and the reason becomes account_forced
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | SMS delivery object | inbound challenge object | Verification was started on the channel named by the response |
| 400 | error response | The number is refused with INVALID_PHONE_NUMBER, was verified twice already and returns PHONE_ALREADY_USED, risk controls demand CAPTCHA_REQUIRED, or the send could not start and returns SMS_VERIFICATION_UNAVAILABLE |
| 403 | error response | The account is not eligible and the request returns PHONE_ADD_NOT_ELIGIBLE |
| 4291 | rate limit response | Risk controls, a provider throttle, or an account or per-number send control denies the request, which returns PHONE_RATE_LIMIT_EXCEEDED |
1 A phone denial uses the rate limit envelope with code set to PHONE_RATE_LIMIT_EXCEEDED rather than the RATE_LIMITED an ordinary route denial returns
On the 429, X-RateLimit-Scope reports shared when the per-number control or a number-scoped provider throttle produced the denial.
Side effects
Section titled “Side effects”On an outbound result, Fluxer asks the configured SMS provider to deliver a one-time code. On an inbound result, it creates a one-use challenge for the current account.
Fluxer records an attempt against the phone attempt risk counters only once it reaches provider lookup.
Rate limit
Section titled “Rate limit”5 requests per minute for each authenticated user, on the phone:send_verification bucket.
Start inbound phone challenge
Section titled “Start inbound phone challenge”POST/v1/users/@me/phone/inbound-challengeIssues an inbound challenge without submitting or examining a phone number. Returns a direct inbound challenge object on success.
The eligibility check does not apply here. The challenge code is six decimal digits, lives for 15 minutes, and can be redeemed once. Requesting a further challenge does not revoke an earlier one, so several codes issued to the same account can be live at the same time until each expires or is redeemed.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | direct inbound challenge object | A challenge was issued |
| 400 | error response | The instance has no receiving number for the inbound challenge and the request returns SMS_VERIFICATION_UNAVAILABLE |
Side effects
Section titled “Side effects”Fluxer creates a one-use challenge for the current account. The challenge completes out of band when the provider delivers the matching inbound message, and the HTTP response has no part of it. On completion, Fluxer sets the verified phone state, clears the phone requirements from the stored suspicious activity bitfield, and dispatches User Update. When the sending number has already completed verification twice, Fluxer abandons the completion and produces no observable error.
Rate limit
Section titled “Rate limit”5 requests per minute for each authenticated user, on the phone:send_verification bucket, shared with Send phone verification.
Verify phone code
Section titled “Verify phone code”POST/v1/users/@me/phone/verifyVerifies an outbound SMS code. Requires an account satisfying eligibility. Returns a phone verification result object on success. Emits a User Update Gateway event.
The number is resolved through provider lookup again before the code is checked, so a number that has become ineligible since the code was sent is refused.
The configured SMS provider checks the number and the code together, so the provider decides the code lifetime and the attempt allowance. A rejected code returns 400 INVALID_PHONE_VERIFICATION_CODE. A provider throttle returns 429 PHONE_RATE_LIMIT_EXCEEDED and records a cooldown that also denies Send phone verification until it expires. An unreachable provider, or one answering with a server error, returns 400 SMS_VERIFICATION_UNAVAILABLE. An account record that has been deleted, or can no longer be read, fails with 400 PHONE_VERIFICATION_REQUIRED after the code has been accepted.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| phone | string | The number in canonical E.164 form, matching ^\+[1-9]\d{1,14}$ |
| code1 | string | The code the outbound SMS delivered (1-32 characters) |
1 Stripped of control and formatting characters and trimmed before the length check, and the normalised value is what the provider checks
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | phone verification result object | The number is now verified for the account |
| 400 | error response | The number returns INVALID_PHONE_NUMBER, the code returns INVALID_PHONE_VERIFICATION_CODE, the provider returns SMS_VERIFICATION_UNAVAILABLE, the number returns PHONE_ALREADY_USED, or the account returns PHONE_VERIFICATION_REQUIRED |
| 403 | error response | The account is not eligible and the request returns PHONE_ADD_NOT_ELIGIBLE |
| 4291 | rate limit response | A provider throttle denies the request, which returns PHONE_RATE_LIMIT_EXCEEDED |
1 A phone denial uses the rate limit envelope with code set to PHONE_RATE_LIMIT_EXCEEDED rather than the RATE_LIMITED an ordinary route denial returns
On the 429, X-RateLimit-Scope reports shared when a number-scoped provider throttle produced the denial.
Side effects
Section titled “Side effects”Fluxer marks the account as holding a verified phone. It then clears every phone requirement from the stored suspicious activity bitfield: REQUIRE_VERIFIED_PHONE, REQUIRE_REVERIFIED_PHONE, the four combined email-or-phone requirements, REQUIRE_INBOUND_PHONE_VERIFICATION, and the marker deferring a phone requirement until the account joins a qualifying community guild. Email-only requirements remain, so an account that also owes email verification stays restricted.
Fluxer clears the spammer flag when present and dispatches User Update. When the spammer flag was cleared, Fluxer also dispatches Guild Member Update for the account in every guild it belongs to, before the HTTP response returns.
Rate limit
Section titled “Rate limit”10 requests per minute for each authenticated user, on the phone:verify_code bucket.
Get phone gate escape preview
Section titled “Get phone gate escape preview”GET/v1/users/@me/required-actions/phone-gate-escapeReturns a phone gate escape preview object for the current account. For an account outside the state Deferred phone requirement describes, available is false and both arrays are empty.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | phone gate escape preview object | The preview was returned |
Rate limit
Section titled “Rate limit”20 requests per minute for each authenticated user, on the user:phone_gate_escape:preview bucket.
Run phone gate escape
Section titled “Run phone gate escape”POST/v1/users/@me/required-actions/phone-gate-escapeLeaves the qualifying guilds the account does not own and defers the phone requirement again. Returns the user object.
Limitations
Section titled “Limitations”- The account is in the state Deferred phone requirement defines, and one outside it is rejected with 400
PHONE_GATE_ESCAPE_UNAVAILABLE. - One request leaves at most 25 guilds.
JSON body
Section titled “JSON body”The body is an empty object. An absent or empty body is read as an empty object, and any supplied property is discarded. A body that is not valid JSON returns 400 INVALID_FORM_BODY with INVALID_FORMAT at the body path. A body that parses as valid JSON but is not an object is rejected with 400.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | user object | The escape ran, wholly or up to the per-request limit |
| 400 | error response | The escape is unavailable and the request returns PHONE_GATE_ESCAPE_UNAVAILABLE |
Side effects
Section titled “Side effects”Fluxer leaves each qualifying guild the account does not own, at most 25 in one request, with the same effects as Leave guild. A qualifying guild the account owns is kept and does not block the escape.
A request that leaves the whole set then defers the requirement again and dispatches User Update. A request that stopped at the limit writes no account field, dispatches nothing, and returns the account with the requirement still due. Repeat the request until the returned required_actions has no phone entry.
Rate limit
Section titled “Rate limit”5 requests per hour for each authenticated user, on the user:phone_gate_escape:execute bucket.