Skip to main content

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.

warning

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​

NameTypeDescriptionRequired
everythingbooleanExpunge 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 destroy lifecycle 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