Skip to content

Mailspace logs & recovery

These endpoints answer "what happened to my mail?" and "can I get it back?" for one mailspace: the delivery logs, the archive of deleted messages that are still restorable, and the mailboxes whose mail the mail server is still holding after a permanent delete. Buying, inspecting, resizing and deleting the mailspace itself is covered by the plan-level endpoints on Mailspace.

Every path on this page begins /api/mailspace/:mailspace_id/…, where :mailspace_id is the mailspace GUID from GET /api/mailspace.

OAuth scopes: reads require mailspace:read, writes require mailspace:write. Session and API-key credentials bypass scope checks entirely (see OAuth). An OAuth token that lacks the required scope is refused with 403 {"error":"insufficient_scope", ...} — the OAuth error envelope, not the {"errors":[...],"code":"..."} envelope every other failure on this page uses. See Scope Enforcement Errors.

Everything on this page is read live from the mail server — no local table backs the delivery logs or the archive — so each call costs one or more round trips to it. Nothing on this page is paginated: page and per_page are not read anywhere here. The normal API rate limit applies, so poll on a human timescale rather than in a loop.

An empty result is not evidence that nothing is wrong

Three separate mechanisms on this page turn a missing capability or a failed upstream call into an empty list rather than an error, and none of them is reported to the client:

  • delivery tracing is an Enterprise feature of the mail server; where it is not licensed the trace lists are always empty while the outbound queue still carries data — see Delivery Logs;
  • archiving of deleted mail is likewise Enterprise-gated, and where it is unavailable the archive reads as empty — see Deleted Mail Archive;
  • an unreachable or erroring mail server degrades the delivery-log arrays and the archive listing to empty instead of a 5xx, because those readers swallow the failure and log it internally. Only the recoverable-mailbox listing answers 503 here — see Read failures.

There is no capability probe. This API cannot tell you which tier the mail server runs, and it does not guess. Never render "no delivery problems" or "nothing to recover" from an empty response alone.

Request Guards

Every endpoint on this page inherits the same gate chain, applied in this order before the action runs. The first gate that fails answers the request.

Applies to Condition Response
all mail hosting is not configured on the platform 503 stalwart_unavailable
all :mailspace_id unknown, or not visible to your credential 404, empty body
writes user has no edit permission on the mailspace's own workspace 403 not_authorized
all mailspace is on hold (staff block or unpaid-invoice hold) 403 mailspace_suspended
writes mailspace is soft-deleted (pending deletion) 403 pending_delete
all mailspace is not provisioned yet 409 not_provisioned

A write is decided by the HTTP verb, not the endpoint

The two write gates decide on the request method: GET and HEAD pass straight through, everything else is gated. So on this page Restore an Archived Item is a write because it is a POST, and Download an Archived Message is a read because it is a GET — even though the download hands back a complete message.

A view-only member holding a mailspace:write token can therefore read the logs, browse the archive, download messages and list recoverable mailboxes, and gets 403 not_authorized on every restore and delete: the token's scope is satisfied, the member's permission is not.

HEAD is treated exactly like GET, so probing a read endpoint with HEAD returns the same status the GET would.

Reads survive the retention window; writes do not

A soft-deleted (pending-deletion) mailspace keeps answering reads for its whole retention window — you can still read its logs, list its archived mail and list its recoverable mailboxes. Every mutation is refused with 403 pending_delete until the mailspace is restored. See Delete a Mailspace.

A held mailspace behaves differently: mailspace_suspended blocks reads as well as writes. A mailspace that is pending deletion is exempt from that gate, so the two codes never both apply.

An unknown or out-of-scope :mailspace_id answers 404 with an empty body, not the usual error envelope — a GUID belonging to another workspace has to be indistinguishable from one that does not exist.


Delivery Logs

GET /api/mailspace/:mailspace_id/logs

Scope mailspace:read.

Everything the mail server knows about this mailspace's recent mail, in four separate arrays: incoming and outgoing delivery traces, the live outbound queue, and issues — the derived list of deliveries that failed or are still being retried.

This endpoint takes no parameters at all: no filters, no date range, no pagination. The reader caps what it fetches at the 100 most recent entries per dataset — at most 100 traces, which are then split across incoming and outgoing (internal mail counts in both), at most 100 queue entries, and at most 100 issues — and the mail server has nothing to page through. Internally one call queries traces and the queue once per domain the mailspace hosts, so it is several round trips: this is not an endpoint to poll tightly.

