Skip to content
FiveCord Docs

Email and password changes

Changing the email address or the password on an account takes several steps. FiveCord opens a ticket, emails a code, and every later step sends that ticket with the codes and proofs collected so far. 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 accept 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. FiveCord 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 sent by another account fails with INVALID_OR_EXPIRED_TICKET. A completed ticket rejects every further step with TICKET_ALREADY_COMPLETED. Starting a flow again issues a fresh ticket and leaves an outstanding one usable.

A verification code is eight characters 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. FiveCord trims a submitted value and the comparison is otherwise exact, so a value with any other stray character fails with INVALID_VERIFICATION_CODE.

FiveCord 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.

FiveCord refuses a resend for 30 seconds after the previous send of the same code. The refusal is HTTP 429 with a Retry-After computed from the exact moment the next send becomes available. This cooldown is separate from the route bucket and the email-send controls.

  • A ticket failure uses the path ticket.
  • A code failure uses the path code.
  • An address failure uses the path new_email or email.
  • A proof failure uses the path original_proof or verification_proof.
  • An email token failure uses the path email_token.
  • A password failure uses the path 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 operation that sends an email counts against its own route bucket and against a 15-minute email-send control for 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 is pending-original, pending-new, or completed. A step allowed in one state is rejected in every other with the code named in the paragraphs 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 ticket that has passed original address verification and on a ticket that never required the stage, so the response does not tell the two apart. On a ticket that has passed original address verification, verifying the original address again returns the same original_proof.

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 newly 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 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 Set 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 sent 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 set

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 returns.

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 newly 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 set

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 returns 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 the client sends every later request with this value

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 whose address has not been verified again since the bounce gets a ticket that skips the original stage. Apply email change marks the address verified again without clearing the bounced marker. An account that has applied a change since the bounce therefore gets a ticket that does require the stage.

Sending an original address code counts against the original address control.

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

StatusBodyCondition
200email change start objectThe ticket was created

When original address verification applies, FiveCord 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 counts against the original address control, and the ticket applies its own 30-second cooldown.

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

FiveCord 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.

FiveCord 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 fails with NEW_EMAIL_MUST_BE_DIFFERENT.
  • A domain that publishes no mail exchange or address records fails with EMAIL_DOMAIN_CANNOT_RECEIVE_MAIL.
  • An address another account already owns fails with EMAIL_ALREADY_IN_USE.

Ownership is checked again when the resulting email token is applied, so a successful request here does not reserve the address. Calling the operation again with a different address replaces the pending address on the same ticket.

The send counts against the new address control, and the ticket applies 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 email is unchanged 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 counts against the new address control, and the ticket applies its own 30-second cooldown.

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

FiveCord 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 reads the token. An unclaimed account is exempt and applies the change with its session alone. 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. FiveCord checks the address against existing account ownership again here. An address another account has taken since fails with EMAIL_ALREADY_IN_USE without consuming the token.

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 sudo fields it merges in are repeated below. An existing sudo proof is sent in the X-FiveCord-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 TOTP secret and no registered WebAuthn credential, and ignored once either exists

2 Required when the account holds a TOTP secret or a registered WebAuthn credential, 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, and FiveCord reads it as a backup code alone while the account holds no TOTP secret

4 Both fields are required together when mfa_method is webauthn

The body has no profile field and no new_password field, and FiveCord 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-FiveCord-Sudo-Mode-JWT when sudo verification issued a proof.

FiveCord replaces the account email, marks it verified, and removes every email-clearable suspicious activity flag, which can empty required_actions and restore ordinary access. The new address is also checked against the instance contact policy, 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, FiveCord sends a revert email to the original address so its holder can use Revert an email change. FiveCord 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 adds a required action. The 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, so it gets ORIGINAL_EMAIL_MUST_BE_VERIFIED_FIRST here and changes its address through the ordinary flow.

The codes and the 30-second cooldown in this flow follow Ticket and code contract. The sends in this flow count against the new address control.

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. FiveCord checks the address exactly as Request new email does: the address must have a domain that publishes mail exchange or address records, belong to no other account, and differ from the bounced address.

The send counts against 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. Keep the returned ticket, because the 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 counts against the new address control, and the ticket applies 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

FiveCord 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

The account gets the verified replacement address, email_bounced becomes false, and satisfied email requirements are removed from required_actions. The ticket is completed and cannot be reused.

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 counts against the password change start control.

The body can be omitted, and any supplied body is an object with no fields. FiveCord 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 counts against the password change resend control, and the ticket applies its own 30-second cooldown.

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

FiveCord 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 fails with INVALID_OR_EXPIRED_TICKET. A proof that does not match fails with 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 is changed and the ticket is completed. All password reset tokens and existing authentication sessions are revoked. The caller receives Auth Session Change with the replacement token before its session ends. The returned token is the account’s only session. OAuth2 access tokens and refresh tokens remain valid.

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