Bonsai API
The Bonsai API lets you read and write the records behind your business: clients and contacts, deals, projects, tasks, time entries and invoices. It is organized around REST, takes and returns JSON, and uses standard HTTP verbs, status codes and authentication.
Authentication
Every request is authenticated with a bearer token, sent in the
Authorization header:
Authorization: Bearer bonsai_at_...
There are two ways to get one, and they grant exactly the same access:
- API key — create one in your Bonsai settings and use it directly. Best for scripts and back-office integrations acting on your own account.
- OAuth 2.1 — run the authorization code flow with PKCE to act on another Bonsai user's behalf. Best for applications you distribute.
A token is scoped to one Bonsai account, and it can only ever do what the
member behind it can do in the Bonsai app. A member who cannot see a project
in the app cannot see it through the API either, so plan for 403 and 404
responses on records outside their reach.
Tokens are secrets. Never ship one in client-side code or commit it to source
control. Requests must use HTTPS; unauthenticated calls fail with 401.
Requests and responses
Every successful response is an object with two top-level keys:
{
"data": { "...": "the record, or an array of records" },
"meta": { "request_id": "b1f0d2c4-7a9e-4c3b-8d5f-1e2a3b4c5d6e" }
}
Write endpoints take their attributes wrapped in a key named after the
resource — { "task": { "title": "..." } } — and reject a body that is
missing the wrapper with 400. PATCH is a partial update: only the fields
present in the body change. Sending an explicit null clears some fields but
is ignored on others, so check the field you are writing — every field that
can be cleared says so.
Money is serialized as a decimal string rather than a float so no precision is
lost in transit; dates are YYYY-MM-DD and timestamps are ISO 8601 in UTC.
Pagination
List endpoints are paginated with page[number] (1-indexed) and page[size]
(25 by default, 100 at most; larger values are silently reduced to 100).
Each list response carries the cursor in meta.pagination:
{ "page": 1, "page_size": 25, "has_more": true }
Keep requesting the next page while has_more is true. There is no total
count — counting every match is expensive on large accounts, and has_more
is enough to walk a collection to the end.
Filtering
List endpoints narrow results with nested filter parameters, such as
?filter[status]=active. Filters that accept several values take either a
comma-separated string or repeated array parameters:
?filter[priority]=high,medium
?filter[priority][]=high&filter[priority][]=medium
Each endpoint documents the filters it supports. An unknown filter key is an
error rather than a no-op, so a typo surfaces as 400 instead of quietly
returning the wrong records.
Errors
Bonsai uses conventional HTTP status codes, and every failure returns the same envelope:
{
"error": {
"type": "validation_error",
"status": 422,
"message": "Validation failed",
"request_id": "b1f0d2c4-7a9e-4c3b-8d5f-1e2a3b4c5d6e",
"details": [
{
"source": { "pointer": "/data/attributes/title" },
"code": "blank",
"message": "Title can't be blank"
}
]
}
}
Branch on type and, for field-level problems, on each entry's code — both
are stable, while message is not. details lists every problem at once, so
a rejected write can be fixed in a single pass. 422 means the body was
understood but rejected; 400 means the request itself could not be read.
Every response includes a request_id, in the body and in the
X-Request-Id header. Log it: it is the fastest way for support to find a
specific call.
Rate limits
Limits are counted per credential, not per IP, so one integration cannot spend another's budget:
| Scope | Limit |
|---|---|
| All requests | 600 per minute, 10,000 per hour |
Writes (POST, PATCH, PUT, DELETE) |
120 per minute |
Exceeding a limit returns 429 with Retry-After and RateLimit-* headers
describing when to try again. Back off for at least Retry-After seconds —
ideally with jitter — rather than retrying immediately.
Versioning
This is version 1 of the API, and every path is prefixed with
/public-api/v1. Additive changes — a new endpoint, a new optional
parameter, a new field in a response — can ship at any time, so treat
unrecognized response fields as safe to ignore. Anything that would break an
existing integration ships as a new version instead. Fields marked deprecated
keep working until the next major version.