The four arrays do not have the same availability — read this before building on them

They come from different mail-server objects, and that is exactly why they are returned under separate keys instead of merged into one list:

  • incoming / outgoing come from the mail server's delivery tracing, which is an Enterprise-only feature. Where it is not licensed, both arrays are always empty, however much mail flowed.
  • queue comes from the outbound queue, which exists on every build. It carries data regardless of licensing.
  • issues is the union of the two — permanent failures come from the traces, still-retrying messages come from the queue — so it is partially degraded wherever tracing is unavailable: retrying messages still show up, completed bounces do not.

Kept apart, a client can see the degradation: queue populated while both trace arrays are empty means tracing is unavailable on this deployment, not that no mail moved. Merged, "Enterprise not licensed" would have been indistinguishable from "no mail problems".

A client must not read empty trace arrays as "no mail problems", and there is no flag to check instead — the API is not told which tier the mail server is on, so it cannot tell you. If your integration needs to distinguish the two, compare the trace arrays against queue and treat "traces permanently empty" as a property of the deployment.

Internal mail is listed twice, on purpose

A message from one mailbox to another inside the same mailspace is genuinely both directions: it drains through the local queue, so it classifies as incoming, while somebody in the mailspace did send it. It is therefore listed under both incoming and outgoing, carrying internal: true so you can badge it rather than double-count it. Without that, a customer looking for a message they sent a colleague would never find it under Outgoing.

Returned Params
  • incoming: Array<Object> | traces for mail delivered to this mailspace's mailboxes
  • outgoing: Array<Object> | traces for delivery attempts to other mail servers, plus every internal message
  • queue: Array<Object> | messages still sitting in the outbound queue
  • issues: Array<Object> | permanent failures (from traces) and still-retrying messages (from the queue)

Each entry in incoming and outgoing is a trace:

  • id: String | null — the mail server's trace id, passed straight through with no guard, so a trace the mail server recorded without one arrives as null. Do not key or de-duplicate the trace arrays on it unguarded — the non-null guarantee on queue and issues below does not extend here
  • timestamp: String | ISO 8601, as the mail server recorded it
  • from: String | the envelope sender, as the mail server recorded it
  • to: String | recipients as a comma-separated string, not an array. On mail from a sender outside this mailspace, co-recipients on domains this mailspace does not host are removed before you see it
  • subject: String | null if the mail server recorded none
  • size: Integer | null — bytes
  • direction: String | incoming or outgoing
  • internal: Boolean | true for mailbox-to-mailbox mail inside this mailspace (see above)
  • status: String | derived from the SMTP log: delivered, bounced, failed, retrying, or sending
  • events: Array<Object> | the SMTP log, in the order the mail server recorded it
    • name: String | the mail-server event name, e.g. delivery.attempt-start
    • timestamp: String | ISO 8601
    • key_values: Array<Object> | the event's detail fields, as a list rather than an object because the same key can legitimately repeat within one event
      • key: String
      • value: String, Integer or Boolean | passed through from the mail server

Each entry in queue and issues is a queued message:

  • id: String | the mail server's own queue id on a queue entry, and a stable synthetic trace-<…> id on an issue derived from a trace. Never null on either of these two arrays — that guarantee is specific to them and does not hold for a trace's own id. See the warning below
  • source: String | queue or trace — which dataset the entry came out of. Present on every entry of both queue and issues; the trace arrays do not carry it. See the warning below for what it is for
  • created_at: String | ISO 8601
  • return_path: String | the envelope sender. Empty means a null return path (<>): the message is a bounce notification
  • subject: String | null
  • size: Integer | null — bytes
  • flags: Array<String> | mail-server flags, e.g. dsnSent once a bounce notification has been sent
  • recipients: Array<Object>
    • address: String
    • status: String | Completed, TemporaryFailure or PermanentFailure; null while the recipient is still queued and nothing has been attempted
    • retry_due: String | null — ISO 8601, when the next attempt is due
    • retry_count: Integer | null
    • error: Object | null when the mail server reported no error detail for that recipient
      • type: String | null — the mail server's error type
      • response_code: Integer | null — the SMTP status code. Passed straight through, so a trace-derived issue carries whatever the SMTP log recorded and may hand back a String
      • response_enhanced: String | null — the enhanced status code, e.g. 5.1.1
      • response_hostname: String | null — the server that answered
      • message: String | null — the remote server's text
  • events: Array<Object> | the SMTP log in the same shape as a trace's. Empty for a live queue entry, which has no trace yet; populated only on an issue that came from a trace

