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.