mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
321 lines
18 KiB
Plaintext
321 lines
18 KiB
Plaintext
---
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
title: Phone verification
|
|
description: Proving control of a phone number over outbound SMS or an inbound challenge, and setting a deferred requirement aside.
|
|
---
|
|
|
|
import RouteHeader from '@/components/RouteHeader.astro';
|
|
|
|
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](/http-api/users/#required-actions) and the `VERY_HIGH` guild [verification level](/http-api/guilds/#verification-levels) check.
|
|
|
|
Every route here needs a non-bot user session, and each one admits a session with suspicious account state.
|
|
|
|
## Eligibility
|
|
|
|
[Send phone verification](#send-phone-verification) and [Verify phone code](#verify-phone-code) admit only an account that satisfies at least one of these conditions.
|
|
|
|
- The account already holds a verified phone.
|
|
- A TOTP [authenticator](/http-api/users/#authenticator-types) is enrolled on the account.
|
|
- The stored suspicious activity bitfield is non-zero.
|
|
- The account belongs to at least one guild whose [verification level](/http-api/guilds/#verification-levels) is `VERY_HIGH`, and the [instance features](/http-api/instance/#instance-features-object) report `phone_verification_enabled` as true.
|
|
|
|
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](#start-inbound-phone-challenge) skips the check entirely and issues a challenge for any non-bot session.
|
|
|
|
:::note[Visible required actions do not determine eligibility]
|
|
An account can qualify even when its [required actions](/http-api/users/#required-actions) array is empty.
|
|
:::
|
|
|
|
## Deferred phone requirement
|
|
|
|
An account can hold a phone requirement together with a marker that defers it. A deferred requirement is absent from the [required actions](/http-api/users/#required-actions) array and restricts nothing. An account whose deferred requirement was later made due owes `REQUIRE_VERIFIED_PHONE` or `REQUIRE_REVERIFIED_PHONE`, and every ordinary user route refuses it with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`.
|
|
|
|
[Get phone gate escape preview](#get-phone-gate-escape-preview) and [Run phone gate escape](#run-phone-gate-escape) set a due requirement aside again without verifying a number and without leaving any guild. Both admit an account the rest of the API refuses. The escape is available only while all of these hold.
|
|
|
|
- The account holds no verified phone.
|
|
- The account owes `REQUIRE_VERIFIED_PHONE` or `REQUIRE_REVERIFIED_PHONE`.
|
|
- The account does not owe `REQUIRE_INBOUND_PHONE_VERIFICATION`.
|
|
- The requirement was deferred before it became due, and it has not been deferred again.
|
|
- The account has at least one visible required action.
|
|
|
|
## 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](/http-api/#validation-error-object) entry whose `path` is `phone` and whose `code` is `PHONE_NUMBER_INVALID_FORMAT`.
|
|
|
|
:::note[Only the verified state is exposed]
|
|
A successful verification sets a boolean on the account. The `phone` field of the [user object](/http-api/users/#user-object) is always null, and no route returns the submitted number.
|
|
:::
|
|
|
|
:::danger[Numbers and codes are confidential]
|
|
Keep phone numbers, verification codes, and challenge codes out of logs, analytics, URLs, and messages to unintended recipients.
|
|
:::
|
|
|
|
## Phone verification channel values
|
|
|
|
| Value | Description |
|
|
| --- | --- |
|
|
| sms<sup>1</sup> | Fluxer asks the SMS provider to deliver a one-time code to the submitted number |
|
|
| inbound_challenge<sup>2</sup> | The user texts the issued challenge code to the returned Fluxer number |
|
|
|
|
<sup>1</sup> Requesting this value is equivalent to omitting the field
|
|
|
|
<sup>2</sup> Requesting this value always yields an inbound challenge, and neither the lookup provider nor the outbound SMS provider is contacted
|
|
|
|
## Number checks
|
|
|
|
Outbound SMS requires the number to pass a line check with the instance's phone provider. A number the provider cannot reach, or one on a line type that cannot receive a code, is refused with 400 `INVALID_PHONE_NUMBER`. Some numbers that pass are routed to an inbound challenge instead. A number the check cannot be run for is refused.
|
|
|
|
## Inbound challenge reason values
|
|
|
|
| Value | Description |
|
|
| --- | --- |
|
|
| verification_required | The instance requires inbound verification for this attempt |
|
|
|
|
## SMS delivery object
|
|
|
|
Confirmation that the SMS provider accepted a one-time code for delivery. [Send phone verification](#send-phone-verification) returns it on an outbound result. The client then sends the same number and the delivered code to [Verify phone code](#verify-phone-code).
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| channel | string | The delivery channel, always the literal `sms` |
|
|
|
|
## Inbound challenge object
|
|
|
|
The instructions for texting a code to Fluxer, returned by [Send phone verification](#send-phone-verification) when policy routed the attempt inbound. `channel` tells the response shapes apart.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| channel | string | The delivery channel, always the literal `inbound_challenge` |
|
|
| challenge_code<sup>1</sup> | string | The code the user texts to `our_number` |
|
|
| our_number | string | The Fluxer E.164 number that receives the code |
|
|
| expires_at<sup>2</sup> | ISO8601 timestamp | The moment the challenge stops being redeemable |
|
|
| reason | string | The [inbound challenge reason](#inbound-challenge-reason-values), always `verification_required` |
|
|
|
|
<sup>1</sup> Six decimal digits
|
|
|
|
<sup>2</sup> 15 minutes after the challenge was issued
|
|
|
|
### Example
|
|
|
|
```json
|
|
{
|
|
"channel": "inbound_challenge",
|
|
"challenge_code": "418207",
|
|
"our_number": "+15550000000",
|
|
"expires_at": "2026-03-04T18:15:00.000Z",
|
|
"reason": "verification_required"
|
|
}
|
|
```
|
|
|
|
## Direct inbound challenge object
|
|
|
|
The code and the Fluxer number for a caller that asked for an inbound challenge directly. [Start inbound phone challenge](#start-inbound-phone-challenge) returns it.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| challenge_code<sup>1</sup> | string | The code the user texts to `our_number` |
|
|
| our_number | string | The Fluxer E.164 number that receives the code |
|
|
| expires_at<sup>2</sup> | ISO8601 timestamp | The moment the challenge stops being redeemable |
|
|
|
|
<sup>1</sup> Six decimal digits
|
|
|
|
<sup>2</sup> 15 minutes after the challenge was issued
|
|
|
|
## Phone verification result object
|
|
|
|
Returned by [Verify phone code](#verify-phone-code) on a successful verification.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| verified | boolean | Always the literal `true` |
|
|
|
|
## Phone gate escape guild object
|
|
|
|
One guild in a [phone gate escape preview](#phone-gate-escape-preview-object).
|
|
|
|
### 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
|
|
|
|
What [Run phone gate escape](#run-phone-gate-escape) would do for the current account.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| available | boolean | Whether the escape is open to this account right now |
|
|
| guilds | array[[phone gate escape guild](#phone-gate-escape-guild-object) object] | Always empty, the escape leaves no guild |
|
|
| owned_guilds | array[[phone gate escape guild](#phone-gate-escape-guild-object) object] | Always empty, the escape leaves no guild |
|
|
|
|
## Send phone verification
|
|
|
|
<RouteHeader method="POST" path="/v1/users/@me/phone/send-verification" />
|
|
|
|
Starts verification of the submitted number. Requires an account satisfying [eligibility](#eligibility). Returns an [SMS delivery](#sms-delivery-object) object or an [inbound challenge](#inbound-challenge-object) object on success.
|
|
|
|
The instance can route the attempt to an inbound challenge instead of outbound SMS, and that overrides a requested `sms`. Requesting `inbound_challenge` always yields a challenge.
|
|
|
|
The number then passes the [number checks](#number-checks). A number that has reached its verification limit is refused with 400 `PHONE_ALREADY_USED`. Sends are limited for each account and for each number, and a denial returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. The phone verification service can also ask for a solved [CAPTCHA](/topics/captcha/) challenge. The route then answers 400 `CAPTCHA_REQUIRED` with a challenge, and a retry with the solved challenge in `X-Captcha-Token` continues the send. When the instance has the check turned off, or the account is exempt from it, that request returns 429 `PHONE_RATE_LIMIT_EXCEEDED` instead. Every other refusal returns 400 `SMS_VERIFICATION_UNAVAILABLE`, as does an instance with no phone verification service.
|
|
|
|
### Request headers
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| X-Captcha-Token?<sup>1</sup> | string | The solved ALTCHA challenge, see [CAPTCHA handling](/topics/captcha/) |
|
|
|
|
<sup>1</sup> Read only when the phone verification service asks for a CAPTCHA. A missing token then returns 400 `CAPTCHA_REQUIRED` and a rejected token returns 400 `INVALID_CAPTCHA`, each with a new challenge
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| phone | string | The number in canonical E.164 form, matching `^\+[1-9]\d{1,14}$` |
|
|
| channel?<sup>1</sup> | string | The preferred [verification channel](#phone-verification-channel-values) |
|
|
|
|
<sup>1</sup> Omitting the field requests outbound SMS. Requesting `inbound_challenge` always yields a challenge
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [SMS delivery](#sms-delivery-object) object \| [inbound challenge](#inbound-challenge-object) object | Verification was started on the channel named by the response |
|
|
| 400 | [error response](/http-api/#error-response) | The number is refused with `INVALID_PHONE_NUMBER`, has reached its verification limit and returns `PHONE_ALREADY_USED`, the attempt asks for a CAPTCHA with `CAPTCHA_REQUIRED` or `INVALID_CAPTCHA`, or the send could not start and returns `SMS_VERIFICATION_UNAVAILABLE` |
|
|
| 403 | [error response](/http-api/#error-response) | The account is not eligible and the request returns `PHONE_ADD_NOT_ELIGIBLE` |
|
|
| 429<sup>1</sup> | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A send limit or a provider throttle denies the request, which returns `PHONE_RATE_LIMIT_EXCEEDED` |
|
|
|
|
<sup>1</sup> A phone denial uses the rate limit envelope with `code` set to `PHONE_RATE_LIMIT_EXCEEDED`. An ordinary route denial returns `RATE_LIMITED`
|
|
|
|
On the 429, `X-RateLimit-Scope` reports `shared` when the per-number control or a number-scoped provider throttle produced the denial.
|
|
|
|
### 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.
|
|
|
|
### Rate limit
|
|
|
|
5 requests per minute for each authenticated user, on the `phone:send_verification` bucket.
|
|
|
|
## Start inbound phone challenge
|
|
|
|
<RouteHeader method="POST" path="/v1/users/@me/phone/inbound-challenge" />
|
|
|
|
Issues an inbound challenge without submitting or examining a phone number. Returns a [direct inbound challenge](#direct-inbound-challenge-object) object on success.
|
|
|
|
The [eligibility](#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
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [direct inbound challenge](#direct-inbound-challenge-object) object | A challenge was issued |
|
|
| 400 | [error response](/http-api/#error-response) | The instance cannot issue an inbound challenge and the request returns `SMS_VERIFICATION_UNAVAILABLE` |
|
|
|
|
### 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](/gateway/events/#user-update). A completion from a number that has reached its verification limit does not verify the account.
|
|
|
|
### Rate limit
|
|
|
|
5 requests per minute for each authenticated user, on the `phone:send_verification` bucket, shared with [Send phone verification](#send-phone-verification).
|
|
|
|
## Verify phone code
|
|
|
|
<RouteHeader method="POST" path="/v1/users/@me/phone/verify" />
|
|
|
|
Verifies an outbound SMS code. Requires an account satisfying [eligibility](#eligibility). Returns a [phone verification result](#phone-verification-result-object) object on success. Emits a [User Update](/gateway/events/#user-update) Gateway event.
|
|
|
|
The number passes the [number checks](#number-checks) again before the code is checked, so a number that has become ineligible since the code was sent is refused.
|
|
|
|
The phone 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 throttle returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. An unavailable provider returns 400 `SMS_VERIFICATION_UNAVAILABLE`. A number that has reached its verification limit fails with 400 `PHONE_ALREADY_USED`. A deleted account, or one that can no longer be read, fails with 400 `PHONE_VERIFICATION_REQUIRED`.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| phone | string | The number in canonical E.164 form, matching `^\+[1-9]\d{1,14}$` |
|
|
| code<sup>1</sup> | string | The code the outbound SMS delivered (1-32 characters) |
|
|
|
|
<sup>1</sup> Stripped of control and formatting characters and trimmed before the length check, and the normalised value is what the provider checks
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [phone verification result](#phone-verification-result-object) object | The number is now verified for the account |
|
|
| 400 | [error response](/http-api/#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](/http-api/#error-response) | The account is not eligible and the request returns `PHONE_ADD_NOT_ELIGIBLE` |
|
|
| 429<sup>1</sup> | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A provider throttle denies the request, which returns `PHONE_RATE_LIMIT_EXCEEDED` |
|
|
|
|
<sup>1</sup> A phone denial uses the rate limit envelope with `code` set to `PHONE_RATE_LIMIT_EXCEEDED`. An ordinary route denial returns `RATE_LIMITED`
|
|
|
|
On the 429, `X-RateLimit-Scope` reports `shared` when a number-scoped provider throttle produced the denial.
|
|
|
|
### 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 combined email-or-phone requirements, `REQUIRE_INBOUND_PHONE_VERIFICATION`, and the markers of a deferred phone requirement. Email-only requirements remain, so an account that also owes email verification stays restricted.
|
|
|
|
Fluxer dispatches [User Update](/gateway/events/#user-update) before the HTTP response returns.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per minute for each authenticated user, on the `phone:verify_code` bucket.
|
|
|
|
## Get phone gate escape preview
|
|
|
|
<RouteHeader method="GET" path="/v1/users/@me/required-actions/phone-gate-escape" />
|
|
|
|
Returns a [phone gate escape preview](#phone-gate-escape-preview-object) object for the current account. For an account outside the state [Deferred phone requirement](#deferred-phone-requirement) describes, `available` is false and both arrays are empty.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [phone gate escape preview](#phone-gate-escape-preview-object) object | The preview was returned |
|
|
|
|
### Rate limit
|
|
|
|
20 requests per minute for each authenticated user, on the `user:phone_gate_escape:preview` bucket.
|
|
|
|
## Run phone gate escape
|
|
|
|
<RouteHeader method="POST" path="/v1/users/@me/required-actions/phone-gate-escape" />
|
|
|
|
Defers the phone requirement again without leaving any guild. Returns the [user](/http-api/users/#user-object) object.
|
|
|
|
### Limitations
|
|
|
|
- The account is in the state [Deferred phone requirement](#deferred-phone-requirement) defines, and one outside it is rejected with 400 [`PHONE_GATE_ESCAPE_UNAVAILABLE`](/http-api/errors/).
|
|
|
|
### JSON body
|
|
|
|
The body is an empty object. Fluxer reads an absent or empty body as an empty object and discards any supplied property. A body that is not valid JSON returns 400 `INVALID_FORM_BODY` with `INVALID_FORMAT` at the `body` path. Valid JSON that is not an object is rejected with 400.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [user](/http-api/users/#user-object) object | The requirement was deferred again |
|
|
| 400 | [error response](/http-api/#error-response) | The escape is unavailable and the request returns `PHONE_GATE_ESCAPE_UNAVAILABLE` |
|
|
|
|
### Side effects
|
|
|
|
Fluxer defers the requirement again and dispatches [User Update](/gateway/events/#user-update). Guild memberships are unchanged.
|
|
|
|
### Rate limit
|
|
|
|
5 requests per hour for each authenticated user, on the `user:phone_gate_escape:execute` bucket.
|