> ## Documentation Index
> Fetch the complete documentation index at: https://braintrust.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Deleting logs via API using correct row IDs

export const plans_0 = "Any"

export const deployments_0 = "Any"

export const data_plane_version_0 = undefined

export const use_case_0 = "Use case - Deleting project logs programmatically via SDK or API for DSR compliance or data cleanup"

<Note>
  **Applies to:**

  * Plan - {plans_0}
  * Deployment - {deployments_0}
  * {data_plane_version_0}
  * {use_case_0}
</Note>

## Summary

**Goal:** Delete project logs programmatically and have them actually disappear from the UI and BTQL queries.

## How deletion works

Deleting a log is done by re-inserting the event with `_object_delete: true` for each target row `id`:

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
delete_events = [{"id": row_id, "_object_delete": True} for row_id in ids]
requests.post(
    f"{API_URL}/v1/project_logs/{PROJECT_ID}/insert",
    headers=headers,
    json={"events": delete_events},
)
```

The `_object_delete` marker matches on the row `id` only. It does **not** match on `span_id` or `root_span_id`. Because the insert endpoint accepts any submitted `id` and echoes it back in `row_ids`, a 200 response means Braintrust recorded a delete tombstone for the IDs you sent, even when those IDs don't correspond to any visible log row. That's why a delete keyed on `span_id` or `root_span_id` "succeeds" but changes nothing.

`id` and `span_id` are different values: `id` is the system-assigned unique row identifier, while `span_id` is an arbitrary value you can set and reuse across projects.

## Solution: resolve row IDs first, then delete

Use BTQL to look up the actual row `id`s, then submit those `id`s as delete events.

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import requests

API_URL = "https://api.braintrust.dev"  # Replace with your data plane API URL if self-hosted
PROJECT_ID = "<project id>"  # UUID, not the project name
trace_root_span_id = "<root span id of the trace to delete>"
headers = {"Authorization": "Bearer " + "sk-your-api-key"}

# Look up the row IDs for every span in the trace.
# Paginate if your traces have more than 100 spans.
btql_resp = requests.post(
    f"{API_URL}/btql",
    headers=headers,
    json={
        "query": (
            f"select id from project_logs('{PROJECT_ID}') "
            f"where root_span_id = '{trace_root_span_id}' limit 100"
        )
    },
)
span_ids = [row["id"] for row in btql_resp.json().get("data", [])]
print(f"Trace contains {len(span_ids)} spans.")

# Delete each span by its row id.
delete_resp = requests.post(
    f"{API_URL}/v1/project_logs/{PROJECT_ID}/insert",
    headers=headers,
    json={"events": [{"id": span_id, "_object_delete": True} for span_id in span_ids]},
)
deleted_rows = delete_resp.json().get("row_ids", [])
print(f"Deleted {len(deleted_rows)} spans.")
```

## Important behaviors

* **Delete the whole trace, not just the root span.** Deleting a root span deletes only that one span. The child spans remain orphaned and still show up in the UI and BTQL. To fully remove a trace, query for all `id`s under its `root_span_id` and delete each one (as shown above).
* **Deletions are eventually consistent.** Rows may briefly reappear after a delete and take a few seconds plus a page refresh to disappear. This is more noticeable right after deletion, not a sign the delete failed.
* **Use the correct API URL.** For hosted Braintrust, use `https://api.braintrust.dev`. For self-hosted or hybrid deployments, use the data plane API URL from **<Icon icon="settings-2" /> Settings** > [**<Icon icon="lock" /> Data plane**](https://www.braintrust.dev/app/~/configuration/org/api-url). Being able to insert logs confirms the URL only for writes; deletion uses the same endpoint but still requires valid row `id`s.
* **`{project_id}` must be the UUID**, not the project name (e.g., `914981cd-29bc-4c94-936e-74bd91d8bef2`). Passing the project name fails the request.

## Verify a deletion

Re-run the BTQL lookup after deleting. If the delete succeeded, the query returns no rows for those IDs (allow a few seconds for consistency):

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
check = requests.post(
    f"{API_URL}/btql",
    headers=headers,
    json={"query": f"select id from project_logs('{PROJECT_ID}') where root_span_id = '{trace_root_span_id}'"},
)
print(check.json().get("data", []))  # Should be empty
```

If rows still persist after deleting by verified row `id`s, collect the delete response `row_ids`, a before/after BTQL result for one `root_span_id`, the request timestamp, the project ID, and your SDK version, and escalate.

## Notes

* For recurring cleanup instead of one-off deletion, use a [data retention automation](https://www.braintrust.dev/docs/admin/automations/configure-data-retention) to delete logs older than a configured window.

## References

* [View your logs — Braintrust docs](https://www.braintrust.dev/docs/observe/view-logs)
* [BTQL reference](https://www.braintrust.dev/docs/reference/btql)
* [Configure data retention](https://www.braintrust.dev/docs/admin/automations/configure-data-retention)
