Manage Sophos Email quarantine through the API
The Email Quarantine API processes messages that Sophos Email moved to the ordinary email security quarantine before delivery. The safe workflow is to search narrowly, evaluate every page, record the X-Sophos-Email-ID and recipient, inspect the content, perform a small-scope change, and check the result for each recipient.
This article uses only paths below /email/v1/quarantine. The similarly named Post-Delivery Quarantine concerns messages that were already delivered and then recalled, and it has a separate endpoint family. Do not mix its paths, IDs, or states with this workflow.
Prepare prerequisites and variables
First complete Getting started with the Sophos Email Management API. The planned guide Authenticate the Sophos Email API and route requests to a tenant covers service principals, tenant resolution, and minimum permissions in more depth. The following values must already have been established securely:
SOPHOS_ACCESS_TOKEN: a short-lived OAuth2 bearer token;SOPHOS_TENANT_ID: the UUID of the target tenant;SOPHOS_API_HOST: that tenant’s regional API host;- an Email Quarantine permission assigned to the service principal.
EMAIL_API_HOST="${SOPHOS_API_HOST%/}/email/v1"
Tokens, download URLs, ZIP passwords, MIME headers, and message text are secrets or personal data. They must not appear in shell history, process arguments, CI output, or tickets. The JSON blocks below are deliberately documentation examples; production clients write payloads to protected temporary files or pass them through standard input.
The contracts described here were checked against Email Management API v1.4.0. Before implementing them, check the method, path, schema, permission, and limits again in the current specification.
Search quarantine narrowly and paginate all results
POST /quarantine/messages/search requires beginDate and endDate as ISO 8601 timestamps. Filters are optional. The following example restricts an inbound malware case to one recipient and requests 100 records, the documented maximum per search page:
{
"beginDate": "2026-09-14T00:00:00.000Z",
"endDate": "2026-09-15T00:00:00.000Z",
"pageSize": 100,
"sort": ["quarantinedAt:DESC"],
"filter": {
"direction": "inbound",
"toContains": "analyst@example.test",
"reason": ["malware"]
}
}
The filter contract includes id, fromContains, toContains, subjectContains, attachmentNameContains, hasAnyAttachment, sizeInMBGreaterThan, sizeInMBLowerThan, direction (inbound or outbound), productType (mailflow, gateway, or ems), and reason. The documented sort fields are from, forRecipient, and quarantinedAt. You can request a partial response with fields; a client must not interpret omitted fields as empty values.
The response contains items and pages. For the next page, copy pages.nextKey unchanged as a string into pageFromKey in the next POST body. Stop only when no nextKey is present, and additionally protect the loop with limits on page count and runtime plus detection of repeated keys. pages.size describes only the current page. A full or empty first page therefore proves neither completeness nor the absence of further matches.
Record at least id, forRecipient, reason, quarantinedAt, direction, and the intended operation for every match. id is the UUID from the X-Sophos-Email-ID MIME header; mimeMessageId is the ordinary Message-ID header and is not a substitute in an API path or bulk body. The same message can have recipient-specific results.
Inspect the message, URLs, and attachments
All three read operations use the URL-encoded id in the path:
GET /quarantine/messages/{id}/preview
GET /quarantine/messages/{id}/urls?pageSize=50&page=1
GET /quarantine/messages/{id}/attachments?pageSize=50&page=1
The preview returns headers, htmlBody, and textBody. Treat HTML as hostile content: do not render it directly in a browser or administration portal, do not load external resources, and do not open embedded links. Prefer text and selected headers for analysis.
URL and attachment lists use numbered pages beginning at 1. The default pageSize is 50; the response reports the permitted maximum in pages.maxSize and, according to the checked schema, can contain up to 500 elements. Iterate until pages.current reaches the last reported page or an empty page follows. Do not use the search endpoint’s key model (nextKey) here.
A URL record is a finding, not a security verdict. An attachment record contains name, sizeInBytes, fileType, and stripped. Actions address attachments by their documented name, not by an invented attachment UUID. If names are duplicated or the list is unexpected, do not guess; resolve the case manually.
Download, strip, or reattach attachments
A download is asynchronous and has two stages. First start a job for one message. If attachments is omitted, the job includes every attachment; an explicit list is safer for a controlled case:
POST /quarantine/messages/{id}/attachments/download
{
"attachments": ["sample.zip"]
}
The response contains at least the job id and status. This job ID becomes the downloadId in the status path and must not be confused with the message ID:
GET /quarantine/downloads/{downloadId}/status
Poll with bounded backoff until status is completed or failed. For completed, attachments, a signed url, and a password for the protected ZIP may appear. The URL is short-lived access material: download it promptly only into an isolated, approved analysis environment, never log it, and use the job status or a new authorized download job if it expires. For failed, evaluate error; the client must not start an endless series of new jobs.
Strip and reattach change recipient-specific attachment state:
POST /quarantine/messages/{id}/attachments/strip
POST /quarantine/messages/{id}/attachments/reattach
{
"attachments": ["sample.zip"],
"forRecipients": ["analyst@example.test"]
}
forRecipients limits the change. Without this restriction, the action can affect other recipients of the message. Always divide the response into successful items and failed errors. Then retrieve the attachment list again and check stripped for the intended name. Reattaching is not releasing the message and does not prove that the attachment is safe.
Release or delete messages in a controlled manner
Release and delete accept at most 50 records per request. Every record requires the X-Sophos-Email-ID; forRecipients is optional, but it is the safe restriction for a recipient-specific operation.
POST /quarantine/messages/release
POST /quarantine/messages/delete
A narrowly scoped release looks like this:
{
"items": [
{
"id": "11111111-1111-4111-8111-111111111111",
"forRecipients": ["analyst@example.test"],
"stripAttachments": ["sample.zip"]
}
],
"allowSender": false,
"enforceSenderAuthentication": false,
"submitMessageToLabs": false
}
Release returns HTTP 202: the message was accepted and queued for release, not proven to have been delivered. allowSender and enforceSenderAuthentication remain false by default. Allow a sender only after a separate security approval; if allowSender is deliberately enabled, sender authentication should not inadvertently remain disabled. submitMessageToLabs applies only to spam and virus messages. stripAttachments operates per release record.
Request deletion separately and without an implicit sender block:
{
"items": [
{
"id": "11111111-1111-4111-8111-111111111111",
"forRecipients": ["analyst@example.test"]
}
],
"blockSender": false
}
Delete returns HTTP 200 on success. blockSender is an additional, broader policy change and remains false without an approved blocking request. Do not blindly retry release or delete after a timeout: the server may already have accepted the first request.
Both bulk responses can contain items and errors at the same time. HTTP success therefore does not mean that every ID/recipient pair succeeded. Match every requested pair exactly once against both arrays; missing, duplicate, or contradictory results must block an automated success report.
Verify the result
Perform these checks for every change:
- Log the request ID or correlation ID, HTTP status, time, tenant, and a redacted list of ID/recipient pairs; do not store content or secrets.
- For strip/reattach, list the attachments again and check
strippedfor each name. - For release, evaluate all
itemsanderrors, then repeat the same quarantine search and check actual delivery through the designated operational evidence.202alone is insufficient. - For delete, evaluate all
itemsanderrorsand repeat the same search. Treat a missing record as evidence only together with the positive bulk response, because time windows and filters can also hide matches. - For downloads, match the
downloadId, final status, and requested filenames; remove the URL and password from memory and temporary storage after completion.
Resolve partial failures, expired jobs, and permissions
- Part of a bulk action fails: Evaluate
errorsbyid,recipient, anderror. Do not resend successful pairs. Process only error pairs that still exist and have been approved again, using a new request. 400 Bad Request: Check JSON types, required fields, ISO timestamps, UUID, enum values, pagination model, and the respective limits of 50 or 100.pageFromKeyis a string, not a page number.401or403: Renew the token through the normal OAuth2 flow, or check the service-principal assignment, Email Quarantine permission, and tenant. Do not switch to another tenant or host.404or invalid ID: Ensure that the UUID comes fromX-Sophos-Email-ID, still exists in ordinary quarantine, and belongs to the tenant. Do not use amimeMessageId,downloadId, or Post-Delivery ID.- Download remains
processing: Bound the polling interval and total duration, check the job ID and tenant, and escalate with the request/correlation ID. Do not continually start parallel jobs. - Download is
failedor the URL has expired: Preserveerror, check the status once more, and start a new download only if the request remains valid. Do not modify or reuse a signed URL. - Attachment is missing or its state differs: Read every numbered attachment page, compare names exactly, and check recipient-specific
errors. Stop if names collide or the message has already been released. - Release disappears from the list but does not arrive: The release was only queued. Check the recipient, repeat the filtering, and inspect downstream delivery evidence; do not automatically release it again.
An escalation should include the API version, regional host without credentials, tenant ID, UTC time window, method and path, HTTP status, request/correlation ID, and redacted object IDs and error codes. Exclude tokens, signed URLs, ZIP passwords, message text, and attachments.