Skip to main content
Every API error returns a consistent JSON structure:
Update generated clients and exhaustive code handling for balance_mismatch, already_reconciled, account_closed, concurrent_change, not_reconciled, not_latest_reconciliation, reconciliation_failed, unreconcile_failed, inbox_item_not_completable, inbox_item_complete_failed, invoice_overdue, invoice_status_unavailable, invalid_email, onboarding_incomplete, answers_incomplete, onboarding_locked and onboarding_complete. Keep a fallback for unfamiliar codes: use the HTTP status and error.type to classify them, and preserve the message and documentation link for diagnosis. Branch on error.code, not the human-readable error.message, which may change.

401 vs 403

The API draws a sharp line between authentication and permission:
  • 401 Unauthorized means the bearer credential itself is the problem (missing, malformed, or unrecognized). The fix is to obtain a valid API key.
  • 403 Forbidden means the credential was successfully looked up, but the caller is not allowed to access the resource. The fix is to regain access (reactivate the user, restore company access, settle overdue linked billing invoices, restore an active subscription, finish onboarding, or ask an administrator for the required product permission), not to rotate the key.
Branch on error.type, not the status code alone, to decide between “rotate the key” (authentication_error) and “regain access” (permission_error).

Permissions

An API key acts as its owner and has no separate permission snapshot of its own. Company endpoints require the owner to currently hold API access: Manage, plus the permission the endpoint itself requires. Public POST /signups is the exception: no API key, company, OAuth grant or product permission is needed. Signup is always available, subject to request and email limits. The table below describes company endpoints. A key whose owner is missing either piece gets 403 insufficient_permission. Permission changes take effect on the next request; keys do not cache a permission snapshot, so there is nothing to refresh or reissue after an administrator grants access. Creating or resending an invitation requires the Admin role in the company, on every plan. Team & access: Manage alone is not enough, even when the invitation grants less access than the owner already holds. A non-Admin gets 403 insufficient_permission before input and plan validation; upgrading does not grant invitation authority. Creation separately checks whether the invitation’s access is allowed by the company’s plan and feature flags. See plan_upgrade_required and feature_disabled. PATCH /team/invitations/{id} applies the stricter version of those checks. Every edit requires role-based access control to be on and an active or trialing Growth subscription or Finta’s internal Enterprise entitlement, Admin and no-op replays included, and the key owner has to be allowed to manage the access the invitation holds now as well as the access being requested. POST /team/invitations still accepts Admin invitations below Growth; editing never does. POST /team/invitations/{id}/resend and DELETE /team/invitations/{id} add no plan or feature gate: plan_upgrade_required and feature_disabled do not apply to either. Resend requires the Admin role; revoke still requires Team & access: Manage and the authority to manage that particular invitation, without an Admin-only rule. Removing API access is different: it permanently revokes the owner’s active keys for that company. Restoring the permission does not bring those keys back, and a new key has to be created.

Early access endpoints

Some features are in early access: documented here, but turned on per company while they roll out. A feature can be whole endpoints, or inputs on endpoints every company uses. Today that includes: For a company that doesn’t have the feature yet, every request to its endpoints, and every request that sends one of its inputs, returns 403 feature_disabled with type: permission_error and changes nothing:
For the invoice endpoints and GET /departments, the check runs after the API access check and before the endpoint’s own permission check, so a key owner who also lacks the endpoint’s permission sees feature_disabled first. A gated endpoint never answers 404, so it can’t be mistaken for a path that doesn’t exist. For a department sent to another endpoint, the check runs after that endpoint’s permission check, so a key owner without the permission still gets insufficient_permission first. On split create and update it also runs after the body is validated, so a malformed split body gets 400 invalid_request first. This is not retryable, and it is not something the key owner can fix in team settings. Contact Finta to request access for the company. Once it is on, the same requests work normally. When the plan doesn’t include departments. Departments come with the Growth and Enterprise plans. On any other plan, a request that names a department, including GET /departments, gets plan_upgrade_required instead, still with type: permission_error. The message links the company’s Plans page in Finta, and says that Finta still has to turn departments on after a plan change:
Department reports are the exception: a department income statement or a department report export always gets feature_disabled, whatever the plan. For departments, neither code is retryable as is. Treat both as “this company can’t use departments”: send the request again without the department input, or stop offering departments for that company.

Summary

Bulk update jobs report their own failures inside a 200 response, as entries in the job’s errors list. Codes there, such as transaction_conflict and update_failed, describe the job rather than your request, and some reuse names from this table. See Reading job errors.Transaction uploads work the same way: problems with individual rows, such as invalid_date or possible_duplicate, come back in a 200 or 201 as entries in the upload’s errors and warnings, not as HTTP errors. See Errors, warnings, and open questions.

invalid_api_key

Type: authentication_error | Status: 401 The API key in your Authorization header is missing, malformed, or unrecognized. This covers credential-level problems only. If the key was valid but the underlying user, company, or subscription has lost access, you will see a 403 with a permission_error instead (see below).
How to fix:
  • Verify the key starts with finta_ and is copied in full with no trailing spaces.
  • Check that you’re passing it as Authorization: Bearer finta_... (not as a query parameter or other header).
  • If the key was revoked, generate a new one from your Finta dashboard under Settings > API Keys. Revoked keys cannot be un-revoked.

user_inactive

Type: permission_error | Status: 403 The user that owns this API key has been deactivated. The key was valid, but the underlying user account is no longer active.
How to fix:
  • Reactivate the user from Settings > Members at app.finta.com, or
  • Create a new API key under an active user.

user_removed_from_company

Type: permission_error | Status: 403 The user that owns this API key no longer has access to the company the key was created for. API keys belong to people, not companies, so removing the user from the company also revokes their keys.
How to fix:
  • Ask a company admin to re-grant the user access to the company.
  • Once access is restored, create a new API key. The old key cannot be reactivated.

company_closed

Type: permission_error | Status: 403 The company associated with this API key is closed, closing, or deleted. The API is not available for closed companies.
How to fix:
  • Contact Finta support if the company should not be in a closed state.

invoice_overdue

Type: permission_error | Status: 403 A billing invoice explicitly linked by Finta to an enabled Enterprise product is overdue and unpaid. Its due date ends in America/Los_Angeles; access pauses at midnight immediately afterward. There is no grace period for ACH or other unsettled payments: only Stripe invoice status paid counts as paid. Disabled products’ links do not count, and companies without linked billing invoices are unaffected. These are invoices for Finta’s services, not the company’s bookkeeping invoice records. After identity, membership and company lifecycle checks, this independent gate applies to every API endpoint before subscription, product-permission and monthly allowance checks or endpoint work. Neither replacing the API key nor changing plans bypasses it. Refused requests do not change endpoint data or consume the monthly allowance. This example message is deliberately generic, not an exact message-string contract:
How to fix: Pay the overdue linked invoices at app.finta.com or contact support@finta.com. Do not use short-interval automatic retries; this refusal sends no Retry-After. Amounts in the human-readable message reflect the remaining unsettled balance after partial payments. Whole-dollar amounts omit decimal places; non-zero cents remain exact rather than being rounded. A smaller displayed balance does not clear an overdue invoice: its status must be paid. Unpaid status is cached for at most 60 seconds; paid status is final. Access returns automatically within a minute after payment, once no linked invoice remains overdue and unpaid. The lock is not latched: if the only remaining unpaid invoice is not due yet, access resumes until that invoice misses its own cutoff.

invoice_status_unavailable

Type: api_error | Status: 503 Payment status for a linked billing invoice cannot currently be verified after its due-date cutoff. Every endpoint can return this before endpoint work, without consuming the monthly allowance. Unknown status is not treated as paid. A Stripe outage before the cutoff does not block access for this reason; companies without linked invoices do not perform this status check.
How to fix: Honor Retry-After, then use bounded exponential backoff if verification remains unavailable. Surface persistent failures rather than retrying indefinitely. This temporary failure does not mean the credential is invalid. The safe message shown above does not expose provider errors or invoice details. Both invoice refusals also apply to every MCP tool call, but MCP uses an HTTP 200 error tool result rather than the API error envelope.

subscription_required

Type: permission_error | Status: 403 The company does not have usable subscription access. This is returned when a subscription is required (no plan), the trial has ended, or the company’s subscription status blocks access. Finta’s internal Enterprise entitlement bypasses ordinary Stripe subscription lockouts, but company closure and permission checks still apply.
How to fix:
  • Visit Settings > Plans at app.finta.com to start or restore a subscription, then retry.

staging_access_denied

Type: permission_error | Status: 403 The staging API only accepts keys owned by users with an @finta.com email address.
How to fix:
  • Send production requests to https://app.finta.com/api/v1.
  • For internal staging tests, use an API key owned by a user with an @finta.com email address.

insufficient_permission

