DELETE
/
api
/
v1
/
me
Delete my account (right-to-erasure)
curl --request DELETE \
  --url https://api.example.com/api/v1/me
import requests

url = "https://api.example.com/api/v1/me"

response = requests.delete(url)

print(response.text)
const options = {method: 'DELETE'};

fetch('https://api.example.com/api/v1/me', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));
<?php

$curl = curl_init();

curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/me",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "DELETE",
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
package main

import (
"fmt"
"net/http"
"io"
)

func main() {

url := "https://api.example.com/api/v1/me"

req, _ := http.NewRequest("DELETE", url, nil)

res, _ := http.DefaultClient.Do(req)

defer res.Body.Close()
body, _ := io.ReadAll(res.Body)

fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.delete("https://api.example.com/api/v1/me")
.asString();
require 'uri'
require 'net/http'

url = URI("https://api.example.com/api/v1/me")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Delete.new(url)

response = http.request(request)
puts response.read_body

Overview

The user’s GDPR right-to-erasure entry point. Calling this:
  1. Re-authenticates the caller via currentPassword — defense in depth, the bearer token alone is not enough. The same access token that just logged you in is not sufficient to delete your account.
  2. Soft-deletes the user row (users.deletedAt = now, users.deletedReason = 'user-request'). The row physically remains so foreign-key references (audit_logs, subscriptions, conversations) stay valid.
  3. Cascades soft-delete to the only child table that has a deletedAt field today — current_homes. Other child rows are hard-deleted 30 days later by the daily gdpr-erasure worker.
  4. Revokes all active refresh tokens for the user. Every device is logged out on the next API call.
  5. Writes a user.deleted admin-audit row (actorType: USER).
This endpoint deliberately skips requireOnboarded. A user mid-onboarding has the same GDPR right to erase their data. The cascade and the worker both treat any soft-deleted row identically, regardless of onboarding state (QUESTIONS.md §9.1 item 4).
Idempotent: re-calling on an already soft-deleted user is a 200 no-op — cascades and audit are skipped, the existing deletedAt is returned unchanged. See GDPR data lifecycle for the full 30-day clock, tombstoning, and the carve-outs (audit_logs, subscriptions).

Authentication

Bearer <accessToken> (user scope) required. Re-auth via currentPassword in the body is also required — see request body.

Path parameters

None.

Query parameters

None.

Request body

FieldTypeRequiredNotesExample
currentPasswordstringyes1..256 chars. The user’s current password. Verified server-side with argon2 against the stored hash. Wrong password returns 401 ADMIN_INVALID_CREDENTIALS — note the reused user-side code from the auth surface.correct-horse-battery-staple

Example payload

{
  "currentPassword": "correct-horse-battery-staple"
}

Response — 200 OK

FieldTypeNotesExample
deletedAtstring (ISO 8601)Server clock at soft-delete. On re-calling for an already-deleted user, the existing deletedAt is returned (idempotent).2026-05-23T09:14:11.412Z
{
  "deletedAt": "2026-05-23T09:14:11.412Z"
}

Error responses

StatusCodeMeaning
400VALIDATION_FAILEDcurrentPassword missing, empty, or longer than 256 chars.
401UNAUTHENTICATEDMissing / malformed / expired token.
401ADMIN_INVALID_CREDENTIALSThe supplied currentPassword did not match the stored hash. Code name is reused from the auth-shared module; the message is generic to avoid leaking which factor failed.

Example error — 401 wrong password

{
  "type": "https://api.swappr.co.uk/errors/admin-invalid-credentials",
  "title": "Invalid admin credentials",
  "status": 401,
  "code": "ADMIN_INVALID_CREDENTIALS",
  "detail": "Invalid credentials",
  "instance": "/api/v1/me",
  "requestId": "01HZQ7K3M4N5P6Q7R8S9T0V1W2"
}

Side effects

EffectNotes
User rowdeletedAt = now, deletedReason = 'user-request'. Row physically remains for FK integrity.
current_homes rowSoft-deleted (deletedAt = now). Best-effort — a cascade failure is logged and swallowed; the erasure worker will catch any leak.
Refresh tokensAll active tokens revoked. The user is logged out of every device.
Audit rowaction: user.deleted, actorType: USER, actorId: <userId>.
30-day hard-deleteThe daily gdpr-erasure worker (04:00 UTC) picks up users with deletedAt < now-30d and tombstones the row + hard-deletes child data.

What happens to my access token?

The current access token continues to work until it expires (15 min default) — the access-token check is stateless. However the refresh token is revoked, so when the access token expires the next refresh call returns TOKEN_REVOKED and the client is forced into the logged-out state. New logins are blocked because the user row’s status and deletedAt are checked at login time.

Changed your mind? (reactivation)

The deletion is reversible for the full 30-day grace window. If the user logs in during that window, POST /auth/login returns 403 ACCOUNT_PENDING_DELETION (carrying meta.deletionScheduledAt) rather than a session — the cue to show a reactivation screen. The user then calls POST /auth/reactivate with their credentials to clear the pending deletion and get a fresh session. After day 30 the row is tombstoned and reactivation is no longer possible.

See also

curl

curl -X DELETE https://api.swappr.co.uk/api/v1/me \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"currentPassword":"correct-horse-battery-staple"}'

Postman

See docs/postman/swappr.postman_collection.jsonMe → Delete Account.