v1
OpenAPI 3.1.1

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.

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.

Server:https://app.hellobonsai.com

Bonsai API

No authentication selected
Client Libraries