Let users delete their own account
Blog GuideSooner or later one of your users asks how to delete their account. Until now the answer went through you: they emailed support, and someone erased them from the panel or from a backend route that held an operator key. Now the signed-in user can make one call, DELETE /api/v1/users/me, and Rekey runs the same erasure an operator would.
This post covers the setting that decides who may make that call, the call from a browser and from your server, the cases where it is refused, and what erasure removes and keeps.
Choose who may delete an account
Each Application has an accountDeletion setting in its auth config. In the panel it is Authentication → Methods → Account deletion.
| Value | Who may call DELETE /users/me |
|---|---|
self_service | The default. The signed-in user from your app in the browser, after confirming with their password or authenticator code, and your backend with a secret key. |
server_only | Your backend only, with a secret key. A browser call is refused, so your server decides and confirms in its own way, such as an email link or a support ticket. |
off | Nobody. Every key is refused, and an account is removed only when an operator erases it from the panel. |
A refusal from this setting is 403 ACCOUNT_SELF_DELETION_DISABLED. It is checked before the password, so an Application with deletion turned off never tells a caller whether a password was right. The same field can be set with authConfig.accountDeletion on the operator auth config route.
From the browser
An access token on its own is not enough to delete an account from the browser. Anyone who has stolen a token could otherwise destroy the account with it. So a call with the publishable key must carry a second proof: the account's current password, or a code from the authenticator app (or an unused backup code) when MFA is enrolled. With the React SDK:
import { RekeyBrowserClient, RekeyError } from "@rekey.dev/react";
const client = new RekeyBrowserClient({ apiUrl, publishableKey: "pk_live_…" });
try {
await client.deleteAccount(accessToken, { password });
// The account is gone, and so is the session. Send the user home.
} catch (err) {
if (err instanceof RekeyError) showError(err.code, err.message);
}A wrong or missing proof answers 401 STEP_UP_REQUIRED. Password attempts here count toward the same lockout as sign-in, so a stolen session is not a second place to guess the password. An account with no password and no MFA, such as one that only ever signed in with Google, has nothing to confirm with and gets 400 STEP_UP_UNAVAILABLE. Ask that user to set a password or enroll MFA first, or delete the account from your server.
From your server
With a secret key, your backend is the gate and the call needs no password or code. The key needs the auth:write scope. With the Node SDK:
import { Rekey } from "@rekey.dev/node";
const rekey = new Rekey({ apiUrl: process.env.REKEY_URL!, secretKey: process.env.REKEY_SECRET! });
const { erasedAt } = await rekey.auth.deleteCurrentUser(accessToken);This is the path for server_only: confirm however suits your product, then call it with the user's access token. Both SDK methods resolve to { erased: true, erasedAt }.
When it is refused
| Code | When, and what to do |
|---|---|
ACCOUNT_SELF_DELETION_DISABLED | 403. accountDeletion is off, or server_only and the call used the publishable key. Delete from your backend with a secret key, or change Account deletion in the panel. |
STEP_UP_REQUIRED | 401. A browser call sent no valid password or code. Ask the user to confirm again. |
IMPERSONATION_ACTION_FORBIDDEN | 403. The token belongs to an operator impersonating the user. Only the user, or your backend on their behalf, can delete the account. |
ACCOUNT_HAS_ACTIVE_SUBSCRIPTION | 409. A subscription is still billing through a payment provider and is not set to cancel. Cancel it, or set it to cancel at the end of the period, then delete the account. |
END_USER_ERASED | 410. The account is already erased, and the token stopped working with it. There is nothing left to do. |
The subscription rule is there so nobody keeps getting charged after their account is gone. Rekey does not cancel the subscription for the user. That stays a separate, visible step, with client.cancelSubscription(accessToken) in the browser or rekey.billing.cancelSubscription(accessToken) on your server. A subscription you granted by hand, with no payment provider behind it, does not block deletion.
What erasure removes and keeps
The call runs the same erasure as the panel's Erase (GDPR) button. It cannot be undone.
- Every session ends. Sign-in methods are deleted: the password, linked OAuth identities, MFA, passkeys and any outstanding magic or reset links.
- The email becomes a placeholder address and metadata is cleared, so the real address is free for a new sign-up.
- Payments, subscriptions and licences stay as the financial record, with their free-form metadata scrubbed.
- The
user.erasedwebhook fires, so you can remove your own copies of the user's data. - The tombstone records the user's own id in
erasedBy, and theend_user.erasedsecurity event names the end user as the actor.
The full list, model by model, is in docs/data-erasure.md.
About GDPR
Where to go next
- Pick the
accountDeletionvalue under Application → Auth before you add the button. - Handle
user.erasedin your backend, the same way you handle the webhooks in sending new users to onboarding.
Was this page helpful?