Derive the message-level status yourself

A queued message has no top-level status field — deliberately, so the rule lives in one place instead of being duplicated. Derive it from recipients and flags:

  1. any recipient PermanentFailurebounced if flags includes dsnSent, otherwise failed;
  2. else any recipient TemporaryFailureretrying;
  3. else all recipients Completeddelivered;
  4. else → sending.

An issues entry is not a queue entry, even though it has the same shape

issues is rendered in the queued-message shape whichever source it came from, which makes it uniform to display but easy to over-read:

  • An issue derived from a trace (a completed bounce or rejection) has no queue id to carry, so it carries a stable synthetic trace-<…> id instead — the same value on every poll, so a client may key or de-duplicate on it. Its recipients array holds a single synthetic entry whose address is the trace's whole comma-separated recipient string, with status PermanentFailure, retry_due and retry_count null, and error.type null. Its flags is ["dsnSent"] when a bounce notification was sent and [] when the message failed without one, and it carries the full SMTP log in events.
  • An issue derived from the queue is a real queue entry, listed with only its failing recipients — so its recipients array is a subset of the one under queue — and its events is empty.

source tells the two apart without inspecting them. Every entry of queue and of issues carries it: "queue" for a row that came out of the live outbound queue, "trace" for one derived from a trace. It says whether the row can still change — a queue entry is still being retried, while a trace-derived issue is terminal, its bounce notification already sent and the message already flushed from the queue — and it is the only way to tell which entries carry an events log. The trace arrays (incoming and outgoing) do not carry it: only the queue-shaped rows are stamped.

Permanent failures only ever come from traces and retrying messages only ever come from the queue, so the two sources never double-count the same message. That also means completed bounces are among the things that disappear where tracing is unlicensed.

curl -H "Authorization: Bearer $CLOUDPRESS_TOKEN" \
  -H "X-Auth-Account: $ACCOUNT_ID" \
  https://my.cloudpress.com/api/mailspace/$MAILSPACE_ID/logs

Deleted Mail Archive

Mail a user deleted that the mail server is still holding. An item lands here when a message is deleted and stays until its archived_until deadline, after which the mail server purges it for good. The retention window is server-wide and cannot be extended or shortened through this API.

An empty archive means one of two things, not three

The archive is an Enterprise-only object on the mail server, and the listing behind it is strict: a query that could not be performed answers 503 archived_items_unavailable. So List Archived Mail answers 200 with an empty archived_items array in two situations, and nothing in the response tells them apart:

  • nothing is archived;
  • the mail server does not license archiving. There is no capability probe, so "not licensed" and "nothing archived" are deliberately indistinguishable — and every by-id endpoint then answers 404 unknown_archived_item.

"The mail server could not be asked" is no longer one of them: that is the 503. The reason the licence gate still degrades to an empty list is that the mail server answers a request for a licensed-only object, and an answer is what the strict read is checking for; a server that gives no answer is the 503. So an unreachable mail server is distinguishable from an empty archive. An unlicensed one is not.

Treat an empty archive as "nothing to show", never as "nothing was ever deleted".

Item ids are opaque, and they are global at the mail server

An archived item is addressed by the :stalwart_id segment, which is the opaque id from the list — not a GUID and not anything you can construct. At the mail server it is an unscoped handle, so CloudPress re-resolves every id against this mailspace's own mailboxes on every by-id request and answers 404 unknown_archived_item when it does not belong here. An item id from another workspace is unreachable on all four by-id endpoints, download included.

Branch on code, never on errors[0]

All three actions that can fail against the archive — List Archived Mail, Restore an Archived Item and Delete an Archived Item — answer a failure with a curated English sentence and never echo the mail server's own error text, which is upstream internals a client cannot act on.

Those messages are English whatever Accept-Language you send, even though the header still sets the locale for the rest of the request, and their wording is free to change. The code is the contract.

List Archived Mail

GET /api/mailspace/:mailspace_id/archived_items

Scope mailspace:read.

Every restorable deleted message across every mailbox in the mailspace, newest-archived first. The archive is queried per mailbox and filtered to this mailspace's own mailboxes, so it can never surface another tenant's deleted mail. Unpaginated: CloudPress caps the query at 500 items per mailbox, and anything past that is dropped without a marker in the response.

