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-idreturnsstatus,message,data, anderror. The detail is indata[].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
Messagefield 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
| Status | What it means | What to do | Retry? |
|---|---|---|---|
| 400 | The request is malformed or failed validation. | Fix the field or rule the body names, then resend. | No, not unchanged |
| 401 | The bearer token is missing, invalid, or expired. | Mint a new token and retry once. | Once, with a new token |
| 403 | The credentials aren't allowed this resource, or the tenant doesn't match. | Check the credentials and tenant. | No |
| 404 | No resource has that id. | Check the id, or create the resource first. | No |
| 409 | The request conflicts with what exists, for example an id already in use. | Read the existing record; see below. | No |
| 429 | Too many requests. | Wait for Retry-After, then retry. | Yes, after waiting |
| 5xx | A 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, withwarnings. See User-provided cost basis. - 200 from
POST /v1/assetsdoesn't mean every asset was created. Each element has its ownstatusofcreatedorerror, so check each one.
Which calls are safe to retry
POST /v1/transactions/external-idcreates or updates by yourid, so resending the same transaction is safe. Send one request peridat 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-ownersandPOST /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-idheader with the request that caused it. Every response carriesx-request-id. - The
messageis written for your developers, not your users. Translate it before you show anything in your app. issues[].pathanddata[].messagename 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].
Updated about 2 hours ago
