Skip to content
Inkbox

Inkbox

DocsPricingBlogContact
GuidesAPI ReferenceChangelog

Ctrl K

GuidesAPI ReferenceChangelog

Jump to

Mail imports

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

FormatDescription
autoDetect MBOX, EML, or ZIP from the uploaded file. This is the default.
mboxA mailbox archive containing multiple messages, such as a common email export.
emlOne RFC 5322 email message.
zipA ZIP archive of mail files at any directory depth. Entries ending .eml import as single messages, entries ending .mbox or .mbx are split like an MBOX, and extensionless entries that begin with RFC 5322 headers are imported. Directories, nested archives, and other files are not imported or traversed, but non-directory entries still undergo archive-wide validation and can cause the job to fail.

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 POST

POST /mailboxes/{email_address}/imports

Create a job in pending_upload and receive an upload target.

Request body

FieldTypeDefaultDescription
source_formatstringautoauto, mbox, eml, or zip
original_addressesstring[] | nullnullAddresses used to send mail from the old mailbox (maximum 20)
mark_as_readbooleantrueWhether imported messages start read

Request example

JSONJSON

Response (201)

JSONJSON

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

Error responses

Statusdetail.errorDescription
404The mailbox does not exist or is not visible to your key
409mail_import_already_in_flightAnother import for this mailbox is pending_upload, queued, or running
429mail_import_quota_exceededThe organization's rolling 24-hour job quota is exhausted. The response carries a Retry-After header in seconds

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

JSONJSON

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 POST

POST /mailboxes/{email_address}/imports/{job_id}/upload-url

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

Statusdetail.errorDescription
404Unknown job for this mailbox
409mail_import_wrong_stateThe job is past the upload phase; upload targets exist only while it is pending_upload

Start import POST

POST /mailboxes/{email_address}/imports/{job_id}/start

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

Statusdetail.errorDescription
404Unknown job, or no file has been uploaded yet. The message distinguishes the two
409mail_import_wrong_stateThe job is in a terminal state and cannot be started
409mail_import_upload_identity_unavailableThe upload could not be verified; upload the file again
413mail_import_upload_too_largeThe uploaded file exceeds the 1 GiB cap

Get import GET

GET /mailboxes/{email_address}/imports/{job_id}

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 GET

GET /mailboxes/{email_address}/imports

List jobs newest first with cursor pagination.

ParameterTypeDefaultDescription
cursorstringOpaque cursor from next_cursor
limitinteger50Jobs per page (1–100)

A cursor that was not produced by this endpoint returns 400.


Cancel import POST

POST /mailboxes/{email_address}/imports/{job_id}/cancel

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

Statusdetail.errorDescription
404Unknown job for this mailbox
409mail_import_wrong_stateThe job is already completed, failed, or cancelled

Statuses

StatusMeaning
pending_uploadWaiting for the file upload and start request
queuedUpload accepted and waiting for processing
runningMessages are being processed
completedProcessing finished, possibly with skipped, failed, or rejected messages
failedThe job could not continue; inspect error_detail
cancelledThe job was cancelled or its upload was never started before expiry

The terminal states are completed, failed, and cancelled.

Counters

FieldDescription
messages_processedMessages examined so far
messages_importedMessages successfully added to the mailbox
messages_skipped_duplicateMessages skipped because that Message-ID already exists in the mailbox
messages_failedMessages that could not be imported, including messages over 50 MiB. An extensionless ZIP entry over 50 MiB is ignored because its content cannot be classified without reading it
messages_rejected_unsafeMessages rejected as unsafe before being added to the mailbox

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

FieldTypeDescription
idUUIDImport job identifier, also stamped on each imported message as import_job_id
mailbox_idUUIDMailbox the messages are imported into
statusstringOne of the six statuses above
source_formatstringauto, mbox, eml, or zip as requested at create time
original_addressesstring[] | nullAddresses supplied at create time for direction inference
mark_as_readbooleanWhether imported messages start read
upload_size_bytesinteger | nullSize of the uploaded file; null until the job is started
messages_processedintegerSee the counters above
messages_importedintegerSee the counters above
messages_skipped_duplicateintegerSee the counters above
messages_failedintegerSee the counters above
messages_rejected_unsafeintegerSee the counters above
error_detailstring | nullWhy a failed or expired job stopped; null otherwise
created_atstringCreation timestamp (ISO 8601)
updated_atstringLast time the job record changed (ISO 8601)
started_atstring | nullWhen processing began; null while pending_upload or queued
finished_atstring | nullWhen the job reached a terminal state; null while active

Limits

LimitValue
Upload size1 GiB
Individual message size50 MiB
Messages per job100,000
ZIP entries65,000
ZIP total uncompressed size1 GiB across every entry, whether or not it is importable
ZIP central directory64 MiB
ZIP compression ratio100:1 per entry
Import jobs20 per organization per rolling 24 hours by default
Concurrent importsOne per mailbox, and one running import per organization
Upload target lifetime5 minutes; request a replacement while the job is pending_upload
Unstarted upload expiry24 hours

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.

Inkbox

Copyright © 2026 Inkbox

This site is protected by reCAPTCHA.

Google Privacy Policy and Terms of Service apply.

Website

Inkbox

Copyright © 2026 Inkbox

This site is protected by reCAPTCHA.

Google Privacy Policy and Terms of Service apply.

Website

Y CombinatorBacked by Y Combinator
Mail imports