Skip to content
Fluxer API

Email and password changes

Changing the email address or the password on an account takes several steps. Fluxer opens a ticket, emails a code, and every later step presents that ticket together with the codes and proofs it has collected. An account whose address the mail provider rejected uses bounced email recovery instead.

Every operation needs a non-bot user session. The email change routes admit a session with suspicious account state. Every password change route rejects that state with 403 ACCOUNT_SUSPICIOUS_ACTIVITY.

A ticket identifier is a version 4 UUID. Fluxer issues it when the flow starts, and every later step sends it as a plain string of 1 to 256 characters. Each ticket belongs to one account, and a step presented by another account fails with INVALID_OR_EXPIRED_TICKET. A completed ticket rejects every further step with TICKET_ALREADY_COMPLETED. Starting a flow again mints a fresh ticket and leaves an outstanding one usable.

A verification code is eight characters drawn from the uppercase Latin alphabet and the decimal digits, written as two four-character groups separated by a hyphen. A code lives for 10 minutes from the moment it is sent. Fluxer trims a submitted value and the comparison is otherwise exact, so a value with any other stray character fails with INVALID_VERIFICATION_CODE.

Fluxer returns a proof, a version 4 UUID, from every step that establishes a fact about the ticket. The email change flow issues original_proof for the original address and requires it on Request new email and Verify new email. The password change flow issues verification_proof for the emailed code and requires it to complete the change.

An email token is a version 4 UUID standing for one verified new address. It lives 30 minutes, belongs to the account it was issued to, and is deleted once consumed. A token presented by another account fails with INVALID_EMAIL_TOKEN, and one past its lifetime is deleted and fails with EMAIL_TOKEN_EXPIRED.

Fluxer refuses a resend for 30 seconds after the previous send of the same code, and that refusal is HTTP 429 with a Retry-After computed from the exact moment the next send becomes available. It is independent of the route bucket and of the longer email-send controls.

A ticket failure uses the path ticket, a code failure code, and an address failure new_email or email. A proof failure uses original_proof or verification_proof, an email token failure uses email_token, and a password failure uses password or new_password. The account-level codes ACCESS_DENIED, ACCOUNT_SUSPICIOUS_ACTIVITY, SUDO_MODE_REQUIRED, and RATE_LIMITED arrive as the top-level code.

Each send-bearing operation passes through its own route bucket and, separately, through a longer email-send control keyed by the account. Exhausting either produces HTTP 429 even when the other has room.

ControlAllowanceOperations
Original address3 sends per 15 minutesStart email change and Resend original email code
New address5 sends per 15 minutesRequest new email, Resend new email code, and both bounced recovery sends
Password change start3 sends per 15 minutesStart password change
Password change resend3 sends per 15 minutesResend password change code

The email change ticket moves through three states. A step legal in one state is rejected in every other with the transition code named below.

Current stateOperationNext state
Pending originalResend original email codePending original
Pending originalVerify original emailPending new
Pending newRequest new emailPending new
Pending newResend new email codePending new
Pending newVerify new emailCompleted

A ticket created without original email verification starts in the pending-new state and receives its original_proof directly from Start email change. Requesting or verifying a new address while the ticket is still pending original fails with ORIGINAL_EMAIL_MUST_BE_VERIFIED_FIRST.

Verifying the original stage on a ticket that never required it fails with ORIGINAL_VERIFICATION_NOT_REQUIRED. Resending the original code fails with ORIGINAL_EMAIL_ALREADY_VERIFIED on a cleared ticket and on a ticket that never required the stage, so the response does not tell the two apart. Verifying the original address again on a cleared ticket returns the same original_proof unchanged.

Resending the new address code before an address has been requested fails with NO_NEW_EMAIL_REQUESTED. A pending-original ticket returns the same code.

Request new email and Resend new email code share one 30-second cooldown. Requesting a different address cannot bypass it.

Reaching the completed state issues an email token and leaves the account email unchanged. Apply email change writes the new address.

The state of a freshly created email change ticket.

FieldTypeDescription
ticketstringThe identifier every later step of this flow sends
require_original1booleanWhether the original address needs verification before a new address can be requested
original_email2?stringThe address currently on the account, or null when the account has none
original_proof3?stringThe proof for the original address stage, or null while that stage is unverified
original_code_expires_at4?ISO8601 timestampThe moment the code sent to the original address expires, 10 minutes after it was sent
resend_available_at4?ISO8601 timestampThe earliest moment the original address code can be resent, 30 seconds after the last send