Type: permission_error | Status: 403 The key is valid and its owner still has access to the company, but that owner lacks either the API access: Manage permission or the specific product-area permission the endpoint requires. See Permissions for the endpoint-to-permission table.
This is distinct from user_removed_from_company, which means the owner lost access to the company altogether rather than to one product area. On POST /files and POST /file_uploads, the permission depends on the file’s purpose: Taxes: Manage for tax_document (the default) and Reconciliation: Manage for bank_statement. It is checked after the request’s basic shape, so a malformed body or an unknown purpose returns 400 invalid_request rather than this code. Creating or resending an invitation requires the key owner to be an Admin in the company, even when they have Team & access: Manage and the invitation grants less access than they already hold. The Admin role has Manage access for every permission area. A non-Admin receives this response with no param:
Do not retry with weaker invitee access or suggest a plan upgrade. Use an authorized Admin’s credentials or ask a company Admin to send the invitation. The same Admin check applies to exact replays of creation. On invitation access edits the same code also covers a grant that exceeds the key owner’s access. Here param is set to permissions, and the message is about granting rather than about a missing permission.
Read param to distinguish the failures. Absent means an endpoint permission or the required Admin role is missing; permissions means the requested grant is too broad. API access: Manage carries no authority to grant Admin. PATCH /team/invitations/{id} checks two grants, not one. The key owner has to be allowed to manage the access the invitation holds now as well as the access being requested, so an edit can fail on the existing permissions even when the new ones are well within what the owner holds. Lowering the request does not help in that case. POST /team/invitations/{id}/resend requires the Admin role and sends no permissions. DELETE /team/invitations/{id} remains available to a non-Admin with Team & access: Manage who may manage the invitation’s existing access. On revoke the param is invitation_id rather than permissions, because the problem is the target, not a requested grant. POST /integrations/{key}/connect and GET /integrations/{key}/connect check a pair: Integrations: Manage and Transactions: Manage, the same pair the web app checks before starting a banking connection. GET needs both even though it only reads. The message names what is missing:
POST /transactions/bulk/update and GET /jobs/{id} both need Transactions: Manage, GET included even though it only reads. Like every permission, it is checked on each call, so a key owner who loses it while polling a job gets this error on the next poll, and replaying a submission with the same Idempotency-Key does not get around it. The submitter’s access is also rechecked when the job runs; losing it then fails the job with a permission_denied entry rather than this error. POST /accounts needs Transactions: Manage, while GET /accounts needs only Transactions: View. A key that can list accounts can still get this error when it tries to create one. POST /invoices/{id}/match, POST /transactions/{id}/match_invoices, and POST /invoices/{id}/unmatch check a pair: Invoices: Manage and Transactions: Manage, because they recategorize and re-date the deposit. A key that can create and void invoices can still get this error when it matches one. Choosing a line item’s category_id on POST /invoices needs only Invoices: Manage, not access to the categories list. POST /transfers and DELETE /transfers/{id} need Transactions: Manage, while reading the transactions they pair needs only Transactions: View. POST, PATCH, and DELETE /transactions/{id}/split need Transactions: Manage, while listing split lines with GET /transactions/splits needs only Transactions: View. POST /inbox/{id}/complete needs Manage on the item’s area, which for a grant_access item is Integrations: Manage. GET /inbox never returns this error for a missing area permission: it leaves out the items in areas you can’t view. Over an OAuth connection such as the MCP, the message reads You need the Integrations: Manage permission in this company to do this. How to fix:
  • Have a company administrator grant the key owner the permission named in the message, under Settings > Members at app.finta.com.
  • Retry immediately after. Permission changes apply to the next request, so the existing key starts working without being reissued.
  • Do not rotate the key. A new key created under the same user has exactly the same permissions and fails identically.
  • For param: "permissions", either lower the access you are requesting, or have an administrator raise the key owner’s own access first.

onboarding_incomplete

Type: permission_error | Status: 403 The company has not finished onboarding, so this endpoint is not available yet. This includes a company that needs a call or is not eligible. The optional onboarding_status is the same status GET /onboarding would return; see the status definitions.
Only GET /onboarding, PATCH /onboarding, GET /integrations, POST /team/invitations, and POST /feedback are open throughout onboarding. POST /integrations/{key}/connect and GET /integrations/{key}/connect are also open after approval, while the status is connect_banks or syncing. All usual endpoint permissions still apply. Before approval, the connect endpoints return the same code with guidance to finish the questionnaire first:
How to fix: call GET /onboarding and follow next_step; save answers with PATCH and finish setup before retrying the gated call. On MCP, use get_onboarding and update_onboarding; the error message names those tools. The connection and tool list remain available, but gated tools return a tool error. Repeating the blocked call does not advance setup. Existing user, company-closed, subscription and other access checks can take precedence. An unknown route still returns 404 resource_missing. No API or MCP call consumes the monthly allowance before onboarding is complete.

plan_upgrade_required

Type: permission_error | Status: 403 The invitation asks for access the company’s plan does not allow. Any permission set that is not full Admin requires an active or trialing Growth subscription or Finta’s internal Enterprise entitlement. On every other plan, POST /team/invitations accepts Admin invitations only.
The rule is evaluated against the resulting permissions, not the template name you sent. A custom object that happens to match Admin exactly satisfies it. read_only, or a custom set with even one area below manage, does not. Editing has no Admin exception. PATCH /team/invitations/{id} requires Growth or the internal Enterprise entitlement for every edit, including setting a pending invitation to Admin and including a replay of the permissions already stored. A company without Growth or Enterprise can still invite Admins; it cannot change a pending invitation at all. This is not subscription_required, which means the company has no usable subscription at all and blocks every endpoint. On invitation endpoints, plan_upgrade_required means the subscription works but sits below Growth. Sheet creation also uses this code when the company has reached its sheet cap. POST /tags uses it when the company has reached its tag limit (Formation 2, Startup 10, Growth 100); reuse an existing tag or upgrade, since renaming and deleting existing tags still work. On POST /categories, R&D Expenses and the three Finta-only templates require Startup or higher. This plan check runs before the Finta-only restriction, including when the categories already exist. Upgrade under Settings > Plans before retrying. Departments. Departments are early access and come with the Growth and Enterprise plans. On any other plan, GET /departments and every request that names a department return this code with no param, and nothing changes. The message says the plan doesn’t include departments and links the company’s own Plans page in Finta. It never names a plan or says to upgrade. Over MCP the API leaves the link out, and the MCP adds it to the tool error itself. See Early access endpoints for an example. How to fix:
  • For departments, send the request again without the department input, or stop offering departments for this company. Someone with billing access can review the plans at the link in the message. After a plan change, Finta still has to turn departments on, so contact Finta before retrying with a department.
  • For category templates, upgrade to Startup or higher in Settings > Plans, then retry.
  • For the invitation permissions described above, upgrade to Growth in Settings > Plans at app.finta.com, then retry the same body.
  • On POST /team/invitations, send access_template: "admin" if full access is appropriate for this person. Granting Admin is still subject to the key owner’s own access.
  • On PATCH /team/invitations/{id}, switching to admin is not a workaround. Upgrade, or change the invitation in Settings > Members instead.
  • Re-check the plan before every retry. It is evaluated on each request, including on an otherwise exact replay, so a company that downgrades after a successful call starts failing here.

feature_disabled

Type: permission_error | Status: 403 A feature the request needs is not turned on for this company. It covers early-access endpoints and inputs, department-grouped income statements, and invitation access controls. Early access endpoints and inputs. The endpoint, or an input the request sent, is in early access and the company doesn’t have it yet. On an early access endpoint this applies to every request, whatever the body. On other endpoints it applies to any request that sends the input, such as a department_id filter, no_department, or a department to set or clear, including null. Nothing changes, and param is absent. When the company’s plan doesn’t include departments at all, the code is plan_upgrade_required instead.
Contact Finta to request access for the company. Once it is on, the same request works unchanged. Granting the key owner more permissions does not help. Department-grouped income statements. GET /reports/income_statement?group_by=department requires the departments feature. A company without it receives the same feature-disabled envelope, with no param. Category grouping, including requests that omit group_by, remains available under the usual Reports: View permission.
Contact Finta to request access, or use group_by=category when you need the standard statement. Repeated polling does not enable the feature. Department report exports. Creating an income-statement export with group_by.rows: "department" without the departments feature returns 403 feature_disabled with param: "group_by.rows". Retrieving that export after the feature is disabled also returns 403 feature_disabled, but omits param, like the JSON income-statement case above. Request access or create a category-grouped export instead. Invitations. Role-based access control is turned off for this company, so Admin is the only access level that can be invited. Any other access_template is rejected, including a custom permission set that matches Admin exactly.
A rejected selection is never quietly promoted. Asking for read_only on a company without RBAC gets you this error, not an Admin invitation you did not intend to send. On PATCH /team/invitations/{id} this blocks the endpoint outright. Editing requires role-based access control on every request, so with it switched off no pending invitation can be edited at all, not even to Admin and not even to the access it already has. Here param is permissions rather than access_template. How to fix:
  • For department-grouped income statements, contact Finta for access or select group_by=category.
  • On an early access endpoint, contact Finta to request access for the company before retrying.
  • For a department sent to another endpoint, send the request again without the department input, or contact Finta to turn departments on.
  • On POST /team/invitations, send access_template: "admin" if the person should have full access.
  • On PATCH /team/invitations/{id}, there is no body that works. Change the invitation in Settings > Members at app.finta.com instead.
  • Otherwise resolve feature availability for the company before retrying a request that needs it.
  • Do not treat this as retryable. It is a property of the company, not a transient condition.

resource_missing

