Skip to main content
Every API error returns a consistent JSON structure:

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, restore an active subscription, 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. Every request requires the owner to currently hold API access: Manage, plus the permission the endpoint itself requires: 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 and editing an invitation are checked twice. Team & access: Manage buys you the endpoint; it does not decide what access you may hand out. The invitation’s own permissions are checked separately against what the key owner holds, and against 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, 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} go the other way. Neither grants anything new, so neither adds a plan or feature gate: plan_upgrade_required and feature_disabled do not apply to either. Both still require Team & access: Manage and the authority to manage that particular invitation. 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.

Summary


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.

subscription_required

Type: permission_error | Status: 403 The company does not have an active subscription. This is returned when the subscription is required (no plan), the trial has ended, or the subscription is closed.
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 the invitation endpoints the same code covers a second situation: the key owner holds Team & access: Manage, but the invitation would hand out access the owner does not have. Here param is set to permissions, and the message is about granting rather than about a missing permission.
Read param to tell the two apart. Absent means the endpoint permission 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 and DELETE /team/invitations/{id} send no permissions, so there is nothing to grant. Both still check that the key owner may manage the invitation’s existing access, which means an invitation you cannot edit is usually one you can neither resend nor revoke. On revoke the param is invitation_id rather than permissions, because the problem is the target, not a requested grant. 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.

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. 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 for every edit, including setting a pending invitation to Admin and including a replay of the permissions already stored. A company below Growth 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. plan_upgrade_required means the subscription works but sits below Growth. How to fix:
  • 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 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:
  • 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 stop and resolve the feature availability for the company before retrying. Nothing in the request body works around 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 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.
  • For transaction updates, only visible standard transactions, selectable categories, and assignable leaf departments are supported.
  • 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.

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

Type: invalid_request_error | Status: 400 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, and a limit outside 1 through 500 on GET /parties/merchants. 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.

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.
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 income statement or cash flow, the start_date is after the end_date. The param field is set to start_date.
How to fix:
  • Swap the dates so start_date is on or before end_date.
  • Both must be valid ISO 8601 dates (for example YYYY-MM-DD); an unparseable value returns invalid_date instead.

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.

rate_limit_exceeded

Type: rate_limit_error | Status: 429 You’ve exceeded 6,000 requests per 60 seconds. Authenticated requests are rate limited per API key. Unauthenticated requests (missing or invalid key) are rate limited to 120 requests per 60 seconds per IP address. The response includes a Retry-After header with the number of seconds to wait, plus the standard X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers.
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. Blocked requests are not counted toward the limit.
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.
  • 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 four documented api_error codes, alongside permissions_update_failed, invitation_resend_failed, and invitation_revoke_failed. Those four are the only 5xx responses that carry a stable code.
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.