# Sync email to Airtable with Nylas

Source: https://developer.nylas.com/docs/cookbook/use-cases/sync/sync-email-airtable/

Airtable makes a great lightweight email log: a spreadsheet-database where each row is a message and each column is a field you can filter, group, and share with non-engineers. The work is connecting a real mailbox to it. This recipe wires Nylas email into an Airtable base so every incoming message becomes a record, with the sender, subject, date, and body mapped to typed Airtable columns.

The two pieces are independent. Nylas gives you the email side: a unified read API and a `message.created` webhook that fires the moment mail arrives across Google, Microsoft, Yahoo, iCloud, IMAP, and EWS. Airtable gives you the storage side: a REST API that creates records in a table. You write the glue that maps one to the other.

## How does the Airtable email integration work?

The flow has three stages: Nylas delivers email data, your handler maps each message to an Airtable record shape, and the Airtable REST API writes it into a base. The API covers 6 email providers behind one schema, so the same code works whether the mailbox is Gmail or Exchange. Airtable allows 5 requests per second per base.

```text
Nylas (webhook or list) ──▶ map message → Airtable fields ──▶ POST /v0/{baseId}/{tableId}
```

Two delivery modes feed the same mapper. Use the `message.created` webhook for real-time logging, or [`GET /v3/grants/{grant_id}/messages`](/docs/v3/email/messages/) for a scheduled backfill of older mail. Both hand you the same message object, so you build the field mapping once. Each section below covers one stage in order.


> **Info:** 
> **New to Nylas?** Start with the [quickstart guide](/docs/v3/getting-started/) to set up your app and connect a test account before continuing here.


## Read messages from Nylas

The Nylas list endpoint pulls recent mail for a backfill or a periodic sync. A single `GET /v3/grants/{grant_id}/messages` request returns up to 50 messages by default and 200 at most, with one schema across all 6 providers. In the SDKs, `messages.list` takes the grant ID positionally, then a query-params object for filters like `limit`.

```bash
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?limit=50" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```js [listAirtable-Node.js SDK]


const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>" });

const messages = await nylas.messages.list({
  identifier: "<NYLAS_GRANT_ID>",
  queryParams: { limit: 50 },
});
```

```python [listAirtable-Python SDK]
from nylas import Client

nylas = Client("<NYLAS_API_KEY>")

# grant_id is the first positional argument
messages = nylas.messages.list(
    "<NYLAS_GRANT_ID>",
    query_params={"limit": 50},
)
```

Each message includes `from`, `to`, `subject`, `date` (a Unix timestamp), `snippet`, and `body`. Those are the fields you'll map to Airtable columns. To log mail the instant it arrives instead of polling, switch to the webhook in the next section.

## Capture new email with a webhook

For real-time logging, subscribe to the `message.created` webhook trigger instead of polling. Nylas sends an HTTP POST to your endpoint typically within 30 seconds of a message landing in any of the 6 supported providers, so an Airtable row appears almost as fast as the email does. The payload carries the same message object the list endpoint returns, which keeps your mapping code identical.

```python
from flask import Flask, request

app = Flask(__name__)

@app.post("/webhooks/nylas")
def on_message():
    event = request.get_json()
    if event.get("type") == "message.created":
        message = event["data"]["object"]
        record = to_airtable_fields(message)  # defined below
        create_airtable_record(record)        # defined below
    return "", 200
```

Nylas delivers each notification at least once, so the same `message.created` event can arrive 2 or more times. Deduplicate on the message ID before writing to Airtable, since duplicate events would otherwise create duplicate records. For the verification handshake and signature checks, see [Receive webhook notifications in real time](/docs/cookbook/use-cases/build/realtime-webhooks/) and [Handle duplicate webhook deliveries](/docs/cookbook/use-cases/build/handle-duplicate-webhooks/).

## Map email fields to Airtable column types

Airtable stores each value in a typed column, so the mapping step converts the Nylas message into a `fields` object keyed by your column names. This example maps 5 message fields: `singleLineText` for the subject, `email` for the sender address, `dateTime` for the timestamp, and `multilineText` or `richText` for the body. Airtable documents every type in its [field model reference](https://airtable.com/developers/web/api/field-model).

Here's a mapping that targets a table with columns named Subject, From, To, Received, and Body:

```python
from datetime import datetime, timezone

def to_airtable_fields(message):
    sender = (message.get("from") or [{}])[0]
    recipients = ", ".join(p["email"] for p in message.get("to", []))
    received = datetime.fromtimestamp(
        message["date"], tz=timezone.utc
    ).isoformat()

    return {
        "Subject":  message.get("subject", ""),   # singleLineText
        "From":     sender.get("email", ""),       # email
        "To":       recipients,                     # singleLineText
        "Received": received,                       # dateTime (ISO 8601)
        "Body":     message.get("snippet", ""),     # multilineText
    }
