Skip to content
Skip to main content

Handle email sync failures

Last updated:

A user changes their mailbox password and your sync stops, but nothing in your app says so. Messages keep flowing for everyone else, so the silence looks normal until a support ticket lands days later. Email sync breaks for ordinary reasons: expired provider tokens, revoked OAuth consent, a webhook handler that returned 502 during a deploy. None of these are bugs in your code, and almost all of them are recoverable if you catch them early.

This page covers the production playbook: detect a stalled grant through status and webhooks, re-authenticate without losing object IDs, survive webhook retries, and backfill the messages that slipped through the gap.

How do I handle email sync failures and reconnects in production?

Section titled “How do I handle email sync failures and reconnects in production?”

In production, you detect a failure two ways and recover in three steps. Subscribe to the grant.expired webhook and check grant_status on GET /v3/grants/{grant_id} for detection. Then re-authenticate through the same OAuth flow, let the API replay up to 72 hours of missed notifications, and backfill anything older. The grant ID never changes.

Treat detection and recovery as separate concerns. Detection answers “is this account still syncing?” and recovery answers “how do I close the gap?”. The most common failure is an expired grant, where the provider invalidates the refresh token. The grant’s grant_status flips from valid to invalid, and every Email API call against it starts returning 401. The fix is re-authentication, which refreshes the tokens against the same grant record and preserves message IDs, thread IDs, and tracking links. Deleting and recreating the grant instead causes permanent data loss. For the full lifecycle and the IMAP ID-hashing caveat, see handle grant expiry and re-authentication.

What is the best way to detect a stalled grant?

Section titled “What is the best way to detect a stalled grant?”

The best detection combines a proactive webhook with reactive status checks. Subscribe a POST /v3/webhooks subscription to the grant.expired trigger so the API tells you the moment a grant goes invalid, and inspect grant_status on the grant object whenever a call returns 401. Together they catch both the accounts you query often and the ones sitting idle.

A webhook alone has a blind spot: the API detects expiry through background sync health checks, which can lag the actual token failure by several minutes. A 401 on a live request closes that window instantly. The grant object exposes a grant_status field with two values, valid and invalid, so a single GET tells you exactly where an account stands. The request below reads one grant. Use it on demand after a 401, never in a polling loop, since list-grants supports a grant_status filter and a tight poll burns through your rate limit budget.

To audit many accounts at once, filter the list endpoint instead of fetching grants one by one. The call below returns only invalid grants, so you can surface every account that needs re-authentication in a single page rather than walking your whole user base.

How do I manage webhook reliability and retries for email event tracking?

Section titled “How do I manage webhook reliability and retries for email event tracking?”

Acknowledge fast and dedupe. The Email API guarantees at-least-once delivery and enforces a 10-second response timeout, so return 200 OK the instant a notification arrives, then process in a background queue. When a delivery fails, the API retries up to two more times for three attempts total, with the final retry landing 10 to 20 minutes after the first.

Two behaviors shape every reliable handler. First, retries fire only for transient status codes: 408, 429, 502, 503, 504, and 507. Every other code, including 4xx validation errors, is treated as permanent and never retried, so a handler that throws 400 drops the event silently. Second, because the same notification can arrive up to three times, dedupe on the top-level id, which stays stable across all attempts. If a destination returns non-200 for 95% of deliveries over a 15-minute window, the API marks it failing; if that keeps up for the next 72 hours, it moves to failed and stops sending. The retry and debug failed webhooks guide covers the async pattern and the failure-state thresholds in depth.

How do I backfill the sync gap after a recovery?

Section titled “How do I backfill the sync gap after a recovery?”

After you re-authenticate, the API resends missed notifications only if the grant was out of service for under 72 hours. For anything older, query GET /v3/grants/{grant_id}/messages with received_after and received_before bounded to the outage window. That fills the gap the webhook replay cannot reach.

Pin the gap to two timestamps. The grant.expired notification marks when sync stopped, and the grant.updated notification (or the moment re-authentication succeeded) marks when it resumed. Both are Unix timestamps in seconds, which is exactly the format received_after and received_before expect. The request below pulls every message that arrived during a one-hour outage so you can reconcile against what your app already stored. Page through results with page_token until the response stops returning one, and throttle the run to stay under your rate limit.

For a full-history import rather than a narrow gap, the same endpoint plus query_imap reaches mail older than the provider’s rolling cache. See backfill historical email for the complete paging and throttling pattern.

Recovery has a hard 72-hour boundary, and missing it changes your strategy. If a grant is invalid for under 72 hours, re-authenticating triggers an automatic replay of the notifications you missed, which can arrive as one large burst, so size your handler to absorb it. Past 72 hours, the replay never fires and the timestamp-bounded backfill above becomes your only safety net.

A few specifics worth building around:

  • Re-auth keeps the grant ID. When the API receives an auth request for an email that already has a grant, it re-authenticates that grant instead of creating a new one. Object IDs and tracking links survive.
  • IMAP IDs depend on the grant. For IMAP-style providers, message and thread IDs are hashed from the grant ID. Recreating a grant changes every ID, which is why deletion is never the recovery path.
  • Status has two states only. grant_status is valid or invalid. There is no intermediate “degraded” value, so a transient provider hiccup that resolves on its own won’t flip it.
  • Webhooks lag, calls don’t. Sync health checks surface an expiry within minutes, but a 401 on a live request is immediate. Lean on both.