# Authenticate from a headless server

Source: https://developer.nylas.com/docs/cookbook/use-cases/build/headless-server-auth/

A cron job, a worker process, or a virtual machine has no screen and no browser, so it cannot run the interactive consent flow that asks a user to approve access at their provider. When you already hold a provider OAuth refresh token from your own authentication code, you skip the browser entirely and hand that token straight to Nylas to mint a grant. This recipe covers Bring Your Own Authentication, secure secret storage on servers, and refresh token rotation.

This is the no-browser counterpart to [OAuth for a desktop email app](/docs/cookbook/use-cases/build/desktop-oauth/), which still opens a system browser with PKCE. If your server can redirect a human to a provider login at any point, use the hosted flow in [Connect user accounts with OAuth](/docs/cookbook/use-cases/build/connect-user-accounts-oauth/) instead.

## How do I authenticate without a browser flow?

A headless server authenticates by sending a provider refresh token it already holds to the `POST /v3/connect/custom` endpoint. Nylas exchanges that token, stores it on a grant, and returns a `grant_id` you reuse for every request. No redirect, no consent screen, no port to listen on.

This path is called Bring Your Own Authentication. It fits two common situations: you run your own OAuth code and want to migrate users into Nylas, or a backend service obtains tokens through a provider's service account or device flow and never has a user present. The request carries just 2 fields, the required `provider` value and a `settings` object holding the `refresh_token`. Everything else about the grant, including the connected email address and scopes, is read from the token. Because no human approves anything at request time, the token you supply must already carry the scopes your features need. The [Bring Your Own Authentication guide](/docs/v3/auth/bring-your-own-authentication/) documents the full request shape and the IMAP credential variant.

```bash
curl --request POST \
  --url "https://api.us.nylas.com/v3/connect/custom" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "provider": "microsoft",
    "settings": {
      "refresh_token": "<REFRESH_TOKEN>"
    }
  }'


```

## What does the custom connect response return?

The `POST /v3/connect/custom` call returns a grant object whose `id` is the `grant_id` your headless service stores and reuses. The response also reports `grant_status`, the resolved `email`, and the `scope` array derived from the token you supplied, so you can confirm the connection succeeded before any worker runs.

A `grant_status` of `valid` means the grant is ready and the underlying token refreshes automatically while you use an API key. The response below is the shape returned for a Microsoft account; the `provider` and `scope` values change per provider, but the `id` field is always the identifier you persist. Store that single value against your user record and discard the raw refresh token from your application memory once Nylas holds it. The Microsoft example below resolves 3 scopes from the supplied token, and each grant maps to exactly one connected account, so a service syncing 500 users holds one grant ID per user. For the field-by-field breakdown, see the [Bring Your Own Authentication guide](/docs/v3/auth/bring-your-own-authentication/).

```json
{
  "request_id": "5fa64c92-e840-4357-86b9-2aa364d35b88",
  "data": {
    "id": "<NYLAS_GRANT_ID>",
    "provider": "microsoft",
    "grant_status": "valid",
    "email": "leyah@example.com",
    "scope": ["Mail.Read", "User.Read", "offline_access"],
    "created_at": 1617817109,
    "updated_at": 1617817109
  }
}
```

## Where do I store secrets on a server with no UI?

Keep the provider refresh token and your Nylas API key in a dedicated secrets manager, never in a config file, a container image layer, or a checked-in environment file. On a headless host the operating system keychain is unavailable, so the standard answer is a managed secret store such as AWS Secrets Manager, Google Secret Manager, HashiCorp Vault, or Kubernetes Secrets backed by an external provider.

Inject secrets at runtime and hold them in process memory only. A leaked container image or a backup of the host filesystem then exposes nothing reusable, because the secret lives in the manager and not on disk. Grant the worker an identity scoped to read only the keys it needs, which limits blast radius if one service is compromised. Once a grant exists, the most valuable secret to protect is your API key, since a single key authenticates calls across every grant in the project. Many teams rotate that key on a 90 day cycle. See [Manage API keys](/docs/cookbook/use-cases/build/manage-api-keys/) for creating and rotating keys, and the [authentication overview](/docs/v3/auth/) for how grants and keys relate.

```bash
# Inject the API key at runtime from a secrets manager, never from a file.
export NYLAS_API_KEY="$(aws secretsmanager get-secret-value \
  --secret-id prod/nylas/api-key \
  --query SecretString --output text)"
```

## How do I rotate refresh tokens without downtime?

Once Nylas holds the grant, you do not rotate the provider refresh token yourself. The API refreshes the underlying token before it expires while you call it with your API key, so a long-running worker keeps syncing without any refresh code. A grant stays valid until the user or the provider revokes it.

You still rotate your own credentials. Rotate the Nylas API key on a schedule by creating a new key, deploying it to your secrets manager, and revoking the old one after a short overlap so in-flight requests never fail. If a grant's provider token is revoked upstream, the `grant_status` flips and API calls return an authentication error, which is your signal to obtain a fresh token and call `POST /v3/connect/custom` again for that account. Build a job that checks grant status on a cadence, since a 1 hour polling interval catches most expirations well before users notice. For the token mechanics and how to read expiry, see [Get and refresh OAuth tokens](/docs/cookbook/use-cases/build/get-refresh-tokens/).

```bash
curl --request POST \
  --url "https://api.us.nylas.com/v3/connect/custom" \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "provider": "microsoft",
    "settings": { "refresh_token": "<NEW_REFRESH_TOKEN>" }
  }'
```

## What's next

- [Bring Your Own Authentication](/docs/v3/auth/bring-your-own-authentication/) for the full custom connect request and the IMAP credential variant
- [Get and refresh OAuth tokens](/docs/cookbook/use-cases/build/get-refresh-tokens/) to read token expiry and keep grants alive
- [Connect user accounts with OAuth](/docs/cookbook/use-cases/build/connect-user-accounts-oauth/) for the hosted flow when a browser is available
- [OAuth for a desktop email app](/docs/cookbook/use-cases/build/desktop-oauth/) for the PKCE flow with a system browser
- [Manage API keys](/docs/cookbook/use-cases/build/manage-api-keys/) for creating and rotating server-side keys
- [Authentication overview](/docs/v3/auth/) for the grant model and every auth method