Type: invalid_request_error | Status: 404 On the tax uploads, a file_id that does not exist, belongs to another company, or expired unattached 24 hours after upload returns this code with param: "file_id". Upload the PDF again. On POST /reconciliations/{id}/reconcile, a statement_file_id that does not exist, belongs to another company, or expired unattached 24 hours after upload returns this code with param: "statement_file_id". Nothing was reconciled. Upload the PDF again. On the tax credit endpoints, a missing or other-company ID returns this code with param: "id". A cursor absent from the filtered company list returns it with param: "starting_after"; restart discovery with the desired filters and no cursor. No matching records on the initial list is a successful empty list, not a 404. The endpoint path does not exist, or a requested resource is missing, hidden, or unsupported for that operation. For write endpoints, Finta may return resource_missing for unsupported resources so callers do not learn whether a hidden or unauthorized object exists. This one code covers two cases: the endpoint itself does not exist (an unknown, removed, or renamed path), or a specific record was not found. Both return this same envelope as JSON, regardless of the Accept header you send. The message distinguishes the two, but it is human-readable and may change, so compare the request path against the API Reference rather than branching on the message. Authentication is checked first, so a request to an unknown path with a missing or invalid API key returns 401 invalid_api_key, not a 404. A 401 is not proof that the path exists; fix authentication first, then interpret the result.
How to fix:
  • Double-check the endpoint path. All endpoints are listed in the API Reference.
  • If a call that used to work starts returning resource_missing, check whether the path was renamed or removed. Report paths use underscores, not hyphens (/reports/income_statement, not /reports/income-statement).
  • Verify you’re using the correct base URL: https://app.finta.com/api/v1.
  • If the endpoint accepts an ID, verify the ID prefix and that the object is visible to the API key owner.
  • On GET /transactions, a starting_after value the server cannot use returns this code: an unknown transaction id in the classic listing, or, in incremental sync mode, anything that is not a cursor token the server issued (with param: "starting_after"). Discard the stored cursor and restart, from the first page in the classic listing or from your last stored watermark in incremental sync. (GET /accounts handles unusable cursors differently: they are invalid_request, not this code.)
  • On GET /cards, an account_id filter value the server cannot match returns this code with param: "account_id". Unknown, malformed, blank, and foreign account IDs all receive the same generic message, so the response never reveals whether an account exists in a company you cannot see. Obtain a valid ID from GET /accounts and correct the request before retrying. A known account with no linked cards is not this error; it returns 200 with an empty list.
  • On GET /forms/1099/{id}, unknown and foreign-company f1099_ IDs return this same code with param: "id". Numeric or malformed IDs instead return 400 invalid_request. An absent report on a year-filtered list is not a 404: it returns an unknown summary and empty records.
  • On GET /journal_entries/{id}, a malformed ID, an unknown ID, and a real entry in another company all return this same error with param: "id", so the response never confirms whether an ID exists elsewhere. Journal entry IDs are also not permanent: rebuilding the books behind a source deletes its entries and creates new ones with new IDs, so a previously valid ID can start returning this error. Do not retry it; re-list with a filter such as transaction_id to find the current entries.
  • On GET /transactions, a department_id filter for a department that doesn’t exist, belongs to another company, or isn’t visible to you returns this code with param: "department_id", as on GET /journal_entries, rather than an empty list. A company that doesn’t use departments returns it for every department_id. Get valid IDs from GET /departments.
  • On POST /transactions/bulk/update, an account_id, card_id, or department_id filter the server cannot match returns this code with param naming the field, such as filters.department_id. Unknown and other-company IDs get the same generic response, and no job is created. Get valid IDs from GET /accounts, GET /cards, or GET /departments. Missing transaction IDs in transaction_ids are not this error: they are found when the job runs and fail the job.
  • On POST /transaction_uploads, an unknown account_id returns this code, and so does an upload_id that does not exist or that another user started. Only the person whose key opened an upload can continue it, so a teammate holding the same ID gets this error rather than the upload. After a lost first response, start a new upload; the abandoned draft is never imported.
  • On GET /aggregations/total and GET /aggregations/totals, a category_id that is malformed, unknown, not available through the API, or another company’s returns this code. The response does not say which case applies. Get a valid ID from GET /categories.
  • On GET /jobs/{id}, an unknown job, a job in another company, and a job past its 30-day retention all return this same error with param: "id", so the response never says which case applies. Finished jobs expire 30 days after they complete; jobs still running do not expire. Do not retry it.
  • On GET /taxes/{id}, a well-formed ID that is unknown, staff-only, or outside the caller’s company returns this code with param: "id". On GET /taxes, a cursor absent from the filtered company collection returns it with param: "starting_after"; restart from the first page with your chosen filters.
  • On GET /rules, a starting_after cursor the server cannot use returns this code with param: "starting_after". Unknown, deleted, malformed, and foreign-company cursors all get the same generic message, so the response never reveals another company’s rules. Discard the stored cursor and restart from the first page.
  • On GET /rules/{id} and PATCH /rules/{id}, a missing rule, a deleted rule, another company’s rule, and Finta’s own global, internal, and category- or department-owned rules all return this same error with param: "id" and the message Rule not found, so the response never confirms that a rule exists somewhere you cannot see. Only rule_ IDs are ever looked up; a numeric ID, a UUID, or a malformed value is invalid_request instead. (DELETE /rules/{id} follows the invitation-revoke convention: an absent rule returns an empty 204, not this error.)
  • On POST /rules and PATCH /rules/{id}, a referenced resource that is missing or not visible to you, such as actions.category_id, actions.department_id, actions.tag_id, or an account_id or card_id condition, returns this code with the offending path in the message (for example actions.category_id: resource not found). Missing and foreign references get the same response, so the error never reveals another company’s data. An unknown, foreign-company or deleted tag omits param; refresh the tag list and use an available ID.
  • On the invoice endpoints, an invoice ID that is malformed, unknown, or in another company returns this same error with param: "id", so the response never confirms whether an invoice exists elsewhere. Unlike the rules endpoints, a malformed invoice ID is this code, not invalid_request. On match and unmatch, a transaction_id that doesn’t exist in this company returns it with param: "transaction_id"; on POST /invoices, so does a line item’s unknown category_id, with param naming the line (such as line_items[0].category_id). On GET /invoices, a starting_after cursor that isn’t found returns it too. On POST /transactions/{id}/match_invoices, a missing or foreign-company invoice returns this error with param: "invoice_ids", without revealing whether it exists in another company. A missing deposit also returns 404 resource_missing. A paid_transaction_id filter that matches nothing is not this error; it returns an empty list.
  • On POST /transfers, a transaction that doesn’t exist in this company returns this code with param naming the side, outgoing_transaction_id or incoming_transaction_id. Transactions the API doesn’t show, such as split lines, deleted transactions, and those held over the storage limit, count as not found. On DELETE /transfers/{id}, so does a transfer that doesn’t exist, including one already unmatched: matching the same two transactions again creates a transfer with a new ID.
  • On POST, PATCH, and DELETE /transactions/{id}/split, a transaction that doesn’t exist in this company returns this code with param: "id". Split lines, deleted transactions, and those held over the storage limit count as not found. A line’s category_id or department_id that isn’t found returns it with param naming the line, such as lines[1].category_id, and so does the Matched Transfer category, with a message pointing to POST /transfers.
  • On PATCH /transactions/{id}, the Matched Transfer category’s category_id returns this code with param: "category_id", because that category is never assigned directly. The message says to match the two transactions with POST /transfers instead.
  • On POST /team/invitations, a team or invitation the caller may not act on is concealed this way, with param naming company or invitation_id. A 404 here does not tell you whether the record exists under someone else.
  • On PATCH /team/invitations/{id}, an invitation that has already been accepted, one belonging to another company, and an ID that never existed all return this same error with param: "invitation_id". A well-formed ID that is not a pending invitation in your company is indistinguishable from one that is not there. A malformed ID is invalid_request instead, because it is rejected before the lookup.
  • POST /team/invitations/{id}/resend conceals the same three cases in the same way, with the same ID rules.
  • DELETE /team/invitations/{id} is the exception. A missing ID and an invitation in another company return an empty 204, not this error, so that the endpoint never confirms whether a record exists somewhere you cannot see. It returns resource_missing only for an invitation that has already been accepted, with param: "invitation_id". That person is a member now, and revoke never removes a teammate, so retrying the 404 will not deactivate them.
  • On POST /integrations/{key}/connect and GET /integrations/{key}/connect, a key for an integration that is not available to the company returns this code, matching the keys GET /integrations omits. An integration that is available but not connectable through the API is 400 integration_not_connectable instead.
  • On GET /integrations/{key}/connect, this code also means the key owner has not started a connection for this integration yet. GET only returns requests the calling key’s owner started, and the message says to call POST first:
  • On POST /inbox/{id}/complete, an unknown or malformed ID, another company’s item, an item in an area you can’t view, a dismissed item, and one snoozed until a later reminder date all return this code. It matches the list: an item GET /inbox wouldn’t show can’t be completed either. Re-list and use a current id.

parameter_missing

Type: invalid_request_error | Status: 400 A required parameter was not provided. The param field in the error response tells you which one.
On POST /accounts, a field sent as null or as a blank string (empty or whitespace only) counts as missing too, and returns this code rather than invalid_request. Public signup uses this code for a missing or blank email:
On GET /aggregations/totals, a missing group_by returns this code, and so does a date the category or grouping requires, such as start_date and end_date for group_by=month. See Dates for grouped totals. On POST and PATCH /transactions/{id}/split, a missing lines returns this code, and so does a line’s missing amount_cents or category_id, with param naming the line, such as lines[0].category_id. A field sent as null counts as missing. On GET /metric_values, a missing or blank metrics returns this code with param: "metrics" and the message Missing required param: metrics. Ask for the keys your question needs from GET /metrics. POST /transaction_uploads does not use this code. A first call missing account_id or file_totals is invalid_request instead. How to fix:
  • Check the param field in the error response to identify the missing parameter.
  • Refer to the endpoint’s documentation in the API Reference for required parameters.

invalid_email

Type: invalid_request_error | Status: 422 The email supplied to Create signup failed Finta’s email validation, including a value that is not a string. Only email is required; Finta trims whitespace and lowercases it. Correct email before retrying. The param field is email.

invalid_request