Returned Params
  • archived_items: Array
    • id: String | the opaque item id — this is the :stalwart_id segment for every by-id endpoint below
    • type: String | the item variant. Email in practice; the mail server declares others but never writes them
    • status: String | archived, or requestRestore once a restore has been queued for it. The mail server omits the field while it holds its default, so it is filled in as archived
    • subject: String | null
    • from: String | null
    • size: Integer | null — bytes
    • received_at: String | null — ISO 8601, when the message originally arrived
    • archived_at: String | null — ISO 8601, when it was deleted into the archive
    • archived_until: String | null — ISO 8601, the permanent-deletion deadline. Server-wide retention, not per item
    • mailbox_email: String | the address the message was deleted from
    • mailbox_id: String | that mailbox's GUID

The item's mail-server blob and account handles are deliberately not emitted: both are global, unscoped handles, and the message itself comes back from Download an Archived Message.

Errors
  • 503 archived_items_unavailable | the archive query could not be performed. Not a client error, so it is retriable — see Read failures. An unlicensed archive still degrades to the empty 200 described above; an unreachable mail server does not
  • plus the shared guards

View an Archived Item

GET /api/mailspace/:mailspace_id/archived_items/:stalwart_id

Scope mailspace:read. One item, in the same field set as the list, under an archived_item object.

The message body is not returned here — use Download an Archived Message for the original message.

Errors
  • 404 unknown_archived_item | no such item, or it does not belong to this mailspace
  • 503 archived_item_lookup_unavailable | the ownership lookup could not be performed, so the item is neither confirmed nor denied. Retriable, and not a sign the item is gone
  • plus the shared guards

Restore an Archived Item

POST /api/mailspace/:mailspace_id/archived_items/:stalwart_id/restore

Scope mailspace:write (it is a POST, so it is gated as a write — see above).

This reports a request, not a finished restore

The mail server has no synchronous restore. The trigger is flipping the item's status to a restore request, which the server then processes on its own schedule, and it emits no completion signal — so the response says restore_queued and never restored, and this API has nothing to poll.

To find out whether a restore happened, re-read the archive: an item that has been restored is no longer listed, and the message returns to the mailbox under a new id. Do not report a restore as complete on the strength of this response.

Returned Params
  • status: String | always "restore_queued"
  • archived_item: Object | the item's fields, exactly as in the list

Two different status fields

The top-level status describes your request (restore_queued). The status inside archived_item is the item's own field, as it stood when the item was looked up — so a first restore shows archived here even though the request was accepted, and a second one on the same item shows requestRestore. Re-read the item rather than reading this field as the outcome.

Errors
  • 404 unknown_archived_item | no such item, or it does not belong to this mailspace. Also what you get if the item disappears between the lookup and the restore itself — see the note below
  • 422 restore_failed | the mail server answered and refused the restore. Nothing was queued
  • 503 archived_item_lookup_unavailable | the ownership lookup could not be performed, so nothing was sent to the mail server. The item is untouched
  • 503 restore_unconfirmed | indeterminate. The restore instruction went out and its fate is unknown, so the restore may already be queued
  • plus the shared guards, including 403 not_authorized and 403 pending_delete

Two 503s that promise opposite things

archived_item_lookup_unavailable means nothing was attempted. restore_unconfirmed means something was attempted and the outcome is unknown — so re-read the item, or the archive, before retrying rather than assuming the restore failed.

Note this is the opposite promise from restore_unavailable on the purged-mailbox restore, which means nothing happened. Do not generalise across the two surfaces from the word "unavailable" — read the code.

The vanished-item race answers 404, not 422 — a deliberate wire-contract change

The item id is resolved twice: once by the shared guard before the action runs, and again inside the restore or the erase itself. An item that disappears between the two — the realistic case is a client retrying its own timed-out request — now answers 404 unknown_archived_item, the same code the guard's own miss returns, because it is the same fact.

It previously answered 422 restore_failed, and 422 delete_failed on Delete an Archived Item. This was changed deliberately while these endpoints are not yet live in production: a client that branches on 422 for a vanished item will not see it any more. restore_failed and delete_failed keep their 422 for a genuine refusal from the mail server.

Download an Archived Message

GET /api/mailspace/:mailspace_id/archived_items/:stalwart_id/download

