IMAP History Backfill Guide
This guide covers the IMAP history backfill API — a one-time import of a connected mailbox's historical emails, delivered to your webhook. You can import a whole mailbox (or a date window), or target a single conversation with one participant.
Overview
When a mailbox is connected, the live poller only forwards mail that arrives after connection. History backfill imports the historical emails that predate (or surround) that point, so a newly connected mailbox can build a complete communication timeline.
How it works:
- You start a backfill for a connected mailer. The platform walks the mailbox newest-first and delivers each email individually to the mailer's configured webhook — the same webhook that receives live incoming mail.
- Delivery is at-least-once: a slice may be retried (on transient errors, redelivery, or resume), so the same email can be delivered more than once. Your webhook must dedupe by
Message-Id. - The job runs asynchronously; poll the status endpoint for progress, or abort it at any time.
Supported connections
History backfill works on any IMAP-capable mailer that has a webhook configured. The connection you used to add the mailbox changes how the mailbox is searched and how fast it imports, but not how this API behaves.
| Connection | Added via | Mailer id prefix | Webhook event | Backfill |
|---|---|---|---|---|
| Google / Gmail OAuth | /v1/oauth connect flow | oauth_imap_, oauth_smtp_imap_ | IMAP_OAUTH | Supported |
| Microsoft / Outlook OAuth | /v1/oauth connect flow | oauth_imap_, oauth_smtp_imap_ | IMAP_OAUTH | Supported |
| Basic IMAP (username + password) | POST /v1/imapbasic/verify | basic_imap_ | BASIC_IMAP | Supported |
| Basic SMTP (send only) | POST /v1/smtpbasic/verify | basic_smtp_ | — | Not eligible |
A mailer whose config_type is smtp has no mailbox to read from, so there is nothing to import and the request returns 422. To be eligible, a basic mailer needs a config_type of imap or smtp_imap, working IMAP credentials, and a registered webhook. Adding IMAP to an existing send-only mailer means creating it through POST /v1/imapbasic/verify.
Behaviour that is identical across all supported connections: the request and response shapes, newest-first walk order, folder selection, participant scoping, at-least-once delivery, the lifecycle events, and the delivered payload shape. Per-connection differences are collected under Notes & limits.
Prerequisites
- A connected IMAP-capable mailer (
mailerID) — Gmail OAuth, Outlook OAuth, or basic IMAP. See Supported connections. - A webhook configured on that mailer (backfilled emails are delivered there). A mailer with no webhook cannot run a backfill.
- A valid API token (the same auth used for other
/v1endpoints).
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /v1/imap/{mailerID}/history | Start a history backfill job |
GET | /v1/imap/{mailerID}/history/{jobId} | Get job status / progress |
POST | /v1/imap/{mailerID}/history/{jobId}/abort | Cancel an in-flight job |
The pre-rename paths /v1/{mailerID}/backfill[...] still work as aliases to the same handlers, so existing integrations keep functioning. New integrations should use the /v1/imap/{mailerID}/history paths.
See the API Reference → IMAP History Backfill section for the full generated schema.
Scope: full mailbox vs single conversation
There is no separate "mode" flag — the presence of participant decides what gets imported:
| Scope | How to select | What it imports |
|---|---|---|
| Full import | omit participant (default) | Every email in the window (and/or up to count), across the selected folders. |
| Single conversation | set participant | Only emails exchanged with that one address. Use this to pull one conversation's history. |
The participant match is delegated to the mailbox, so it differs slightly by connection:
| Connection | Matches on |
|---|---|
| Gmail OAuth | from: OR to: |
| Outlook OAuth | Graph $search across the message's participants |
| Basic IMAP | FROM OR TO OR CC |
Start a history backfill
POST /v1/imap/{mailerID}/history
| Field | Type | Required | Notes |
|---|---|---|---|
participant | string (email) | optional | A single bare email address, e.g. x@gmail.com. Provide it to import only the conversation with that address; omit for a full import. |
window_start | string (RFC3339) | see below | Oldest point to walk back to, e.g. 2025-01-01T00:00:00Z. |
window_end | string (RFC3339) | optional | Newest point. Defaults to now. |
count | integer | optional | Cap on number of emails (newest-first). 0/omitted = no cap; bounded only by the window. Clamped to a server maximum (default 1000). |
folders | object | optional | Include non-INBOX folders: { "inbox": true, "sent": false, "spam": false, "trash": false }. Defaults to inbox-only. |
When is window_start required?
- Full import — provide at least one of
window_startorcount. - Single conversation (
participantset) —window_startis optional. If omitted, it defaults to the last 1 year. (window_endstill defaults to now.)
Examples
Full backfill of a date window (inbox + sent):
curl -X POST "https://<your-host>/v1/imap/{mailerID}/history" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"window_start": "2025-01-01T00:00:00Z",
"count": 1000,
"folders": { "sent": true }
}'
Single conversation with one participant (defaults to the last year):
curl -X POST "https://<your-host>/v1/imap/{mailerID}/history" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"participant": "x@gmail.com"
}'
{ "success": true, "jobId": "01JC3BBW8S9YGX2VNKG5MD7BTA" }
Idempotent per mailer: only one active backfill per mailer at a time. A second request while one is in flight returns 409 with the existing jobId:
{ "success": false, "error": "active history backfill already exists", "jobId": "01JC3BBW8S9YGX2VNKG5MD7BTA" }
Check status
GET /v1/imap/{mailerID}/history/{jobId}
{
"success": true,
"jobId": "01JC3BBW8S9YGX2VNKG5MD7BTA",
"status": "processing",
"processed": 250,
"target": 1000,
"percent": 25,
"startedAt": "2026-06-03T10:07:23Z",
"endedAt": ""
}
status—pending→processing→done(orfailed/cancelled).percent— time-based progress over the requested window (clamped 0–100;100when done).processed/target— emails delivered so far / the count cap (0= uncapped).endedAt— set once the job reaches a terminal state.
The job must belong to the addressed mailerID; a job from another mailer (or another tenant) returns 404.
Abort a history backfill
POST /v1/imap/{mailerID}/history/{jobId}/abort
Cancels an in-flight job; the worker stops cleanly at its next slice, leaving the job cancelled. Already-finished jobs (done/failed/cancelled) return 409.
{ "success": true, "jobId": "01JC3BBW8S9YGX2VNKG5MD7BTA", "status": "cancelled" }
After a job is cancelled (or finished), you can start a new backfill for the same mailer. Emails already delivered before cancellation stay delivered (dedupe by Message-Id).
Webhook delivery & dedupe
- Each historical email is delivered as a normal incoming-email webhook, with the full email content inline including attachments. The payload
eventisINCOMING_EMAILfor every connection type;IMAP_OAUTHandBASIC_IMAPare webhook registration types, not delivered event names. - The payload shape is identical to live incoming mail for the same connection, so an existing inbound handler needs no changes.
- Backfill deliveries carry an
X-History-Backfill-Job-Idheader so you can distinguish them from live mail. - Because delivery is at-least-once, dedupe by
Message-Idon your side — this is required, not optional.
Lifecycle events (optional)
Alongside the per-email deliveries, the job emits small lifecycle events to the same webhook so you can track it end-to-end:
HISTORY_BACKFILL_STARTED— when the first slice begins.HISTORY_BACKFILL_PROGRESS— throttled progress updates (~every 10%).HISTORY_BACKFILL_COMPLETED— on success.HISTORY_BACKFILL_FAILED— on terminal failure.
Notes & limits
All connections
- Send-only mailers return 422. See Supported connections.
- Window precision is rounded to whole seconds. Every mailbox's date filter is day-granular, so the platform widens the query and then filters on each message's real timestamp.
- Throughput is bounded by bytes, not message count. A mailbox of 200 large attachments takes far longer than one of 2,000 short replies, because every message is transferred from the mailbox and then posted to your webhook in full. Use the status endpoint rather than estimating from message counts.
- Your webhook is part of the critical path. A slow or failing endpoint slows or drops deliveries, so keep it fast and return 2xx quickly.
Gmail OAuth
- Throughput is additionally capped by Gmail's per-user API quota, so a large mailbox is paced rather than pulled at full speed.
Outlook OAuth
- Single-conversation search is capped at 1,000 results per query by Microsoft Graph. If a conversation is larger, narrow the window and run more than one job; the platform logs a warning when it hits the cap.
Basic IMAP
- Port 993 with SSL only. Other port and security combinations are rejected when the mailer is created.
- One connection per job. Messages are fetched sequentially over a single IMAP session, so basic IMAP is slower per byte than the OAuth connections, which fetch in parallel.
- Shared session limits. Many hosted IMAP servers cap concurrent sessions per account. If the live poller holds the only available session, the backfill cannot connect and the job retries. Running a backfill during quiet hours avoids this.
- Folder names are mapped, not assumed. The platform discovers the mailbox's real folders and maps them onto
INBOX,SENT,SPAMandTRASH, so server-specific names such as[Gmail]/Sent Mailresolve correctly.