Type: invalid_request_error | Status: 400 On POST /tags and PATCH /tags/{id}, this covers invalid names, a missing or non-string name, unknown fields, invalid JSON, the wrong content type or a body that is not a JSON object. param names the field when one is at fault; malformed JSON and the wrong content type have no field-specific param. Follow the tag name rules, then retry. A duplicate name instead returns tag_already_exists. On POST /categories, this covers unknown fields, an unknown or unavailable template, a non-string template, invalid JSON, the wrong content type, or a body that is not a JSON object. param identifies the field for field-specific errors; it is omitted for invalid JSON, the wrong content type, or a non-object body. Correct the input before retrying. A missing or empty template uses parameter_missing instead. On POST /files and POST /file_uploads, a purpose other than tax_document or bank_statement returns this code with param: "purpose". On POST /file_uploads, a size over 10 MB (10485760 bytes) for a bank_statement returns it with param: "size". On POST /reconciliations/{id}/reconcile, a statement_file_id that isn’t a file_ ID string returns it with param: "statement_file_id".
On GET /transactions, amount_cents must be signed integer cents of at most 100000000000000 in absolute value. A decimal dollar amount such as 35.27, text, or a larger number returns this code with param: "amount_cents"; an invalid amount is not ignored. On the tax credit endpoints, this covers an invalid type, four-digit year, claim_status, or limit, and an ID that does not match tcred_ followed by a nonempty alphanumeric suffix. param names the invalid field. submitted is a payroll-submission status, so passing it as claim_status is invalid. On the 1099 filing endpoints, invalid year/stage/boolean/name filters, invalid page sizes, and malformed or differently scoped cursors return this code. error.param identifies the input to repair. Retrieval accepts only f1099_ followed by a nonempty alphanumeric suffix; numeric database IDs are rejected before lookup. A parameter is malformed or does not apply to the requested endpoint. The param field identifies the problem. Current uses include sending the wrong date shape for a category’s point_in_time value on GET /aggregations/total, mixing deprecated month parameters with full-date parameters on that same endpoint, an invalid filter value on GET /transactions or GET /journal_entries, a limit outside 1 through 500 on GET /parties/merchants, GET /accounts, GET /cards, GET /rules, or GET /jobs/{id}, a rule ID that is not rule_ followed by a nonempty alphanumeric suffix on the /rules/{id} endpoints (rejected with param: "id" before any lookup runs; numeric IDs and UUIDs are never accepted), an invalid rule definition on the rule writes (unknown fields, reversed between bounds, legacy account/card_name conditions submitted as new conditions, a null action value on a create where an update would accept null as action removal, or an unsupported combination, with a useful path such as conditions[0].value or actions.spread in the message), an empty PATCH /rules/{id} body or one carrying only an empty actions object, any request body at all on DELETE /rules/{id} (an empty {} included), and an updated_after value on GET /transactions that is not an integer between 0 and 253402300799. On that last one, check your units: a JavaScript consumer passing Date.now() is sending milliseconds where the API expects seconds. On rule create and update, an actions.tag_id that is malformed or is not a string identifies param: "actions.tag_id". IDs must start with tag_ followed by a nonempty alphanumeric suffix. Create rejects null and update accepts null to remove the action, provided another action remains. A tag deleted after lookup but before saving can return 400 invalid_request with param: "rule"; refresh the tag list before retrying. On GET /accounts and GET /cards, a starting_after cursor the server cannot use is also this code, with param: "starting_after". Malformed cursors, unknown cursors, cursors issued for another company or another collection, and, on /cards, a cursor issued under a different account_id filter all return the same generic message, so the response never confirms whether a cursor exists elsewhere. Note the asymmetry with the classic GET /transactions listing, where an unknown cursor is resource_missing instead; either way, discard the stored cursor and restart from the first page (with the filter you want, on /cards) rather than retrying it. On POST /transactions/bulk/update, this code covers everything checked before a job is accepted, and none of it creates a job:
  • A body that is not JSON sent with Content-Type: application/json (form-encoded bodies included) or is malformed JSON.
  • A missing, empty, whitespace-only, or longer-than-255-character Idempotency-Key, with param: "Idempotency-Key".
  • A body with both selectors or neither, an empty filters or updates object, an unknown key, a bad ID, date, or enum value, an inverted date range, duplicate or more than 5,000 transaction_ids, or accounting_date sent with accounting_shift. Update errors carry the field’s path as param, such as updates.spread.
  • A card_id filter for a real card that does not belong to the account_id filter, with param: "card_id".
  • A no_department filter that is anything but the JSON boolean true (including false and the string "true"), with param: "filters.no_department", or a department_id filter that is not a dept_ ID, with param: "filters.department_id".
On GET /transactions, no_department accepts only true. no_department=false returns this code with param: "no_department" and the message no_department accepts only true. To list transactions in a department, filter by department_id instead; see Finding transactions by department. On GET /aggregations/total, a periodic start_date after end_date is this error with param: "start_date", even within one month. On GET /aggregations/totals, the same ordering is rejected for periodic requests and for group_by=month on any category, including point-in-time categories. Full dates are exact inclusive Pacific calendar dates, not month selectors. On GET /aggregations/totals, this code also covers a group_by, order, or limit outside the allowed values, a malformed date or one that does not fit the category or grouping, a month grouping longer than 120 months (with param: "end_date"), period or department_id, and a starting_after cursor that is malformed or came from a different request. Cursors are bound to the exact category, dates, merchant filter, group_by, and order they were issued for: changing only a day within the same month invalidates the cursor too. Keep every parameter the same while paging, or start again without a cursor. On GET /metric_values, an unknown metric key returns this code with param: "metrics". The message names the unknown keys and lists the valid ones, so a caller can correct the request from the error alone. A group_by other than month returns it with param: "group_by". See Metric value errors. On GET /reports/income_statement, a nonblank group_by other than category or department returns this code with param: "group_by". Omit it for the category-based statement. month is not a valid value here; request separate date ranges for a monthly series.
Correct group_by before retrying; repeating the unchanged request will not help. On POST /transaction_uploads, every malformed request is this code, and nothing is saved: a body that is not a JSON object sent as application/json, an unknown field, a first call missing account_id or file_totals, a malformed file_totals (including a row_count above 2,000), rows without columns, more than 500 rows in one call, a row with the wrong number of values or a row number outside 1 to row_count, a bad exclude entry or acknowledged_warnings code, an attempt to change file_totals or account_id after the first call, and an account_id for a Ramp or Brex card account, whose transactions sync automatically and cannot be uploaded. param names the field when there is one. Problems with row values are not this error; they come back in the upload’s own errors list. On POST /accounts, this code covers a field that is not a string, a type other than bank or credit, any field besides account_name, vendor, and type (with param naming it; institution and name are not accepted aliases), and a body that is not a JSON object sent with Content-Type: application/json. Nothing is created. On POST /transfers, this code covers a body that isn’t a JSON object or has a field other than outgoing_transaction_id and incoming_transaction_id. On DELETE /transfers/{id}, it covers any request body at all. On POST and PATCH /transactions/{id}/split, this code covers a body that isn’t a JSON object sent as application/json, a field other than lines, fewer than 2 or more than 100 lines, a line with an unknown field or a value of the wrong type or range, and a line’s spread, department, or accounting date that can’t be used with its category. param names the field, such as lines[1].spread. It also covers lines that don’t add up to the transaction’s amount_cents, with param: "lines", and, on PATCH, an income or expense line that must send accounting_date because the split’s old lines had different ones. On DELETE /transactions/{id}/split, it covers any request body at all. See Splitting transactions.
On the invoice endpoints, this code covers malformed JSON, an unknown or invalid field on create, match, or unmatch (with param naming it, such as line_items[0].spread_months), a category_id that is not a revenue category, any body at all on POST /invoices/{id}/void, and a GET /invoices filter that is repeated, a date not written exactly as YYYY-MM-DD, or an amount_cents that isn’t an integer of at most 100000000000000 in absolute value. On GET /invoices, a bad date is this code, not invalid_date. On POST /transactions/{id}/match_invoices, the body must contain only invoice_ids, an array of 2–100 distinct strings starting with inv_. An invalid array returns 400 invalid_request with param: "invoice_ids". Fix the input before retrying:
On GET /jobs/{id}, starting_after pages through the job’s errors and must be an errors.next_cursor issued for that same job. A malformed, tampered, empty, or foreign cursor returns this code with param: "starting_after". This is an error in the retrieval, not a change to the job’s status; restart from the first page of errors without a cursor. On the invitation endpoints it also covers the request body: an unknown field, a permissions object sent next to admin or read_only (even as null), and a custom set that is missing an area, carries an extra one, or uses an unsupported level. On PATCH /team/invitations/{id} the rejected fields include first_name, last_name, email, and version, since that endpoint changes access only. A path ID that is not invite_ followed by letters and digits is rejected the same way with param: "id", before any lookup runs. POST /team/invitations/{id}/resend and DELETE /team/invitations/{id} are stricter still: they take no request body and no query parameters, so anything you send is this error. That includes an empty JSON object, null, whitespace, a company override, and a version field, which earlier drafts of resend accepted and neither shipped endpoint does.
How to fix:
  • Check param, then use the parameter rules on the endpoint’s reference page.
  • Do not retry the same request unchanged. Correct or remove the named parameter first.
For PATCH /onboarding, an unknown answer or top-level field, a wrong type, an invalid new state code, or an invalid answer value rejects the entire update and saves nothing. Saved no_bank_yet / no_credit_cards_yet values can be echoed when their bank/card pair is unchanged, before or after the decision; new or changed selections containing them are invalid. Unchanged answers after a web decision preserve the underlying web values, including legacy states and ranges. Invalid JSON or the wrong content type can omit param. This differs from answers_incomplete, which saves valid answers before refusing submission.

answers_incomplete

Type: invalid_request_error | Status: 400 PATCH /onboarding received submit: true before every required question was answered. The valid answers sent with this request were saved. The missing array names the keys still required; it is absent from unrelated error responses.
How to fix: read the current onboarding object, ask only the missing questions, save their answers, and submit again after the user separately confirms the full recap. Do not treat this as an atomic validation rejection or discard answers already saved.

onboarding_locked

Type: invalid_request_error | Status: 409 An eligibility answer cannot change after Finta’s decision. The param names the answer. While a call is required, the exact incorporation_year, accounting_software, files_taxes_with, goals, and subsidiary_countries follow-ups can still change.
A follow-up sent when a call is no longer needed uses the same code with the message “<key> can only change while Finta needs a call with the company.” How to fix: follow the current next step instead of retrying the rejected edit. An answer identical to the saved value is not a change: replaying saved answers or a successful submit returns 200 while onboarding is unfinished. This includes replaying GET’s answers after a web decision: null for a saved incorporation range or unmappable legacy state preserves it, and echoing the returned first industry preserves additional saved industries. Unchanged bank/card pairs use normalized comparisons. Actual edits still obey the lock, and an otherwise unchanged approved update can still start the import.

onboarding_complete

Type: invalid_request_error | Status: 409 The company has finished onboarding. Every PATCH /onboarding, including an otherwise empty or repeated request, returns this error.
How to fix: use GET /onboarding to read the completed setup or continue with other API endpoints. Repeating the update will not reopen onboarding.

invalid_date

Type: invalid_request_error | Status: 400 A date query parameter (date, start_date, or end_date) was present but is not a valid ISO 8601 date. The whole value must parse as a calendar date (2025-12-31) or a datetime (2025-12-31T19:00:00Z, 2025-12-31T23:59:00-08:00). Trailing characters (2025-01-31oops), locale formats (12/31/2025), and loose or partial forms (Jan 2025, bare 2025) are rejected rather than coerced to a default or to a leading date. The param field names the offending parameter. An absent or blank date parameter is not this error: it falls back to the endpoint’s documented default. GET /transactions/splits and GET /spreads return this code for an unreadable start_date or end_date. GET /transactions ignores one instead. GET /invoices does not use this code. Its start_date and end_date must be exactly YYYY-MM-DD, and anything else, a datetime included, is invalid_request.
How to fix:
  • Send dates in strict ISO 8601. 2025-12-31 (date only), 2025-12-31T19:00:00Z (UTC), and 2025-12-31T23:59:00-08:00 (with offset) are all accepted.
  • To accept the default, omit the parameter entirely or send it blank. Do not send a placeholder string.
  • Check the parameter name. The balance sheet uses date; the income statement and cash flow use start_date and end_date. An unrecognized name is silently ignored (you get the default, not an error), so a 200 with an unexpected date can mean the name was wrong rather than the value.

