Authenticate API requests
Both methods start with an API key, which has a public key and a secret key. For the full reference, see authentication options, access tokens, and scopes.
| 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, Drops, and the client-side TypeScript SDK. 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:
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 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, 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 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).
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:
{
"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:
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 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.writeand.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 lists every scope and explains when to generate multiple tokens.
Origin and Referer domains
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 > API keys when you create the key.
Originis the scheme and domain only, with no path (for example,https://your-domain.com).Referermay include a path.- Send only one domain per request: the one the request comes from. If both headers are present,
Origintakes precedence. localhostis not an accepted domain. For local development, register and use a tunnel domain (such as ngrok) or a deployed preview domain.- Browsers set
Originautomatically. Server-side code that requests a token must set it explicitly.
The create access token endpoint 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_tokenfor a new access token. A refreshed token keeps the original client type.
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.
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, 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
httpsdomain.localhostisn't treated as secure, even if your browser shows it ashttps. - That domain is on your API key's allowed domain list. See API keys.
- 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 about an authentication problem.