Using the APIErrors

Using the API

Errors

The error shape every endpoint uses, and every error code with its HTTP status, when it happens and how to fix it.

When a request fails, the API answers with an HTTP status code and a JSON body in one fixed shape. The code in that body is stable: write your error handling against it, not against the message. This page explains the shape, then lists every code.

The error shape

Error response
{
  "error": {
    "code": "document_has_payments",
    "message": "This invoice has recorded payments and cannot be voided. Create a credit note instead.",
    "details": { "document_id": "0193f2b4-5a6b-7c8d-9e0f-1a2b3c4d5e6f", "amount_paid_minor": "50000" },
    "request_id": "req_0193f2a2c0e3",
    "docs_url": "https://docs.freeinvoiceautomation.com/errors/document_has_payments"
  }
}
errorobjectrequired

The error.

Validation issues

validation_failed (422)
{
  "error": {
    "code": "validation_failed",
    "message": "Some fields are not valid.",
    "details": {
      "issues": [
        { "path": ["lines", 0, "quantity"], "message": "Must be greater than 0" },
        { "path": ["currency"], "message": "Required" }
      ]
    },
    "request_id": "req_0193f2a2c0e4"
  }
}

Each issue has a path (field names and array positions from the top of the body) and a message.

What errors never contain

Secrets never appear in errors or in our logs: no API keys, passwords, tokens or connection secrets.

Handling errors in code

const response = await fetch(url, options);
if (!response.ok) {
  const { error } = await response.json();
  switch (error.code) {
    case "rate_limited":
      await sleep(Number(response.headers.get("Retry-After")) * 1000);
      return retry();
    case "confirmation_required":
      return askPersonToConfirm(error.details);
    case "validation_failed":
      throw new Error(error.details.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("\n"));
    default:
      throw new Error(`${error.code} (${error.request_id}): ${error.message}`);
  }
}

Which errors to retry

Retry?Codes
Yes, with the same Idempotency-Key and backoffrate_limited (after Retry-After), idempotency_in_progress, upstream_error, internal_error, maintenance
After a person actsfresh_signin_required, confirmation_required, quota_exceeded, ai_credits_exhausted, not_configured
No; fix the requestevery other code

Summary table

The codes and HTTP statuses are proposed in the API specification; the behaviours behind them are fixed.

Request errors

validation_failed

HTTP 422. The body or query did not pass input validation: a required field is missing, a value has the wrong type or format, a money amount is a float, or a value is out of range.

Fix: read details.issues. Each issue names the field by path and says what is wrong. Correct those fields and send the request again with a new Idempotency-Key. For money rules see Money and currencies.

invalid_json

HTTP 400. The request body is not valid JSON.

Fix: check for trailing commas, unquoted keys or a body cut short. Send Content-Type: application/json and serialise the body with a JSON library rather than by hand.

Authentication errors

unauthenticated

HTTP 401. There is no session, API key or token, or the one sent is wrong, expired or revoked. This is also what an API key or MCP token returns after its creator was deactivated or removed from the account, because those keys are revoked.

Fix: send Authorization: Bearer <key> with a current key. If the key was revoked or has expired, create a new one in Settings > API keys. See Authentication.

fresh_signin_required

HTTP 401. The action is sensitive and the session is not fresh enough. Examples: connecting a payment rail, deleting a business, changing billing, creating an auto_send key, buying a credit pack, deleting the account.

Fix: sign in again in the web app and repeat the action there. API keys cannot perform fresh-sign-in actions (proposed).

two_factor_required

HTTP 401. A route that requires two-factor authentication was called from a session without it. This only applies to our internal administration routes, not to customer accounts or API keys.

Fix: none for API users; you will not see this code from your own integrations.

Permission and plan errors

permission_denied

HTTP 403. The person behind the request (or the creator of the API key) does not hold the permission key the action needs, in this account.

Fix: ask an Owner or a member with team.manage to give your role the key, or to run the action for you. Remember that an API key is limited to its creator's current permissions.

scope_missing

HTTP 403. The API key or MCP token was not given the scope (permission key) this action needs, even though its creator may hold it.

Fix: create a new key with the extra scope in Settings > API keys, or reconnect your MCP client and approve the scope on the consent screen. Keys cannot gain scopes after creation.

business_out_of_scope

HTTP 403. The caller cannot reach this business. Either the member's business scope does not include it, or the API key is restricted to other businesses.