invalid_date_range

Type: invalid_request_error | Status: 400 On the JSON income statement or cash flow, or when creating any report export, start_date is after end_date. The param field is set to start_date. Creating an export can also return this code with param: "end_date" when the range exceeds 120 calendar months (both boundary months count), or a PDF would contain more than 13 displayed periods. PDF period counting follows the company’s calendar/fiscal report settings. These requests create no export or worker job. On GET /metric_values, this code always has param: "start_date". It covers start_date in a future month (start_date cannot be after today.), start_date after end_date, and a range over 36 months (A request covers at most 36 months.). Dates select whole months there, so only the months matter. A future end_date is not an error; it is capped at today.
How to fix:
  • Swap the dates so start_date is on or before end_date.
  • For metric values, keep the range within 36 months and start no later than the current month.
  • For exports, keep the range within 120 calendar months. For PDFs, use at most 13 displayed periods: shorten the range, select coarser grouping, or use CSV/XLSX.
  • Both must be valid ISO 8601 dates (for example YYYY-MM-DD); an unparseable value returns invalid_date instead.

integration_not_connectable

Type: invalid_request_error | Status: 400 The integration is available to the company but cannot be connected through the API yet: none of its connection_methods in GET /integrations has connect_via: "api". Both POST /integrations/{key}/connect and GET /integrations/{key}/connect return it, with param: "key".
An integration the company cannot use at all is 404 resource_missing instead, matching the keys GET /integrations omits. How to fix:
  • Connect the integration in Settings > Integrations at app.finta.com.
  • Do not retry. The same request fails until the integration has a connection method with connect_via: "api".
  • Read connection_methods from GET /integrations before calling rather than hard-coding which integrations are connectable. An integration can gain an api method at any time.

inbox_item_not_completable

Type: invalid_request_error | Status: 400 POST /inbox/{id}/complete was called on an item that isn’t grant_access. Finta closes every other type on its own once it sees the work done, so there is nothing to mark. The response has param: "id", and the message says what closes the item.
How to fix:
  • Do not retry. The same request fails for as long as the item exists.
  • Follow the item’s instructions and url instead. The item leaves GET /inbox once the work is done.

closed_books

Type: invalid_request_error | Status: 400 POST /invoices was sent an invoice_date in a tax year the company has finished with Finta. Books for a filed year are locked, because filed numbers can’t change. Nothing was created.
How to fix:
  • Use an invoice_date after the locked year. The message names the first open date.
  • Do not retry the same request. The lock is a property of the company’s books, not a transient condition.

invalid_file

Type: invalid_request_error | Status: 400 The file a tax upload or POST /reconciliations/{id}/reconcile was given cannot be used: it is not a PDF, it is over its purpose’s size limit (10 MB for a bank_statement), it was uploaded with a different purpose, or, for a file from POST /file_uploads, its bytes were never PUT to the upload URL. POST /files returns it with param: "file" when the uploaded file is not a PDF or is too large; the tax uploads return it with param: "file_id", and reconcile with param: "statement_file_id". The content is checked, not the file name or declared type. Nothing was recorded or reconciled. A file attaches only to what its purpose names: a tax_document (the default) to a tax upload, a bank_statement to a reconciliation period. Passing the other kind returns this code:
A file with no content yet:
How to fix:
  • For a direct upload whose bytes were never PUT, PUT the PDF to the returned url with every header in headers before expires_at, then retry the tax upload or reconcile with the same file ID.
  • If the URL expired, or the file is not a PDF, is too large, or has the wrong purpose, upload the PDF again for a new file ID, with the right purpose. A URL only accepts the bytes whose size and checksum it was created for.

wrong_document_type

Type: invalid_request_error | Status: 400 POST /taxes/income_uploads was given a Delaware franchise tax report (a PDF whose text contains “Annual Franchise Tax Report”), not an income tax return. Nothing was recorded and the file is still unattached.
How to fix:

transaction_update_failed

Type: invalid_request_error | Status: 422 The update reached the bookkeeping layer, but eligibility or validation rules rejected it. The param field names the field that was rejected. The message varies by validation, so branch on error.code, not exact message text.
Updates are atomic. When you send several fields and one is rejected, none of them are applied, so there is no partial write to unwind. accounting_date also has to land inside a bounded window, and a date outside it is rejected with this same code:
The window runs from the company’s incorporation date (or two years before today when Finta has no incorporation date on file) up to, but not including, three years after today. Both ends move: they are computed from the server’s current date on each request, so a date that was accepted once can fall out of range later. See Updating transactions for the full rule. How to fix:
  • Check param to see which field was rejected, then drop or correct that field and retry.
  • Verify you are editing a visible standard transaction. Transfers and splits reject many changes that are valid elsewhere.
  • Verify category_id points to a selectable category and department_id to an assignable leaf department.
  • For accounting_date, check the value against the company’s incorporation.date from GET /company and against the three-year forward bound.
  • Surface the response message to a human if bookkeeping rejects a change that looks valid.

category_update_failed

Type: invalid_request_error | Status: 422 The legacy code for a rejected category update, superseded by transaction_update_failed. It remains in the error enum so existing handlers keep compiling. Handle both codes if you are matching on 422 responses.

invitation_invalid

Type: invalid_request_error | Status: 422 The body was well-formed and every access check passed, but the invitation service rejected the data itself. On POST /team/invitations this covers an email that is not a valid address, an email that already belongs to a teammate (active, inactive, or deleted), and a pending invitation that conflicts with the one you are creating. param names the field at fault.
A conflicting invitation is not the same as a replay. An existing pending invitation that matches your request exactly (same inviter, same first and last names, same email ignoring case, same permissions) returns 200 OK instead. You get this error when the email matches but something else does not. On PATCH /team/invitations/{id} and POST /team/invitations/{id}/resend it means something different: the stored invitation fails validation, not your request. Both validate the whole stored record, so a legacy invitation with, say, a blank first name is rejected with param: "first_name" even though the edit body only carried access_template and the resend carried nothing at all. Adding the named field to the body does not fix it, because identity fields are rejected there with 400 invalid_request, and resend has no body to add anything to. An incomplete custom permission set is also a 400, not this error. The message wording varies by validation. Branch on error.code and read param. How to fix:
  • Correct the field named in param and retry.
  • If param names an identity field on an edit or a resend, repair the invitation in Settings > Members at app.finta.com first, then retry the same call.
  • If the person is already a teammate or already holds a different pending invitation, use Settings > Members. POST /team/invitations only creates; changing a pending invitation’s access is PATCH /team/invitations/{id} and sending its email again is POST /team/invitations/{id}/resend. Withdrawing one is DELETE /team/invitations/{id}. Editing or removing an accepted teammate is still a Settings action.

rule_already_exists

Type: invalid_request_error | Status: 409 Returned by POST /rules and PATCH /rules/{id} when the resulting definition exactly matches an automation rule the company already has. The comparison is against the normalized definition: condition order, action key order, and the casing of case-insensitive text values do not make two definitions different, and neither does the create-only apply_to_existing_transactions flag. Rules with the same conditions but different actions are not duplicates.
Nothing was saved and no historical run was scheduled. The message names the existing rule’s public ID so you can work with it directly. How to fix:
  • Read the existing rule with GET /rules/{id} using the ID from the message, and use or edit that rule instead of sending the same definition again.
  • If you meant to create a different rule, change the definition. Replaying the identical body accomplishes nothing and never schedules another historical run.
  • Expect this response when retrying an uncertain create. If your first attempt timed out after the rule was saved, an identical retry returns this conflict rather than a second 201. Treat it as confirmation that the rule exists and retrieve it.
  • If the duplicate’s ID is temporarily unavailable, you get 503 rules_not_ready instead of this error. Retry later; no extra rule or historical run is created in the meantime.

integration_already_connected

Type: invalid_request_error | Status: 409 POST /integrations/{key}/connect was called for an integration that is already connected for this company. For OAuth providers, a new connection link is available when the status is requires_reconnect. Plaid banks (chase, svb, arc, citi, boa, american_express) return this error for any existing connection, including one that requires reconnecting. Reconnect those banks in Settings > Integrations in the Finta web app.
For an existing Plaid bank, the message points to the web app:
How to fix:
  • Stop retrying this request: it conflicts with the current connection. For a Plaid bank needing reconnection, send the user to Settings > Integrations in the Finta web app.
  • If you expected a reconnect, check the integration’s status in GET /integrations. For OAuth providers, POST returns a link when it is requires_reconnect; this exception does not apply to Plaid banks. Branch on error.code, not the provider-specific message.

Type: invalid_request_error | Status: 409 Another user in the company already holds the open (pending or authorizing) connect request for this integration. A company has at most one open request per integration, so POST /integrations/{key}/connect returns this until that request finishes or expires. Your own open request never produces this error: calling POST again returns it with 200.
The message gives the time the other request expires. For a request already authorizing, that includes the extra time Finta allows to finish it. How to fix:
  • Do not retry with backoff. This is not a transient failure.
  • Wait for the other user’s request to finish or pass the time in the message, then call POST again.
  • Check GET /integrations before trying again. If the other user’s attempt succeeded, the integration is connected and there is nothing left to do.

idempotency_key_conflict

Type: invalid_request_error | Status: 409 POST /transactions/bulk/update or POST /reports/exports was called with an Idempotency-Key that already identifies accepted work with different normalized inputs. Keys are scoped to the company, submitting user and operation; another API key belonging to the same user shares that operation’s keys.
No new job was created. Bodies are compared after normalization; object-key order does not matter. For bulk updates, array order matters and explicit null differs from omission; identical replay returns the original job’s 202. Report exports normalize omitted PDF/month/category defaults and return the existing export with 201, including terminal failures. See Report export retries. How to fix:
  • If you are retrying after a timeout, resend the exact body you sent the first time. Do not edit it between attempts.
  • If you already have the original job’s ID, poll it instead of changing the key to find out whether the work ran.
  • If you meant to start different work, use a new key for it.

upload_committed

