# Errors & rate limits

> HTTP status codes, error response shapes, and rate-limiting behavior for the AccountFUN public API.

Source: https://www.accountfun.com/docs/api/errors-and-limits/

This page covers the error responses, status codes, and rate limits that apply across all AccountFUN API endpoints.

## HTTP status codes

| Status                      | Meaning                                                                                         |
| --------------------------- | ----------------------------------------------------------------------------------------------- |
| `200 OK`                    | Success (GET, POST token exchange, revoke).                                                     |
| `201 Created`               | A resource was created (POST to a collection).                                                  |
| `400 Bad Request`           | The request was malformed or failed validation.                                                 |
| `401 Unauthorized`          | Missing, invalid, or expired token; or bad client credentials at the token endpoint.            |
| `403 Forbidden`             | The token is valid but lacks the required scope, or the user’s role does not permit the action. |
| `404 Not Found`             | The resource does not exist in the token’s organization.                                        |
| `409 Conflict`              | A uniqueness or business-rule violation (e.g. duplicate contact name).                          |
| `429 Too Many Requests`     | A rate limit was exceeded. See [Rate limits](#rate-limits).                                     |
| `500 Internal Server Error` | An unexpected server error. Check the retry guidance below before repeating a mutation.         |

***

## Resource API errors

When a bearer-token request fails authentication or authorization, the API responds with a [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750) challenge in the `WWW-Authenticate` header.

### `401 invalid_token`

The access token is missing, malformed, expired, revoked, or bound to a user who has left the organization.

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api-v1", error="invalid_token"
Content-Type: application/json


{ "error": "invalid_token" }
```

**Recovery:** Refresh the token (if you have a valid refresh token), or re-prompt the user to authorize.

### `403 insufficient_scope`

The token is valid but does not carry the scope required for the endpoint, or the user’s live role is below the scope’s ceiling.

```http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="invoices:write"
Content-Type: application/json


{ "error": "insufficient_scope" }
```

**Recovery:** The `scope` header tells you what was needed. You must re-authorize with the broader scope (a refresh will not help — scopes are fixed at consent time). For an **opt-in** scope — the approve scopes, `payments:write`, `bank_transactions:write`, `prepayments:write`, and the `credit_notes:settle` / `prepayments:settle` scopes — the user must also tick it on the consent screen, and their role must allow it.

### Validation errors (`400`)

For request-body validation failures, the response includes a `details` object mapping each invalid field to its error messages:

```json
{
  "error": "Validation failed",
  "details": {
    "contactId": ["Contact ID is required"],
    "dueDate": ["Due date must be on or after issue date"]
  }
}
```

For query-parameter validation, the shape is the same but keyed under the query field names.

A body that isn’t a JSON object returns `400` with `"Request body must be valid JSON"` (or `"… must be a JSON object"`). Every id is a UUID: a malformed id in the body or query is a `400` naming the field (`"lines.0.accountId": ["Must be a valid id"]`), and a malformed id in the path is a `404`, the same as an id that doesn’t exist.

***

## OAuth token-endpoint errors

The token endpoint (`/api/v1/oauth/token`) returns RFC 6749 §5.2 errors. Every failure returns the **same uniform shape** `{ "error": "..." }` so a client cannot enumerate which field was wrong (no information leak).

| Error                    | Status | Cause                                                                                                                                                            |
| ------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_client`         | `401`  | Unknown `client_id` or wrong `client_secret`.                                                                                                                    |
| `invalid_grant`          | `400`  | Expired, consumed, or reused authorization code; wrong `redirect_uri`; expired or revoked refresh token; or the token’s family was invalidated (reuse detected). |
| `unsupported_grant_type` | `400`  | The `grant_type` is missing or not one of `authorization_code` / `refresh_token`.                                                                                |

A `401 invalid_client` includes `WWW-Authenticate: Basic realm="oauth"`.

invalid\_grant is broad on purpose

`invalid_grant` covers several distinct conditions because distinguishing them would let an attacker probe token validity. Treat any `invalid_grant` as “this grant is dead” and recover by re-authorizing.

***

## Authorize-endpoint errors

Problems with an authorize request — `response_type` isn’t `code`, `state` or a valid PKCE `code_challenge` is missing, the `redirect_uri` doesn’t match a registered URI, or none of the requested scopes is registered on your client — are shown on an AccountFUN error page. AccountFUN does **not** redirect back to your app with them. Requested scopes your client isn’t registered for are ignored rather than rejected.

The only error delivered to your `redirect_uri` (per RFC 6749 §4.1.2.1, with your `state` echoed back) is:

| Error           | Cause                                            |
| --------------- | ------------------------------------------------ |
| `access_denied` | The user clicked **Deny** on the consent screen. |

A consent can also end on an error page after the user clicks **Allow** — for example when their role in the selected organization can’t grant any of the requested access.

***

## Rate limits

The API applies rate limiting at two layers to protect the service from abuse.

### Token endpoint

These limits apply to the token endpoint (`/api/v1/oauth/token`) and the revoke endpoint (`/api/v1/oauth/revoke`).

| Limit                                        | What counts                                                                    | Window                |
| -------------------------------------------- | ------------------------------------------------------------------------------ | --------------------- |
| Per IP address                               | Every request                                                                  | 300 requests / minute |
| Failed client authentication, per IP address | Missing or wrong client credentials (token and revoke endpoints combined)      | 10 / minute           |
| Failed requests, per client and IP address   | Token-endpoint errors after your client authenticated, such as `invalid_grant` | 30 / minute           |
| Per OAuth client                             | Requests that authenticated as your client, per endpoint                       | 120 requests / minute |

Only requests that prove your client secret count against your client’s limit, so someone who knows your `client_id` (it is in every authorization URL) can’t lock your app out. The strict limit is on failed client authentication: an IP address that keeps sending a wrong or stale secret is paused for the rest of the minute.

### Resource endpoints

| Limit                                               | Window                                                                                                                  |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Per access token                                    | 300 requests / minute (60 requests / minute for a [sandbox](https://www.accountfun.com/docs/api/sandbox/) organization) |
| Per developer client, or per public client and user | 600 requests / minute                                                                                                   |

For a developer client, the client limit aggregates all its tokens. Shared public clients (including dynamically registered MCP connectors) have a separate client bucket per user, so one user’s traffic does not throttle everyone using that connector. The per-token limit still applies independently.

### `429` response

When a limit is exceeded:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 28
Content-Type: application/json


{ "error": "Too many requests. Please try again later." }
```

The `Retry-After` header gives the number of seconds to wait before retrying.

Implement exponential backoff

Honor `Retry-After` on `429`s, and use exponential backoff with jitter for retryable failures. For mutations, follow the idempotency guidance below before retrying: a missing response does not prove that nothing changed.

***

## Token lifetimes (reference)

| Artifact           | Lifetime               | Notes                                                                                   |
| ------------------ | ---------------------- | --------------------------------------------------------------------------------------- |
| Authorization code | 60 seconds             | Single-use. Reuse revokes the tokens issued on that code (its family), not the consent. |
| Access token       | 30 minutes             | Send as `Bearer`. Refresh when expired.                                                 |
| Refresh token      | 30 days idle (rolling) | No absolute cap. Rotated on every use, which resets the window.                         |

***

## Best practices summary

### Token storage

* Store access and refresh tokens **server-side**, encrypted at rest.
* Never expose tokens to the browser, mobile client, or any untrusted code.
* Discard the old refresh token the instant you’ve rotated it.

### Making requests

* Always send `Authorization: Bearer <token>` — never put tokens in URLs.
* Treat monetary fields as **strings** (never numbers) to avoid rounding.
* Use `Content-Type: application/json` for resource POSTs and `application/x-www-form-urlencoded` for OAuth endpoints.

### Error handling

* On `401 invalid_token`: try refreshing; if that fails, re-authorize.
* On `403 insufficient_scope`: re-authorize with the broader scope.
* On `429`: honor `Retry-After` and back off.
* On `5xx`: back off; retry reads, and check idempotency or the resulting record before repeating a mutation.

### Security

* **Verify `state`** on every authorize callback (CSRF defense).
* **Verify `redirect_uri`** matches what you registered at client setup.
* **Rotate the `client_secret`** if you suspect it has leaked (from the Developers area in AccountFUN).
* **Revoke tokens** via the revoke endpoint when a user disconnects your app.
* Use **HTTPS** for all traffic and all redirect URIs.

### Idempotency and retries

* **Create invoices, bills and expense claims**, and the **money endpoints**, support an `Idempotency-Key` header. Send a unique key (up to 255 characters from `A–Z a–z 0–9 . _ -`) with `POST /invoices`, `/bills`, `/expense-claims`, `/payments`, `/bank-transactions`, `/transfers`, `/credit-notes` (create, allocate, refund) and `/prepayments` (create, allocate, refund). A retry with the same key, endpoint and body **replays the stored response** instead of repeating the operation.

  * The key and its response are kept for **24 hours**. The same key with a **different body** returns `409` (`idempotency_fingerprint_mismatch`); a retry while the first request is still running returns `409` (`idempotency_in_flight`).
  * A `5xx` is never stored, so a failed request can be retried with the same key.

* The rest of the API is **not** idempotent on POST. A retried create can produce a duplicate. Use a natural key (like a unique reference or invoice number) to deduplicate, or check for an existing record before creating.

* **Do not blindly retry an authorization-code exchange after a lost response.** If the first exchange committed, reusing that code returns `invalid_grant` and revokes its token family. Start a new authorization flow if the outcome is unknown. A failed exchange that rolled back leaves the code unconsumed.

### AI assistants: money guardrails

MCP money tools are refused server-side **before anything runs**, and the refusal comes back as a tool error (not an HTTP status) explaining why:

* **Missing confirmation** — the 18 destructive money tools require `confirm: true` in the call; without it the call is refused (`refused_confirm`) and nothing is performed. The assistant is told to ask the user first, then retry. `submit_prepayment` and the non-destructive tools don’t need it.
* **Organization policy** — an admin can set **Settings → Organization → AI Assistants → Money Actions** to **Ask the Assistant to Confirm** (default), **Allow** or **Block**. Block is a kill switch: every money tool is refused immediately (`refused_policy`). The policy is fail-closed to Confirm.
* **Velocity** — money tools are capped at **10 per minute and 100 per day per connection (grant)**. An over-limit call is refused with a retry-after hint (`rate_limited`), and only calls that pass the policy and confirmation checks consume quota.
* **Failed** — an unexpected error mid-call (`failed`) means the assistant should check the record in AccountFUN before retrying, in case part of it landed.

Every outcome (`executed`, `refused_policy`, `refused_confirm`, `rate_limited`, `failed`) is recorded as an `mcp.money_action` security event for the organization’s audit trail. See [MCP money guardrails](https://www.accountfun.com/docs/api/mcp/#guardrails).
