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
posteddefault 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 asv2alongside 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.