Fix: use a business_id you can reach (see List businesses), or ask the Owner to widen your business scope.

feature_locked

HTTP 403. The feature is not available on the account's current plan or setup. Examples: the AI agent on Free with no welcome credits, packs or own AI key; viewing the audit log on Free; editing roles on Free.

Fix: follow the suggestion in the message: add a card for welcome credits, buy a credit pack, add your own AI key, or upgrade to Team.

email_not_verified

HTTP 403. The platform mailer only sends for accounts whose email address is verified.

Fix: open the verification email we sent and click the link, or send it again from the app. Alternatively connect your own SMTP.

plan_limit_reached

HTTP 403. A plan limit was reached, such as the number of members included, or a fair-use limit on Free.

Fix: remove or deactivate a member, add a paid seat on Team, or wait for the monthly fair-use counter to reset. See Fair-use limits and Member seats.

permission_not_grantable

HTTP 403. A role change would grant a permission key that you do not hold yourself, or would put an Owner-only key (account.delete, ownership transfer) on a role other than Owner.

Fix: only grant keys you hold. Ask the Owner to make the change if it needs keys you lack. Owner-only keys cannot be granted to any other role.

role_locked

HTTP 409. The Owner role cannot be edited, reset or deleted.

Fix: edit another role, or create a custom role.

role_in_use

HTTP 409. A role that still has members cannot be deleted.

Fix: move its members to another role with Update a member, then delete the role.

last_owner

HTTP 409. The change would leave the account without an Owner.

Fix: every account keeps at least one Owner. Transfer ownership first if the current Owner is leaving.

member_deactivated

HTTP 403. The member is deactivated in this account, either by the Owner or a member with team.manage, or because a Team account dropped to Free. Their old API keys and MCP tokens were revoked and return unauthenticated.

Fix: ask the Owner to reactivate the member in Settings > Team, then create new keys. See Deactivate and remove.

Usage, email and billing errors

platform_mail_paused

HTTP 403. The account moved to Free and had already sent 20 or more platform emails this month, so the platform mailer is paused until next month. details.suggestion is "connect_smtp".

Fix: connect your own SMTP, which has no monthly quota, or wait for the next month. Drafts are kept.

quota_exceeded

HTTP 403. The monthly platform email quota is used up (20 on Free, 100 on Team). details.suggestion is "connect_smtp".

Fix: connect your own SMTP, send by share link instead, or wait for the reset on the first of the month. See Platform email.

ai_credits_exhausted

HTTP 403. The account has no AI credits left for the agent.

Fix: buy a credit pack (1,000 credits for $5), upgrade to Team, or use your own AI key, which bypasses credits.

billing_card_required

HTTP 409. Free welcome credits need a verified billing card first. The card gets a $0 verification and is never charged.

Fix: add a card in Settings > Billing. See Billing card.

welcome_credits_not_eligible

HTTP 409. This card or account already received welcome credits, or the first month after sign-up has passed.

Fix: welcome credits are given once per card and per account. Use a credit pack, Team credits or your own AI key.

card_not_eligible

HTTP 422. The card cannot unlock the Free welcome credits because its funding type is blocked (by default, prepaid cards).

Fix: add a regular debit or credit card for the welcome credits. Prepaid cards can still be used to buy credit packs.

money_back_not_eligible

HTTP 409. A money-back refund was asked for, but it is not the first Team payment, the payment was more than 30 days ago, or the account already used the guarantee.

Fix: the guarantee covers the first Team payment only, within 30 days, once per account. See 30-day money-back.

Lookup and token errors

not_found

HTTP 404. The resource does not exist, was deleted, or is not visible to you. Records of other accounts always answer not_found, never a permission error, so their existence is not revealed.

Fix: check the id and the account your key belongs to.

token_invalid

HTTP 404. A public token (pay page, estimate page, portal link) is unknown.

Fix: check that the full link was copied. Ask the business for a new link.

token_expired

HTTP 410. The public token has expired.

Fix: the business creates a new share link, or the client asks for a new portal link.

token_revoked

HTTP 410. The public token was revoked, for example because the invoice was voided.

Fix: the old link will not work again. The business shares the current document if there is one.

State errors

conflict

HTTP 409. The request conflicts with the current state of the resource in a way that has no more specific code.

Fix: fetch the resource again, check its status, and retry with a request that fits it.

idempotency_key_reused

HTTP 409. The Idempotency-Key was already used with a different request body.

Fix: use a new key for each distinct operation. See Idempotency.

