Skip to main content
GitHub

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.

ParameterTypeDescription
session_idstringDelete all data for this session
agent_idstringDelete all data for this agent
trace_idsstring[]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 with mutations_sync=1, so they are not queued behind a background merge
  • On 202, only the stages already reported done have run; stores_outstanding names 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 warnings and complete is set to false
  • For compliance purposes, an erasure is effective when it reports 200 with complete: true, or when its job reaches succeeded. A 202, a partial, or complete: false means 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."
}
StatusMeaning
200Erasure completed in-request — check complete before treating it as final
202Erasure accepted and still running — poll GET /api/v1/data/erasure-jobs/{job_id}
422No identifiers provided (validation failure)
401Invalid or missing API key
403Caller lacks the admin role
500Deletion failed — safe to retry

What Gets Deleted

IdentifierSpansSessionsTraces
session_idAll spans in sessionThe session recordAll traces in session
agent_idAll spans by agentAll traces with agent
trace_idsAll spans in tracesThe 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:

DataRetention
Traces, spans, sessions, agent spans, agent messages90 days
Prompt/completion content90 days — it is stored on the span itself
Evaluations365 days
Scorer results365 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.