# Handle email sync failures

Source: https://developer.nylas.com/docs/cookbook/email/email-sync-failures/

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?

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](/docs/cookbook/use-cases/build/handle-grant-expiry/).

## 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.

```bash
curl --request GET \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

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.

```bash
curl --request GET \
  --url 'https://api.us.nylas.com/v3/grants?grant_status=invalid&limit=50' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

## 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](/docs/cookbook/use-cases/build/retry-failed-webhooks/) guide covers the async pattern and the failure-state thresholds in depth.

## 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.

```bash
curl --request GET \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?received_after=1718800000&received_before=1718803600&limit=50' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

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](/docs/cookbook/email/backfill-historical-email/) for the complete paging and throttling pattern.

## Things to know about sync recovery

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.

## What's next

- [Handle grant expiry and re-authentication](/docs/cookbook/use-cases/build/handle-grant-expiry/) for the detection signals and the re-auth flow
- [Retry and debug failed webhooks](/docs/cookbook/use-cases/build/retry-failed-webhooks/) for at-least-once delivery, retry codes, and idempotency
- [Backfill historical email](/docs/cookbook/email/backfill-historical-email/) for paging through a full mailbox after a long outage
- [Getting started with Nylas](/docs/v3/getting-started/) to create a project, connector, and your first grant