Skip to main content

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.

ConnectionAdded viaMailer id prefixWebhook eventBackfill
Google / Gmail OAuth/v1/oauth connect flowoauth_imap_, oauth_smtp_imap_IMAP_OAUTHSupported
Microsoft / Outlook OAuth/v1/oauth connect flowoauth_imap_, oauth_smtp_imap_IMAP_OAUTHSupported
Basic IMAP (username + password)POST /v1/imapbasic/verifybasic_imap_BASIC_IMAPSupported
Basic SMTP (send only)POST /v1/smtpbasic/verifybasic_smtp_Not eligible
Send-only mailers are 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

  1. A connected IMAP-capable mailer (mailerID) — Gmail OAuth, Outlook OAuth, or basic IMAP. See Supported connections.
  2. A webhook configured on that mailer (backfilled emails are delivered there). A mailer with no webhook cannot run a backfill.
  3. A valid API token (the same auth used for other /v1 endpoints).

Endpoints

MethodPathPurpose
POST/v1/imap/{mailerID}/historyStart a history backfill job
GET/v1/imap/{mailerID}/history/{jobId}Get job status / progress
POST/v1/imap/{mailerID}/history/{jobId}/abortCancel an in-flight job
Legacy paths

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:

ScopeHow to selectWhat it imports
Full importomit participant (default)Every email in the window (and/or up to count), across the selected folders.
Single conversationset participantOnly 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:

ConnectionMatches on
Gmail OAuthfrom: OR to:
Outlook OAuthGraph $search across the message's participants
Basic IMAPFROM OR TO OR CC

Start a history backfill

POST /v1/imap/{mailerID}/history

FieldTypeRequiredNotes
participantstring (email)optionalA 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_startstring (RFC3339)see belowOldest point to walk back to, e.g. 2025-01-01T00:00:00Z.
window_endstring (RFC3339)optionalNewest point. Defaults to now.
countintegeroptionalCap on number of emails (newest-first). 0/omitted = no cap; bounded only by the window. Clamped to a server maximum (default 1000).
foldersobjectoptionalInclude 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_start or count.
  • Single conversation (participant set) — window_start is optional. If omitted, it defaults to the last 1 year. (window_end still 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": ""
}
  • statuspendingprocessingdone (or failed / cancelled).
  • percent — time-based progress over the requested window (clamped 0–100; 100 when 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 event is INCOMING_EMAIL for every connection type; IMAP_OAUTH and BASIC_IMAP are 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-Id header so you can distinguish them from live mail.
  • Because delivery is at-least-once, dedupe by Message-Id on 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, SPAM and TRASH, so server-specific names such as [Gmail]/Sent Mail resolve correctly.