1 True only when the account holds an email address and that address is verified

2 Recorded on the ticket at creation, so a later change to the account email does not move it. Request new email compares against this recorded value

3 Populated exactly when require_original is false. Request new email and Verify new email require it

4 Null whenever require_original is false

{
"ticket": "6f1a4c9e-3d52-4a77-9b0e-2c8f5d41ab30",
"require_original": true,
"original_email": "old@example.com",
"original_proof": null,
"original_code_expires_at": "2026-03-04T18:10:00.000Z",
"resend_available_at": "2026-03-04T18:00:30.000Z"
}

The state of a ticket with a candidate address bound to it and a code in flight to that address.

FieldTypeDescription
ticket1stringThe identifier every later step of this flow sends
new_email2stringThe address now bound to the ticket
new_code_expires_atISO8601 timestampThe moment the code sent to the new address expires, 10 minutes after it was sent
resend_available_at3?ISO8601 timestampThe earliest moment the new address code can be resent, 30 seconds after the last send

1 Echoes the ticket the request named, except on Request replacement email for bounced address, where it identifies the ticket that operation created

2 The trimmed form of the submitted address, which is what a later Verify new email writes

3 Always populated

The proof that a ticket has cleared its original address stage.

FieldTypeDescription
original_proof1stringThe proof Request new email and Verify new email require

1 Stable for the lifetime of the ticket. Verifying the original address again returns the same value

The email token a completed email change ticket hands back.

FieldTypeDescription
email_token1stringThe token Apply email change consumes to write the address

1 The token lives 30 minutes and stands for the exact address the ticket verified

The state of a freshly created password change ticket.

FieldTypeDescription
ticketstringThe identifier every later step of this flow sends
code_expires_atISO8601 timestampThe moment the emailed code expires, 10 minutes after it was sent
resend_available_at1?ISO8601 timestampThe earliest moment the code can be resent, 30 seconds after the last send

1 Always populated

The proof that the emailed code was accepted.

FieldTypeDescription
verification_proof1stringThe proof Complete password change consumes

1 Stable for the lifetime of the ticket. Verifying the same code again returns the same value

The replacement session Complete password change hands back after the password is written.

FieldTypeDescription
token1stringThe authentication token for the replacement session
auth_session_id_hashstringThe base64url-encoded hash of the replacement authentication session

1 The same operation destroys the token the client authenticated with, so adopt this value before issuing another request

POST/v1/users/@me/email-change/start

Creates an email change ticket. Returns an email change start object on success.

Only an account that holds an email address or is unclaimed can start an email change. Every other account fails with MUST_HAVE_EMAIL_TO_CHANGE_IT.

Original address verification applies only when the account holds a verified email address. A bounce clears the verified state. An account that has done nothing since the bounce gets a ticket that skips the original stage. Apply email change marks the address verified again without clearing the bounced marker, so an account that has applied a change since the bounce gets a ticket that does require the stage.

Sending an original address code consumes the original address control.

The body can be omitted, and any supplied body is an object with no fields. Fluxer strips unknown keys before it handles the request.

StatusBodyCondition
200email change start objectThe ticket was created

When original address verification applies, Fluxer creates the ticket in the pending-original state and sends one verification email to the account’s current address. A ticket that skips the stage is created pending-new with its original_proof already issued. No account field changes.

10 requests per minute for each authenticated user, on the user:email_change:start bucket.

POST/v1/users/@me/email-change/resend-original

Sends a fresh verification code to the account’s original address for an active ticket. Returns 204 with an empty body.

A ticket that is no longer pending-original fails with ORIGINAL_EMAIL_ALREADY_VERIFIED. A ticket recording no original address fails with NO_ORIGINAL_EMAIL_ON_RECORD.

The send consumes the original address control, and the ticket enforces its own 30-second cooldown.

FieldTypeDescription
ticketstringThe identifier returned by Start email change (1-256 characters)
StatusBodyCondition
204emptyA replacement original address code was sent

Fluxer replaces the ticket’s original address code, send time, and code expiry, which invalidates the previous code. One verification email goes to the original address.

10 requests per minute for each authenticated user, on the user:email_change:resend_original bucket.

POST/v1/users/@me/email-change/verify-original

Verifies the original address stage and advances the ticket to pending-new. Returns an original email verification object on success.

