# Two-way sync between Salesforce and email

Source: https://developer.nylas.com/docs/cookbook/use-cases/sync/salesforce-two-way-sync/

Most Salesforce email integrations only run one direction: they scrape the inbox and drop activity into the CRM. A two-way sync also pushes the other way. When a rep logs a follow-up in Salesforce or moves an Opportunity stage, that action turns into a real sent message or a calendar event on the rep's Gmail or Outlook account. This recipe wires both directions together with change-driven webhooks so the two systems stay aligned without a polling loop hammering either side every few minutes.

If you only need the inbound half (email landing in Salesforce as Tasks), use [Export email data to Salesforce](/docs/cookbook/use-cases/sync/export-to-salesforce/) and stop there. This page assumes that mapping already exists and focuses on the second direction plus the plumbing that keeps both directions consistent.

## What does two-way Salesforce sync mean?

Two-way sync means changes flow both ways: email and calendar activity from Gmail or Outlook lands in Salesforce, and actions taken inside Salesforce produce real messages or events back on the connected account. One direction alone leaves a gap that reps fill by hand.

The inbound direction is covered by the existing export recipe, so treat it as a given here. The outbound direction is where the work is. A rep clicks "Log and send" on a Salesforce Task, and your integration calls Nylas to actually send that message from their mailbox. A closed-won Opportunity creates a kickoff event on the customer's calendar. Across a 50-user sales team, outbound actions typically run 300 to 800 events per day, which is light traffic but demands ordering guarantees so a later edit never loses to an earlier one. The two halves share one identifier map, described below, so a record written by one direction is never re-imported by the other.

## How do you capture email activity into Salesforce?

Inbound capture pulls new messages and calendar events from the connected grant and writes them to Salesforce as Tasks and Events. You read from Nylas, map each item to the Salesforce object model, then upsert through the Salesforce Composite or Bulk API on a schedule or in response to a webhook.

The read side uses two Nylas endpoints. `GET /v3/grants/{grant_id}/messages` returns email, and `GET /v3/grants/{grant_id}/events?calendar_id=<CALENDAR_ID>` returns calendar items, both filterable by a time window so each run only fetches what newly arrived. A typical incremental run over 5 minutes of activity pulls fewer than 40 messages per grant, which keeps you well under provider rate limits. For the field-by-field Task mapping (sender to Contact, domain to Account, subject to Task), follow [Export email data to Salesforce](/docs/cookbook/use-cases/sync/export-to-salesforce/) rather than repeating it here.

```bash
# Pull messages received in the last sync window
curl -X GET "https://api.us.nylas.com/v3/grants/GRANT_ID/messages?received_after=1718351400&limit=50" \
  -H "Authorization: Bearer NYLAS_API_KEY"
```

Calendar capture works the same way against the events endpoint, and both feeds funnel into the same Salesforce write path.

## How do you push Salesforce changes back to email and calendar?

The outbound direction listens for Salesforce changes (via a Platform Event, Change Data Capture stream, or outbound message) and translates each one into a Nylas call. A logged email becomes a send request, and a scheduled meeting becomes a calendar event on the rep's connected account.

Two Nylas endpoints carry the outbound traffic. `POST /v3/grants/{grant_id}/messages/send` sends a message from the rep's mailbox, and `POST /v3/grants/{grant_id}/events?calendar_id=<CALENDAR_ID>` creates a calendar event. Each call should complete in roughly 400 to 900 ms depending on the provider, so a queue worker can clear hundreds of actions per minute. Always tag the outbound payload with the Salesforce record ID so the inbound direction recognizes the result as its own and skips re-importing it.

```python


# A Salesforce "log and send" action becomes a real sent message
resp = requests.post(
    "https://api.us.nylas.com/v3/grants/GRANT_ID/messages/send",
    headers={"Authorization": "Bearer NYLAS_API_KEY"},
    json={
        "to": [{"email": "ada@acme.test"}],
        "subject": "Following up on Q2 plan",
        "body": "Thanks for the call earlier...",
        # Tracking label tags the send so inbound sync recognizes it
        "tracking_options": {"label": "sf-task-00T5g000004abcd"},
    },
)
resp.raise_for_status()
```

