Errors & rate limits
This page covers the error responses, status codes, and rate limits that apply across all AccountFUN API endpoints.
HTTP status codes
Section titled “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. |
500 Internal Server Error | An unexpected server error. Check the retry guidance below before repeating a mutation. |
Resource API errors
Section titled “Resource API errors”When a bearer-token request fails authentication or authorization, the API
responds with a RFC 6750
challenge in the WWW-Authenticate header.
401 invalid_token
Section titled “401 invalid_token”The access token is missing, malformed, expired, revoked, or bound to a user who has left the organization.
HTTP/1.1 401 UnauthorizedWWW-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
Section titled “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/1.1 403 ForbiddenWWW-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)
Section titled “Validation errors (400)”For request-body validation failures, the response includes a details object
mapping each invalid field to its error messages:
{ "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
Section titled “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".
Authorize-endpoint errors
Section titled “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
Section titled “Rate limits”The API applies rate limiting at two layers to protect the service from abuse.
Token endpoint
Section titled “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
Section titled “Resource endpoints”| Limit | Window |
|---|---|
| Per access token | 300 requests / minute (60 requests / minute for a 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
Section titled “429 response”When a limit is exceeded:
HTTP/1.1 429 Too Many RequestsRetry-After: 28Content-Type: application/json
{ "error": "Too many requests. Please try again later." }The Retry-After header gives the number of seconds to wait before retrying.
Token lifetimes (reference)
Section titled “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
Section titled “Best practices summary”Token storage
Section titled “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
Section titled “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/jsonfor resource POSTs andapplication/x-www-form-urlencodedfor OAuth endpoints.
Error handling
Section titled “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: honorRetry-Afterand back off. - On
5xx: back off; retry reads, and check idempotency or the resulting record before repeating a mutation.
Security
Section titled “Security”- Verify
stateon every authorize callback (CSRF defense). - Verify
redirect_urimatches what you registered at client setup. - Rotate the
client_secretif 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
Section titled “Idempotency and retries”- Create invoices, bills and expense claims, and the money endpoints,
support an
Idempotency-Keyheader. Send a unique key (up to 255 characters fromA–Z a–z 0–9 . _ -) withPOST /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 returns409(idempotency_in_flight). - A
5xxis never stored, so a failed request can be retried with the same key.
- The key and its response are kept for 24 hours. The same key with a
different body returns
- 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_grantand 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
Section titled “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: truein 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_prepaymentand 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.