API Requests, Responses & Errors

Base URL, headers, JSON shapes, dates, money, pagination, filtering, the error format, status codes, rate limits and versioning.

Base URL

https://app.beeswaxapp.com/new_api/v1

All paths in these docs are relative to that base. Requests must use HTTPS. The v1 segment is the API version; see Versioning.


Requests

Header Use
Authorization: Bearer <token> Required on every request. See Authentication.
Content-Type: application/json Required on POST, PATCH and PUT with a JSON body. File uploads use multipart/form-data.
Accept: application/json Optional; responses are always JSON.
Idempotency-Key: <uuid> Recommended on every write. See Sync & safe retries.
User-Agent: myapp/1.4.0 (+https://example.com) Please identify your integration. It helps us help you, and lets us warn you before a change affects you.

Body shape. Write bodies are wrapped in a key named after the singular resource, matching Rails conventions:

{ "invoice": { "project_id": 88, "title": "Brand refresh, phase 2", "groups": [ ... ] } }
{ "payment": { "bank_account_id": 412, "paid_on": "2026-09-04", "allocations": [ ... ] } }

Unknown keys are ignored. Missing required keys return 400 with the parameter named.

Methods. GET reads, POST creates or performs an action (/finalise, /void, /switch), PATCH updates (a PUT is accepted as an alias where offered), DELETE removes drafts. Reads never change state; you can retry them freely.


Responses

Every response is JSON with Content-Type: application/json.

A single resource is wrapped in its singular name:

{ "invoice": { "id": 16102, "type": "Invoice", "number": "INV-1043", ... } }

A listing is wrapped in its plural name and always carries meta:

{
  "invoices": [ ... ],
  "meta": {
    "current_page": 1,
    "total_pages": 17,
    "total_count": 423,
    "per_page": 25,
    "filter": { "posted": true, "updated_since": null },
    "server_time": "2026-09-07T06:41:20.004Z"
  }
}

meta.filter echoes what the server actually applied, so a client can see when a default kicked in. meta.server_time is the value to feed back as updated_since on the next sync run.

Actions (/finalise, /void, /switch, payments) return the affected resource in the same wrapped shape, so you rarely need a follow-up GET.

Response headers

Header Meaning
X-Request-Id Unique id for this request. Quote it when reporting a problem.
X-Total-Count, X-Page, X-Per-Page, X-Total-Pages Pagination, duplicated from meta for clients that only look at headers.
Idempotent-Replayed: true This response was replayed from a stored earlier request with the same Idempotency-Key.
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After Sent with a 429.
X-Beeswax-MCP-Latest-Version Current release of the Beeswax MCP server; ignore unless you are an MCP client.

Data types

Identifiers are integers, unique within a resource type, never reused. Version numbers on documents are per-document counters starting at 1. Theme keys are slugs.

Dates (sent_on, due_on, paid_on, date) are YYYY-MM-DD strings and are interpreted in the account's time zone, which you can read from the web app's account settings. Send dates, not timestamps, for document dates.

Timestamps (created_at, updated_at, server_time) are ISO 8601 in UTC with millisecond precision, for example 2026-09-07T06:41:20.004Z. Send ISO 8601 with an explicit offset or Z whenever a parameter takes a timestamp (updated_since).

Money is returned as a decimal string such as "4950.0" or "1250.5", in the document's currency (currency_code), so that no precision is lost in transit. Parse it into a decimal type (BigDecimal, Decimal, decimal.js), never a binary float, and never do arithmetic on floats before comparing to what Beeswax holds. When you send money, send a string or a number with at most two decimal places; both are accepted. A few endpoints return JSON numbers instead: the per-account ledger (balances and running_balance), document versions and some tax-return summaries. Parse defensively and convert to decimal on arrival.

Multi-currency. Documents carry currency_code, exchange_rate, foreign_currency and base_currency_total. total is in the document currency; base_currency_total is what hit the ledger in the account's base currency.

Booleans in query strings accept true, 1, t and yes (and their negatives).

Enumerations such as state, kind and type are lower-case strings. New values may be added over time; treat unknown values as "other", not as an error.

Nulls. Optional fields are present with null rather than omitted, so you can rely on the shape.


Pagination

Listings are paginated. Pass page (1-based) and per_page (default 25, maximum 100). Both meta and the X-Total-* headers tell you where you are.

GET /invoices?page=3&per_page=100

Listings are ordered newest first (documents by sent_on then id, descending). Because a new document can land on page 1 while you are reading page 3, a full walk can show a row twice or miss one. For anything that must be complete, use updated_since as described in Sync & safe retries rather than walking every page every time.


Filtering

Filters are query parameters. Unknown parameters are ignored. The common ones on document listings (/invoices, /expenses, /quotes, /payments, /journal_entries):

Parameter Effect
state Exact state, for example finalised, draft, paid. Setting it turns off the posted-only default.
posted / include_drafts posted=false for unposted only; include_drafts=true or posted=all for everything. See the ledger rule.
project_id Documents on one project.
company_id Documents for one client or supplier. company=<text> does a case-insensitive name match instead.
from, to Inclusive document date range on sent_on, YYYY-MM-DD.
outstanding Not fully settled: excludes paid, draft and voided.
overdue Outstanding and past the account's payment term.
updated_since Rows whose updated_at is on or after the given ISO 8601 instant.
type On /journal_entries only: invoices, expenses, quotes, payments, credits, bank_transfers, payrolls.

Other resources have their own: companies take role=client|supplier and name; products & services take kind, side=sell|buy, active and query; the per-account ledger takes from and to. Each is listed in the Endpoint reference.


Errors

Every error has the same shape:

{ "error": "Human-readable message", "details": ["optional", "list", "of", "specifics"] }

error is stable enough to show a user; details appears on validation failures and lists each field problem. Neither is meant for switch statements: branch on the HTTP status.

Status When What to do
400 Bad Request Missing required parameter, malformed date or timestamp, invalid updated_since, unknown type Fix the request. Do not retry unchanged.
401 Unauthorized No Authorization header, or the token is unknown, revoked or expired, its account's subscription is not active, or its user no longer belongs to the account Stop and alert a human. Retrying cannot succeed.
403 Forbidden Token lacks the scope; the creating user's role does not allow the action; the plan lacks the feature; or the resource is not writable (system template, owner company) Fix scopes or ask an Owner/Super Admin to create the token. Do not retry.
404 Not Found No such resource in this account. Another account's id looks identical to a missing one, on purpose. Check the id.
409 Conflict A retry with the same Idempotency-Key arrived while the original was still running Wait a moment and retry with the same key.
422 Unprocessable Entity Validation failed; an invalid state transition (finalising a voided invoice); a value too long for its field; an Idempotency-Key reused with a different request; the storage quota is full Read details, fix the payload, send with a new key.
429 Too Many Requests Rate limit hit Wait Retry-After seconds, then retry.
500, 502, 503 Something went wrong on our side Retry with exponential backoff and the same Idempotency-Key. Nothing was stored for a 5xx, so the retry runs cleanly. Check the status page.

Examples you will meet:

HTTP 403  { "error": "Insufficient permissions. Required scope: payments:write" }
HTTP 403  { "error": "Your role does not permit recording payments" }
HTTP 422  { "error": "Unbalanced: debits 41854.00 do not equal credits 41845.00" }
HTTP 422  { "error": "Value too long for 'narration'; shorten it and retry" }
HTTP 422  { "error": "Invalid state transition: Event 'finalise' cannot transition from 'voided'" }
HTTP 429  { "error": "Rate limit exceeded", "retry_after": 37 }

Rate limits

Two limits apply:

Limit Scope Window
300 requests per token 1 minute
300 requests per source IP address, across all of Beeswax 5 minutes

The per-IP limit matters if your integration runs many tokens from one server, or shares a NAT gateway with other traffic: keep to about one request a second per IP, or spread work over time. A limited request returns 429 with Retry-After (seconds), X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix time). Honour Retry-After; do not hammer.

Practical guidance: use per_page=100, use updated_since instead of full re-reads, and batch where the API batches (one payment can settle several invoices; one invoice POST carries every section and line).


Versioning and compatibility

The version is in the path: /new_api/v1. Within v1:

  • Additive changes ship without a version bump: new endpoints, new optional parameters, new fields in responses, new enumeration values. Your client must tolerate fields and values it has not seen.
  • Behaviour changes are announced in the changelog with the date and the endpoints affected, and are kept to cases where the old behaviour was misleading (the posted default in September 2026 is the example). Subscribe to the changelog if you run a production integration.
  • Nothing is removed from v1. A breaking redesign would ship as v2 alongside it.

The /api/... prefix (an older API) has been removed and returns 404.


Request ids and logging

Every response carries X-Request-Id. Log it next to your own correlation id. When you report a problem, include the request id, the timestamp, the endpoint and the token name; we log one line per API call with the token, account and user, so a request id finds the exact call.

Browse Topics