idempotency_in_progress

HTTP 409. A request with the same Idempotency-Key is still running.

Fix: wait briefly and retry with the same key and body. You will get the first request's result.

document_not_editable

HTTP 409. The document has been sent, so it cannot be edited in place.

Fix: sent documents change only by revision: update it through Update an invoice, which records a new version, or void or credit it.

document_has_payments

HTTP 409. The invoice has recorded payments, so it cannot be voided.

Fix: issue a credit note with Create a credit note, choosing a refund or client credit for the paid part.

document_voided

HTTP 409. The action is not allowed on a void document, for example sending it or recording a payment against it.

Fix: create a new document if the work is still owed.

credit_exceeds_balance

HTTP 422. The credit note is larger than what can still be credited on the invoice.

Fix: reduce the amount or lines. Check amount_credited_minor and total_minor on the invoice first.

schedule_total_mismatch

HTTP 422. The instalments of a payment schedule do not add up to the document total.

Fix: adjust the instalment amounts or percentages so they sum exactly to the total. See Deposits and schedules.

currency_mismatch

HTTP 422. The currency does not fit the request, for example a local QR rail on an invoice that is not in PKR or INR.

Fix: use a payment method that supports the document's currency. See Local QR payments.

rail_not_connected

HTTP 409. No active connection exists for the chosen payment method.

Fix: connect the rail in Settings > Payments (for example Stripe or bank transfer), or choose a method that is connected.

rail_capability_missing

HTTP 409. The rail cannot do what was asked, for example a refund on a rail without refunds.

Fix: do it outside our system and record it manually.

HTTP 409. Auto-charge was requested without a saved card that the client consented to.

Fix: the client saves a card with consent on their next Stripe checkout. Until then, send the pay link. See Saved cards and auto-charge.

contract_sealed

HTTP 409. A signed contract cannot change.

Fix: create a new version with New contract version; it must be signed again.

contract_hash_mismatch

HTTP 500. The stored fingerprint of a signed contract does not match the file. The download is refused and an alert is raised on our side.

Fix: nothing to change in your request. Contact support with the request_id.

journal_unbalanced

HTTP 422. A journal entry's debits do not equal its credits.

Fix: correct the lines so total debits equal total credits. See Accounting ledger.

period_locked

HTTP 409. The entry would post into a locked accounting period.

Fix: post into an open period, or have someone with ledger.manage unlock the period (a destructive action).

entry_already_reversed

HTTP 409. The journal entry was already reversed.

Fix: none needed; each entry can be reversed once. Post a new entry if you need a further correction.

time_entry_locked

HTTP 409. The time entry has been billed on an invoice, so it is locked.

Fix: correct the invoice by revision or credit note instead. See Time tracking.

confirmation_required

HTTP 428. A destructive action, or a send from a draft_only caller, was requested without "confirm": true. The response includes the preview of what would happen.

Fix: show the preview to a person, then repeat the request with "confirm": true. See Drafts and confirmations.

Limits and service errors

rate_limited

HTTP 429. Too many requests for the key, the account or the IP address. The Retry-After header says how many seconds to wait.

Fix: wait, then retry with the same Idempotency-Key. Spread out bulk work. See Rate limits.

upstream_error

HTTP 502. A provider we called on your behalf failed: Stripe, Whop, an SMTP server, QuickBooks, Xero or an AI provider.

Fix: retry later with the same key. If it persists, check the provider's status and your connection in Settings > Integrations.

connection_failed

HTTP 502. A connection test failed, for example SMTP or a rail's credentials.

Fix: check the details you entered (host, port, username, API key), then run Verify a connection again.

not_configured

HTTP 503. A feature depends on a platform integration that has not been set up yet. It shows as "Not configured" rather than failing in another way.

Fix: try again later or contact support.

maintenance

HTTP 503. The feature is switched off for maintenance.

Fix: retry later. Check status.freeinvoiceautomation.com.

webhook_signature_invalid

HTTP 400. An inbound webhook to us (for example from Stripe or a store connector) failed signature verification, so it was rejected and nothing was processed.

Fix: if you build a connector, sign the raw body with the right secret and a current timestamp. This code is not about the webhooks we send you; for those, you check our signature.

internal_error

HTTP 500. Something unexpected went wrong on our side. It is reported to our error tracking with the request id.

Fix: retry with the same Idempotency-Key. If it keeps happening, email [email protected] with the request_id.