Skip to main content
Import historical email into an existing mailbox from an MBOX file, one EML file, or a ZIP archive. Imports run asynchronously: create a job, upload the file, start the job, then poll until it reaches a terminal state. Imported messages keep their original Date header, so threads and list views sort historically. Dates that are implausible, meaning before 1990 or in the future, are clamped to the import time. Threading is rebuilt from In-Reply-To and References exactly as it is for live mail, and imported threads land in the inbox folder. Imported messages appear in normal message lists and threads, but do not generate inbound-message webhooks and are not evaluated against contact rules. Messages imported by a job carry its ID in import_job_id. Creating, refreshing the upload target, starting, and cancelling an import require a human session, an organization API key, or an API key scoped to a claimed agent. Reading and listing import jobs also accept API keys scoped to unclaimed agents, subject to normal mailbox visibility.

Formats

Because ZIP entries ending .mbox are imported, a mail export archive that contains one or more .mbox files can be uploaded as downloaded, with no need to unpack or repack it first.

Producing an importable file

Most providers offer a full-mailbox export that produces an MBOX file, sometimes delivered inside a ZIP archive. Upload size is capped at 1 GiB, and a full export of a long-lived mailbox often exceeds that. If yours does, narrow the export to a subset of labels or folders and run one import per file, or split the MBOX and import each part. Duplicate messages across those files are skipped rather than imported twice, so overlapping exports are safe.

Complete SDK flow

The Python and TypeScript SDKs upload directly using the target returned by create, so you do not need to construct the multipart upload yourself.
wait fetches immediately, then polls every five seconds by default. It returns a job for every terminal state, including failed and cancelled; those states are results, not request errors. A local timeout only stops waiting and does not cancel the import. Call cancel separately when you want processing to stop.

Direction and read state

Set original_addresses to every address from which you sent mail in the old mailbox. An imported message whose From address matches the destination mailbox or one of these addresses is classified as outbound; other messages are classified as inbound. Without original_addresses, sent mail from an old address may appear inbound. Direction inference is best-effort by design. From headers in an export are unauthenticated, so a message that spoofed one of your addresses classifies as outbound. Do not treat direction on imported mail as a trustworthy signal. Imported messages are marked read by default. Set mark_as_read to false in the API, mark_as_read=False in Python, markAsRead: false in TypeScript, or pass --mark-unread to the CLI to import them unread.

Create import

Create a job in pending_upload and receive an upload target.

Request body

Request example

JSON

Response (201)

JSON
Only one active import is allowed per mailbox. Creating another while a job is pending_upload, queued, or running returns 409.

Error responses

Import errors use a structured detail object rather than a plain string:
JSON

Upload file

Send a multipart/form-data POST to upload.url. Include every value from upload.fields as a form field, in the order returned and unmodified, followed by the file in a field named file. The field set is opaque and may change, so copy it verbatim rather than hard-coding names. The SDK upload methods handle this directly.

Refresh upload target

Issue a replacement upload target when the previous one lapses before the file is uploaded. The response is a new upload object. Python uses refresh_upload_target(email_address, job_id) and TypeScript uses refreshUploadTarget(emailAddress, jobId).

Error responses


Start import

Validate the completed upload and queue the job. Starting a job that is already queued or running is idempotent. The response is the updated import job. Starting also records the uploaded file’s identity. Replacing that file afterward does not restart or extend the import: the job fails instead of processing different data. If an upload was interrupted, re-upload before calling start, never after.

Error responses


Get import

Get the current status, counters, timestamps, and any job-level error. Poll this endpoint while the job is active. An unknown job for this mailbox returns 404.

List imports

List jobs newest first with cursor pagination. A cursor that was not produced by this endpoint returns 400.

Cancel import

Cancel a pending_upload, queued, or running job. Processing stops at the next item or control boundary, so a running import may continue briefly. Messages already imported remain in the mailbox.

Error responses

Statuses

The terminal states are completed, failed, and cancelled.

Counters

Unsafe rejection is per message: the rejected message is counted in messages_rejected_unsafe, and the import continues when possible. Counters are durable checkpoints, not a percentage. They can remain unchanged while a large message is processed, and they stay cumulative if processing restarts: a restart resumes from the last checkpoint rather than recounting. Use status to determine completion and display the counters as independent totals.

Job fields

Limits

ZIP imports support stored or deflated, unencrypted, single-disk archives. ZIP64 archives are not supported, and the archive must contain at least one importable mail entry. Imported messages count toward the mailbox storage allowance.

Queueing and terminal states

Queued jobs are processed oldest first, with at most one running import per organization. Imports also share service-wide processing capacity, so a queued job may wait while imports for other organizations run. A job that sits in queued with unchanging counters is waiting its turn, not stuck. A job ends failed, with the reason in error_detail, when it cannot continue. The common causes are:
  • The upload contained no importable messages, including a ZIP with no usable mail entries.
  • The next message would exceed the mailbox storage allowance. Messages imported before that point remain.
  • The uploaded file was replaced after start.
  • The archive violated one of the safety limits above.
Terminal jobs are not resumable. To retry, create a new job and upload again. Re-importing the same mail is safe: uniqueness is based on Message-ID within the mailbox, so previously imported messages are counted in messages_skipped_duplicate instead of being duplicated.