Type: invalid_request_error | Status: 409 POST /transaction_uploads was asked to change an upload that is already committed: new rows, exclusions, answers, or acknowledgements. A committed upload cannot be edited.
Posting only upload_id is never this error, and neither is repeating commit: true: both return the upload’s current status, so a commit retried after a timeout is harmless. How to fix:
  • Post only upload_id to read the upload’s status and result.
  • If the imported rows look wrong, the user can review or undo the upload in Finta at result.url.

invoice_already_exists

Type: invalid_request_error | Status: 409 POST /invoices found an invoice that is not voided with the same customer name (ignoring case and surrounding spaces), invoice date, and total. Finta treats it as the same invoice, so nothing was created. The message names the existing invoice’s ID.
This is what makes create safe to retry without an idempotency key. See Duplicate invoices and retries. How to fix:
  • If this was a retry, treat it as success: the first attempt created the invoice. Read its ID from the message, or find it with GET /invoices filtered by customer, start_date and end_date, and amount_cents.
  • If it really is a second invoice to the same customer on the same date for the same total, record it with its own date, or put both amounts on one invoice as two line items.
  • A voided invoice never causes this error, so an invoice voided by mistake can be entered again.

invoice_not_open

Type: invalid_request_error | Status: 409 POST /invoices/{id}/void, POST /invoices/{id}/match, or POST /transactions/{id}/match_invoices named an invoice that is not open. For a group, one non-open invoice refuses the entire match. Only open invoices can be voided or matched. Nothing changed.
Repeating a void on an invoice that’s already voided is not this error; it returns the invoice with 200. Neither is repeating a single-invoice match with the same deposit, or a group match with the identical invoice set in any order. How to fix:
  • To void a paid invoice, unmatch its payment first, then void it. Unmatching reopens every invoice on that payment; inspect paid_with_invoice_ids before proceeding.
  • To match a different deposit to a paid invoice, unmatch the current one first.
  • A voided invoice can’t be reopened. Record a new invoice instead.

payment_already_matched

Type: invalid_request_error | Status: 409 POST /invoices/{id}/match named a deposit that already paid another invoice, or POST /transactions/{id}/match_invoices named a deposit matched to a different invoice set. Nothing changed. Repeating the exact same group in any order returns 200 unchanged.
For group matching, the message is “The payment is already matched to a different invoice set.” The code and param: "transaction_id" are unchanged. How to fix:
  • Find every invoice the deposit paid with GET /invoices?paid_transaction_id=txn_.... If the match is wrong, explain that unmatching any member reopens the entire group before unmatching it.
  • Otherwise pick another deposit.

transaction_not_matchable

Type: invalid_request_error | Status: 409 POST /invoices/{id}/match or POST /transactions/{id}/match_invoices named an ineligible transaction. It must be an unsplit bank deposit for exactly the invoice’s total, or the sum of the group’s totals, in the same currency. Transfers are refused. Nothing changed.
For group matching, the message explains that the payment must be an unsplit bank deposit for exactly the invoices’ total and currency. Split deposits are refused rather than having their split discarded. How to fix:
  • Pick another deposit. See Matching a payment for how to find candidates.
  • For a split deposit, remove the split first, or match it in the Finta app, which asks how to reconcile it.

invoice_not_matched

Type: invalid_request_error | Status: 409 POST /invoices/{id}/unmatch named a transaction the invoice is not matched to. Naming the transaction protects against undoing a different match, so when the invoice is now matched to some other deposit, nothing changes.
Unmatching a pair that is already unmatched is not this error; it returns the invoice with 200. How to fix:
  • Re-read the invoice with GET /invoices/{id} and check its paid_transaction_id before deciding whether to unmatch it.

invoice_operation_failed

Type: invalid_request_error | Status: 409 A void, match, or unmatch would break a bookkeeping rule, for example matching an invoice dated before the company was incorporated. The message gives the reason. Nothing changed. Group matching checks every invoice date: on or after incorporation (or two years ago when unset), and strictly before three years from today. An invalid later invoice refuses the entire group even if the earliest date is valid. Correct the date before retrying; spread month dates have no additional bound.
How to fix:
  • Do not retry the same request. It fails the same way until the underlying condition changes.
  • Show the message to the user, and resolve the condition in the Finta app.

transaction_already_matched

Type: invalid_request_error | Status: 409 POST /transfers named a transaction that already belongs to a transfer. A transaction belongs to at most one transfer. Nothing changed.
Matching the same pair again after a success also returns this code, so on a retry after a timeout it can mean the first request went through. How to fix:
  • Read both transactions and check their transfer_id. If one is already paired with the other, the match you wanted exists.
  • If a transaction is paired with something else and that match is wrong, unmatch it with DELETE /transfers/{id} first, then match again.

transfer_not_eligible

Type: invalid_request_error | Status: 409 POST /transfers named two transactions that can’t form a transfer: amounts that aren’t opposite, dates more than 14 days apart, an outgoing transaction that isn’t money out, a transaction that paid an invoice, or one of the Finta app’s other eligibility rules, such as both sides being in the same account. The message gives the reason, and param names the transaction when the problem is one side. Nothing changed.
A transaction that paid an invoice is refused with its own message:
The date window and the app’s other rules share the message “Those transactions are not eligible to be matched.” How to fix:
  • Check that outgoing_transaction_id is the side with a negative amount_cents. Swapped sides are a common cause.
  • Otherwise pick another pair. See Finding the other side for how to find candidates.
  • For a transaction that paid an invoice, unmatch the invoice payment first if it really was a transfer.

transfer_operation_failed

Type: invalid_request_error | Status: 409 Matching or unmatching a transfer would break a bookkeeping rule, for example when one of the transactions is in a period its account is reconciled through. The message gives the reason. Nothing changed.
How to fix:
  • Do not retry the same request. It fails the same way until the underlying condition changes.
  • Show the message to the user, and resolve the condition, such as unreconciling the period in the Finta app or with POST /reconciliations/{id}/unreconcile.
  • An account’s reconciled_through in GET /accounts shows the cutoff before you write.

transaction_already_split

Type: invalid_request_error | Status: 409 POST /transactions/{id}/split named a transaction that is already split. Nothing changed.
Splitting the same transaction again after a success also returns this code, so on a retry after a timeout it can mean the first request went through. How to fix:

transaction_not_split

Type: invalid_request_error | Status: 409 PATCH or DELETE /transactions/{id}/split named a transaction that isn’t split by category. That includes one whose split was already removed, and one in a department split or a payroll split, which only the Finta app can change. Nothing changed.
Removing the same split again after a success also returns this code, so on a retry after a timeout it can mean the first request went through. How to fix:

transaction_not_splittable

Type: invalid_request_error | Status: 409 The transaction can’t be split. The message gives the reason: it paid an invoice, is a matched transfer, is spread, is a capitalized purchase, is part of a department split or a payroll split, has a department that now has sub-departments, or isn’t in USD. Nothing changed.
How to fix:

split_operation_failed

Type: invalid_request_error | Status: 409 Splitting, updating, or removing a split would break a bookkeeping rule, for example when a line would fall in a period its account is reconciled through. The message gives the reason, and param names the line when the problem is one line. Nothing changed.
How to fix:
  • Do not retry the same request. It fails the same way until the underlying condition changes.
  • Show the message to the user, and resolve the condition, such as unreconciling the period in the Finta app or with POST /reconciliations/{id}/unreconcile.
  • An account’s reconciled_through in GET /accounts shows the cutoff before you write.

balance_mismatch

Type: invalid_request_error | Status: 422 POST /reconciliations/{id}/reconcile was sent a statement_balance_cents that doesn’t match Finta’s balance for the period to the cent. Nothing was reconciled. The error carries both balances and the difference, all in cents in the statement’s terms.
difference_cents is finta_balance_cents minus statement_balance_cents. How to fix:
  • Check the statement balance first. For a credit card, the balance owed is positive.
  • Look for the difference in GET /transactions filtered by account_id over the period: a missing transaction to upload, a duplicate, or an item that clears next period. Leave out a duplicate or an uncleared item with excluded_transaction_ids, then reconcile again.
  • See Reconciling a period.

already_reconciled

Type: invalid_request_error | Status: 409 POST /reconciliations/{id}/reconcile named a period that is already reconciled, either on its own or as part of a later period. Nothing changed.
There is no idempotency key, so on a retry after a timeout this code can mean the first request went through. How to fix:

account_closed

Type: invalid_request_error | Status: 409 POST /reconciliations/{id}/reconcile named a period of an account that was closed in Finta. A closed account can’t be reconciled. Nothing changed.
How to fix:
  • Do not retry. It fails the same way while the account is closed. Tell the user the account is closed in Finta.

concurrent_change

Type: invalid_request_error | Status: 409 The account’s transactions changed while POST /reconciliations/{id}/reconcile was running. Nothing was reconciled.
How to fix:
  • Retry the same request after a short wait. If the new transactions change Finta’s balance, the retry returns balance_mismatch instead.

not_reconciled

Type: invalid_request_error | Status: 409 POST /reconciliations/{id}/unreconcile named a period that isn’t reconciled. Nothing changed.
On a retry after a timeout, this can mean the first request went through. How to fix:

not_latest_reconciliation

Type: invalid_request_error | Status: 409 POST /reconciliations/{id}/unreconcile named a reconciled period that isn’t the account’s latest reconciliation. Only the latest can be undone, so a call never undoes more than the period it names. Nothing changed.
How to fix:
  • To go back to the period you named, undo last_reconciliation_id first, then each later period newest first, confirming with the user before each one.

file_already_attached

Type: invalid_request_error | Status: 409 A tax upload was given a file that is already attached to a tax record, or POST /reconciliations/{id}/reconcile was given a statement_file_id already attached to a period. A file attaches once.
On reconcile, this response has no param, and nothing was reconciled:
If you are retrying a tax upload that timed out, this means the first attempt succeeded: read the record with GET /taxes. After a timed-out reconcile, read the period with GET /reconciliations/{id}. How to fix:
  • To attach the PDF to another independently eligible record, upload it again and use the new file_id. A fresh file cannot replace an existing return or confirmation.
  • To attach a statement PDF to another period, upload it again with purpose: "bank_statement".

tax_upload_not_allowed