```

Two field-type details matter. A `dateTime` column expects an ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`) string, which is why the code converts the Nylas Unix `date` first. A `singleSelect` column rejects any value that isn't already an option unless you send `"typecast": true`, which tells Airtable to create the option on the fly.

## Create Airtable records

Airtable writes rows through `POST https://api.airtable.com/v0/{baseId}/{tableIdOrName}`. The request body holds a `records` array, where each element is a `{ "fields": {...} }` object. Batching keeps you under the rate limit: send up to 10 records per request rather than one call per message. Authenticate with a personal access token in the `Authorization` header.

```python


AIRTABLE_TOKEN = "<AIRTABLE_PAT>"
BASE_ID = "<BASE_ID>"
TABLE = "Emails"

def create_airtable_record(fields):
    url = f"https://api.airtable.com/v0/{BASE_ID}/{TABLE}"
    headers = {
        "Authorization": f"Bearer {AIRTABLE_TOKEN}",
        "Content-Type": "application/json",
    }
    body = {"records": [{"fields": fields}], "typecast": True}
    resp = requests.post(url, json=body, headers=headers)
    resp.raise_for_status()
    return resp.json()
```

The `typecast: true` option lets Airtable coerce strings into the column's type and create missing `singleSelect` options automatically. Drop it if you want strict validation that rejects unexpected values. The endpoint returns each created record with its Airtable record ID, which you can store back in Nylas metadata if you need a two-way link.

## Stay under Airtable's rate limit

Airtable limits each base to 5 requests per second. Exceed it and the API returns a 429 status code, after which, per Airtable's [rate-limits documentation](https://airtable.com/developers/web/api/rate-limits), you "will need to wait 30 seconds before subsequent requests will succeed." A burst of inbound mail can blow past 5 writes per second fast, so a high-volume mailbox needs a queue, not direct writes from the webhook handler.

```python

from collections import deque

_recent = deque()  # timestamps of recent Airtable calls

def throttle(max_per_sec=5):
    now = time.monotonic()
    while _recent and now - _recent[0] > 1.0:
        _recent.popleft()
    if len(_recent) >= max_per_sec:
        time.sleep(1.0 - (now - _recent[0]))
    _recent.append(time.monotonic())
```

Call `throttle()` before each `create_airtable_record`. Two defenses keep a busy inbox healthy: batch up to 10 records per POST to cut request count, and push webhook events onto a queue so a spike of email drains at a steady 5 requests per second instead of hammering the base.

## Things to know about Airtable

A few constraints shape how the integration behaves over time:

- **Rate limit is per base, not per token.** The 5 requests per second cap applies to each base. Two tables in the same base share the budget, so splitting the email log across tables won't buy you more throughput. Use separate bases or a queue.
- **The 429 penalty is long.** A single overage triggers a 30-second cool-down, far longer than the 1-second window that caused it. Throttle proactively rather than catching 429 responses and retrying.
- **Authentication uses personal access tokens.** Airtable retired API keys in February 2024. Create a [personal access token](https://airtable.com/developers/web/guides/personal-access-tokens) scoped to `data.records:write` and the specific base, and store it as a secret.
- **Field names must match exactly.** The `fields` object keys are the column names in your table, and they're case-sensitive. A typo writes an empty or wrong value without raising an error, so keep the mapping in sync when you rename a column.
- **Attachments need a public URL.** Airtable's `multipleAttachments` type ingests files by URL, not raw bytes. To log attachments, host them somewhere reachable first, then pass the URL. The body text from `snippet` or `body` covers most logging needs without that step.

## What's next

- [List and read email messages with Nylas](/docs/v3/email/messages/)
- [Receive webhook notifications in real time](/docs/cookbook/use-cases/build/realtime-webhooks/)
- [Handle duplicate webhook deliveries](/docs/cookbook/use-cases/build/handle-duplicate-webhooks/)
- [Sync email contacts to a CRM](/docs/cookbook/use-cases/sync/sync-email-crm/): pull and enrich senders instead of logging raw messages
- [Monitor an inbox for support tickets](/docs/cookbook/use-cases/ingest/monitor-inbox-support-tickets/): classify and route inbound mail
- [Build an email analytics dashboard](/docs/cookbook/email/email-analytics-dashboard/)
- [Airtable create records API](https://airtable.com/developers/web/api/create-records)