Scope mailspace:read — a GET, so it is a read for both the scope and the write gates, even though it hands back a whole message.

This is the one endpoint here that does not return JSON

A successful download is the original RFC822 message as raw bytes: Content-Type: message/rfc822 with an attachment disposition. It is not base64 inside a JSON envelope — that would inflate the payload and force every client to decode it. The bytes are proxied through CloudPress rather than linked straight to the mail server, because the mail server's own download URL is not scoped to your mailspace.

Failures are still JSON, in the usual {"errors":[...],"code":"..."} envelope, so branch on the response status before treating the body as a message.

The attachment filename is derived from the message's own stored subject, then sanitised so a hostile subject cannot steer the saved file or break out of the header: every character outside letters, digits, underscores, spaces, hyphens and dots becomes _, runs of _ collapse to one, a leading dot is dropped, and surrounding whitespace is trimmed. If that leaves nothing — the message carried no subject at all, or sanitising emptied the stem — the item id is used instead, so the filename is never a bare .eml. Whichever of the two is used is then capped at 80 characters before .eml is appended.

The filename does not vary with Accept-Language. It comes from the subject the mail server stored, never from a translated "no subject" placeholder, so the same item downloads under the same name for every client.

curl -o message.eml \
  -H "Authorization: Bearer $CLOUDPRESS_TOKEN" \
  -H "X-Auth-Account: $ACCOUNT_ID" \
  https://my.cloudpress.com/api/mailspace/$MAILSPACE_ID/archived_items/$ITEM_ID/download
Errors
  • 404 unknown_archived_item | no such item, or it does not belong to this mailspace
  • 404 / 503 download_unavailable | the message could not be delivered, and the status is the difference: 404 when the mail server answered that the stored message is gone (terminal — stop), 503 for any other read failure (the message may still be there — retry). One code, because it is one fact about one resource; the status carries what to do next
  • 503 archived_item_lookup_unavailable | the ownership lookup could not be performed
  • plus the shared guards

Delete an Archived Item

DELETE /api/mailspace/:mailspace_id/archived_items/:stalwart_id

Scope mailspace:write.

Irreversible — there is no second archive behind this one

This erases the archived message and its stored copy ahead of the retention deadline. Nothing is recoverable afterwards and there is no confirmation parameter: the only precondition is that the item belongs to this mailspace.

Every successful erase is written to the platform audit log with the credential that made the call, after the mail server confirms it — a log line for a destruction that did not happen would be worse than none.

An item that is already gone answers 404 unknown_archived_item, not 200 — the id is resolved against the mail server before anything is deleted, so a duplicate DELETE and an item purged at its own deadline both land there. This endpoint is not idempotent from the client's point of view.

Returned Params
  • deleted: Boolean | always true
  • id: String | the item that was erased
  • subject: String | null — captured before the erase
Errors
  • 404 unknown_archived_item | no such item, or it does not belong to this mailspace. Also what you get if the item disappears between the lookup and the erase itself — see the note above
  • 422 delete_failed | the mail server answered and refused the erase. Nothing was destroyed
  • 503 archived_item_lookup_unavailable | the ownership lookup could not be performed, so nothing was sent. The item is untouched
  • 503 delete_unconfirmed | indeterminate, and this erase is irreversible: the item may already be gone
  • plus the shared guards

On delete_unconfirmed, read the item back before doing anything else

It is the one place on this surface where an unknown outcome cannot be undone, so treating it as "the delete failed" is the worst of the three available guesses. GET the item: a 404 means the erase went through.

No audit line is written on either failure. The audit line is written after the mail server confirms the erase, so its absence is not evidence the item survived.


Purged Mailboxes

Mailboxes CloudPress has already permanently deleted — the local record is gone — whose mail the mail server is still holding, because deleting a mail account does not erase anything immediately: it drops the account and schedules an erase task for the end of the server's retention window. Until that task fires, the account can be brought back with its original id.

These are deliberately not part of the mailspace's mailbox list, which omits that bucket entirely — that is why they have their own endpoint.

List Recoverable Mailboxes

GET /api/mailspace/:mailspace_id/purged_mailboxes

Scope mailspace:read.

The purged mailboxes that can still be restored, ordered by recoverable_until so the one about to be lost comes first. Unpaginated.