Type: invalid_request_error | Status: 409 A tax upload is not available in the filing’s current state. The message explains whether the year is unavailable or locked, a Taxes page blocker remains, an income filing or Delaware confirmation already exists, or a Finta onboarding/preparation/payment/filing workflow is protected. Nothing changes and the file remains unattached.
How to fix:
  • Show the message to the user and resolve an actual eligibility blocker in the Finta app.
  • Do not retry unchanged filing state or create a new file ID to bypass the refusal. Existing documents and protected Finta workflows cannot be replaced through these endpoints.
  • When retrying a request whose response was lost, read the tax record first to see whether it already succeeded.

rate_limit_exceeded

Type: rate_limit_error | Status: 429 Authenticated requests are limited to 6,000 requests per 60 seconds per API key. Unauthenticated REST requests are limited to 120 per 60 seconds per IP address. Public signup always uses that 120/IP request budget, even with a credential, and also returns this code when a recipient, ordinary IP, or shared provider email allowance is exhausted. The higher Anthropic/OpenAI provider allowance applies only to signup verification emails; recipient limits are unchanged. The response includes Retry-After seconds. Request-bucket rejections also include the standard X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Those describe request traffic, not email attempts; email waits can exceed 60 seconds. See Signup email allowances for limits, a full HTTP example, and retry guidance. Account states consume the same allowances; a 429 does not reveal whether an account exists.
How to fix:
  • Honor Retry-After as the minimum wait. Do not retry sooner.
  • For retries beyond the first, apply exponential backoff with jitter and cap the total number of attempts (e.g. 3 to 5) so you fail fast rather than retrying indefinitely.
  • Treat X-RateLimit-Remaining as a leading indicator: if it is near zero on a successful response, slow down voluntarily.
  • If you are paginating, use larger limit values (up to 500) to reduce the number of requests.
See Usage Limits for the headers table and full retry guidance.

monthly_limit_exceeded

Type: limit_error | Status: 429 Your company has exceeded its monthly API call limit. Each plan has a cap on total API calls per company per calendar month: The limit is per company, not per API key. If a company has multiple API keys, they all share the same monthly pool. The count resets at midnight Pacific Time on the 1st of each month. A temporary override set by Finta support also applies to the whole company, every user and key alike, and the message then quotes that limit instead of the plan’s. Before the company is onboarded, no API or MCP call counts or returns this error. GET /onboarding and PATCH /onboarding, and MCP get_onboarding and update_onboarding, remain exempt after setup too. Blocked requests are not counted toward the limit. GET /plans and GET /usage are exempt: they are never counted and never return this error, so a caller at its limit can still read what each plan allows and how much of each limit the company has used.
Retry-After is not sent in this case, deliberately. The meaningful remediation is to upgrade the plan or wait until the 1st of next month, not to schedule a retry.
The error type is limit_error, not rate_limit_error. These are different situations: rate_limit_error means “slow down and retry in a few seconds.” limit_error means “you are out of quota for the month.” Upgrade your plan or wait for the reset.
How to fix:
  • Branch on error.code first to distinguish this from rate_limit_exceeded. Naive retry loops that do not check the code will burn quota that is already exhausted.
  • Stop retrying. Surface the error to your user with the reset date from the message body.
  • Upgrade your plan in Settings > Plans at app.finta.com. To show what upgrading offers, call GET /plans, which keeps answering at the limit. To check whether calls can resume, for example after an upgrade, read api_calls.stopped from GET /usage, which also keeps answering and isn’t counted.
  • If you can’t upgrade, wait for the limit to reset on the 1st of next month.
  • Reduce unnecessary API calls by caching responses and using larger pagination limit values.

invitation_create_failed

Type: api_error | Status: 500 The invitation cleared validation, permissions, plan, and feature checks, but could not be saved. This is one of the four invitation api_error codes, alongside permissions_update_failed, invitation_resend_failed, and invitation_revoke_failed. The rules endpoints add three more stable-coded 5xx responses of their own: rules_not_ready, rule_busy, and rule_deletion_failed. Connecting an integration adds connect_link_failed, creating an account adds account_create_failed, and committing a transaction upload adds upload_commit_failed.
Unlike the other write-path errors on this page, this one is safe to retry with the identical body. The endpoint matches an exact pending invitation and returns it with 200 OK rather than creating a second one, so resending is not a duplicate risk. There is no idempotency key to send. How to fix:
  • Retry the same body with exponential backoff and a bounded number of attempts.
  • Do not change the names, email, or permissions between attempts. Any difference breaks the exact match and can create a second invitation. Matching email alone does not make a request a replay.
  • Treat an uncertain network result the same way: resend the identical body and read the status code to learn what happened.
  • If it keeps failing after a few attempts, surface it rather than looping. The condition is on Finta’s side and will not clear because you asked again.

permissions_update_failed

Type: api_error | Status: 500 The edit cleared validation, grant authority, plan, and feature checks, but the new permissions could not be saved. It is the edit-path member of the invitation api_error family and is returned by PATCH /team/invitations/{id} only.
Any unexpected failure while saving surfaces under this one code. Creation failures keep their own code, so an edit never returns invitation_create_failed, and internal exception detail is never exposed in the message. This is safe to retry with the identical body. The endpoint sets permissions to the state you asked for rather than appending anything, so a request that actually landed before the error comes back as changed: false on the retry. How to fix:
  • Retry the same body with exponential backoff and a bounded number of attempts.
  • Treat an uncertain network result the same way. Resend and read changed to learn whether the first attempt had already applied.
  • Expect every check to run again on each attempt. A retry can return 403 instead if the plan, the feature flag, or the key owner’s access changed in between.
  • If it keeps failing, surface it rather than looping. The condition is on Finta’s side. Read GET /team to confirm the invitation’s current access before deciding what to do next.

invitation_resend_failed

Type: api_error | Status: 500 The resend cleared authorization and the invitation lookup, but renewing the link or requesting delivery failed. It is returned by POST /team/invitations/{id}/resend only. An internal revision check that fails surfaces here too, and there is no client-side version parameter that could correct it, because the endpoint accepts no body.
This is the one 5xx on this page that is not freely retryable. invitation_create_failed, permissions_update_failed, and invitation_revoke_failed all replay onto the same record, so repeating those requests costs nothing. Resend has no replay semantics: each call is a fresh resend that renews the acceptance link and requests another email, and the new link replaces the one already sitting in the invitee’s inbox.
How to fix:
  • Retry with bounded exponential backoff only if another email and a replaced link are acceptable. That is a product decision, not a transport one.
  • If it is not acceptable, read GET /team and compare the invitation’s expires and version against what you saw before. A renewal that already landed shows there.
  • Treat a timeout or a dropped connection the same way. The failure tells you nothing about whether the email went out, and an automatic retry layer that does not know this will send duplicates.
  • Do not send an Idempotency-Key header expecting deduplication. The endpoint does not implement one.

invitation_revoke_failed

Type: api_error | Status: 500 The revoke cleared authorization, but the deletion itself failed. It is returned by DELETE /team/invitations/{id} only.
This is safe to retry, and more comfortably so than the other write-path 5xx codes. Revoke has no second effect to cause: if the first attempt actually succeeded, the retry finds nothing and returns 204 just the same. There is no version and no idempotency key involved. How to fix:
  • Retry the same DELETE with bounded exponential backoff and a capped number of attempts.
  • Treat a timeout or a dropped connection identically. You cannot tell whether the deletion landed, and it does not matter.
  • Remember that each attempt still runs authorization and still counts against your monthly call allowance, so cap the loop rather than retrying indefinitely.
  • A retry can come back 401 or 403 even though the deletion already succeeded, if the credential or the key owner’s access changed in between. Confirm with GET /team rather than assuming the invitation survived.

taxes_not_ready

Type: api_error | Status: 503 GET /taxes cannot return its matching records because at least one is missing a public tax ID. The request fails instead of returning a partial list or inventing an ID.
Retry-After is not sent for this error. Retry with exponential backoff and a bounded number of attempts. If it persists, contact support. Reading taxes never assigns IDs or runs a backfill, so the retry itself does not repair the condition. See Tax filings.

rules_not_ready

Type: api_error | Status: 503 The rules endpoints could not return or reference saved rules faithfully. This covers eligible rules that are still waiting on a public rule_ ID during Finta’s ID rollout, a stored definition the API cannot represent, and a duplicate rule whose ID is temporarily unavailable when a write hits 409 rule_already_exists territory.
Rules are never silently dropped or repaired to avoid this error. GET /rules fails the whole request rather than returning a page with a rule missing or altered, and GET /rules/{id} fails rather than returning a partial object. Only the requested rule matters on retrieve: unrepresentable definitions or missing IDs on other rules do not block it. How to fix:
  • Retry with exponential backoff. Retry-After is not sent for this error.
  • Do not treat the response as partial success. Nothing was returned, and on a write nothing was saved or scheduled.
  • A 503 from a read does not mean the read started any work. Reading rules never assigns missing IDs, runs rules, or changes transactions, so retrying a GET does not fix the condition; time does.
  • If the error persists, the stored definition needs attention on Finta’s side. Contact support rather than continuing to retry.

forms_not_ready

Type: api_error | Status: 503 The 1099 endpoints cannot faithfully return the selected records yet. This covers report years that are missing or outside 1000–9999, historical records waiting for public identities, and unsupported workflow data. An empty report with an invalid year is also rejected.
List readiness is checked for the selected annual reports before vendor/status filters. A filter cannot hide an unready record, though an exact valid year excludes unrelated malformed reports. Retrieval checks the selected record. This error is not an unknown summary and must not be interpreted as no filing requirements. How to fix:
  • Retry with exponential backoff and a bounded number of attempts. No Retry-After is sent for this error.
  • Do not treat it as partial success: the response does not provide a usable page or filing record.
  • GET does not generate reports, assign IDs, or repair data; retrying does not start those operations.
  • If it persists, contact support instead of polling indefinitely.
See 1099 Filings for record and yearly-summary semantics.

rule_busy

