Errors

The error shapes the Taxbit API returns, which ones are worth retrying, and how to log and surface them.

Every Taxbit API error arrives as an HTTP status with a JSON body. The status tells you what kind of problem it is, and whether trying again can help. The body says what went wrong, but its shape isn't the same everywhere in the API, so read the status first and the body second.

How errors arrive

Most endpoints use one envelope: error names the status and message gives the detail. When a request body fails schema validation, an issues array names each field that failed. Here name was sent as a number:

{"error":"Bad Request","message":"Validation failed","issues":[{"expected":"string","code":"invalid_type","path":["name"],"message":"Invalid input: expected string, received number"}]}

Two other shapes exist:

  • POST /v1/transactions/external-id returns status, message, data, and error. The detail is in data[].message:

    {
      "status": "error",
      "message": "Invalid TDM Transaction V2",
      "data": [
        {
          "code": "custom",
          "message": "A rate object must be provided for transaction with type 'deposit' and subtype 'cost-basis-fmv', 'gift' or 'inheritance'.",
          "path": []
        }
      ],
      "error": {
        "errorType": "VALIDATION_ERROR",
        "errBody": [
          {
            "code": "custom",
            "message": "A rate object must be provided for transaction with type 'deposit' and subtype 'cost-basis-fmv', 'gift' or 'inheritance'.",
            "path": []
          }
        ],
        "errorMessage": "Invalid TDM Transaction V2"
      }
    }
  • A 403 from the gateway carries a single Message field with a capital M:

    {
      "Message": "User is not authorized to access this resource with an explicit deny"
    }

So read message case-insensitively, and don't assume error is present.

Status codes

StatusWhat it meansWhat to doRetry?
400The request is malformed or failed validation.Fix the field or rule the body names, then resend.No, not unchanged
401The bearer token is missing, invalid, or expired.Mint a new token and retry once.Once, with a new token
403The credentials aren't allowed this resource, or the tenant doesn't match.Check the credentials and tenant.No
404No resource has that id.Check the id, or create the resource first.No
409The request conflicts with what exists, for example an id already in use.Read the existing record; see below.No
429Too many requests.Wait for Retry-After, then retry.Yes, after waiting
5xxA server or downstream error.Retry with backoff.Yes, with backoff

400

Schema validation failures come back with issues, as above. Each issue has a code, a path naming the field, and a message saying what was expected; for a field with a fixed set of values, values lists the accepted ones. The transactions endpoint reports its validation failures in data[].message instead.

401

An invalid token and a missing header read differently:

{"message":"Invalid token","error":"Unauthorized"}
{"message":"Invalid authorization header","error":"Unauthorized"}

Tenant tokens last 24 hours. Mint a new one as in Authenticate your tenant and retry the request once.

403

Two shapes: the gateway's explicit deny, with its capital-M Message, and an application 403 in the standard envelope, {"error": "Forbidden", "message": "Forbidden"}. Either way, the credentials aren't allowed to do this. Retrying won't help.

404

The message names the resource:

{"message":"Account Owner with id usr_8a3b1c9d cannot be found","error":"Not Found"}
{"message":"No asset is configured for the supplied id.","error":"Not Found"}

A transaction lookup uses the transactions endpoint's shape: {"status": "error", "message": "no transaction with externalId <id> was found for tenantId <tenant>", ...}.

409

A conflict with what already exists:

{"message":"Account Owner with ID usr_8a3b1c9d already exists","error":"Conflict"}

POST /v1/account-owners and POST /v1/accounts return 409 when the id is already in use. The assets endpoints return 409 for a name or code already in use, or for deleting an asset a transaction references; DELETE /v1/filers/{id} for the default filer or one with accounts; and GET /v1/withholding/austria when the account doesn't use the Austria method or the tax year doesn't match.

A create that you retry after a timeout comes back 409 if the first attempt succeeded. The API has no idempotency keys, so after a timeout or a 409, read the record back by your id and check it's the one you meant to create.

429

You've hit a rate limit (see Rate limits). The limiter's body says how long to wait, and the Retry-After header carries the same:

{"error":"rate_limit_exceeded","message":"Rate limit exceeded. Please retry after 30 seconds."}

Two other 429 bodies can appear: {"error": "rate_limit_exceeded", "message": "Too many requests."} and {"error": "Too Many Requests", "message": "Too Many Requests"}. Wait for Retry-After when it's present, otherwise back off, then retry.

5xx

Server errors don't share one body. Two examples: {"error":"Internal Server Error","message":"Failed to create Account Owner"} (500) and {"message":"Endpoint request timed out"} (504). Retry 5xx responses and network timeouts with bounded exponential backoff and jitter, decide on the status code rather than the body, and treat a failed write as an unknown outcome: it may have been saved.

Statuses that aren't errors

  • 202 from POST /v1/reports/inventory-summary: the report was triggered, not finished.
  • 203 from GET /v1/transfer-lots/transactions/{transactionId}: the lots came back, with warnings. See User-provided cost basis.
  • 200 from POST /v1/assets doesn't mean every asset was created. Each element has its own status of created or error, so check each one.

Which calls are safe to retry

  • POST /v1/transactions/external-id creates or updates by your id, so resending the same transaction is safe. Send one request per id at a time: two in flight together can fail with a 400 uniqueness error, and once the other has finished you can resend. Never resend an old payload after a newer update, because it overwrites the newer values.
  • Creates that reject a duplicate id, such as POST /v1/account-owners and POST /v1/accounts: a retry after a success returns 409, so read the record back as above.
  • Reads (GET) are safe to retry.

Rate limits

The default limit is 100 requests per second with a burst of 1,000, applied per tenant to each method and route. Some endpoints and organizations have their own limits; your implementation manager can confirm yours.

Authenticated responses carry two headers: x-ratelimit-limit, the burst (1,000 by default), and x-ratelimit-remaining. A 401 carries neither.

Logging and surfacing errors

  • Log the full response body and the x-request-id header with the request that caused it. Every response carries x-request-id.
  • The message is written for your developers, not your users. Translate it before you show anything in your app.
  • issues[].path and data[].message name the field or rule that failed, which is what to map to a form field or a fix in your data.

Where to go next

  • Authenticate your tenant: minting and caching tokens, so 401s stay rare.
  • Webhooks: what your endpoint should return to Taxbit, which is a different question from the errors on this page.

Questions? Contact your Taxbit implementation manager or [email protected].