The list is cross-checked against the mail server, not served from local state alone: the retention window is a server-wide setting staff can shorten, and an expedited erase removes an account at once — either leaves a local record whose deadline is still in the future while the mail is already gone. A record the mail server no longer holds an erase task for is omitted entirely, because offering a restore that cannot be honoured is worse than not offering one. The cross-check costs two round trips — the erase tasks, and the domain lookup that resolves them — and none at all when there is nothing to check.

A listed mailbox is real; an empty list is not proof the mail is gone

The cross-check is a strict read, so a mail server that cannot be asked at all answers 503 purged_mailboxes_unavailable rather than an empty 200 — see Read failures. That closes one gap but not the other.

The erase tasks are read 500 rows at a time across the whole mail server and filtered down to this mailspace afterwards, and the response carries no total and no truncation flag. On a busy server this mailspace's tasks can fall outside that window, so a mailbox the mail server would still restore can be silently missing from a perfectly healthy 200.

Read the list the way it is safe to read: a record that is listed is genuinely recoverable, and an empty list means "nothing found", not "the mail is gone". Never drop your own last record of a mailbox on the strength of it — if you keep a copy of what was deleted, keep it until the record's own recoverable_until has passed.

Returned Params
  • purged_mailboxes: Array
    • id: String | the record's GUID — this is the :guid segment Restore a Purged Mailbox takes
    • email: String | the address that would come back
    • display_name: String | null
    • purged_at: DateTime | when CloudPress permanently deleted the mailbox
    • recoverable_until: DateTime | the mail server's own erase deadline, mirrored at delete time — never a locally computed window
    • days_remaining: Integer | whole days left, floored at 0

The stored quota, aliases, memberships and mail-server account id are deliberately not emitted: they are restore input, not a description of the deleted mailbox.

Errors
  • 503 purged_mailboxes_unavailable | the mail server could not be asked which accounts it is still holding, so the list cannot be answered. Retriable, and deliberately not an empty 200 — a client would read that as "nothing to recover"
  • plus the shared guards

Restore a Purged Mailbox

PATCH /api/mailspace/:mailspace_id/purged_mailboxes/:guid

Scope mailspace:write.

Cancels the mail server's pending erase and rebuilds the mailbox. The mail server reconstructs the account from the erase task alone, which carries only its name and domain, so everything else is written back from the snapshot CloudPress kept at delete time.

password is required, because a recovered mailbox comes back with no credentials

The mail server hands the account back with no password at all, so this endpoint sets one. That makes it a credential-handing write: it produces a working mailbox on an address that receives mail. A missing or blank password is refused with 400 password_blank before anything is touched, and every success is written to the platform audit log with the credential that made the call.

Params
  • password: String (required) | the password the restored mailbox comes back with. A blank or absent value is 400 password_blank

The mailbox comes back with its display name, and with its stored quota — clamped down rather than refused if the plan no longer has room for it, so a mailbox may come back slightly smaller than it was.

Its aliases are re-applied on the same terms as its memberships below: the alias write silently drops any address whose domain cannot be resolved, and still reports success. A 200 is not proof every alias came back — read the mailbox back and compare.

Group and mailing-list memberships are re-applied, but are not guaranteed

The snapshot's group and mailing-list memberships are written back where those groups and lists still exist; one that has since been deleted is skipped, and a failure while re-applying them does not fail the restore. So a 200 here does not mean the memberships came back. Read the mailbox's groups and lists afterwards and re-add what is missing.

The record is consumed on success, so a second PATCH of the same :guid answers 404 unknown_purged_mailbox.

Returned Params
  • mailbox: Object | the same shape the mailbox delete and restore endpoints return
    • id: String | the restored mailbox's GUID
    • email: String
    • status: String | active
    • scheduled_deletion_at: DateTime | null
    • days_until_deletion: Integer | null

A mailbox can stop being recoverable while it is still listed

Recoverability is the mail server's decision, and it can change between your list call and your restore. Three outcomes are possible, and only the last one is worth retrying:

  • The record's own deadline has passed → 409 not_recoverable. Nothing was touched, and this cannot be retried into success.
  • The mail server was asked, and answered, that it no longer holds an erase task for the account (retention was shortened, or the account was erased on demand) → 409 not_recoverable, and the snapshot is discarded, so the same :guid answers 404 unknown_purged_mailbox on a retry and the mailbox drops out of the list. This cannot be retried into success either — the mail is genuinely gone. It shares the code, the status and the wording with the expired-deadline case above, because it is the same fact: terminal, do not retry.
  • The mail server could not be asked — the pre-flight read failed, or came back degraded → 503 restore_unavailable, and the snapshot is left exactly where it is. Retry it.