Type: invalid_request_error | Status: 503 The rule is currently applying itself to existing transactions, so a conflicting mutation was refused. A historical run requested at create time holds a lock on the rule while it executes; an update or delete arriving mid-run returns this error and changes nothing.
Only a run that is actually executing is busy. A run that is still queued does not block anything: a successful update or delete cancels it before it starts. How to fix:
  • Retry the same request with exponential backoff. The run ends on its own, and the retry succeeds once the lock is released.
  • Do not treat the error as a partial write. The rule’s definition, and any queued historical work, are exactly as they were before the request.
  • There is no way to cancel a running application through the API, and no job or polling endpoint to watch. Retrying the mutation is the mechanism.

rule_deletion_failed

Type: api_error | Status: 500 A DELETE /rules/{id} cleared validation and authorization, but the deletion itself failed. Everything rolls back together: the rule still exists, linked transactions keep their categories, and queued historical work is not cancelled. A failed deletion is never converted into a 204.
How to fix:
  • Retry the same DELETE with bounded exponential backoff. Deletion is safe to retry: once the rule is gone, a repeat returns 204 without repeating any effects.
  • Treat a timeout or a dropped connection the same way. Rule deletes follow the same absent-is-204 convention as invitation revokes, so a retry tells you the outcome.
  • If it keeps failing, surface it rather than looping. The condition is on Finta’s side and needs operator attention.

tag_already_exists

Type: invalid_request_error | Status: 409 POST /tags or PATCH /tags/{id} would use a name that already exists, ignoring case. A repeated create returns this error rather than an idempotent success.
How to fix:
  • Call GET /tags and reuse the existing tag’s ID.
  • When renaming, choose a different name if you need to keep both tags. Retrying the same name does not resolve the conflict.
A company at its tag limit gets 403 plan_upgrade_required on create instead, even for a duplicate name, because the limit is checked first.

tag_busy

Type: invalid_request_error | Status: 503 A concurrent tag change conflicted with DELETE /tags/{id} or assigning or removing a tag through PATCH /transactions/{id}. A failed transaction PATCH changes no fields, including other fields submitted with tag_id. A refused tag deletion leaves all transactions and rules unchanged.
The example is a transaction PATCH; a tag deletion identifies param: "id" instead. How to fix:
  • Retry the identical request with bounded exponential backoff.
  • A delete retried after a timeout or dropped connection can return 404 resource_missing because the tag is already gone. Check the tag list if the outcome is uncertain.
  • If it keeps failing, surface the error rather than looping.

tag_deletion_failed

Type: api_error | Status: 500 DELETE /tags/{id} could not delete the tag.
How to fix:
  • Retry the same DELETE with bounded exponential backoff. If an earlier attempt succeeded but its response was lost, a retry returns 404 resource_missing.
  • If it keeps failing, surface the error rather than looping. The condition is on Finta’s side.

Type: api_error | Status: 500 POST /integrations/{key}/connect passed its checks, but the connection link could not be created.
How to fix:
  • Retry the same POST with exponential backoff and a bounded number of attempts. Repeated calls never produce a second link: while a request is open, POST returns it with 200.
  • Remember that each attempt counts against your monthly call allowance, so cap the loop.
  • If it keeps failing, surface it rather than looping. The condition is on Finta’s side.

inbox_item_complete_failed

Type: api_error | Status: 500 POST /inbox/{id}/complete passed its checks, but the item could not be completed. This is rare. Nothing was changed.
How to fix:
  • Retry the same POST with exponential backoff and a bounded number of attempts. Completing is idempotent: if an earlier attempt succeeded, a retry returns 200 with the completed item.
  • If it keeps failing, surface it rather than looping. The condition is on Finta’s side.

request_required

Type: permission_error | Status: 403 POST /categories returns this error only while the company is missing Intercompany Expenses, Investments or Shareholder Dividends: only Finta adds these templates. Nothing was created. Request the category in Settings > Categories. MCP callers receive a message directing them to submit_feedback, with an explanation of the intended use. Once Finta has added it, the API returns the template’s categories with 200 if the plan permits it. Below Startup, the earlier plan check returns plan_upgrade_required instead.
Do not retry unchanged until Finta has added the category. Do not substitute a different template.

category_create_failed

Type: api_error | Status: 500 A valid POST /categories request could not save its categories.
Retry the same body with exponential backoff. No idempotency key is needed; concurrent attempts and retries do not create duplicate categories. Successful calls return 201 when anything was added or repaired, including linking an existing same-name category that is missing its template key, or 200 when all already existed as template categories and nothing changed. For Equipment, that repair turns on automatic 5-year depreciation.

account_create_failed

Type: api_error | Status: 500 POST /accounts validated the request, but the account could not be saved. No account was created.
How to fix:
  • Retry the same request with exponential backoff and a bounded number of attempts. Creation is duplicate-safe: an existing manual account with the same type, vendor, and account_name is returned with 200 rather than created again, so a retry cannot produce two accounts. See Retries and duplicates.
  • Treat a timeout or a dropped connection the same way, and read the status code of the retry: 201 means this attempt created the account, 200 means an earlier one did.
  • If it keeps failing, surface it rather than looping. The condition is on Finta’s side.

reconciliation_failed

Type: api_error | Status: 500 POST /reconciliations/{id}/reconcile passed every check but the reconciliation could not be saved, or the statement_file_id PDF could not be attached to the period. When the PDF couldn’t be attached, nothing was reconciled and the file is still unattached, so it is safe to retry with the same body. Both write endpoints also return this code when the change was saved but the period could not be read back afterward; the message then says the change was saved.
How to fix:
  • Read the period with GET /reconciliations/{id} before doing anything else. Its reconciled field shows whether the change landed.
  • Retry only if it didn’t. A repeated reconcile can’t reconcile twice: it returns already_reconciled instead.

unreconcile_failed

Type: api_error | Status: 500 POST /reconciliations/{id}/unreconcile passed every check but the undo could not be saved. The message gives the reason. How to fix:
  • Read the period with GET /reconciliations/{id}. If it still shows reconciled: true, retry with backoff and a bounded number of attempts. A repeated undo can’t undo more than the period it names.

upload_commit_failed

Type: api_error | Status: 503 POST /transaction_uploads passed every check on a commit, but the import could not be started. Nothing was imported, and the draft is kept exactly as it was.
The message names the upload because a first call that commits has not otherwise returned its ID. How to fix:
  • Commit again with the upload_id from the message and commit: true, with exponential backoff and a bounded number of attempts. An upload commits once, so a retry cannot import it twice.
  • Do not start a new upload to work around it. The draft already holds every row, exclusion, and acknowledgement you sent.
  • If it keeps failing, surface it rather than looping. The condition is on Finta’s side.

tax_upload_failed

Type: api_error | Status: 500 A tax upload passed every check but the record could not be saved. Nothing changed, and the file is still unattached.
How to fix:
  • Retry with the same body and file_id, with backoff, while the file is still valid (24 hours from upload). If it persists, contact support.

report_export_limit_exceeded

Type: limit_error | Status: 429 The company already has five unfinished API exports across all submitters. This new submission did not create or enqueue an export. Existing-key replay and retrieval remain available, subject to ordinary API request limits.
There is no Retry-After: a worker finish time is unknown. Poll existing jobs with backoff and retry the rejected submission after a terminal success or failure releases a slot. This differs from burst throttling and monthly quota exhaustion. See Report Exports.

report_export_failed

Type: api_error | Status: 503 Finta could not accept the export, or could not confirm queue dispatch after acceptance. A pending record and queued worker may already exist.
Retry with backoff using the same body and Idempotency-Key. Do not switch keys to guess whether the first submission ran: that could create duplicate exports. No Retry-After is supplied. A persistence failure can leave no record; same-key retry is still safe. This HTTP error is separate from status: "failed" in an HTTP 200 export resource. Terminal job errors use an embedded code/message, not this envelope; a terminal failed export requires a new key for a new job. See Report failures.

billing_unavailable

Type: api_error | Status: 503 GET /billing could not read the company’s billing details from the payment provider, so it returns this error rather than partial or guessed figures.
How to fix:
  • Retry with exponential backoff and a bounded number of attempts. Retry-After is not sent for this error.
  • If it persists, contact support.

revision_conflict

Type: invalid_request_error | Status: 409 A sheet changed since the caller’s read. For PATCH, the supplied base_revision no longer matches; none of the batch was saved. Reread the document and replan the edit. For deletion, the supplied base_revision no longer matches; nothing was deleted. Read the changed document and decide whether deleting it is still intended. Never blindly substitute current_revision for a destructive retry.
For example, a stale PATCH is rejected before any of its edits are saved:
For cell-search pagination, discard the old cursor and restart the search after inspecting the changed document. Keep the query, match mode and worksheet filter consistent within one traversal.
These failures have no Retry-After or revision header. Read the JSON current_revision; time alone does not fix a stale edit/deletion revision or search cursor.

Sheets validation and calculation

Custom creation and PATCH return 400 invalid_request for mismatched rectangular value matrices, unknown fields or exceeding the shared 500-expanded-operation budget. Creation also rejects invalid template options. PATCH additionally rejects missing worksheet/chart selectors and out-of-bounds clear selections with 400; a missing document is 404. error.param identifies the relevant input where available. Invalid selected results use return_ranges; invalid chart anchors use chart.anchor_cell. These failures are atomic and must be corrected before retrying; splitting ranges within one request does not reduce its total operation count.
A company at its sheet cap receives 403 plan_upgrade_required. An immediate retry does not raise that cap; review the plan or remove an unneeded document only when that deletion is intended.
An HTTP 201 creation or HTTP 200 PATCH with a calculation error still means the document was saved. Retain its ID and revision, then inspect or reread it rather than blindly repeating the mutation. See creation response recovery. Sheets reject unknown parameters, invalid selections, unsupported view/match, oversized selections and invalid pagination with 400 invalid_request. A missing/inaccessible document or unavailable sheet-list cursor returns 404 resource_missing. See complete examples. Formula failures are different: selected reads can return HTTP 200 with calculation.status: error or unavailable. Inspect the diagnostics; unavailable calculation omits value matrices and does not create a job. Formula-only reads remain available without evaluation.

revision_history_mismatch

Type: invalid_request_error | Status: 409 PATCH /sheets/{id} found a mismatch between the saved document revision and its legacy operation history. No edits were saved. This differs from a stale caller’s revision_conflict.
How to fix: Contact Finta support for revision-history reconciliation. Rereading or retrying does not repair this condition. There is no Retry-After or revision response header, and this error does not supply current_revision.