> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cognee.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Gmail

> Connect a personal or Google Workspace mailbox over OAuth, pick the labels to cover, and sync matching messages into a dataset of that account's own.

Connect one **Google account** — personal or Workspace — to Cognee through a per-user OAuth consent, choose which labels to cover, and have the matching messages become searchable memory in a dataset of that account's own.

<Info>
  This integration is for self-hosted Cognee deployments — you create and own the Google Cloud OAuth client. It is the server-side, multi-user counterpart to calling [`gmail_source()`](/integrations/dlt-integration#gmail-connector) from Python yourself: the same connector runs underneath, but Cognee holds the OAuth credential instead of a local `token.json`.
</Info>

**Unreleased.** The integration landed after `v1.6.0` and is not tagged yet, so it needs a `dev` checkout until the next release.

## What It Does

* **One Google account, one Cognee user.** Whoever completes the consent owns the credential, and the mailbox is indexed into that user's memory. Personal and Workspace accounts take the same path and are told apart by the hosted-domain claim (`account_type: workspace` or `personal`).
* **Read-only.** The requested scopes are `openid email https://www.googleapis.com/auth/gmail.readonly` — Cognee never modifies the mailbox.
* **Its own grant.** Like [Google Drive](/integrations/google-drive-integration), Gmail is a separate provider with its own settings and its own consent. The authorize URL does not fold its grant together with Drive's, so disconnecting one never revokes the other.
* **Opt-in by label.** A new connection selects nothing. No mail is read until you choose labels and start a sync.
* **Incremental and deletion-aware.** The first sync backfills the selected scope and records Gmail's `historyId`; later syncs fetch only what changed, and messages that are deleted, trashed or move out of scope are forgotten.

## Prerequisites

* A Cognee backend on `dev` — see the version note above.
* The **`gmail` extra** installed on that backend:

  ```bash theme={null}
  pip install "cognee[gmail]"
  ```

  It pulls in `google-api-python-client`, `google-auth` and `google-auth-oauthlib`, which the sync builds its Gmail client with. OAuth and label listing work without them, so a backend missing the extra connects fine and then fails its first sync with an `ImportError`.
* A **Google Cloud project** with the **Gmail API** enabled.

## Setup

### 1. Create the Google Cloud OAuth client

In the [Google Cloud Console](https://console.cloud.google.com/), enable the Gmail API, then:

| Step                                                   | Value                                                                                                                |
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| **OAuth consent screen**                               | Configure it for your audience. While the app is in **Testing**, add every account that will connect as a test user. |
| **Scopes**                                             | `openid`, `email`, and `https://www.googleapis.com/auth/gmail.readonly`.                                             |
| **Credentials → Create credentials → OAuth client ID** | Application type **Web application**.                                                                                |
| **Authorized redirect URI**                            | `https://<your-host>/api/v1/integrations/gmail/callback` — must match `GOOGLE_GMAIL_REDIRECT_URI` verbatim.          |

<Warning>
  `gmail.readonly` is a **restricted scope**. Publishing the app to the public requires Google's OAuth verification, including a security assessment. Test users work without verification.
</Warning>

### 2. Configure Cognee

Add the client's values to your backend's `.env`:

| Variable                         | Value                                                                                                                 |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `GOOGLE_GMAIL_CLIENT_ID`         | From the OAuth client.                                                                                                |
| `GOOGLE_GMAIL_CLIENT_SECRET`     | From the OAuth client.                                                                                                |
| `GOOGLE_GMAIL_STATE_SECRET`      | Any long random string. It signs the OAuth `state` parameter. Use a different value from `GOOGLE_DRIVE_STATE_SECRET`. |
| `GOOGLE_GMAIL_REDIRECT_URI`      | The callback registered on the client, e.g. `http://localhost:8000/api/v1/integrations/gmail/callback`.               |
| `GOOGLE_GMAIL_FRONTEND_BASE_URL` | Your frontend's origin, e.g. `http://localhost:3000` — where the callback sends the browser afterwards.               |

Stored tokens are encrypted with the integrations framework's credential key, the same `INTEGRATION_CREDENTIALS_KEY` (or `INTEGRATION_CREDENTIALS_KEYS` keyring) described on the [Google Drive](/integrations/google-drive-integration#2-configure-cognee) page.

The settings are checked when they are used, not at startup, so a backend without them still boots. `POST /authorize` answers **503** (`gmail integration is not configured on this server.`) until they are set. Restart the backend after changing `.env`.

### 3. Connect

```bash theme={null}
curl -X POST https://<your-host>/api/v1/integrations/gmail/authorize \
  -H "Authorization: Bearer <your-cognee-token>"
# {"authorizeUrl":"https://accounts.google.com/o/oauth2/v2/auth?...&access_type=offline&prompt=consent"}
```

The connect flow is the same one Google Drive uses: finish it in the browser that called `/authorize`, within 10 minutes, and read the result from the redirect to `<GOOGLE_GMAIL_FRONTEND_BASE_URL>/integrations?gmail=<outcome>`. The outcomes (`connected`, `cancelled`, `error_invalid_state`, `error_already_connected`, `error_exchange_failed`) and the split-domain cookie caveat are described under [Google Drive → Connect](/integrations/google-drive-integration#3-connect).

In the local Cognee UI, a **Gmail** card sits under **Data sources** on the Integrations page, alongside Google Drive. It drives this same API: connect, choose labels, **Save selection**, then **Refresh** to sync.

## Choosing Labels

List the account's labels:

```bash theme={null}
curl https://<your-host>/api/v1/integrations/gmail/labels \
  -H "Authorization: Bearer <your-cognee-token>"
```

Each entry carries the label `id`, its `name`, and `attributes` with the label `type` (`system` or `user`) and `messages_total` (usually `null`, because Gmail's label listing omits message counts). Persist a selection with `PUT`:

```bash theme={null}
curl -X PUT https://<your-host>/api/v1/integrations/gmail/labels \
  -H "Authorization: Bearer <your-cognee-token>" \
  -H "Content-Type: application/json" \
  -d '{"resourceIds": ["INBOX"]}'
```

`/labels` is an alias for the generic `/resources` path on both verbs. The selection has three states:

| `resourceIds`       | Meaning                                         |
| ------------------- | ----------------------------------------------- |
| `null`              | The whole mailbox, excluding Spam and Trash.    |
| `[]`                | Nothing. A newly connected account starts here. |
| `["<label id>", …]` | Messages that carry **every** listed label.     |

<Warning>
  **Several labels narrow the selection, they don't widen it.** Gmail applies selected labels as an intersection: `["INBOX", "Label_123"]` indexes only messages that carry both labels, not every message in either one.
</Warning>

Changing the label set makes the next sync start with a full backfill of the new scope, so older messages that now match are picked up. Reordering the same labels keeps the incremental cursor.

**Narrowing the selection deletes data.** The backfill after a label change forgets every previously synced message outside the new scope, from the graph, vector and relational stores alike. A sync that fails midway forgets nothing, so a transient error never takes mail with it. Setting the selection to `[]` and syncing clears everything the account has indexed; so does [disconnecting with `delete_data=true`](#token-lifecycle-and-disconnect).

## Syncing and Status

Connecting does not start a sync. Start one after choosing labels:

```bash theme={null}
curl -X POST https://<your-host>/api/v1/integrations/gmail/sync \
  -H "Authorization: Bearer <your-cognee-token>"
# {"accepted": true}
```

The sync runs **detached**, and the response doesn't carry its result. It passes the account's `gmail_source()` to [`remember()`](/python-api/remember) with `write_disposition="merge"`, `primary_key="id"` and no row cap, so a re-sync updates messages instead of duplicating them. It runs with `self_improvement=False`, so no whole-graph [`improve()`](/python-api/improve) pass fires on each refresh.

* **Only the first sync reads the whole scope.** Later syncs ask Gmail's History API for changes since the stored `historyId`. If that cursor has expired (Gmail keeps about a week of history), the sync falls back to a full backfill.
* **Transient Gmail API errors are retried** — each Gmail API call retries up to 6 times before a failure counts against the sync.
* **One sync per account at a time.** While a sync is running, another `/sync` call returns the same `{"accepted": true}` and does nothing. The guard lives in the API process, so a deployment with several workers needs its own queue or lock.
* **Nothing refreshes on its own.** There are no Gmail webhooks and no built-in scheduler. Call `/sync` again, or from your own scheduler, to pick up new mail.

Messages land in **one dataset per connected account**, named `gmail_<email-slug>_<hash>`. The slug is built the same way as for [Google Drive datasets](/integrations/google-drive-integration#syncing-and-status); the hash is the first 10 hex characters of the SHA-256 of the Google account id.

```python theme={null}
import cognee

results = await cognee.recall(
    "What did the vendor say about the renewal?",
    datasets=["gmail_jane_example_com_3f2a1b9c4d"],
)
```

`GET /api/v1/integrations/gmail/connection` returns the same fields as the [Drive connection state](/integrations/google-drive-integration#reading-the-connection-state): `connected`, `accountLabel`, `datasetId`, `storedItems` (items actually stored in the dataset), `syncStatus` (`ok`, `degraded`, `syncing`), `lastSyncedAt` and `syncCounts`. Gmail's `syncCounts` carry `scanned`, `skipped`, `failed` and `deleted`, plus `skipped_unavailable` (a message that vanished before it could be fetched), `failed_message_fetch`, `failed_ingestion` and `failed_rate_limit`.

Reconnecting an account keeps its stored label selection. Only a first-time connection starts with `[]`.

## Token Lifecycle and Disconnect

Tokens are handled exactly as for [Google Drive](/integrations/google-drive-integration#token-lifecycle): the access token is refreshed 5 minutes before it expires, and an `invalid_grant` on refresh (access revoked at [myaccount.google.com/permissions](https://myaccount.google.com/permissions)) revokes the connection locally so it stops reading as connected.

```bash theme={null}
# Revoke the connection, keep the indexed mail
curl -X DELETE "https://<your-host>/api/v1/integrations/gmail/connection" \
  -H "Authorization: Bearer <your-cognee-token>"

# Revoke it and forget the account's gmail_… dataset as well
curl -X DELETE "https://<your-host>/api/v1/integrations/gmail/connection?delete_data=true" \
  -H "Authorization: Bearer <your-cognee-token>"
```

Disconnecting revokes the local credential and makes a best-effort call to revoke the token at Google. If the dataset deletion fails, the endpoint answers **502** (`gmail disconnected, but its data could not be deleted`). The connection is already gone at that point.

## Limitations

* **Labels intersect.** There is no way to select "INBOX or Label\_123" in one connection.
* **No automatic refresh.** `/sync` is the only thing that updates memory.
* **One Cognee user per Google account.** Connecting an account that another user already holds is refused with `error_already_connected`.
* **`gmail.readonly` is a restricted scope**, so public distribution needs Google's OAuth verification.

## Related

<CardGroup cols={2}>
  <Card title="Google Drive" icon="hard-drive" href="/integrations/google-drive-integration">
    The sibling Google OAuth integration, with the same connect flow, status fields and token handling.
  </Card>

  <Card title="Gmail from Python" icon="download" href="/integrations/dlt-integration#gmail-connector">
    Call `gmail_source()` directly with a local OAuth token, without the integrations API.
  </Card>
</CardGroup>
