# Use your own user IDs instead of grant IDs

Source: https://developer.nylas.com/docs/cookbook/use-cases/build/reuse-your-user-ids/

Every Nylas integration starts the same way: a user connects a mailbox, you get back a `grant_id`, and now you own a second identifier for a person you already had an ID for. That `grant_id` spreads. It lands in a database column, then a join in every query that touches mail, then your logs, your support tooling, and the analytics event you fire when someone reads a message.

You can skip that. If you authenticate users through an identity provider, Nylas will link the grant to the `sub` claim in your provider's JWT, and you address the mailbox with your own user ID instead. This recipe covers how the linkage works, how to call the API with it, and where the approach stops being the right choice.

For the opposite case, where you deliberately store the mapping because one user connects several mailboxes, see [connect multiple accounts per user](/docs/cookbook/use-cases/build/multi-account-per-user/).

## How do I call Nylas with my own user ID?

Send your identity provider's access token as the bearer token, put your user's ID in the `X-Nylas-External-User-Id` header, and address the mailbox as `/v3/grants/me`. Nylas resolves `me` to the grant linked to that external ID, so no `grant_id` appears in your request at all.

The header value is whatever your identity provider puts in the JWT's `sub` claim, which is the same ID your application already stores on its user record. Auth0 returns it from `getUser()`, Clerk from `useAuth()`, and WorkOS on the authenticated session.

```bash
curl --request GET \
  --url 'https://api.us.nylas.com/v3/grants/me/messages?limit=5' \
  --header 'Authorization: Bearer <IDP_ACCESS_TOKEN>' \
  --header 'X-Nylas-External-User-Id: <YOUR_USER_ID>'
```

```ts
const token = await auth0.getTokenSilently();
const user = await auth0.getUser();

const res = await fetch(
  "https://api.us.nylas.com/v3/grants/me/messages?limit=5",
  {
    headers: {
      Authorization: `Bearer ${token}`,
      "X-Nylas-External-User-Id": user?.sub ?? "",
    },
  },
);

const { data } = await res.json();
```

The same 2 headers work across the Email, Calendar, and Contacts endpoints. Swap `/messages` for `/events` or `/contacts` and nothing else changes.

## How does Nylas learn my user ID?

The linkage is established once, during the OAuth token exchange, by the `@nylas/connect` library. You give it an `identityProviderToken` callback that returns your provider's JWT. The library forwards that JWT as `idp_claims`, and the `sub` claim is stored on the resulting grant.

The JWT is validated against the JSON Web Key Set (JWKS) endpoint you register in the Dashboard, so a caller can't simply assert someone else's user ID in the header. The signature has to check out against your provider's published keys. This happens once per grant, not on the 2 headers you send afterwards.

```ts
const nylasConnect = new NylasConnect({
  clientId: "<NYLAS_CLIENT_ID>",
  redirectUri: window.location.origin + "/callback",
  identityProviderToken: async () => auth0.getTokenSilently(),
});

// The grant created here is linked to the JWT's sub claim.
await nylasConnect.connect({ method: "popup" });
```

Return a fresh token on every call. The callback runs during each token exchange, and most identity provider SDKs refresh silently. Returning `null` completes the flow without claims, which means no linkage and you're back to storing a grant ID. Throwing fails the exchange with a `NETWORK_ERROR` event.

## What does this save me?

It removes 1 column, 1 join, and 1 class of bug. The column is `grant_id` on your users table. The join is every query that needs it before it can call the API. The bug class is drift between your user record and the Nylas grant, which happens when a mailbox is reconnected and the ID your database holds no longer points anywhere.

Concretely, the version that stores the mapping looks like this on every request:

```ts [comparison-Storing the grant ID]
const user = await db.users.findUnique({ where: { id: session.userId } });
if (!user.nylasGrantId) throw new Error("No mailbox connected");

const res = await fetch(
  `https://api.us.nylas.com/v3/grants/${user.nylasGrantId}/messages`,
  { headers: { Authorization: `Bearer ${process.env.NYLAS_API_KEY}` } },
);
```

```ts [comparison-Reusing your user ID]
const res = await fetch("https://api.us.nylas.com/v3/grants/me/messages", {
  headers: {
    Authorization: `Bearer ${session.idpToken}`,
    "X-Nylas-External-User-Id": session.userId,
  },
});
```

The second version has no database read before the API call, which also means one fewer round trip on a hot path like rendering an inbox.

## When should I not do this?

Don't use this when a single user connects more than 1 mailbox. `/v3/grants/me` resolves an external user ID to a grant, so a user with a work Gmail account and a personal iCloud account needs something that distinguishes the two, and their user ID alone doesn't. Store the mapping instead, as described in [connect multiple accounts per user](/docs/cookbook/use-cases/build/multi-account-per-user/).

Two other cases where the mapping table is the better answer:

- **No identity provider.** The linkage comes from a signed JWT. If your app issues opaque session cookies rather than JWTs, there's nothing for Nylas to validate. You can stand up your own JWKS endpoint, but you then own issuing and rotating the signing keys, which is more work than a `grant_id` column.
- **Server-side batch jobs.** A nightly sync running without a signed-in user has no IdP token to present. Those jobs authenticate with your API key and a grant ID, so they need the mapping regardless. See [act on behalf of a user](/docs/cookbook/use-cases/build/act-on-behalf-of-user/).

A reasonable middle path is to do both: rely on `/v3/grants/me` for user-facing requests, and keep a grants table that your background workers read. You get the clean request path without blocking your batch jobs.

## Things to know about external user IDs

The `X-Nylas-External-User-Id` header and the `idp_claims` field aren't yet in the [API reference](/docs/reference/api/). They work, and they're covered in the [identity provider guides](/docs/v3/auth/nylas-connect/use-external-idp/), but you won't find a parameter entry for them on the endpoint pages.

A few more things worth knowing before you commit to this:

- **Your user ID becomes a Nylas-side identifier.** Pick something stable. An IdP `sub` claim is stable by design; an email address is not, because people change them.
- **Changing identity providers breaks the link.** Grants stay linked to the `sub` values from your old provider, so an Auth0-to-Clerk migration means re-linking every grant. Keeping a grants table alongside makes that migration far easier.
- **The bearer token is your IdP's, not a Nylas token.** Requests authenticate with the same short-lived access token your frontend already holds, so no API key reaches the browser.
- **Grants still expire.** Password changes and revoked access break a grant whether or not you store its ID. Handle it the same way either way, per [handle grant expiry](/docs/cookbook/use-cases/build/handle-grant-expiry/).

## What's next

- [Quickstart: auth with @nylas/connect](/docs/v3/getting-started/nylas-connect/) sets up the library from scratch and compares the 3 configurations
- [External identity providers](/docs/v3/auth/nylas-connect/use-external-idp/) has per-provider setup for Auth0, Clerk, Google, WorkOS, and custom JWKS
- [Connect multiple accounts per user](/docs/cookbook/use-cases/build/multi-account-per-user/) for the case this recipe doesn't cover
- [Multi-tenant OAuth for SaaS apps](/docs/cookbook/use-cases/build/multi-tenant-oauth/) for tenant isolation on top of either model
- [Store OAuth tokens securely](/docs/cookbook/use-cases/build/store-oauth-credentials/) if you do keep a grants table