Skip to content
AccountFUNDocs

Errors & rate limits

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

StatusMeaning
200 OKSuccess (GET, POST token exchange, revoke).
201 CreatedA resource was created (POST to a collection).
400 Bad RequestThe request was malformed or failed validation.
401 UnauthorizedMissing, invalid, or expired token; or bad client credentials at the token endpoint.
403 ForbiddenThe token is valid but lacks the required scope, or the user’s role does not permit the action.
404 Not FoundThe resource does not exist in the token’s organization.
409 ConflictA uniqueness or business-rule violation (e.g. duplicate contact name).
429 Too Many RequestsA rate limit was exceeded. See Rate limits.
500 Internal Server ErrorAn unexpected server error. Check the retry guidance below before repeating a mutation.

When a bearer-token request fails authentication or authorization, the API responds with a RFC 6750 challenge in the WWW-Authenticate header.

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

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.

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 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.

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.


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).

ErrorStatusCause
invalid_client401Unknown client_id or wrong client_secret.
invalid_grant400Expired, consumed, or reused authorization code; wrong redirect_uri; expired or revoked refresh token; or the token’s family was invalidated (reuse detected).
unsupported_grant_type400The grant_type is missing or not one of authorization_code / refresh_token.

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


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:

ErrorCause
access_deniedThe 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.


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

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

LimitWhat countsWindow
Per IP addressEvery request300 requests / minute
Failed client authentication, per IP addressMissing or wrong client credentials (token and revoke endpoints combined)10 / minute
Failed requests, per client and IP addressToken-endpoint errors after your client authenticated, such as invalid_grant30 / minute
Per OAuth clientRequests that authenticated as your client, per endpoint120 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.

LimitWindow
Per access token300 requests / minute (60 requests / minute for a sandbox organization)
Per developer client, or per public client and user600 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.

When a limit is exceeded:

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.


ArtifactLifetimeNotes
Authorization code60 secondsSingle-use. Reuse revokes the tokens issued on that code (its family), not the consent.
Access token30 minutesSend as Bearer. Refresh when expired.
Refresh token30 days idle (rolling)No absolute cap. Rotated on every use, which resets the window.

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.

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.