# Authenticate API requests

> Authenticate Moov API requests with Basic auth on your server or scoped OAuth2 access tokens for Moov.js and Drops, plus token refresh and 401/403 fixes.

> AI agents: search and read all Moov docs with the docs MCP server at https://docs.moov.io/mcp (no API key). Setup: https://docs.moov.io/guides/developer-tools/mcp-ai/docs-mcp/index.md

Calling Moov from your own server? Use Basic authentication with your API key. Calling from a browser or mobile app (Moov.js, Drops)? Mint a short-lived, scoped access token on your server and send only the token to the client.

Both methods start with an [API key](/guides/get-started/api-authentication/api-keys/index.md), which has a public key and a secret key. For the full reference, see [authentication options](/api/authentication/api-authentication/index.md), [access tokens](/api/authentication/access-tokens/index.md), and [scopes](/api/authentication/scopes/index.md).

| Method               | Header                                               | Use it for                                                                                                                                                                                                  |
|----------------------|------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Basic authentication | `Authorization: Basic <base64(publicKey:secretKey)>` | Server-side code you control. The secret key never leaves your server.                                                                                                                                      |
| OAuth2 access token  | `Authorization: Bearer <access_token>`               | Browser and mobile code: [Moov.js](/moovjs/index.md), [Drops](/moovjs/drops/index.md), and the client-side [TypeScript SDK](/sdks/typescript/index.md). Also useful on the server when you want a token limited to specific scopes. |

Never put your secret key in a browser, mobile app, or public repository. If code runs on a device you don't control, mint an access token on your server and send only the token to the client.

## Basic authentication (server-side)

Set the `Authorization` header to `Basic` followed by the Base64 encoding of your public key and secret key joined by a colon. Most HTTP clients do the encoding for you. With curl, use `-u`:

```zsh
curl -X GET "https://api.moov.io/accounts/{accountID}" \
  -u "{publicKey}:{secretKey}" \
  -H "X-Moov-Version: v2026.07.00"
```

Basic authentication uses your secret key directly and isn't narrowed by scopes, which is why it belongs only in server-side code. The Moov [server-side SDKs](/sdks/index.md) take the same public and secret key pair.

## OAuth2 access tokens

An access token is a short-lived JSON Web Token (JWT) that carries a list of [scopes](/api/authentication/scopes/index.md), the permissions the holder has. Your server requests the token with your API key, then uses it or hands it to a browser.

### Request a token

Call the create access token [endpoint](/api/authentication/access-tokens/create/index.md) from your server with the `client_credentials` grant and a space-delimited list of scopes. Pass your public key and secret key as the Basic auth username and password (or as `client_id` and `client_secret` in the body).

```zsh
curl -X POST "https://api.moov.io/oauth2/token" \
  -u "{publicKey}:{secretKey}" \
  -H "Origin: https://your-domain.com" \
  -H "X-Moov-Version: v2026.07.00" \
  --data-urlencode "grant_type=client_credentials" \
  --data-urlencode "scope=/accounts.write /accounts/{accountID}/profile.read"
```

The endpoint accepts a form-encoded body, as shown, or a JSON body with the same fields. The response contains the token:

```json
{
  "token_type": "Bearer",
  "access_token": "eyJhbGciOiJFZERTQSIsImN0eSI6IkpXVCIsImtpZCI6IkR...",
  "refresh_token": "eyJhbGc0eSI6TQSIsImN0kpXVCIsImtp6IkpXVsImtpZC0a...",
  "expires_in": 1736964352,
  "scope": "/accounts.write /accounts/{accountID}/profile.read"
}
```

### Use the token

Send the token in the `Authorization` header with the `Bearer` scheme. Include the same `Origin` (or `Referer`) header you used to request it:

```zsh
curl -X GET "https://api.moov.io/accounts/{accountID}" \
  -H "Authorization: Bearer {access_token}" \
  -H "Origin: https://your-domain.com" \
  -H "X-Moov-Version: v2026.07.00"
```

In a browser, pass the token to Moov.js or a Drop instead; they set the header for you. See [Moov.js](/moovjs/index.md) for initialization.

The examples in these guides use `Authorization: Bearer {token}`. If you call the API from your server, you can replace that header with Basic authentication (`-u "{publicKey}:{secretKey}"`) in any example.

### Scopes basics

- Request only the scopes the task needs. A token used to create an account needs `/accounts.write`; a token used to add a card to that account needs `/accounts/{accountID}/cards.write`.
- Most scopes include an account ID, so you need the ID before you request the token. If you're creating an account and then acting on it, request one token for `/accounts.write`, create the account, then request a second token with scopes for the new account ID.
- For transfer scopes (`/accounts/{accountID}/transfers.write` and `.read`), the account ID is your **partner** account ID. For all other scopes, it's the user's account ID.
- Each API reference page lists the scope that endpoint requires.