Running the operation against a ticket that already cleared the stage returns the same proof and changes nothing. A ticket that never required the stage fails with ORIGINAL_VERIFICATION_NOT_REQUIRED. A ticket with no issued code fails with VERIFICATION_CODE_NOT_ISSUED, a code past its 10-minute lifetime with VERIFICATION_CODE_EXPIRED, and a mismatch with INVALID_VERIFICATION_CODE.

FieldTypeDescription
ticketstringThe identifier returned by Start email change (1-256 characters)
codestringThe code sent to the original address (1-256 characters)
StatusBodyCondition
200original email verification objectThe original address is verified

20 requests per minute for each authenticated user, on the user:email_change:verify_original bucket.

POST/v1/users/@me/email-change/request-new

Binds a new address to an active ticket and sends it a verification code. Returns a new email request object on success.

A ticket that has not cleared original address verification fails with ORIGINAL_EMAIL_MUST_BE_VERIFIED_FIRST. An original_proof that does not match the ticket fails with INVALID_PROOF_TOKEN.

Fluxer trims the address and then checks it in order. An empty address fails with EMAIL_IS_REQUIRED, an address equal to the ticket’s recorded original with NEW_EMAIL_MUST_BE_DIFFERENT, a domain that fails DNS eligibility with INVALID_EMAIL_ADDRESS, and an address another account already owns with EMAIL_ALREADY_IN_USE. Ownership is checked again when the resulting email token is applied, so passing here reserves nothing. Calling the operation again with a different address replaces the pending address on the same ticket.

The send consumes the new address control, and the ticket enforces its own 30-second cooldown.

FieldTypeDescription
ticketstringThe identifier returned by Start email change (1-256 characters)
new_email1stringThe address to bind to the ticket (1-254 characters)
original_proofstringThe proof issued by Start email change or Verify original email (1-256 characters)
new_password?2stringThe password to check against the breached-password corpus (8-256 characters)

1 A syntactically invalid address fails with INVALID_EMAIL_FORMAT, one outside the length bound with EMAIL_LENGTH_INVALID, and a local part outside the permitted ASCII set with INVALID_EMAIL_LOCAL_PART

2 The value is only checked against the breached-password corpus, which reports a match as PASSWORD_IS_TOO_COMMON. Nothing on this page writes it

StatusBodyCondition
200new email request objectThe address was bound and a code was sent

The ticket stores the requested address, a fresh code, its send time, and its expiry, and stays pending-new. One verification email goes to the requested address. The account’s own email address is untouched at this stage.

10 requests per minute for each authenticated user, on the user:email_change:request_new bucket.

POST/v1/users/@me/email-change/resend-new

Sends a fresh verification code to the address already bound to an active ticket. Returns 204 with an empty body.

A ticket with no requested address fails with NO_NEW_EMAIL_REQUESTED.

The send consumes the new address control, and the ticket enforces its own 30-second cooldown.

FieldTypeDescription
ticketstringThe identifier returned by Start email change (1-256 characters)
StatusBodyCondition
204emptyA replacement new address code was sent

Fluxer replaces the ticket’s new address code, send time, and code expiry, which invalidates the previous code. One verification email goes to the requested address.

10 requests per minute for each authenticated user, on the user:email_change:resend_new bucket.

POST/v1/users/@me/email-change/verify-new

Verifies the requested address and completes the ticket. Returns a new email verification object on success.

An original_proof that does not match the ticket fails with INVALID_PROOF_TOKEN. A ticket with no requested address or no issued code fails with VERIFICATION_CODE_NOT_ISSUED, a code past its 10-minute lifetime with VERIFICATION_CODE_EXPIRED, and a mismatch with INVALID_VERIFICATION_CODE.

FieldTypeDescription
ticketstringThe identifier returned by Start email change (1-256 characters)
codestringThe code sent to the new address (1-256 characters)
original_proofstringThe proof issued by Start email change or Verify original email (1-256 characters)
StatusBodyCondition
200new email verification objectThe new address is verified

The ticket moves to the completed state and a 30-minute email token is issued for the verified address. The account’s email address is still unchanged.

20 requests per minute for each authenticated user, on the user:email_change:verify_new bucket.

POST/v1/users/@me/email-change/applyMFA

Consumes an email token and writes its address to the account. Returns the updated user object on success. Emits a User Update Gateway event.

A claimed account proves sudo mode before the route examines the token. The route accepts suspicious account state.

A token that belongs to another account fails with INVALID_EMAIL_TOKEN, and one past its 30-minute lifetime fails with EMAIL_TOKEN_EXPIRED. An expired token is deleted when rejected. Fluxer checks the address against existing account ownership again here, and an address another account has taken since fails with EMAIL_ALREADY_IN_USE without consuming the token.