Store the returned message ID against the Salesforce Task so a later edit updates the same record instead of creating a duplicate.

## Which webhooks drive the sync?

Webhooks replace polling so each direction reacts only to real changes. Nylas notifications cover the email and calendar side, and Salesforce Change Data Capture covers the CRM side. Subscribing to both means neither system needs a timer loop checking for updates every few minutes.

On the Nylas side, subscribe to `event.created`, `event.updated`, and `event.deleted` for calendar changes, plus `grant.expired` so a broken connection surfaces immediately instead of failing silently for hours. Those 4 trigger types cover every change the outbound direction needs to react to. One important caveat: `message.send_success` only fires for scheduled sends, not immediate sends, so do not rely on it to confirm a normal outbound message landed. Confirm those by checking the response from the send call instead. A healthy deployment of 50 users generates somewhere around 2,000 to 6,000 webhook deliveries per day across all trigger types. See [Notifications](/docs/v3/notifications/) for the full payload reference and signature verification.

```bash
# Register a webhook for calendar changes and grant health
curl -X POST "https://api.us.nylas.com/v3/webhooks" \
  -H "Authorization: Bearer NYLAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger_types": ["event.created", "event.updated", "event.deleted", "grant.expired"],
    "webhook_url": "https://yourapp.example.com/nylas/webhook",
    "description": "Salesforce two-way sync"
  }'
```

Each incoming webhook enqueues a single sync job keyed by the changed record, which is what keeps the two directions from racing.

## How do you handle sync conflicts?

A conflict happens when both systems change the same record before either change syncs. The fix is a deterministic rule applied to every record: pick a winner by timestamp, treat one system as authoritative per field, and make every write idempotent so replays are safe.

Three rules cover almost every case. First, last-write-wins by the source system's modified timestamp resolves simple edit races, and in practice fewer than 2% of records ever hit a true conflict. Second, assign field ownership: let Salesforce own deal fields and let the calendar own meeting times, so the two systems rarely fight over the same attribute. Third, make writes idempotent by keying on the shared ID map (Nylas message or event ID on one side, Salesforce record ID on the other) so a redelivered webhook never creates a duplicate. The [event endpoints](/docs/v3/calendar/using-the-events-api/) include updated timestamps you can compare directly. For the inbound contact and Task mapping that feeds this map, reuse [Export email data to Salesforce](/docs/cookbook/use-cases/sync/export-to-salesforce/).

```python
def resolve(local, remote):
    # Last-write-wins on the source modified time; idempotent by shared id
    if remote["updated_at"] > local["updated_at"]:
        return remote
    return local
```

## Things to know

- **Send confirmation.** `message.send_success` is scheduled-send only. For immediate sends, trust the HTTP response from the send call, not a webhook.
- **Grant expiry.** Subscribe to `grant.expired` and surface re-authentication to the rep fast. A dead grant silently stops both directions, and a stale CRM erodes trust within days.
- **Loop prevention.** Tag every outbound write with its Salesforce source ID and skip those on the inbound pass. Without this guard a single logged email can ping-pong between systems forever.
- **Rate limits.** Batch Salesforce writes through the Composite API and keep Nylas reads windowed. A 50-user team rarely exceeds 5,000 requests per day to the API in steady state.

## Next steps

- [Export email data to Salesforce](/docs/cookbook/use-cases/sync/export-to-salesforce/): the inbound Task mapping this page builds on
- [Sync calendar events to a CRM](/docs/cookbook/use-cases/sync/sync-calendar-events-crm/): calendar-specific capture detail
- [Notifications](/docs/v3/notifications/): webhook payloads, triggers, and signature verification