Skip to content
Skip to main content

Use your own user IDs instead of grant IDs

Last updated:

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.

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.

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

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.

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.

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:

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.

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.

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.

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.

The X-Nylas-External-User-Id header and the idp_claims field aren’t yet in the API reference. They work, and they’re covered in the identity provider guides, 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.