The [scopes reference](/api/authentication/scopes/index.md) lists every scope and explains when to generate [multiple tokens](/api/authentication/scopes/index.md#generating-multiple-tokens).

### Origin and Referer domains

> **Warning:** Requests to `POST /oauth2/token`, and every API call that uses the resulting token, must include an `Origin` or `Referer` header whose domain matches a domain registered on your API key.

- Register domains in the Dashboard under **Developers &gt; API keys** when you [create the key](/guides/get-started/api-authentication/api-keys/index.md#create-a-new-api-key).
- `Origin` is the scheme and domain only, with no path (for example, `https://your-domain.com`). `Referer` may include a path.
- Send only one domain per request: the one the request comes from. If both headers are present, `Origin` takes precedence.
- `localhost` is not an accepted domain. For local development, register and use a tunnel domain (such as ngrok) or a deployed preview domain.
- Browsers set `Origin` automatically. Server-side code that requests a token must set it explicitly.

The create access token [endpoint](/api/authentication/access-tokens/create/index.md) also accepts an optional `client_type` field. It defaults to `web`; `device` and `service` clients don't require browser origin binding.

### Token expiry and refresh

Access tokens are short-lived and meant for a single task. The `expires_in` field in the response tells you when the token expires. When it does, you have two options:

- Request a new token with `grant_type=client_credentials`. This is the simplest approach, and lets you change the scopes.
- Exchange the `refresh_token` for a new access token. A refreshed token keeps the original client type.

```zsh
curl -X POST "https://api.moov.io/oauth2/token" \
  -u "{publicKey}:{secretKey}" \
  -H "Origin: https://your-domain.com" \
  -H "X-Moov-Version: v2026.07.00" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "refresh_token={refresh_token}"
```

When a user signs out, or you no longer need a token, revoke the access token or refresh token with the revoke access token [endpoint](/api/authentication/access-tokens/revoke/index.md).

## Test mode and production mode keys

Test mode and production mode each have their own API keys, and the key you authenticate with determines the mode. Both modes use the same base URL, `https://api.moov.io`. Accounts, sources, transfers, and webhooks created in one mode aren't available in the other, so use IDs from the same mode as your key.

Keep keys in environment variables or a secrets manager, never in source code. When you [go live](/guides/go-live/production-mode/index.md), swap in production keys, register your production domains on them, and replace any hard-coded test account IDs.

## Common authentication errors

| Status                          | Likely cause                                                                                                       | Fix                                                                                                      |
|---------------------------------|--------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|
| `401`                           | The `Authorization` header is missing, malformed, or the token has expired                                         | Check the `Basic` or `Bearer` prefix, re-encode `publicKey:secretKey`, or request a new token            |
| `401`                           | The `Origin` or `Referer` header is missing, includes a path in `Origin`, or doesn't match a domain on the API key | Send exactly one registered domain, using the same header on the token request and the calls that follow |
| `401`                           | The API key was deleted, or the secret key is wrong                                                                | Create a new key in the Dashboard; the secret key can't be viewed again after creation                   |
| `403`                           | The token doesn't include the scope the endpoint requires, or the scope names a different account ID               | Check the endpoint's required scope in the API reference and request a token with it                     |
| `422` from `POST /oauth2/token` | Invalid `scope` or `refresh_token` value                                                                           | Check scope spelling (scopes start with `/`) and that the refresh token is current                       |
| `404` on any request            | Malformed `X-Moov-Version` value (for example, `2022`), or an ID from the other mode                               | Use `v2026.07.00`, and confirm the key and the ID are from the same mode                                 |

The domain in `Origin` or `Referer` is embedded in the access token when it's created, so the header must be on the `POST /oauth2/token` request itself, not only on later calls. If it's missing or doesn't match a registered domain, every call made with that token fails with a `401`.

### CORS errors and failed browser requests

If a request from Moov.js, a Drop, or other client-side code fails with a CORS error, check that:

- The page making the request is served from a secure `https` domain. `localhost` isn't treated as secure, even if your browser shows it as `https`.
- That domain is on your API key's allowed domain list. See [API keys](/guides/get-started/api-authentication/api-keys/index.md).
- The access token was created with every scope the request needs, and it's included in the request.

Every response includes an `x-request-id` header. Include it when you [contact support](https://support.moov.io/) about an authentication problem.

## Next steps

- [Scopes](/api/authentication/scopes/index.md): Every scope and the endpoints it unlocks.
- [Quick start](/guides/get-started/quick-start/index.md): Onboard your first account and take a payment.
- [Recipes](/guides/get-started/recipes/index.md): End-to-end API walkthroughs that use these headers.
- [Moov.js](/moovjs/index.md): Use access tokens from the browser.

