Error response shape
Every non-2xx response returns a JSON body with this shape:
{
"error": "Short label",
"message": "Human-readable explanation of what went wrong"
}
Some responses include an additional code field for programmatic handling:
{
"error": "Plan Limit",
"message": "API access requires the API plan ($49/mo). Upgrade your subscription to enable programmatic lodgement.",
"code": "plan_api_access_required"
}
Validation errors return an array of messages in message:
{
"error": "Validation Failed",
"message": [
"Abn must be 11 digits",
"Legal name can't be blank"
]
}
HTTP status codes
| Code | Label | When |
|---|---|---|
| 200 | OK | GET, PATCH, and action endpoints (mark_ready, submit, etc.) that succeeded |
| 201 | Created | POST endpoints that created a new resource |
| 202 | Accepted | Async operations where the server has queued work but not finished it |
| 400 | Bad Request | The request was malformed (missing required params, bad JSON, invalid state transition) |
| 401 | Unauthorized | Missing, invalid, expired, or revoked API token |
| 402 | Payment Required | Token is valid but the account isn't on a paying plan or is past the grace period |
| 403 | Forbidden | Token is valid but lacks the required scope, or trying to modify a resource that doesn't belong to you |
| 404 | Not Found | Resource doesn't exist, or the token's environment can't see it |
| 409 | Conflict | Resource conflict (duplicate unique field, concurrent modification) |
| 422 | Unprocessable Entity | Validation failed on the supplied data |
| 429 | Too Many Requests | Rate limit exceeded — see Rate limits |
| 500 | Internal Server Error | Unhandled exception on our side. We log every 500 and get paged — try again in a minute |
| 503 | Service Unavailable | Planned maintenance or the ATO gateway is down (rare) |
Specific error codes
For errors where the HTTP status alone isn't specific enough, we include a code field.
| Code | HTTP | When |
|---|---|---|
plan_api_access_required |
402 | Token user's plan doesn't include API access |
plan_limit_exceeded |
402 | A plan quota has been hit (employer count, employee count) |
rate_limit_exceeded |
429 | Per-token per-minute rate limit exceeded |
invalid_idempotency_key |
400 | Idempotency-Key header present but over 255 characters |
employer_already_exists |
422 | POST /employers hit the (abn, branch_number) uniqueness constraint against an employer you already own. The response body includes existing_employer_id so you can recover without a second GET. See below. |
on_behalf_of_not_permitted |
403 | A non-platform token sent X-On-Behalf-Of-Employer or X-Acting-User. Only tokens with the platform:act_on_behalf scope may use those headers. |
on_behalf_of_required |
400 | A platform token was used without both on-behalf-of headers. Both are mandatory on every request — there is no default employer. |
on_behalf_of_employer_unknown |
403 | The X-On-Behalf-Of-Employer ID doesn't resolve to an employer under this token's account, or its environment doesn't match the token. |
More codes will be added over time; any missing code should be handled based on the HTTP status.
Examples by status
401 Unauthorized
{
"error": "Unauthorized",
"message": "Invalid or expired API token"
}
What to do: Verify the token string is copied correctly, isn't expired, and hasn't been revoked. If you're swapping between test and live environments, make sure the prefix matches.
402 Payment Required
{
"error": "Plan Limit",
"message": "API access requires the API plan ($49/mo). Upgrade your subscription to enable programmatic lodgement.",
"code": "plan_api_access_required"
}
What to do: The account owner needs to upgrade from the Billing & Plan settings page. Non-recoverable programmatically — you'll need to notify a human.
403 Forbidden
{
"error": "Forbidden",
"message": "This token does not have the 'pay_events:write' scope"
}
What to do: Create a new token with the correct scopes, or edit the existing one.
404 Not Found
{
"error": "Not Found",
"message": "Couldn't find Employer with 'id'=999"
}
What to do: Verify the ID exists and that your token's environment can see it. The most common cause of unexpected 404s is querying a live resource with a test token or vice versa.
422 Unprocessable Entity
{
"error": "Validation Failed",
"message": [
"Tfn is invalid",
"Employment basis must be one of F, P, C, L"
]
}
What to do: Fix the values in your request and retry. Validation errors are idempotent — retrying the same invalid request will always fail the same way.
Recovery-friendly variant for employer collisions. When POST /employers trips the (abn, branch_number) uniqueness rule against one of your own existing employers, the body includes code and existing_employer_id:
{
"error": "Validation Failed",
"code": "employer_already_exists",
"message": ["Abn and branch number combination already exists"],
"existing_employer_id": 4821
}
What to do: Treat it as a successful upsert — store existing_employer_id as your mapping to our record and carry on. No second GET needed. The ID is only surfaced when the existing employer is visible to the calling token; collisions against records outside your scope return the plain validation shape with no ID leak.
429 Too Many Requests
{
"error": "Too Many Requests",
"message": "You've exceeded the rate limit of 120 requests per minute.",
"code": "rate_limit_exceeded"
}
Response headers include:
Retry-After: 60
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
What to do: Honour Retry-After, then retry. See Rate limits for backoff guidance.
500 Internal Server Error
{
"error": "Server Error",
"message": "An unexpected error occurred. We've been notified."
}
What to do: Retry with exponential backoff. If persistent, email support@beeswaxapp.com with a rough timestamp and we'll investigate. The X-Request-Id header in the response helps us find the exact log entry.
Retry guidance
| Status | Retry? | Strategy |
|---|---|---|
| 4xx (except 429) | No | These are client errors. Retrying won't help. Fix the request. |
| 429 | Yes | Wait for Retry-After seconds, then retry. |
| 5xx | Yes | Exponential backoff: 1s, 2s, 4s, 8s, 16s, up to 5 minutes. Give up after ~10 attempts. |
| Network errors | Yes | Same as 5xx — treat as transient. |
Idempotency and retries
For mutating requests (POST, PATCH), use the Idempotency-Key header so retries don't create duplicate resources. See Idempotency.
The X-Request-Id header
Every response includes an X-Request-Id header:
X-Request-Id: a3f2e910-4b47-4e3a-9c6d-ffc4fe7b7a12
Include this value in any support ticket — it lets us look up the exact request in our logs and diagnose the issue fast.