An unclaimed account is exempt from sudo mode and applies the change with its session alone.

Modify current user accepts the same token alongside an unrelated profile edit. The token is single use, so a retry after success fails with INVALID_EMAIL_TOKEN.

The body extends the sudo verification object with email_token, and the five sudo fields it merges in are repeated below. An existing sudo proof travels in the X-Fluxer-Sudo-Mode-JWT request header.

FieldTypeDescription
email_tokenstringThe token returned by Verify new email (1-256 characters)
password?1stringThe account password (8-256 characters)
mfa_method?2stringThe sudo MFA method, either totp or webauthn
mfa_code?3stringThe sudo authenticator code or an unconsumed backup code (1-32 characters)
webauthn_response?4WebAuthn assertion objectThe assertion produced for the supplied challenge
webauthn_challenge?4stringThe challenge returned by create sudo WebAuthn authentication options (1-256 characters)

1 Considered only while the account holds no MFA authenticator, and ignored once TOTP or a WebAuthn credential exists

2 Required when the account holds any MFA authenticator, unless the request already presents an accepted sudo proof

3 Required when mfa_method is totp. The value is a current authenticator code or an unconsumed backup code

4 Both fields are required together when mfa_method is webauthn

The body has no profile field and no new_password field, and Fluxer strips unknown keys before it handles the request.

An unclaimed account that also wants a first password makes a single Modify current user call with email_token and new_password.

StatusBodyCondition
200user objectThe verified email change was applied
403error responseSudo mode was not proven and the request returns SUDO_MODE_REQUIRED with the sudo mode methods object

The 200 has X-Fluxer-Sudo-Mode-JWT when sudo verification issued a proof.

Fluxer replaces the account email, marks it verified, and removes every email-clearable suspicious activity flag, which can empty required_actions and restore ordinary access. Fluxer evaluates the new address against the instance contact policy separately, and that check can add suspicious activity flags. The bounced marker is not cleared, so an account that has it recovers through bounced email recovery.

For an ordinary change from an existing address, Fluxer sends a revert email to the original address so its holder can use Revert an email change. Fluxer deletes the email token, records the contact change in the account’s contact change log, and emits User Update. No public member field changes, so no Guild Member Update follows.

20 requests per minute for each authenticated user, on the user:email_change:apply bucket.

An account whose stored address the mail provider rejected has the bounced marker, reported as email_bounced on the user object. The bounce also clears the verified email state and imposes a required action. The three operations below bind a replacement address and finish without the sudo-gated apply step.

Every one of them requires the bounced marker. An account without it is rejected with 403 ACCESS_DENIED, and an account with no address at all is rejected with MUST_HAVE_EMAIL_TO_CHANGE_IT.

The flow also requires the account to hold no verified address, which is the state a bounce leaves. Apply email change marks an address verified without clearing the bounced marker. An account that took the ordinary flow after its bounce has both states, gets ORIGINAL_EMAIL_MUST_BE_VERIFIED_FIRST here, and changes its address through the ordinary flow.

Ticket and code contract governs its codes, cooldown, and new address send control. The verification step writes the address and clears the marker in one call.

Request replacement email for bounced address

Section titled “Request replacement email for bounced address”
POST/v1/users/@me/email-change/bounced/request-new

Starts the recovery flow. Returns a new email request object on success.

This operation creates a ticket and binds the replacement address in the same call. The route accepts suspicious account state. Fluxer checks the address exactly as Request new email checks it, so it passes DNS eligibility, belongs to no other account, and differs from the bounced address.

The send consumes the new address control.

FieldTypeDescription
new_emailstringThe address to replace the bounced one (1-254 characters)
StatusBodyCondition
200new email request objectThe replacement address was bound and a code was sent
403error responseThe account is not marked bounced and the request returns ACCESS_DENIED

A ticket is created pending-new with the replacement address bound to it, and one verification email goes to that address. No account field changes. Retain the returned ticket, because the two remaining steps take no other identifier.

10 requests per minute for each authenticated user, on the user:email_change:bounced:request_new bucket.

POST/v1/users/@me/email-change/bounced/resend-new

Sends a fresh verification code to the replacement address bound to an active recovery ticket. Returns 204 with an empty body.

A ticket with no replacement address fails with NO_NEW_EMAIL_REQUESTED.

The send consumes the new address control, and the ticket enforces its own 30-second cooldown.

