> ## Documentation Index
> Fetch the complete documentation index at: https://docs.montereyfinancial.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Exchange an org-scoped API key for a short-lived access token and handle authentication errors.

The client portal gives you two long-lived values:

* `client_id`: the API key prefix
* `client_secret`: the full API key shown once at creation

Exchange them for an access token before calling the API. Send the access token—not the API key—in the `Authorization` header.

## Choose API permissions

Create each API key with only the API permissions your integration needs. The public client portal requires at least one API permission when you create a key. API permissions are fixed at key creation. To change them later, create a replacement key and revoke the old key after you cut over.

Read and write are independent. For example, `payment_methods:write` does not include `payment_methods:read`, and `autopay:read` does not include `autopay:write`.

Legacy or manual zero-permission keys are different. If one of those keys still has a valid organization allowlist, it can exchange for an access token, but every resource endpoint returns `403` with `{"detail":{"error_code":"insufficient_scope"}}`.

| Scope | Description | Resource family |
| - | - | - |
| `organizations:read` | Read organizations visible to the key. | Organizations |
| `persons:read` | Read persons visible through in-scope accounts. | Persons |
| `accounts:read` | Read accounts and account identifier lookups. | Accounts |
| `transactions:read` | Read transaction history, including account transaction lists. | Transactions |
| `payment_methods:read` | Read stored payment methods, including account payment-method lists. | Payment methods |
| `payment_methods:write` | Capture, update, and remove payment methods. | Payment methods |
| `autopay:read` | Read autopay schedules, including account autopay lists. | Autopay |
| `autopay:write` | Create, update, and cancel autopay schedules. | Autopay |
| `payments:write` | Create one-time payments when the endpoint is enabled for your environment. | Payments |
| `external_payments:read` | Read payments your organization recorded through the API. | Collected payments |
| `external_payments:write` | Record and reverse payments your organization collected outside Monterey. | Collected payments |

## Exchange your API key

```http theme={null}
POST /v1/oauth/token HTTP/1.1
Host: api.montereyfinancial.app
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "mfs_AbCd1234",
  "client_secret": "mk_your_key_here"
}
```

You receive an access token valid for 3600 seconds:

```json theme={null}
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600
}
```

There is no refresh token. Your API key is long-lived. Exchange the API key again before the access token expires.

> [!WARNING]
>
> Revoking an API key stops new token exchanges immediately. An access token already issued from that key remains valid until its one-hour expiration.

## Call the API

```bash cURL theme={null}
set -euo pipefail
ACCESS_TOKEN=$(curl --fail-with-body -sS -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "mfs_AbCd1234",
    "client_secret": "mk_your_key_here"
  }' \
  https://api.montereyfinancial.app/v1/oauth/token | jq -er .access_token)

curl -H "Authorization: Bearer $ACCESS_TOKEN" \
  https://api.montereyfinancial.app/v1/accounts
```

```python Python theme={null}
import httpx

base_url = "https://api.montereyfinancial.app"

token_response = httpx.post(
    f"{base_url}/v1/oauth/token",
    json={
        "grant_type": "client_credentials",
        "client_id": "mfs_AbCd1234",
        "client_secret": "mk_your_key_here",
    },
)
token_response.raise_for_status()
access_token = token_response.json()["access_token"]

response = httpx.get(
    f"{base_url}/v1/accounts",
    headers={"Authorization": f"Bearer {access_token}"},
)
response.raise_for_status()
print(response.json()["items"][:3])
```

```typescript TypeScript theme={null}
const baseUrl = "https://api.montereyfinancial.app";

const tokenResponse = await fetch(`${baseUrl}/v1/oauth/token`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    grant_type: "client_credentials",
    client_id: "mfs_AbCd1234",
    client_secret: "mk_your_key_here",
  }),
});
if (!tokenResponse.ok) throw new Error(await tokenResponse.text());
const { access_token: accessToken } = await tokenResponse.json();

const response = await fetch(`${baseUrl}/v1/accounts`, {
  headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) throw new Error(await response.text());
console.log((await response.json()).items.slice(0, 3));
```

Cache the token in memory with its expiry time. Re-mint shortly before expiry. Do not write access tokens to logs or persistent storage.

> [!NOTE]
>
> The same access token also works on `https://secure-api.montereyfinancial.app/v1/payment-methods/cards`. That route requires the `payment_methods:write` scope.

## Organization scoping

An API key is bound to an explicit organization allowlist. Every token minted from that key carries the same allowlist. Account, person, transaction, payment-method, and autopay queries enforce it on every request.

## 401, 403, and 404

| Status | When | Body |
| - | - | - |
| `401 Unauthorized` | Token missing, malformed, expired, signed for another audience, or otherwise invalid | `{"detail": {"error_code": "missing_token" \| "invalid_token"}}` |
| `401 Unauthorized` | Token exchange uses an unknown, revoked, expired API key, or an API key paired with an incorrect secret, or a key with a missing or empty organization allowlist | `{"detail": {"error_code": "invalid_client"}}` |
| `403 Forbidden` | Token is valid but lacks the endpoint's required scope | `{"detail": {"error_code": "insufficient_scope"}}` |
| `403 Forbidden` | `?organization_id=X` targets an org outside the token's allowlist | `{"detail": {"error_code": "organization_out_of_scope"}}` |

An empty result is not a 403. A valid query with no matching rows returns `200` with `{"items": []}`.

Lookups by resource ID—such as `/v1/accounts/{id}`, `/v1/payment-methods/{id}`, and `/v1/autopay/{id}`—return `404` for both nonexistent and out-of-scope IDs. This prevents callers from probing for resources they cannot see.

## Rotate an API key

1. Create a replacement API key with the organization allowlist and API permissions you want.
2. Update your token-exchange configuration with the new `client_id` and `client_secret`.
3. Confirm your integration can mint and use a token from the new key.
4. Revoke the old key.

## What Monterey logs

Monterey may log the API key prefix and resolved organization IDs for audit and troubleshooting. Monterey never logs the API key plaintext or issued access tokens.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.