- Updated
- Reading
- 7 min
- By
- Anlyon Team
Deleting a user from your AI agent: connections, limit counters and receipts
What endUsers.erase removes when a user deletes their account: OAuth grants revoked, approvals cancelled, counters cleared, receipts kept under a hash.
- end users
- deletion
- privacy
- oauth
- receipts
When a user deletes their account, call anlyon.endUsers.erase(userId) from your deletion flow. It revokes their OAuth grants at the provider, deletes every connection, cancels their pending approvals and clears their per-user limit counters, and it keeps the record of what your agent did with the user's id replaced by a keyed one-way hash.
This guide covers what erase deletes, what it keeps, how to wire it into account deletion in TypeScript and Python, how to remove one connection, and what is still yours to handle. It describes what the call does. Whether that meets an obligation you have under a particular law is for you and your counsel to decide.
Why deletion is harder for an agent
A user who connected HubSpot to your agent left three things behind: a live OAuth grant in HubSpot, a token in whatever stores it, and a trail of actions under their id. Deleting your users row removes none of them. The grant still works until someone revokes it, and the token still decrypts.
What erase does
import { Client } from '@anlyonhq/sdk';
const anlyon = new Client({ apiKey: process.env.ANLYON_API_KEY! });
const { data } = await anlyon.endUsers.erase('user_42');
// { connectionsDeleted: 2, approvalsCancelled: 1, countersCleared: 3, erasedAs: 'erased~5c0f...' }
| Field | What it counts |
|---|---|
connectionsDeleted | Connections revoked at the provider where it supports that, then deleted |
approvalsCancelled | Pending approvals cancelled. Their calls were never sent |
countersCleared | Settled per-user limit counters cleared |
erasedAs | The keyed one-way hash the kept records now carry in place of the id |
The key needs the end-users:write scope, which no key gets by default.
What is deleted and what is kept
| Record | What happens |
|---|---|
| OAuth connections | Revoked at the provider, then deleted with their encrypted tokens |
| API-key connections | Deleted with the encrypted key |
| Pending approvals | Denied by the system with the note "Cancelled: the end user was erased." The parked call ends denied with decision code END_USER_ERASED, and nothing is sent |
| Per-user limit counters | Settled counters deleted. A period still holding a reservation keeps its row until the effect behind it settles, and stops applying to anyone |
| Browser session tokens | Tokens from createSession issued before the erasure are refused |
| Invocations, receipts, previews, approvals, function runs, audit events | Kept, with the user id replaced by erasedAs |
| Audit records already in the archive | Exported under erasedAs, rewritten as they are read |
The erasure itself is recorded as an end_user.erased audit record and event, under the hash.
The hash is an HMAC keyed with a server secret and scoped to your workspace. An id that is easy to guess, such as an email address, cannot be recovered by hashing candidates, and an erased id cannot collide with a live one. It is not reversible by you or by Anlyon's API, and endUsers.list no longer shows the user.
If the same id signs up again later, it starts as a new end user. Records made after the erasure carry the id as normal.
Revocation at each provider
Revocation is what turns the grant off in the user's own account. Anlyon revokes the way each provider expects:
| Provider | What Anlyon revokes |
|---|---|
| HubSpot | The refresh token, at HubSpot's token revoke endpoint |
| Slack | The access token, with auth.revoke |
| Linear | The refresh token, at Linear's revoke endpoint |
| GitHub | That one token, by DELETE on the application token endpoint, so other grants the person gave the app stay |
| Notion | The access token, at Notion's revoke endpoint |
| API-key integrations (Attio, Asana, Jira, Stripe and the rest) | Nothing at the provider. The user created the key, so Anlyon deletes its copy |
Revocation is best effort. If the provider does not answer, the token is deleted anyway, so it can no longer be used through Anlyon. A connection already marked needs_reconnect is deleted without a revocation call, because the provider has already refused it.
Wire it into your account deletion flow
Erase with Anlyon before you delete your own user row, because the user id is what names them.
// The user asked to delete their account.
export async function deleteAccount(userId: string) {
// One client per environment the user was active in: erase is scoped to the key's environment.
const environments = [
new Client({ apiKey: process.env.ANLYON_PRODUCTION_KEY! }),
new Client({ apiKey: process.env.ANLYON_STAGING_KEY! }),
];
for (const anlyon of environments) {
const { data } = await anlyon.endUsers.erase(userId);
await recordDeletionStep('anlyon', {
erasedAs: data!.erasedAs, // store the hash, not next to the user id
connectionsDeleted: data!.connectionsDeleted,
approvalsCancelled: data!.approvalsCancelled,
});
}
await db.users.delete(userId);
}
Run it from a job that retries on failure. Calling erase again for the same id is safe: a second run finds no connection or approval left, and returns the same erasedAs.
The same flow in Python:
import os
from anlyon import Client
ENVIRONMENTS = [
Client(api_key=os.environ["ANLYON_PRODUCTION_KEY"]),
Client(api_key=os.environ["ANLYON_STAGING_KEY"]),
]
def delete_account(user_id: str) -> None:
for anlyon in ENVIRONMENTS:
result = anlyon.end_users.erase(user_id).data
record_deletion_step(
"anlyon",
erased_as=result["erasedAs"],
connections_deleted=result["connectionsDeleted"],
approvals_cancelled=result["approvalsCancelled"],
)
db.users.delete(user_id)
If you keep a deletion log, decide whether storing erasedAs there is right for you. It lets you find the kept records later, which is useful for an audit and is a link back to the person if the log also names them.
Delete one connection, not the user
When a user disconnects HubSpot in your settings page but keeps their account, remove that one connection:
import { NotFoundError } from '@anlyonhq/sdk';
try {
await anlyon.endUsers.deleteConnection(user.id, 'hubspot'); // revoked at HubSpot, then deleted
} catch (error) {
if (!(error instanceof NotFoundError)) throw error; // 404: no HubSpot connection for this user
}
anlyon.end_users.delete_connection(user.id, "hubspot")
The user's history, approvals and limit counters stay as they were. Their next HubSpot call answers 409 END_USER_NOT_CONNECTED with a fresh connect link, and nothing is sent. The connection flow is covered in OAuth for AI agents.
What you still own
Erase covers what Anlyon holds. These are yours:
| Where the data is | Who handles it |
|---|---|
| Your own database, logs and traces, including any tool-call logging that recorded inputs | You |
| Records you kept from Anlyon webhooks, which named the user before the erasure | You |
| Notes, deals, messages and issues the agent already wrote into HubSpot, Slack, Linear, GitHub or Notion | The user's account at that provider. Erase does not delete them |
| An API key the user created at a provider | The user, who can delete it in the provider's settings |
| Prompts and outputs sent to your model provider | Your model provider's retention terms |
| The content of kept Anlyon records | Erase replaces the user id. The request a call sent, its target, the provider's response and an approval's title and payload keep what they held |
That last row matters if your agent passed personal data as action input, such as a contact's email in a hubspot-create-contact call. The receipt keeps the request it sent, under the hash. If that content must go too, keep personal data out of action inputs where you can, and pass your own record ids instead.
Limits
- Erase is per environment. Call it with each environment's key the user was active in.
- Revocation at the provider is best effort. The token is deleted either way.
- API-key connections have nothing to revoke at the provider. The key keeps working there until the user deletes it.
- Kept records keep their content, with the user id replaced.
- Erase does not make your product compliant with any law. It is one step in your deletion process, and it tells you exactly what it removed.
To find what to expect before you erase, run the per-user lookup in how to see what your AI agent did for one user.
Next step: add endUsers.erase to your account deletion job and run it against a test user in staging, then check the console's Users page no longer lists them. The act for your users docs guide has the full end-user reference.
Frequently asked questions
- How do I revoke a user's OAuth tokens when they delete their account?
- Call the provider's revocation endpoint for each grant, then delete the token from your database. With Anlyon, anlyon.endUsers.erase(userId) revokes the user's HubSpot, Slack, Linear, GitHub and Notion grants at the provider and deletes every connection in one call.
- Do I have to delete an AI agent's action logs when a user asks to be erased?
- That depends on your legal basis and retention policy, which is a question for your counsel. What Anlyon does is keep invocations, receipts, approvals and audit records with the user's id replaced by a keyed one-way hash, so the history of what the agent did stays without naming the person.
- What does Anlyon's erase return?
- It returns connectionsDeleted, approvalsCancelled, countersCleared and erasedAs. erasedAs is the keyed one-way hash the kept records now carry in place of the user's id.
- Does erasing a user in Anlyon delete data the agent wrote into HubSpot or Slack?
- No. A note the agent added in HubSpot or a message it posted in Slack belongs to the user's account at that provider. Erase revokes Anlyon's access to that account; it does not delete what is already there.