That read is deliberately strict, and the snapshot is the only copy of the mailbox's quota, display name, aliases and memberships: a failed read must never be mistaken for "the mail server has erased it" and destroy the record of a mailbox the server would still hand back. So the snapshot is discarded on a successful read only — never on a failed one.

Branch on the status, and the three answer three different questions: 409 means nothing can be done, 422 means the mail server refused and a retry may work, 503 means the check could not be made and nothing was attempted — the one failure here that can be stated to have changed nothing. There is no need to re-read the list to work out which failure you got.

The list is ordered soonest-to-expire first for the same reason: restore in that order.

restore_failed is retriable, and it no longer carries the terminal case

503 restore_unavailable covers the pre-flight read only — the one that decides whether the mail server is still holding the account. A transport failure later in the restore, after that decision has been made, still surfaces as 422 restore_failed, so that code does not prove the mail server said no. The restore is idempotent, which is why a retry is the right move on anything that looks transient.

What restore_failed no longer covers is the one case where a retry could never work: an account the mail server has already erased answers 409 not_recoverable instead. So you no longer have to re-read the list to tell a retriable 422 from a terminal one.

download_unavailable on Download an Archived Message remains an exception to the 503 convention Read failures describes — it carries both statuses, 404 for a message the mail server says is gone and 503 for a read it could not perform.

restore_failed also covers a restore the mail server or the plan refuses:

  • the address is in use again by a new mailbox — delete or rename that one first, then restore this one;
  • the plan's mailbox limit is already full — delete another mailbox or upgrade first;
  • any other refusal from the mail server.
curl -X PATCH \
  -H "Authorization: Bearer $CLOUDPRESS_TOKEN" \
  -H "X-Auth-Account: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{"password": "a-strong-password"}' \
  https://my.cloudpress.com/api/mailspace/$MAILSPACE_ID/purged_mailboxes/$PURGED_ID
Errors
  • 404 unknown_purged_mailbox | no such record in this mailspace, or it has already been consumed
  • 409 not_recoverable | terminal — do not retry. Either the recovery window has closed, or the pre-flight read succeeded and reported that the mail server is no longer holding the account. On the second path the record is consumed on the way out, so a retry answers 404
  • 400 password_blank | no password supplied
  • 422 restore_failed | the mail server answered and refused — see above. Retriable
  • 503 restore_unavailable | the pre-flight read of the mail server's pending-erase list could not be performed, so nothing was attempted. The record is untouched — retry
  • plus the shared guards

Error Codes

All errors except the OAuth scope failure use the standard {"errors": [...], "code": "..."} envelope described in Error Responses. An unknown or out-of-scope :mailspace_id is the exception in the other direction: 404 with an empty body and no envelope at all. On Download an Archived Message the errors are JSON even though a success is not.

Shared by every endpoint on this page — see Request Guards:

Code Status Raised on
stalwart_unavailable 503 all requests
not_authorized 403 POST, PATCH, DELETE
mailspace_suspended 403 all requests
pending_delete 403 POST, PATCH, DELETE
not_provisioned 409 all requests

Per endpoint:

Code Status Raised by
archived_items_unavailable 503 list archived mail
purged_mailboxes_unavailable 503 list recoverable mailboxes
unknown_archived_item 404 view, restore, download, delete an archived item; also a restore or delete whose item vanishes between the two id lookups
archived_item_lookup_unavailable 503 view, restore, download, delete an archived item — the ownership lookup could not be performed, so nothing was sent
restore_failed 422 restore an archived item; restore a purged mailbox
restore_unconfirmed 503 restore an archived item — the instruction went out and its fate is unknown
restore_unavailable 503 restore a purged mailbox — nothing was attempted
download_unavailable 404 / 503 download an archived message — 404 the message is gone (terminal), 503 any other read failure (retriable)
delete_failed 422 delete an archived item
delete_unconfirmed 503 delete an archived item — the erase went out and may already have happened. Irreversible
unknown_purged_mailbox 404 restore a purged mailbox
not_recoverable 409 restore a purged mailbox
password_blank 400 restore a purged mailbox

The delivery-log endpoint has no failure codes of its own — only the shared guards.