Guides
Email change and session management
Let end users update their login email address and revoke all active sessions from any device or browser.
Both features are available to end users through your app, and the session-termination endpoint has a server-side variant so your backend can sign out a specific user when needed (for example, after an account suspension or a security event).
Change email address
The email-change flow uses a two-step verification to protect against account takeover: the user must know their current password, and they must prove access to the new address before the change takes effect. The old address receives a notice with a cancel link so the account owner can abort if the request was not theirs.
The flow has three endpoints:
- Request (sends the code and the cancel notice)
- Confirm (validates the code and swaps the email)
- Cancel (abort link in the old-address notice email)
Step 1: Request the change
Call this endpoint with the user's Bearer token. The user must supply their current password. If the password is correct and the new address is not already taken in the same project, Mudbase:
- Sends a 6-digit confirmation code to the new address (expires in 10 minutes).
- Sends a cancel notice with a single-use link to the current address (valid for 25 hours).
POST /api/users/email-change/request
Authorization: Bearer <userToken>
Content-Type: application/json
{
"currentPassword": "MyCurrentP@ss1",
"newEmail": "new-address@example.com"
}Success response (200):
{
"message": "A confirmation code has been sent to your new email address."
}Rate limit: 3 requests per user (or IP) per hour.
Step 2: Confirm with the code
The user enters the 6-digit code that arrived at the new address. Use the same Bearer token as the request step.
POST /api/users/email-change/confirm
Authorization: Bearer <userToken>
Content-Type: application/json
{
"code": "481923"
}Success response (200):
{
"message": "Email address updated successfully. Other sessions have been signed out.",
"token": "<newJwt>",
"refreshToken": "<newRefreshToken>",
"expiresIn": 1800
}On success, Mudbase updates the user record to the new email address, invalidates all existing sessions (including tokens on other devices), and returns a fresh token pair for the calling session so it keeps working. Store the new token and refreshToken immediately and discard the old ones.
Cancel (old-address abort link)
If the account owner did not initiate the change request, they can cancel it by clicking the link in the notice email sent to their current address. The link calls this endpoint automatically:
GET /api/users/email-change/cancel?token=<rawCancelToken>This endpoint requires no authentication. The token is single-use and expires 25 hours after the original request. The response is always a plain message and never reveals account details.
// Cancel succeeded - HTTP 200
{ "message": "Your email change has been cancelled. No change was made to your account." }
// Request was already cancelled (idempotent) - HTTP 200
{ "message": "This email change request has already been cancelled." }
// Token not found or already used - HTTP 200
{ "message": "This cancellation link is not valid or has already been used." }
// Cancel link expired (request also expired; no change was made) - HTTP 200
{ "message": "This cancellation link has expired. The change request also expired and no change was made." }
// Token parameter missing from the request - HTTP 400
{ "error": "Cancel token is required." }Error codes
Errors from the request and confirm endpoints include a code field alongside the HTTP status and error message.
| Code | HTTP status | When it occurs |
|---|---|---|
WRONG_PASSWORD | 400 | The supplied currentPassword does not match the account's stored password. |
NO_PASSWORD | 400 | The account uses a linked provider (SSO or OAuth) and has no local password. See SSO accounts below. |
SAME_EMAIL | 400 | The requested new email is identical to the current one. |
EMAIL_TAKEN | 409 | Another user in the same project already uses that email address. Email uniqueness is scoped per project. |
NO_PENDING_REQUEST | 404 | The confirm endpoint was called but no active request exists for this user (none was created, or it was already confirmed). A request voided by MAX_ATTEMPTS keeps its record and returns MAX_ATTEMPTS (400) on subsequent calls, not this code. |
CODE_EXPIRED | 400 | The 10-minute code TTL has passed. Submit a new request to get a fresh code. |
WRONG_CODE | 400 | The submitted code is incorrect. The response includes how many attempts remain. |
MAX_ATTEMPTS | 400 | 5 incorrect code submissions. The pending request is voided. Start over with a new request. |
SSO and OAuth-only accounts
Users who signed up exclusively through an OAuth provider or SSO (no local password set) cannot use this email-change flow. The request endpoint returns NO_PASSWORD (400) for those accounts.
If you allow mixed authentication (some users have a local password, others do not), show the email-change UI only when the user has a local password. You can detect this from GET /api/users/me: the response includes a hasPassword boolean that is true when the account has a local password set and false for OAuth-only accounts. Show the email-change option only when user.hasPassword === true.
Interaction with pending-account gate
If your project uses blockPendingLogin: true (see Securing end-user data), a user whose accountStatus is "pending" cannot sign in and therefore cannot reach the email-change endpoints at all: they have no valid session token. There is no special interaction to handle.
A user whose status is "active" is unaffected by this setting and can use the email-change flow normally.
Sign out all sessions
Users can revoke every active session across all their devices and browsers with a single call. The calling session stays alive and receives a fresh token pair.
POST /api/users/sessions/end-all
Authorization: Bearer <userToken>
Content-Type: application/jsonSuccess response (200):
{
"message": "All other sessions have been signed out.",
"token": "<newJwt>",
"refreshToken": "<newRefreshToken>",
"expiresIn": 1800
}Replace the stored token and refresh token with the returned ones. Any token that was issued before this call is rejected on the next authenticated request.
This action writes an audit log entry (user.sessions_ended_all) but does not fire a webhook.
Server-side: sign out a specific user
Your backend can end all sessions for a specific end-user using a server API key scoped to the project. This is useful for security events (for example, after detecting suspicious activity or revoking access for a terminated employee).
POST /api/users/{userId}/sessions/end-all
X-API-Key: <serverApiKey>
Content-Type: application/jsonSuccess response (200):
{
"message": "User sessions ended."
}404 without revealing whether the user exists in that other project.JWT caller tokens are not accepted for this endpoint. It is a server-side operation only. To end your own sessions as an end user, use POST /api/users/sessions/end-all above.
Webhook: user.email.changed
When a user successfully confirms an email change, Mudbase fires a user.email.changed event to your configured webhook endpoint. The event is fired only for project-scoped users (not for org-level accounts that have no associated project).
Mudbase delivers this event to your configured webhook endpoint. Projects with no event filter set receive all events by default, which includes this one. To receive only a specific set of events, select user.email.changed in Console → Webhooks → Events, or include it in your webhook configuration via the API. See Handling Webhooks for how to configure an endpoint and verify signatures.
Full delivery envelope:
{
"event": "user.email.changed",
"data": {
"userId": "685acbe0e129932fbb7a0fc2",
"previousEmail": "old@example.com",
"newEmail": "new@example.com",
"changedAt": "2026-10-13T19:30:00.000Z"
},
"timestamp": "2026-10-13T19:30:00.123Z",
"projectId": "64fa3c2b1d3e4a0012345678"
}In your handler, read the event data from the data key: payload.data.userId, payload.data.previousEmail, payload.data.newEmail, and payload.data.changedAt.
| Field | Type | Description |
|---|---|---|
userId | string | The ID of the user whose email changed. |
previousEmail | string | The email address before the change. |
newEmail | string | The newly confirmed email address. |
changedAt | ISO 8601 string | Timestamp of when the email was updated (UTC). |
Use this event to sync user records in your own database, update mailing list subscriptions, or invalidate any cached user profile that stores the old address. Make your handler idempotent using userId and changedAt as a composite key.
POST /api/users/sessions/end-all and its server-side variant) do not fire a webhook. They write an audit log entry only.