Handling a deletion request: the four calls and the right order
Revoke sessions, withdraw consent, unlink identities, then delete the user. Why order matters and which records you are supposed to keep.
A deletion request touches four Infrai endpoints, and running them in the wrong order leaves live sessions attached to a user record that no longer exists. The sequence that works: POST /v1/auth/session/revoke_all_for_user/{user_id} to close every device, POST /v1/auth/consent/revoke/{user_id} per category, DELETE /v1/auth/identity/remove/{user_id}/{identity_id} for each linked login method, then DELETE /v1/auth/user/delete/{user_id}.
Sessions first. Always.
Why that order
A revoked session is a closed session. A session attached to a deleted user is a question your code has to answer, and the answer differs between a cached JWT verify and a live lookup — which is precisely the ambiguity you don’t want in an audit.
Close the doors, withdraw the permissions, unlink the keys, then remove the record.
The sequence
curl -sS -X POST \
"https://api.infrai.cc/v1/auth/session/revoke_all_for_user/au_usr_lMmXGySJeGVM1xicqHGJaJbB" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"user_id": "au_usr_lMmXGySJeGVM1xicqHGJaJbB"}'
{ "ok": true, "data": { "ok": true, "count": 3 } }
Then the consents, one call per category you hold:
curl -sS -X POST "https://api.infrai.cc/v1/auth/consent/revoke/au_usr_lMmXGySJeGVM1xicqHGJaJbB" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"user_id": "au_usr_lMmXGySJeGVM1xicqHGJaJbB", "category": "marketing"}'
Then the user itself:
curl -sS -X DELETE "https://api.infrai.cc/v1/auth/user/delete/au_usr_lMmXGySJeGVM1xicqHGJaJbB" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{ "ok": true, "data": { "ok": true, "user_id": "au_usr_lMmXGySJeGVM1xicqHGJaJbB" } }
A second delete of the same id answers AUTH_USER_NOT_FOUND, which makes the whole routine safe to retry — useful, because a deletion job that can’t be retried is a deletion job that will one day be half-done.
As one idempotent routine
import os
import requests
API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
CATEGORIES = ("marketing", "analytics", "third_party", "essential")
SESSION = requests.Session()
SESSION.headers.update(HEADERS)
def erase_user(user_id: str) -> dict:
"""Close, withdraw, unlink, delete. Each step tolerates having already run,
so the whole routine is safe to re-run after a partial failure."""
report = {"user_id": user_id}
revoked = SESSION.post(
f"{API}/v1/auth/session/revoke_all_for_user/{user_id}",
json={"user_id": user_id}, timeout=15,
)
report["sessions_closed"] = revoked.json().get("data", {}).get("count", 0)
withdrawn = []
for category in CATEGORIES:
resp = SESSION.post(
f"{API}/v1/auth/consent/revoke/{user_id}",
json={"user_id": user_id, "category": category}, timeout=15,
)
if resp.json().get("ok"):
withdrawn.append(category)
report["consents_withdrawn"] = withdrawn
listed = SESSION.get(f"{API}/v1/auth/identity/list/{user_id}", timeout=15)
identities = listed.json().get("data", {}).get("items", []) if listed.ok else []
for identity in identities:
ident_id = identity.get("identity_id") or identity.get("id")
if ident_id:
SESSION.delete(f"{API}/v1/auth/identity/remove/{user_id}/{ident_id}", timeout=15)
report["identities_removed"] = len(identities)
deleted = SESSION.delete(f"{API}/v1/auth/user/delete/{user_id}", timeout=20)
body = deleted.json()
report["deleted"] = bool(body.get("ok")) or body.get("error", {}).get("code") == "AUTH_USER_NOT_FOUND"
return report
if __name__ == "__main__":
print(erase_user(os.environ["USER_ID"]))
Treating AUTH_USER_NOT_FOUND as success on the final step is the detail that makes it re-runnable.
What you delete, and what you keep
Not everything about a person is theirs to erase, and deleting too much is its own compliance failure.
| Data | Action | Why |
|---|---|---|
| Identity record, profile, metadata | delete | the request covers it |
| Live sessions | revoke before deleting | no orphaned access |
| Consent records | revoke, not delete | the withdrawal itself is the evidence |
| Invoices and payment records | keep | statutory retention beats a deletion request |
| Aggregated analytics with no identifier | keep | no longer personal data |
| Your own copies in other systems | delete deliberately | the platform can’t reach them |
That last row is the one that gets missed. The user’s avatar in object storage, their queued jobs, their captured errors and their analytics events are separate resources and nothing here reaches them for you.
The upside is that they’re all on the same credential, so the fan-out is a handful of calls rather than a procurement exercise: DELETE /v1/storage/object/delete/{bucket}/{key} for stored files, POST /v1/queue/purge/{queue} where a queue is user-scoped, and GET /v1/errors/search to find what still carries their id. One key, one usage view, one place to prove you did it.
Limitations
There’s no single “erase everything about this subject” endpoint, and no export-then-delete bundle: a data-portability request is a set of reads you assemble yourself. Nor is there a scheduled grace period, so if your policy promises thirty days before permanent deletion, the timer is your own — put it on POST /v1/cron/create and have the job call this routine when it fires.
Auth0 ships more compliance tooling around this, including subject-access workflows, and a team whose main constraint is audit paperwork rather than engineering time would be better off there.
Every route in this routine reports billing_class: free in discovery — erasure isn’t billed per call. Identity is metered per monthly active user, and a deleted user stops counting in the month after they’re gone; read your own figure from GET /v1/account/usage (verified 2026-09-21) rather than trusting a printed number, and expect platform rates to keep drifting down.