FieldTypeDescription
ticketstringThe identifier returned by Request replacement email for bounced address (1-256 characters)
StatusBodyCondition
204emptyA replacement code was sent
403error responseThe account is not marked bounced and the request returns ACCESS_DENIED

Fluxer replaces the ticket’s code, send time, and code expiry, which invalidates the previous code. One verification email goes to the replacement address.

10 requests per minute for each authenticated user, on the user:email_change:bounced:resend_new bucket.

Verify replacement email for bounced address

Section titled “Verify replacement email for bounced address”
POST/v1/users/@me/email-change/bounced/verify-new

Verifies the replacement address and writes it to the account in the same call. Returns the updated user object on success. Emits a User Update Gateway event.

No sudo verification and no email token step applies.

FieldTypeDescription
ticketstringThe identifier returned by Request replacement email for bounced address (1-256 characters)
codestringThe code sent to the replacement address (1-256 characters)
StatusBodyCondition
200user objectThe replacement address was verified and written
403error responseThe account is not marked bounced and the request returns ACCESS_DENIED

Fluxer replaces the account email, marks it verified, clears the bounced marker, and removes every email-clearable suspicious activity flag, which can empty required_actions and restore ordinary access. It marks the ticket completed and deletes its email token immediately. The contact change is recorded in the account’s contact change log.

No revert email is sent.

20 requests per minute for each authenticated user, on the user:email_change:bounced:verify_new bucket.

POST/v1/users/@me/password-change/start

Creates a password change ticket and sends a verification code to the account email. Returns a password change start object on success.

An account holding no email address fails with MUST_HAVE_EMAIL_TO_CHANGE_IT.

The send consumes the password change start control.

The body can be omitted, and any supplied body is an object with no fields. Fluxer strips unknown keys before it handles the request.

StatusBodyCondition
200password change start objectThe ticket was created and a code was sent

A ticket is created in its pending state and one verification email goes to the account address. No account field changes.

10 requests per minute for each authenticated user, on the user:password_change:start bucket.

POST/v1/users/@me/password-change/resend

Sends a fresh verification code for an active password change ticket. Requires the account to still hold an email address. Returns 204 with an empty body.

The send consumes the password change resend control, and the ticket enforces its own 30-second cooldown.

FieldTypeDescription
ticketstringThe identifier returned by Start password change (1-256 characters)
StatusBodyCondition
204emptyA replacement code was sent

Fluxer replaces the ticket’s code, send time, and code expiry, which invalidates the previous code. One verification email goes to the account address.

10 requests per minute for each authenticated user, on the user:password_change:resend bucket.

POST/v1/users/@me/password-change/verify

Verifies the emailed code. Returns a password verification object on success.

Running the operation against an already verified ticket returns the same proof. A ticket with no issued code fails with VERIFICATION_CODE_NOT_ISSUED, a code past its 10-minute lifetime with VERIFICATION_CODE_EXPIRED, and a mismatch with INVALID_VERIFICATION_CODE.

FieldTypeDescription
ticketstringThe identifier returned by Start password change (1-256 characters)
codestringThe code sent to the account address (1-256 characters)
StatusBodyCondition
200password verification objectThe code was accepted

The ticket moves to its verified state and stores a verification proof. The account password is still unchanged.

20 requests per minute for each authenticated user, on the user:password_change:verify bucket.

POST/v1/users/@me/password-change/complete

Consumes a verified ticket and replaces the account password. Returns a password change completion object on success. Emits an Auth Session Change Gateway event with the replacement token.

No sudo verification applies. A ticket outside its verified state or a proof that does not match fails with INVALID_OR_EXPIRED_TICKET or INVALID_PROOF_TOKEN. A replacement password in the public breached-password corpus is rejected with PASSWORD_IS_TOO_COMMON. The ticket is marked completed once the password is written, so a retry fails with TICKET_ALREADY_COMPLETED.

FieldTypeDescription
ticketstringThe identifier returned by Start password change (1-256 characters)
verification_proofstringThe proof issued by Verify password change code (1-256 characters)
new_passwordstringThe password to write to the account (8-256 characters)
StatusBodyCondition
200password change completion objectThe password was replaced

The password and its change timestamp are replaced and the ticket is marked completed. Every outstanding password reset token is deleted. Fluxer deletes and terminates every other authentication session on the Gateway first, then emits Auth Session Change, then deletes and terminates the caller’s own session. The returned replacement is then the account’s only session. OAuth2 access tokens and refresh tokens are not revoked.

10 requests per minute for each authenticated user, on the user:password_change:complete bucket.