Data Management
Delete user data for GDPR compliance.
Risicare provides a data deletion API for GDPR Article 17 (Right to Erasure) compliance. Use it to delete all traces and sessions associated with a data subject.
Delete by Subject
POST /v1/data/delete-by-subject
Content-Type: application/json
Authorization: Bearer rsk-...
{
"session_id": "user-session-abc",
"agent_id": "support-agent",
"trace_ids": ["trace-id-1", "trace-id-2"]
}Provide at least one identifier. All matching spans and sessions are deleted, scoped to your project.
| Parameter | Type | Description |
|---|---|---|
session_id | string | Delete all data for this session |
agent_id | string | Delete all data for this agent |
trace_ids | string[] | Delete specific traces by ID |
Requires the admin role — a viewer-role key receives 403.
Response
The response is polymorphic. You get 200 when the erasure finished inside
the request budget, and 202 when it did not — a large subject will routinely
return 202.
200 — finished:
{
"spans_deleted": 42,
"sessions_deleted": 1,
"traces_affected": 3,
"redis_entries_purged": 0,
"warnings": [],
"complete": true,
"erasure_job_id": "6f1c…",
"stores_outstanding": []
}202 — accepted, still running. A Location header points at
GET /v1/data/erasure-jobs/{job_id}, which you poll until it reaches a terminal
state:
{
"job_id": "6f1c…",
"erasure_job_id": "6f1c…",
"location": "/v1/data/erasure-jobs/6f1c…",
"status": "running",
"stores_outstanding": ["spans", "sessions"]
}warnings lists any store that could not be erased, and complete is false
whenever warnings is non-empty. stores_outstanding names the stores that
have not been erased yet.
Example: Delete a User's Session
curl -X POST https://app.risicare.ai/api/v1/data/delete-by-subject \
-H "Authorization: Bearer rsk-..." \
-H "Content-Type: application/json" \
-d '{"session_id": "user-123-session"}'Example: Delete Specific Traces
Branch on the status code — do not assume 200:
import time
import httpx
BASE = "https://app.risicare.ai"
headers = {"Authorization": "Bearer rsk-..."}
resp = httpx.post(
f"{BASE}/api/v1/data/delete-by-subject",
headers=headers,
json={"trace_ids": ["abc123", "def456"]},
)
PENDING = {"pending", "running"} # job is still owed an outcome
# Terminal states are "succeeded", "partial", "failed".
# Only "succeeded" claims a complete erasure.
if resp.status_code == 200:
body = resp.json()
print("erased:", body["spans_deleted"], "spans; complete:", body["complete"])
elif resp.status_code == 202:
# Not finished in-request. Follow the Location header the server handed
# back rather than building the URL yourself.
status_url = resp.headers.get("Location") or resp.json()["location"]
while True:
job = httpx.get(f"{BASE}{status_url}", headers=headers).json()
if job["status"] not in PENDING:
break
time.sleep(2)
if job["status"] == "succeeded":
print("erasure complete")
else:
# "partial" or "failed" — the subject's data is NOT fully erased.
print(job["status"], "- outstanding:", job.get("stores_outstanding"))
else:
resp.raise_for_status()When Is the Data Actually Gone?
A 202 means the erasure is still running
Only a 200 means the erasure finished in-request. On a 202 the work
continues in the background, and the data is not fully gone when you receive
the response — you must poll GET /v1/data/erasure-jobs/{job_id}. Treating
every response as final will under-report a partial erasure.
What this means in practice:
- On
200, the counts are accurate and the covered rows are physically removed — ClickHouse deletes run withmutations_sync=1, so they are not queued behind a background merge - On
202, only the stages already reported done have run;stores_outstandingnames the rest - Re-issuing the identical request adopts the same subject's unfinished job rather than starting a new one, so retries are safe
- A span the ingestion worker has already read into its in-memory batch is purged within roughly one flush interval rather than instantly
- If any store could not be erased, it is named in
warningsandcompleteis set tofalse - For compliance purposes, an erasure is effective when it reports
200withcomplete: true, or when its job reachessucceeded. A202, apartial, orcomplete: falsemeans the erasure is not yet finished
Error Handling
If the deletion fails (e.g., database unreachable), the API returns 500 with
an error message. A 200 means the erasure finished in-request — not that
it was queued. A 202 means it is still running and you must poll the job.
{
"detail": "Data deletion failed. Please retry. If this persists, contact support."
}| Status | Meaning |
|---|---|
200 | Erasure completed in-request — check complete before treating it as final |
202 | Erasure accepted and still running — poll GET /api/v1/data/erasure-jobs/{job_id} |
422 | No identifiers provided (validation failure) |
401 | Invalid or missing API key |
403 | Caller lacks the admin role |
500 | Deletion failed — safe to retry |
What Gets Deleted
| Identifier | Spans | Sessions | Traces |
|---|---|---|---|
session_id | All spans in session | The session record | All traces in session |
agent_id | All spans by agent | — | All traces with agent |
trace_ids | All spans in traces | — | The specified traces |
All deletions are scoped to your project — you can only delete data that belongs to the API key's project.
Automatic Data Retention
In addition to on-demand deletion, Risicare automatically purges aged data with fixed ClickHouse TTLs, applied uniformly to every project regardless of plan. Retention is not a single window — it varies by data type:
| Data | Retention |
|---|---|
| Traces, spans, sessions, agent spans, agent messages | 90 days |
| Prompt/completion content | 90 days — it is stored on the span itself |
| Evaluations | 365 days |
| Scorer results | 365 days |
Content is kept as long as the span — not 30 days
A dedicated 30-day content table exists in the schema and is the source of a widely-repeated "content is deleted after 30 days" claim, including in earlier versions of this page. Nothing writes to that table. Captured prompt and completion text is stored in the span's own attributes, so it lives for the span's full 90 days. Plan for 90, not 30.
If you need prompts and completions kept for a shorter period than the
surrounding trace, there is no setting for that today — disable content
capture instead with trace_content=False.
One more thing that is easy to miss: evaluations and scorer results are kept for a full year, four times longer than the traces they were computed over; if the scored content is sensitive, that is the window that governs.
Coming Soon
Per-plan, configurable retention windows ship with the post-beta billing system. Until then the 90-day TTL is fixed and not adjustable from Project Settings.