Resource $expunge
The $expunge operation permanently deletes a resource and its history from the database. A minimal tombstone remains in the resource's history table. Its meta.tag uses the FHIR audit lifecycle code destroy, and it contains no resource data.
This operation is irreversible. Once a resource is expunged, it cannot be recovered. Use with caution.
Use Cases
- Data Cleanup: Remove test data from development environments
- Compliance: Fulfill data deletion requests under regulations like GDPR
- Tenant Offboarding: Clean up all data when a tenant leaves the platform
- Error Correction: Remove erroneously created resources that shouldn't exist
Authorization
This operation requires admin privileges. You must be a project admin (Membership.admin = true).
Invoke the $expunge operation
Single Resource
[base]/[resourceType]/[id]/$expunge
For example, to expunge a single Patient:
curl -X POST 'https://api.medplum.com/fhir/R4/Patient/example-id/$expunge' \
-H "Content-Type: application/fhir+json" \
-H "Authorization: Bearer MY_ACCESS_TOKEN"
Parameters
| Name | Type | Description | Required |
|---|---|---|---|
everything | boolean | Expunge all resources in the compartment (for Projects only) | No |
Expunge Everything (Project Compartment)
When expunging a Project, or when everything=true, Medplum starts an async job that walks every FHIR resource type in the target compartment (currently Patient and Project compartments). For each type it searches _compartment, then hard-deletes matching rows in batches of 10,000, including associated Binary storage. The HTTP request returns 202 Accepted immediately; poll the AsyncJob for completion.
Each deleted id still gets a history tombstone, including AuditEvent rows in the compartment.
curl -X POST 'https://api.medplum.com/fhir/R4/Project/project-id/$expunge?everything=true' \
-H "Content-Type: application/fhir+json" \
-H "Authorization: Bearer MY_ACCESS_TOKEN"
Response
Successful Single Resource Expunge
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "information",
"code": "informational",
"details": {
"text": "All OK"
}
}
]
}
Async Job Started (Everything Mode)
When expunging a project or using everything=true, the operation returns immediately with a 202 Accepted status and a location header pointing to the async job:
HTTP/1.1 202 Accepted
Content-Location: https://api.medplum.com/fhir/R4/AsyncJob/job-id
You can poll the AsyncJob to check the status:
curl 'https://api.medplum.com/fhir/R4/AsyncJob/job-id' \
-H "Authorization: Bearer MY_ACCESS_TOKEN"
Access Denied
If you don't have admin privileges:
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "forbidden",
"details": {
"text": "Forbidden"
}
}
]
}
Behavior
Single Resource Expunge
- Deletes the resource and all prior history versions
- Writes a minimal history tombstone tagged with the ISO 21089
destroylifecycle code - Removes associated Binary resources referenced by the resource
- The operation is synchronous and returns immediately
Everything Mode (Project Expunge)
- Iterates through all resource types in the compartment
- Deletes resources in batches of 10,000
- Also deletes associated Binary resources
- Runs as an async job to handle large datasets
- Leaves a history tombstone for each deleted id
Related Documentation
- AsyncJob $cancel - Cancel a running expunge job
- FHIR Delete - Standard FHIR delete (creates tombstone)