# Act on behalf of a user

Source: https://developer.nylas.com/docs/cookbook/use-cases/build/act-on-behalf-of-user/

A user connected their Google account to your app last week. Tonight your backend needs to read their calendar, pull the next day's meetings, and queue reminder email, all without that user being online. That's the work that happens after OAuth: your server makes authenticated requests against their connected account on a schedule you control.

This recipe picks up where [connecting accounts with OAuth](/docs/cookbook/use-cases/build/connect-user-accounts-oauth/) leaves off. You already have a `grant_id`. Now you use it with your API key to read calendars, send mail, and act on each user's data from your backend.

## Use the grant ID to act on an account

A grant represents one connected account, and its `grant_id` is the address you target for every request on that user's behalf. You authenticate with your API key as a Bearer token, then put the `grant_id` in the path. One integration covers all 6 providers, Google, Microsoft, Yahoo, iCloud, IMAP, and Exchange, with the same request shape.

You never send the user's provider token. Nylas stores the access and refresh tokens on the grant and refreshes them before they expire, so a grant stays usable for months without any token code in your app. The request below lists events for one account: pass the API key in the header and the `grant_id` in the path, plus a required `calendar_id` to pick which calendar.

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

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


const nylas = new Nylas({ apiKey: process.env.NYLAS_API_KEY });

const events = await nylas.events.list({
  identifier: grantId,
  queryParams: { calendarId: "primary" },
});
```

```python [actOnBehalf-Python SDK]

from nylas import Client

nylas = Client(api_key=os.environ.get("NYLAS_API_KEY"))

events = nylas.events.list(
    grant_id,
    query_params={"calendar_id": "primary"},
)
```

Swap `events` for `messages`, `contacts`, or `notetakers` and the model stays the same: same API key, same `grant_id`, different resource path.

## Read a user's calendar on their behalf

Read a connected user's calendar with a `GET /v3/grants/{grant_id}/events` request. This answers the common need to access Google Calendar on behalf of a user after they sign in once. The endpoint requires a `calendar_id`, where `primary` targets the user's default calendar, and it works the same across Google, Microsoft, and other connected providers.

Pass a `start` and `end` window as Unix timestamps to scope the read to a date range, which is what a nightly job wants when it pulls tomorrow's meetings. The call below fetches events from one calendar inside a time window. Nylas returns up to 50 events per page by default, and you page through the rest with the `page_token` cursor from each response.

```bash
curl --request GET \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=primary&start=1735689600&end=1735776000' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```js [readCalendar-Node.js SDK]
const events = await nylas.events.list({
  identifier: grantId,
  queryParams: {
    calendarId: "primary",
    start: "1735689600",
    end: "1735776000",
  },
});
```

```python [readCalendar-Python SDK]
events = nylas.events.list(
    grant_id,
    query_params={
        "calendar_id": "primary",
        "start": "1735689600",
        "end": "1735776000",
    },
)
```

Because the grant already holds calendar scope, you don't re-prompt the user or refresh a token to run this read. It works whether the user is online or asleep.

## Send email as the user

Send mail from a user's own mailbox with `POST /v3/grants/{grant_id}/messages/send`. The message goes out through the connected account, so recipients see the user's real address and the sent copy lands in their provider's Sent folder. You don't configure SMTP or store mailbox credentials, and the same request works across all 6 supported providers.

This is how a reminder, a confirmation, or a reply gets delivered as the user rather than from a generic system address. Give the `subject`, `body`, and a `to` array. The API key and `grant_id` decide which mailbox sends. For the full message model, attachments, and tracking options, see [sending email](/docs/v3/email/).

```bash
curl --request POST \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "subject": "Your meeting reminder",
    "body": "Your 9:00 AM sync is confirmed for tomorrow.",
    "to": [{ "name": "Leyah Miller", "email": "leyah@example.com" }]
  }'
```

```js [sendAsUser-Node.js SDK]
const sent = await nylas.messages.send({
  identifier: grantId,
  requestBody: {
    subject: "Your meeting reminder",
    body: "Your 9:00 AM sync is confirmed for tomorrow.",
    to: [{ name: "Leyah Miller", email: "leyah@example.com" }],
  },
});
```

```python [sendAsUser-Python SDK]
sent = nylas.messages.send(
    grant_id,
    request_body={
        "subject": "Your meeting reminder",
        "body": "Your 9:00 AM sync is confirmed for tomorrow.",
        "to": [{"name": "Leyah Miller", "email": "leyah@example.com"}],
    },
)
```

## Things to know about acting on behalf of users

Acting on behalf of users means your backend holds one long-lived secret, your API key, and uses it against many grants. The model is reliable, but a few operational facts decide whether it stays secure and keeps working. Treat the API key like a production database password: one leaked key exposes every connected account at once.

**Your API key is a backend-only secret.** It authenticates as your whole application, so it must live in a secrets manager or environment variable and never reach a browser, mobile binary, or public repository. If it leaks, rotate it from the Dashboard. Client-side code should call your backend, and your backend calls Nylas, so the key stays on the server every time.

**Tokens refresh automatically.** Nylas stores each provider's access and refresh tokens on the grant and renews them before they expire, so a nightly job keeps working for months with zero refresh code. You hold only the non-secret `grant_id` and your API key, which keeps the riskiest credentials out of your database.

**Handle expired or revoked grants.** A grant stops working when the user changes their password, revokes access at the provider, or the provider invalidates the token, and the API then returns a `401`. Subscribe to the `grant.expired` webhook to catch it early and prompt re-authentication, which preserves the grant ID and sync state. See [handling grant expiry](/docs/cookbook/use-cases/build/handle-grant-expiry/).

**Request least-privilege scopes.** A grant can only do what its scopes allow, so a read-only calendar job should hold read scope, not send-and-modify. Google sorts scopes into 3 tiers, and the restricted tier needs a security assessment before launch. The [scopes reference](/docs/dev-guide/scopes/) lists the exact strings per API.

**Rate limits apply per grant.** Providers cap requests per connected account, so a job that hammers one mailbox can hit a `429` while other grants stay fine. Spread reads across time and use [webhooks](/docs/v3/notifications/) instead of tight polling loops so your server reacts to changes rather than asking for them.

## What's next

- [Connect user accounts with OAuth](/docs/cookbook/use-cases/build/connect-user-accounts-oauth/) for the hosted flow that produces the grant ID
- [Handle grant expiry](/docs/cookbook/use-cases/build/handle-grant-expiry/) to detect and recover revoked grants
- [Granular scopes reference](/docs/dev-guide/scopes/) for the exact scope strings per API
- [Authentication overview](/docs/v3/auth/) for the full grant model and every auth method