Bonsai brings freelance business operations into one workspace, covering client management, proposals, contracts, project tracking, time tracking, invoicing, and payments. Create estimates, manage retainers, track expenses, monitor outstanding invoices, and review business finances from a unified dashboard.
Try asking
Encrypted at rest, isolated from the model
Resolved from an AES-256-GCM vault at the moment of the call and attached to the request — the model never sees the secrets.
Fetch a single task from Bonsai by its UUID. Returns the full task including pre-resolved `assignee_member_name`, `due_date`/`start_date`, `time_estimate_in_minutes`, both the `description_html` and `description_plain_text` views, and `company_tags` (the labels on the task, each `{ id, name }`). Two keys depend on permissions and are left out entirely without them: `billable` without the billing permission, and `deal_id` (the deal the task sits on, `null` when it sits on none) without permission to see deals — never read a missing key as `false` or "none".
List tasks in the user's current company, paginated, ordered by creation date (newest first). Supports filtering by `title` (free-text search, case-insensitive), `assignee_id` (accepts the literal "me"), `scope`, due-date window (`due` or `due_from`/`due_to`), `priority`, `project_id`, `deal_id` (the tasks on one or more deals — resolve ids via `list_deals`), `billable`, `tag_id`, and `task_status_id` (board column — resolve ids via `list_task_statuses`). `due` is mutually exclusive with `due_from`/`due_to`; invalid combinations return a `bad_request` error. Filtering by `task_status_id` without an explicit `scope` returns tasks in any state (otherwise the given scope applies). Each task includes `assignee_member_name`, a pre-resolved display label for the assignee — prefer it over a second round-trip to `list_team_members`. Each task also includes `company_tags`, the labels on the task (each `{ id, name }`); resolve tag names to `tag_id` filter values via `list_company_tags`. Task descriptions (`description_html`/`description_plain_text`) are not included here — call `get_task` with a task's `uuid` to fetch a task's full description. Two keys depend on permissions and are left out entirely without them: `billable` without the billing permission, and `deal_id` (the deal a task sits on) without permission to see deals — never read a missing key as `false` or "none". Filtering by `billable` or `deal_id` without the matching permission fails the call rather than returning unfiltered tasks.
List a parent task's subtasks (its child tasks), paginated, ordered by the manual subtask order. Each subtask carries the same summary fields as `list_tasks` (including `assignee_member_name`, `due_date`, `task_status`, and `company_tags`), plus `parent_task_uuid` pointing back at the parent. The same two keys depend on permissions and are left out entirely without them: `billable` without the billing permission, and `deal_id` without permission to see deals — never read a missing key as `false` or "none". A subtask does not inherit its parent's deal, so `deal_id` is `null` on the subtasks of a task that sits on one, unless it was put on a deal of its own. There is no filtering here; to narrow by deal, use `list_tasks` with `deal_id`, which returns subtasks alongside top-level tasks. Archived and deleted subtasks are excluded; completed subtasks remain visible. Returns a `not_found` error when the parent task does not exist, belongs to another company, or is archived, deleted, or a template. Task descriptions are not included here — call `get_task` with a subtask's `uuid` to fetch its full description.
List the caller's company task statuses, paginated. A task status is a board column a task can occupy (e.g. "To Do" / "In Progress" / "Done", plus any custom columns), ordered by board `position`. Each entry exposes `id` (the `task_status_id` accepted by `create_task` and the `list_tasks` `task_status_id` filter), `status` (the column name), `state` ("active" or "complete", where "complete" marks a done column), `color` (nullable), and `position`. Use this to resolve a status name to the `task_status_id` those endpoints expect.
Create a task in the user's current company. Supports `title` (required), optional `project_id`, `deal_id` (put the task on a deal — resolve via `list_deals`; independent of `project_id`, and it needs permission to create deals as well as tasks), `parent_task_uuid` (create the task as a subtask of an existing task — inherits the parent's project, so it can't be combined with `project_id`, though `deal_id` can), `assignee_member_id` (Company member id or the literal "me"; `null` for unassigned), `priority` (`urgent`/`high`/`medium`/`low`), `due_date` and `start_date` (YYYY-MM-DD), `description` (HTML fragment or plain text), `time_estimate_in_minutes` (positive integer), `task_status_id` (TaskStatus UUID for the board column — resolve via `list_task_statuses`; defaults to the company's "To Do" column when omitted), `billable` (whether time tracked on the task is billable — honored only with permission to set task billing, otherwise silently ignored; omitted it inherits from the parent task or project), and `tag_ids` (company tag ids to set — resolve via `list_company_tags`; `[]` or omitted creates the task with no tags). Returns the full task including pre-resolved `assignee_member_name`, the resulting `task_status`, the applied `company_tags`, and the `description_html`/`description_plain_text` views. Two keys depend on permissions and are left out entirely without them: `billable` without the billing permission, and `deal_id` without permission to see deals — never read a missing key as `false` or "none". Includes `url`, a direct link to the record in Bonsai — share it with the user so they can open it.
Update an existing task in the user's current company. Requires `uuid` (resolve via `list_tasks`, `get_task`, or `list_subtasks`); every other field is optional and only the ones supplied are changed. Supports `title`, `project_id` (`null` detaches), `deal_id` (move the task to another deal or `null` to take it off one — resolve via `list_deals`; independent of `project_id`, and it needs permission to create deals), `assignee_member_id` (Company member id or the literal "me"; `null` unassigns), `priority` (`urgent`/`high`/`medium`/`low`), `due_date` and `start_date` (YYYY-MM-DD; `null` clears), `description` (HTML fragment or plain text; `null` clears), `time_estimate_in_minutes` (positive integer; `null` clears), `task_status_id` (TaskStatus UUID — resolve via `list_task_statuses`), `billable` (whether tracked time is billable — honored only with permission to set task billing, otherwise silently ignored; it wins over the default a `project_id` in the same call would imply), and `tag_ids` (full-replace set of company tag ids; `[]` clears). Clearing `title`, `priority`, or the status is not supported. Returns the full updated task. Two keys depend on permissions and are left out entirely without them: `billable` without the billing permission, and `deal_id` without permission to see deals — never read a missing key as `false` or "none". Includes `url`, a direct link to the record in Bonsai — share it with the user so they can open it.
Soft-delete a task in Bonsai by its UUID. The task is removed from every subsequent read; archived tasks can be deleted directly. Returns the deleted task's final state. Two keys depend on permissions and are left out entirely without them: `billable` without the billing permission, and `deal_id` without permission to see deals — never read a missing key as `false` or "none". Tasks the caller cannot see return not-found; visible tasks the caller is not allowed to delete return a permission error.
List time entries in the user's current company, paginated, newest first. Each entry exposes `key`, `seconds`, `formatted_time`, `date`, `notes`, `rate`, `non_billable`, `billable_amount`, `billing_status` (`billed`, `unbilled`, or `non_billable`), `status`, `currency`, `project_id`, `task_uuid`, `owner_member_id`, and `created_at`. Supports filtering by `date_from`/`date_to` (inclusive YYYY-MM-DD window), `owner_member_id` (Company member id(s) or the literal "me"; listing other members needs the "all time entries" permission), `project_id`, `company_id` (client company billed — resolve via `list_companies`), `billing_status`, and `currency`. Multi-value filters accept a single value or an array. Without the billing permission `rate`, `non_billable`, `billable_amount` and `billing_status` are omitted from every entry — never read a missing key as zero, `false`, or unbilled — and `billing_status` is refused as a filter; retry without it rather than reading the refusal as "no access to time entries".
Log a time entry in the user's current company. Only `seconds` (duration) and `date` (YYYY-MM-DD) are required. Attach the entry to work with `project_id` (resolve via `list_projects`) or `task_uuid` (resolve via `list_tasks`); when `task_uuid` is set the project is taken from the task and `project_id` is ignored. `owner_member_id` records who logged the time — a Company member id or the literal "me" (defaults to the caller; logging for another member needs the "all time entries" permission). `rate` (a non-negative decimal string) and `non_billable` are honored only with the billing permission; otherwise they default from the project/member rate and the task/project billing type. Returns the created time entry including the server-computed `status`; without the billing permission `rate`, `non_billable`, `billable_amount` and `billing_status` are left out of it entirely — never read a missing key as zero, `false`, or unbilled. Each call logs a new entry.
Update an existing time entry in the user's current company. Requires `key` (resolve via `list_time_entries`); every other field is optional and only the ones supplied are changed. Supports `seconds` (duration), `date` (YYYY-MM-DD), `project_id` (`null` detaches; ignored when linked to a task), `task_uuid` (`null` unlinks and clears the derived project), `notes` (empty string or `null` clears), `rate` (a non-negative decimal string) and `non_billable` (both honored only with the billing permission), `owner_member_id` (Company member id or the literal "me"; reassigning to another member needs the "all time entries" permission), and `billing_status` ("billed" or "unbilled", to mark an entry billed by hand or move it back; needs the billing permission and is refused rather than ignored without it). Entries billed on a finalized invoice cannot be edited. Returns the full updated time entry including the server-computed `status`; without the billing permission `rate`, `non_billable`, `billable_amount` and `billing_status` are left out of it entirely — never read a missing key as zero, `false`, or unbilled.
List projects in the user's current company, paginated, ordered by creation date (newest first). Defaults to active projects; pass `status` to retrieve archived, completed, or all projects. Each entry exposes `id`, `number`, `title`, `public_url_token`, `company_name` (the company the project is for), `board_group_id` (the project's Project Group, nullable — resolve names via `list_board_groups`), its `start_date` / `finish_date` (`finish_date` is null when the project is open-ended), and `currency` (ISO 4217 — the unit of every money amount on the project). How each project bills comes as `billing_type` (`time`, `fixed_fee`, `retainer`, or `not_billable`), `billing_fee` (the agreed fee as a decimal string in `currency`, set on `fixed_fee` and `retainer`, null otherwise), and `billing_cycle` (how often a `retainer` bills, null for every other type). All three are absent without permission to see project finances, so treat a missing key as "not visible to this user" rather than as a project with no billing setup. Each entry also carries `budget` — the ceiling, what it is measured in (`fee` for money in `currency`, `time` for hours), and a `current_cycle` with the amount and percentage consumed so far. `budget` is null on a project with no budget and absent entirely without permission to see project budgets, so treat a missing key as "not visible to this user" rather than as an unbudgeted project. Supports filtering by `title` (free-text search, word-start match, case-insensitive), `public_url_token` (exact lookup, single value or array), `board_group_id` (exact Project Group lookup, single value or array), and `status` (project lifecycle status). Budgets are filterable too: `has_budget` (whether the project tracks one at all) and the inclusive `budget_amount_min` / `budget_amount_max` bounds, each of which works alone and leaves out projects that track no budget. Both bounds compare `budget.amount` as a bare number: the same value means hours on a `time` budget and money on a `fee` one, and the filter tells neither those nor the different project currencies apart. So "budgeted over 10,000" and "budgeted over a week" both land in the same mixed result set — narrow it on `calculation_mode` and `currency` yourself. `has_budget: false` cannot be combined with either bound. All three need permission to see project budgets; without it the call fails with a permission error naming the filter, rather than returning every project.
Create a project for an existing company. Requires `title`, `company_id` (the company the project is for — resolve via `list_companies`), and `billing_type` (`time`/`fixed_fee`/`retainer`/`not_billable`). `billing_fee` (non-negative decimal string) is required for `fixed_fee` and `retainer`; `billing_cycle` (`weekly`/`monthly`/`quarterly`/`biannually`/`yearly`) is required for `retainer`; both are rejected for `time` and `not_billable`. Optional `currency` (ISO 4217, defaults to the company currency), `start_date` (YYYY-MM-DD, defaults to today), and `finish_date` (YYYY-MM-DD, on or after `start_date`; omit to leave the project open-ended). The project number, public URL token, and members are assigned server-side. Returns the created project (`id`, `number`, `title`, `public_url_token`, `company_name`, `board_group_id`, `start_date`, `finish_date`, `currency`, and the saved `billing_type`, `billing_fee`, and `billing_cycle`, which are absent without permission to see project finances, so treat a missing key as "not visible to this user" rather than as a project with no billing setup). A new project has no budget yet, so `budget` comes back null — and absent entirely without permission to see project budgets, which is not the same thing. Budgets cannot be set here; start one with `create_project_budget` once the project exists. Each call creates a new record — search with `list_projects` first to avoid duplicates. Includes `url`, a direct link to the record in Bonsai — share it with the user so they can open it.
Update an existing project in the user's current company. Requires `id` (resolve via `list_projects`); every other field is optional and only the ones supplied are changed. Supports `title`, `start_date` and `finish_date` (YYYY-MM-DD), `billing_type` (`time`/`fixed_fee`/`retainer`/`not_billable`), `billing_fee` (non-negative decimal string), `billing_cycle` (`weekly`/`monthly`/`quarterly`/`biannually`/`yearly`), and `board_group_id` (Project Group UUID — resolve via `list_board_groups`, and it must be a stage on the board the project already sits on). Only `finish_date`, `billing_fee`, and `billing_cycle` can be cleared with `null`. Changing any of `billing_type`, `billing_fee` or `billing_cycle` re-checks the whole billing combination against the saved project, so clear or set the fee and cycle in the same call. A saved project can carry a leftover `billing_fee` or `billing_cycle` its type does not use, which reads back as null; if a billing change is refused over one you did not send, send it again with `billing_fee: null` or `billing_cycle: null` added. `finish_date` must land on or after `start_date` once applied, so send both when moving a project earlier. Changing `billing_type`, `billing_fee` or `billing_cycle` needs permission to see billing; without it the whole request is rejected and nothing is saved, so a refused billing change takes any title or date change sent with it down too. The project stays with the same company and the same currency — neither can be changed here, and the currency is fixed once the project exists because the amounts already recorded against it are never converted, so set it with `create_project`. Returns the updated project (`id`, `number`, `title`, `public_url_token`, `company_name`, `board_group_id`, `start_date`, `finish_date`, `currency`, and the saved `billing_type`, `billing_fee`, and `billing_cycle`, which are absent without permission to see project finances, so treat a missing key as "not visible to this user" rather than as a project with no billing setup), along with its `budget` — the ceiling, what it is measured in (`fee` for money in `currency`, `time` for hours), and a `current_cycle` with the amount and percentage consumed. Moving the dates rebuilds the budget cycles, so the `current_cycle` returned here is the rebuilt one, not the one that was running before the call. Budgets themselves cannot be set or changed here; use `update_project_budget`, or `create_project_budget` on a project with none yet. `budget` is null on a project with no budget and absent entirely without permission to see project budgets, so treat a missing key as "not visible to this user" rather than as an unbudgeted project. Includes `url`, a direct link to the record in Bonsai — share it with the user so they can open it.
Stop tracking a project's budget. The budget and its cycles are removed, and the project reads back with `budget: null` on `list_projects`. The project itself, and the time and expenses logged on it, stay as they are. Requires `project_id` (resolve via `list_projects`). A removed budget cannot be restored, only started again from scratch with `create_project_budget`, so confirm with the user before calling this. Removing a budget needs permission to edit the project and see its budget, but not a plan that includes budgets, so a company that moved to a plan without them can still remove the ones it has; a project the user may not edit is refused with a permission error. The project needs a `start_date`, because budget cycles run from it — set one with `update_project` first. Returns the budget as it stood when removed, every setting included, with its final `current_cycle`, so you can tell the user what went. A project with no budget is reported as not found, which is also what a second call gets; after a lost response, read that as the first call having gone through.
List team members (company member entities) in the user's current company, paginated. Each entry exposes `company_member_id`, the value to pass as `create_task.assignee_member_id`, plus `user_id` for read/filter workflows. Two distinct labels are surfaced: `role` is the user-defined job-role label (e.g. "Software Engineer"); `permission_profile` is the Bonsai permission tier (e.g. "Owner", "Project Manager", "Collaborator"). Lifecycle timestamps `confirmed_at`, `declined_at`, and `deactivated_at` are returned raw; `deactivated_at` takes precedence, then `declined_at`, then `confirmed_at` (all null means pending). Supports filtering by `id`, `user_id`, `project_id`, and `name` (free-text, word-start match). Client-portal members are never included.
List the caller's company board groups, paginated. A board group is a pipeline stage on a Deal board and a Project Group on a Project board (see `resource_type`). Use this to resolve a group name to its `board_group_id` (the `id` field) accepted by `list_projects` and the deal create/update endpoints. Each entry exposes `id`, `name`, `color`, `position`, `state` ("active" or "complete"), `deal_probability` (a nullable stringified percentage, only meaningful on Deal boards), and `resource_type`. Supports filtering by `name` (case-insensitive substring) and `resource_type` ("Deal" or "Project"). Deal stages need permission to list deals and Project groups need permission to list projects, so a narrower role sees only one board — or none.
List the caller's company tags as a single flat collection across every tag type, paginated. A company tag is a reusable label attached to Bonsai records. Each entry exposes `id` (the integer CompanyTag id accepted by the `list_tasks` `tag_id` filter), `name`, `tag_type` (which record type the tag applies to), and `color`. Supports filtering by `name` (case-insensitive substring match) and `tag_type` (exact match — one of `client`, `deal`, `project`, `vendor`, `task`, `time_entry`, `expense`, `supplier`). An unknown `tag_type` returns a `bad_request` error. Only the tag types whose underlying records the caller may list come back, so a narrower role sees fewer types — an empty result for a given `tag_type` can mean "no permission" rather than "no tags".
List deals in the user's current company, paginated. Each entry exposes `id`, `title`, `deal_number`, `deal_value` (nullable decimal string), `currency`, `probability` (nullable integer percentage), `close_date`, `status`, `company_name` (the company the deal is with), `board_group_id` (the pipeline stage — resolve names via `list_board_groups`), `assignee_member_id`, `assignee_member_name`, and `contact_ids`. Supports filtering by `title` and `deal_number` (free-text, word-start match), `assignee_member_id` (Company member id(s) or the literal "me", single value or array), and `board_group_id` (pipeline stage UUID(s), single value or array).
List invoices in the user's current company, paginated, newest first. Each entry exposes `id`, `invoice_number` (nullable until numbered; a sequential string e.g. "1032"), `title`, `currency`, `issued_date`, `due_date`, `sent_date`, `status`, `total_amount`, `subtotal`, `tax_amount`, `discount_amount`, `project_id`, `company_id` (the client company billed — read it with `get_company`), `client_name`, `client_email`, `public_url_token`, `created_at`, and `invoice_items`, plus how it repeats: `kind` (`single`, `recurring`, `subscription`, or `bundled`), `repeat_interval`, `repeat_finish_date`, `series_id` — the id of the first invoice of the series, shared by every invoice in it, so group on it to answer questions about a whole series — and `recurrence_active`: read that, not `kind`, to tell a live series from a stopped one, since counting `kind` alone reports stopped series as live. There is no filter for any of these, so to find a series or every live subscription, page through and group client-side. Supports filtering by `invoice_number` (exact lookup — the primary way to resolve a number to its id; single value or array), `project_id` (single value or array), and `company_id` (client company id(s), single value or array).
List CRM companies in the user's current company, paginated. Use this to find an existing company — by `name` free-text search — before deciding whether to create a new one. Each entry carries the full company record: `id`, `name`, `default_contact_id`, `domains`, `created_at`, the profile fields `description`, `industry`, `company_size`, `source`, `linkedin`, `instagram`, `twitter` and `facebook`, and `avatar_url` (the company logo, null when it has none). Archived companies (`archived_at` non-null) are excluded by default.
Fetch a single CRM company by its id, in the same shape `list_companies` returns per row. Use it when you already hold a company id — from `list_companies`, a contact's `company_ids` or `default_company_id`, an invoice's `company_id`, or `create_company` — instead of paging through the list. Returns `id`, `name`, `default_contact_id`, `domains`, `created_at`, the profile fields `description`, `industry`, `company_size`, `source`, `linkedin`, `instagram`, `twitter` and `facebook`, `avatar_url` (the company logo, null when it has none), and `url`, a direct link to the company in the Bonsai app. Archived and deleted companies are reported as not found, just as `list_companies` leaves them out.
List what happened with a CRM company, newest first: its own events plus the events of every contact on it, merged into one stream — documents, invoices, emails, meetings, notes, deals, forms, client portal events, and changes to the company and its contacts. Each occurrence appears once, even when it touched several contacts. A document is the exception: it is recorded per recipient, so one sent to two contacts is two activities (a legacy one, `details.legacy` true, appears once). Each activity carries `id`, `type` (`group.action`, e.g. `invoice.paid`), `occurred_at`, a one-line `summary`, the `actor` who did it (a team `member`, a `contact`, or `system`), the `subject` record it is about with a `url` to open it in Bonsai (an email, meeting or form has no page of its own, so its `url` opens the contact or company page), `contact_id`, `company_id`, and type-specific `details` (a changed field with its old and new values, an invoice amount, an email subject, …) or null. Only the activity types this user may read are returned. Email subjects and meeting titles are null when the mailbox they came from does not share them. Filter by `type` and by an `occurred_from` / `occurred_to` window of UTC days. Pages stop at 40: to read further back, call again with the same filters, `page` 1, and `occurred_to` set to the day of the last activity you read (the date in its `occurred_at`). The activity of that day you already read comes back again, and activity arriving while you page can repeat across pages, so dedupe by `id`. When all 40 pages fall on one day, paging cannot get past it: split that day into calls narrowed by `type`. Narrow to some of the contacts on the company with `contact_id`, which also leaves out the company's own activity and, like `type`, splits a day too busy to page through. Resolve `company_id` via `list_companies` or `get_company`, or from a contact's `company_ids` (`list_contacts`). Archived and deleted companies are reported as not found.
List CRM contacts in the user's account, paginated. Each entry exposes `id`, `name`, `email`, `job_title`, `phone_number`, `created_at`, `archived_at` (null unless archived), `company_ids` (the companies the contact is linked to, not your own account — read one with `get_company`), and `default_company_id` (the primary link). Archived contacts are excluded by default. Supports filtering by `name` and `email` (free-text search) and `company_id` (company id(s), single value or array).
List what happened with one CRM contact, newest first: its own events — documents, invoices, emails, meetings, deals it was assigned to, form submissions, client portal events, and changes to the contact — the notes linked to it, and when it was added to or removed from a company. Activity recorded on the company is not here: changes to the company, and forms sent to this contact. Read it with `list_company_activities`, where `contact_id` keeps the forms sent to this contact. `contact_id` is always this contact, and `company_id` is null for activity tied to no company. Each activity carries `id`, `type` (`group.action`, e.g. `invoice.paid`), `occurred_at`, a one-line `summary`, the `actor` who did it (a team `member`, a `contact`, or `system`), the `subject` record it is about with a `url` to open it in Bonsai (an email, meeting or form has no page of its own, so its `url` opens the contact or company page), `contact_id`, `company_id`, and type-specific `details` (a changed field with its old and new values, an invoice amount, an email subject, …) or null. Only the activity types this user may read are returned. Email subjects and meeting titles are null when the mailbox they came from does not share them. Filter by `type` and by an `occurred_from` / `occurred_to` window of UTC days. Pages stop at 40: to read further back, call again with the same filters, `page` 1, and `occurred_to` set to the day of the last activity you read (the date in its `occurred_at`). The activity of that day you already read comes back again, and activity arriving while you page can repeat across pages, so dedupe by `id`. When all 40 pages fall on one day, paging cannot get past it: split that day into calls narrowed by `type`. Resolve `contact_id` via `list_contacts`, or from a company's `default_contact_id` (`list_companies`, `get_company`). Archived and deleted contacts are reported as not found.
Create a CRM company in the user's current account. Supports `name` (required), optional `default_contact_id` (resolve via `list_contacts`; `null` for none), optional `domains` (one or more domain strings, normalised server-side), and the optional profile fields `description`, `industry`, `company_size`, `source`, `linkedin`, `instagram`, `twitter` and `facebook` (pass `null` or omit any of them to leave it unset). `industry`, `company_size` and `source` must each be one of a fixed list. The company's logo (`avatar_url`) is returned but cannot be set here. Returns the created company. Each call creates a new record — search with `list_companies` first to avoid duplicates. Includes `url`, a direct link to the record in Bonsai — share it with the user so they can open it.
Create a CRM contact in the user's account. Requires `name` and `email`. Optional `job_title` and `phone_number` (pass `null` to leave unset), and `company_id` (the company to link the contact to — resolve via `list_companies`; omit for a standalone contact). Returns the full contact including `id`, `company_ids`, and `default_company_id`. Each call creates a new record — search with `list_contacts` first to avoid duplicates. Includes `url`, a direct link to the record in Bonsai — share it with the user so they can open it.
Create an invoice for a client company in the user's current company. Requires `company_id` (the company being billed — resolve via `list_companies`), `contact_id` (the billing contact — resolve via `list_contacts`), and `project_id` (an existing project on the same company — resolve via `list_projects`). When the user does not name a contact, default to the company's primary contact (`default_contact_id` from `list_companies`) and confirm before creating, rather than asking for the contact outright. Optional `currency` (ISO 4217; defaults to the project currency), `title` (defaults to "<Client> Invoice"), and `due`/`due_date` (`custom` requires `due_date`; `upon_receipt` rejects it; omit to inherit the company terms). `kind` chooses what to create: `single` (a one-off, the default), `recurring`, or `subscription` (recurring and auto-charged to the client's card on file). The last two require `repeat_interval` and accept an optional `repeat_finish_date`; `single` rejects both. A recurring invoice starts reissuing itself shortly after creation, so confirm the cadence and end date with the user before creating one. `bundled` invoices are assembled in the Bonsai app and cannot be created here. Pass `invoice_items` for inline lines — each needs `name`, `amount`, and `rate` (non-negative decimal strings) plus an optional `unit_type` (defaults to `flat`); omit for an invoice with no lines. Returns the created invoice including `invoice_number`, `total_amount`, `status`, `kind`, `series_id`, and the resolved line items. Each call creates a new invoice. Includes `url`, a direct link to the record in Bonsai — share it with the user so they can open it.
Partially update an existing invoice in the user's current company. Requires `id` (the invoice — use the `id` from a prior `create_invoice` call or resolve via `list_invoices`); every other field is optional and only the ones supplied are changed. Supports `contact_id` (the billing contact — must belong to the invoice's existing client company; resolve via `list_contacts`), `currency` (ISO 4217), `title` (blank rejected), and `due`/`due_date` (`custom` requires `due_date`; `upon_receipt` rejects it; a standalone `due_date` sets a custom date). The client company, project, line items, and recurrence cannot be changed here — the client and project are fixed at creation, line items are managed with `create_invoice_item`, `update_invoice_item`, and `destroy_invoice_item`, and `kind`, `repeat_interval` and `repeat_finish_date` are set once by `create_invoice`; changing a live schedule is only possible in the Bonsai app. A paid or pending invoice is not editable. Returns the full updated invoice, including its read-only `kind`, recurrence fields, and `series_id`. Includes `url`, a direct link to the record in Bonsai — share it with the user so they can open it.
End a recurring or subscription invoice series, so no further invoices are issued and a subscription stops charging the card the client has on file — the same thing ending a billing cycle does in the Bonsai app. Nothing already issued changes: an outstanding invoice stays outstanding and still has to be paid, and nothing is deleted. Pass `invoice_id` — any invoice in the series will do, because the schedule lives on the most recent one, and that is the invoice acted on and returned, so the `id` that comes back is often not the one you sent. Confirm from `recurrence_active` on the result: false means the series is over. An invoice that had not repeated yet comes back as `single`, left behind as a one-off. Check `recurrence_active` before calling: when it is already false there is nothing left to stop and the call answers with a validation error, which is also what a second call gets — read that as the stop having already gone through, not as a failure. Stopping a subscription also turns off the auto-send that issued it. This cannot be undone, so confirm with the user before calling it. Returns the full invoice that carried the schedule, including `url`, a direct link to the record in Bonsai — share it with the user so they can open it.
Delete a draft invoice, or cancel one that is scheduled or has been sent; either way it stops appearing in `list_invoices`. Its `status` decides which: a `drafted` invoice is deleted, while a `scheduled`, `outstanding` or `overdue` one is cancelled — it keeps its invoice number and history, a scheduled one is never sent, and a sent one's payment link stops accepting payment. Hours and expenses billed on it go back to unbilled; nothing logged is deleted. Refused with a validation error that says why and what to do instead (most have to be handled in the Bonsai app): a `paid` or `pending` invoice, a payment or refund in progress on any other, any payment recorded against it (even on a draft), the deposit a contract or proposal is waiting on, one that is part of an invoice bundle, and the most recent invoice of a series that is still running (`recurrence_active` true). For that last one, call `stop_invoice_recurrence` first, which ends the whole series, so confirm that with the user separately. An older invoice in a series can be deleted and leaves the series running. An invoice that has already been deleted comes back as not found. This cannot be undone, so confirm with the user before calling it; `sent_date` tells you whether the client has already received the invoice. Returns the invoice as it was removed, with `status: "deleted"`.
Add a single line item to an existing invoice; the invoice total is recomputed. Requires `invoice_id` (the invoice to bill on — use the `id` returned by a prior `create_invoice` call, or resolve it via `list_invoices`), `name`, `amount`, and `rate` (`amount`/`rate` are non-negative decimal strings). Optional `description` and `unit_type` (`flat`/`per_hour`/`per_day`/`per_item`/`per_word`/`per_week`/`per_month`/`per_year`/`per_quarter`; defaults to `flat`). The invoice must still be editable — a paid or pending invoice is rejected. Returns the created line item including its server-computed `total`. Each call adds a new line.
Update the supplied fields of a single line item on an invoice; the invoice total is recomputed. Requires `invoice_id` (the invoice — use the `id` from a prior `create_invoice` call or resolve via `list_invoices`) and `id` (the line item — use the `id` from a prior `create_invoice_item` call); every other field is optional and only the ones supplied are changed. Supports `name` (blank rejected), `description`, `amount`, `rate` (non-negative decimal strings), and `unit_type` (`flat`/`per_hour`/`per_day`/`per_item`/`per_word`/`per_week`/`per_month`/`per_year`/`per_quarter`). The invoice must still be editable — a paid, pending, or partially paid invoice is rejected. Returns the updated line item including its server-computed `total`.
Remove a single line item from an invoice; any linked time entries are unbilled and the invoice total is recomputed. Requires `invoice_id` (the invoice — use the `id` from a prior `create_invoice` call or resolve via `list_invoices`) and `id` (the line item to remove — use the `id` from a prior `create_invoice_item` call). The invoice must still be editable — a paid, pending, or partially paid invoice is rejected. Returns the deleted line item.
Update an existing CRM company in the user's current account. Requires `id` (resolve via `list_companies`); every other field is optional and only the ones supplied are changed (omit a field to leave it unchanged). Supports `name`, `default_contact_id` (resolve via `list_contacts`), and `domains` (a populated array ADDS domains — additive, re-asserting an existing one is a no-op — and `[]` removes every domain), plus the profile fields `description`, `industry`, `company_size`, `source`, `linkedin`, `instagram`, `twitter` and `facebook` (pass `null` to clear any of them). `industry`, `company_size` and `source` must each be one of a fixed list. Clearing `name` or the primary contact is not supported by this endpoint, and the company's logo (`avatar_url`) is returned but cannot be set here. Returns the full updated company. Includes `url`, a direct link to the record in Bonsai — share it with the user so they can open it.
Update an existing CRM contact in the user's account. Requires `id` (resolve via `list_contacts`); every other field is optional and only the ones supplied are changed (omit a field to leave it unchanged). Supports `name` and `email` (both non-clearable — a blank value is rejected), and `job_title` and `phone_number` (pass `null` to clear, or omit to leave unchanged). Re-linking the contact to a company is not supported by this endpoint. Returns the full updated contact. Includes `url`, a direct link to the record in Bonsai — share it with the user so they can open it.
List the comments on a task or a deal, paginated, newest first. Provide exactly one parent — `task_id` (a task UUID) or `deal_id` (a deal id). By default only user-authored comments are returned; pass `kind` = `events` for system-generated activity (status changes, assignments) or `all` for both. Each comment carries `body_plain_text` (HTML stripped), its `kind`, `created_at`, the parent `commentable_id`, and `author_member_id` (null when the author is not a company member). Returns a `not_found` error when the parent does not exist or is not accessible.
Post a user-authored comment on a task or a deal. Provide exactly one parent — `task_id` (a task UUID) or `deal_id` (a deal id) — and a `body`. The comment is attributed to the authenticated member and stored with unsafe HTML stripped. Mentions and attachments are not supported: "@Name" in `body` is stored as literal text and notifies no one; notifications are automatic and determined by the parent record. Returns the created comment. Returns a `not_found` error when the parent does not exist or is not accessible.
List notes in the user's current company, paginated, ordered by when each note was written (newest first) — not by `date`, so a note backdated to last year still leads the page if it was written today. There is no way to sort by `date`; narrow with `date_from`/`date_to` instead. Each note exposes `id`, `title`, `date` (the day the note is about, which is what the app sorts and groups by), `visibility` (`everyone` or `author_only`), `created_by_member_id`, `created_at`, `updated_at`, and `note_links` — the company, project or contact records the note is attached to, empty for a note that stands on its own in the workspace or whose records this user cannot reach. The note body is never part of a list response; read a single note to get it. Filter by the attached record with `record_type` plus `record_id` (both together, or neither), by author with `created_by_member_id` (member id(s) or the literal "me"), and by a `date_from`/`date_to` window over the day each note is about (both bounds inclusive, either works alone).
Write a note in the user's current company. Only `content` is required, and it is Markdown — Bonsai stores it as rich text, so headings, bold/italic, bullet, numbered and task lists, links, quotes, tables and code blocks all survive; leave a blank line between paragraphs, since a single newline is only a soft break. Attach the note to a client company, a project or a contact by passing `record_type` and `record_id` together (resolve ids via `list_companies`, `list_projects` or `list_contacts`); omit both for a note that stands on its own in the workspace. `title` defaults to empty and `date` — the day the note is about, which is what the app sorts and groups by — defaults to today in the account timezone. `visibility` defaults to `everyone` in the account; pass `author_only` for a private note only the user you are writing for can read, and which only they can change afterwards. Returns the created note with its stored body — `content_plain_text` to read, and `content_html` to feed back to `update_note`, which takes HTML rather than Markdown. Each call writes a new note.
Fetch a single note by its id, including what it says — the body a list response always leaves out, so read a note here whenever you need it. The body comes back twice: `content_plain_text` to read, and `content_html`, the editor HTML, which is the field `update_note` takes — to change a note, read `content_html` here, edit the part you need and send the whole thing back. `content_html` is `null` only for a note with no body at all, while `content_plain_text` is `null` whenever the body carries no text, so an image-only or table-only note has one and not the other. Also returns `title`, `date` (the day the note is about, which is what the app sorts and groups by), `visibility` (`everyone` or `author_only`), `created_by_member_id`, `created_at`, `updated_at`, and `note_links` — the company, project or contact records the note is attached to, empty for a note that stands on its own in the workspace or whose records this user cannot reach. Resolve ids via `list_notes`. Anyone can read a note they wrote; reading someone else's needs permission to read notes.
Rewrite an existing note in the user's current company. Requires `id` (resolve via `list_notes`); `content_html`, `title`, `date` and `visibility` are all optional and only the ones supplied change. To change what a note says, read it with `get_note` first, take its `content_html`, edit the part the user asked about and send the whole thing back — `content_html` replaces the entire body, so anything you leave alone is preserved. Keep the editor's `data-*` attributes as they came: task-list items carry their tick state in `data-checked`, so rewriting them as plain list items loses the checkboxes. Markdown is not accepted here, unlike `create_note`, and `content_plain_text` cannot be sent back — this tool takes HTML. The body cannot be emptied. `title` clears on an empty string or `null`. `date` — the day the note is about, which the app sorts and groups by — can be moved but not cleared. `visibility` turns a note private (`author_only`) or opens it to `everyone` in the account, and is the author's alone to set: from anybody else a changed value fails the whole request and writes nothing at all, though sending back the value the note already has is always accepted. The records a note is attached to cannot be changed here — they are managed with `create_note_link` and `destroy_note_link`. Anyone can edit a note they wrote; editing someone else's needs permission to write notes. Returns the full updated note, body included.
Delete a note by its id. The note stops appearing in every subsequent read, and there is no way to restore it, so confirm with the user before calling this. Returns the deleted note's final state — `title`, `date`, `visibility`, `created_by_member_id`, `created_at`, `updated_at`, its body (as `content_plain_text` and `content_html`), and the `note_links` it was attached to — so you can tell the user what went. Resolve ids via `list_notes`, and read a note with `get_note` first if you need to check what it says before deleting. Anyone can delete a note they wrote; deleting someone else's needs permission to delete notes. Notes the caller cannot see return not-found, as does a note that is already deleted; a visible note the caller may not delete returns a permission error.
Attach a note that already exists to a client company, a project or a contact — use this to file a standalone note under a record after the fact, or to add a second record to a note that already has one. Requires `note_id` (use the `id` from a prior `create_note` call or resolve via `list_notes`) plus `record_type` and `record_id` together (resolve ids via `list_companies`, `list_projects` or `list_contacts`). Attaching is idempotent: passing a record the note already carries hands back the attachment that was there and writes nothing, so a retry is safe. A note holds at most 10 records; an eleventh is rejected. Attaching takes the same permission as editing the note, so whoever wrote a note can always attach to it. Notes the caller cannot see return not-found; a visible note the caller may not edit returns a permission error, and a record the caller cannot reach is rejected. Returns the attachment, whose `id` is what `destroy_note_link` takes to undo it.
Detach one record from a note, so the note stops appearing under that company, project or contact. The note itself is untouched — detaching its last record leaves it in the workspace as a note without records, it does not delete it. Pass `note_id` and `id`, where `id` is the attachment id from `note_links` on any note response, not the id of the attached record. There is no way to restore an attachment, so confirm with the user before calling this, and read the note with `get_note` first if you need to check which records it carries. Unlike attaching, this is not idempotent: a second call for the same attachment returns not-found. Detaching takes the same permission as editing the note, so whoever wrote a note can always detach from it; notes and attachments the caller cannot see return not-found, and a visible note the caller may not edit returns a permission error. Returns the attachment as it was, so you can tell the user what went.
List the templates the user can start a new document from, paginated: the account's own saved templates first (`source` "own"), then Bonsai's library (`source` "library"). Only templates the user could actually use come back — published, in a region that applies to the account, built in the current editor; drafts stay in the Bonsai app. `type` picks the kind of template, and today only `Doc` exists. Each entry exposes `id`, `name`, `source`, `region` (null when it applies everywhere), the `categories` and `industries` it is filed under, `roles` (the parts a document made from it expects somebody to fill: `sender`, `receiver` or `cc`), `updated_at`, and `url` (null when the user cannot edit it, as on a library template unless the account curates the library). Filter by `name` (case-insensitive substring), `source` (`own`, `library` or `all`), `category` and `industry` (names from `list_template_categories`), and `region` (defaults to the account's country). When no category fits the kind of document the user wants — a contract, say — filter by `name` instead of guessing one. Prefer the account's own template over a library one when both fit.
List the names `list_templates` accepts in its `category` and `industry` filters, paginated, categories first. Each entry is a `name` plus its `kind` — "category" or "industry" — which says which of the two filters the name feeds; pass the name verbatim. This is Bonsai's fixed vocabulary, the same for every account, so fetch it once and reuse it. A name here does not mean any template is filed under it: filtering by one that has none returns an empty page. The account's own saved templates are all filed under the "My Templates" category. `type` picks the kind of template the names belong to, and today only `Doc` exists. Filter by `kind` and by `name` (case-insensitive substring). When no category fits the kind of document the user wants — a contract, say — filter `list_templates` by `name` instead.
List the user's documents — proposals, contracts, quotes and the like — in the current company, paginated, newest first. Templates are not documents and never appear here; find them with `list_templates`. Each document comes back in the shape `create_doc` returns: `id`, `title`, `doc_number`, `status` (`draft` until it is sent from the Bonsai app, then `sent`, and `completed` once everybody has signed, or `expired` if it lapsed first; a document that asks for an upfront payment waits in `awaiting_payment` between the last signature and that payment), `recipients` (sender first, then receivers, then people copied in, each with how far they have got: `pending`, `viewed` or `completed`), `unassigned_roles` (the roles nobody fills yet), `deal_id`, `project_ids`, `created_at`, `updated_at`, and `url`, a direct link to the document in the Bonsai app. Signing links are never included. Filter by `status`, `company_id` (the client company a recipient represents — resolve via `list_companies`), `contact_id` (a contact who is a recipient — resolve via `list_contacts`), `project_id` (resolve via `list_projects`; a project this user cannot see matches nothing), `deal_id` (resolve via `list_deals`), and a `created_from`/`created_to` window over the day each document was created, in UTC (both bounds inclusive, either works alone). A document has to match every filter passed; a filter given several values matches any one of them.
Fetch a single document by its id — the `id` that `list_docs` or `create_doc` returned — in the shape `list_docs` uses, plus its `pricing_table`. Read it again after the user acts in Bonsai to see where the document stands: its `status` (`draft` until it is sent from the Bonsai app, then `sent`, and `completed` once everybody has signed, or `expired` if it lapsed first; a document that asks for an upfront payment waits in `awaiting_payment` between the last signature and that payment), how far each of the `recipients` has got (`pending`, `viewed` once they open it, `completed` once they are done), and which `unassigned_roles` still need somebody. Also returns `title`, `doc_number`, `deal_id`, `project_ids`, `created_at`, `updated_at`, and `url`, a direct link to the document in the Bonsai app. Signing links are never included. `pricing_table` holds the packages on offer, their line items and what they add up to, or null when the document has none. Totals leave out optional packages and optional rows, since the client has not picked them yet. Each total is rounded to the cent on its own, so `subtotal` plus `tax_total` can be a cent off `total`: quote `total` rather than adding the two up. To change the table, use `set_doc_pricing_table`, passing this `updated_at` as its `expected_updated_at`. Templates are not documents: the id of a `list_templates` entry is not accepted here.
Create a draft document — a proposal, contract, quote and the like — from a template in the user's current company. Nothing is sent: the draft waits in Bonsai for the user to review and send, which this tool cannot do. Requires `template_id` (the `id` of a `list_templates` entry; there is no blank document). The user is always the sender. `contact_id` fills the template's first receiver role; without it, a `project_id` or `deal_id` fills it from that record's client, and `company_id` alone fills it with the company's primary contact. `company_id`, `contact_id`, `project_id` and `deal_id` must all belong to the same client company — resolve them via `list_companies`, `list_contacts`, `list_projects` and `list_deals`. The client comes from `company_id`, else the project, else the deal, and every field that names a different company is refused with a message saying which field set the client. With only `contact_id`, the client is the contact's company when it has exactly one; a contact in several companies (`company_ids` in `list_contacts`) leaves the draft with no client, so pass `company_id` too. `title` defaults to the template name followed by the client's name. Only one receiver can be set here. Returns the draft with `recipients` (sender first, then receivers, then people copied in) and `unassigned_roles`, the roles nobody fills yet — tell the user to fill those in Bonsai before sending, since a receiver left unassigned cannot sign. Also returns `pricing_table`, the pricing table the draft took from its template, or null when the template has none; its totals leave out optional packages and optional rows, and each total is rounded to the cent on its own. To change the table, use `set_doc_pricing_table` with the draft's `id`, passing its `updated_at` as its `expected_updated_at`. Each call creates a new draft. Includes `url`, a direct link that opens the draft in the Bonsai editor — share it with the user so they can review and send it.
Replace the whole pricing table on a draft document and return it with the totals Bonsai computed, plus `warnings`. Requires `doc_id` (from `list_docs`, `get_doc` or `create_doc`) and `packages`. This is a full replace: send every package and row the table should have, in order; anything left out is removed. To change one row, read the document with `get_doc`, change that row, and send the whole table back, leaving out the `id`, `total` and `tax_total` it returned — packages and rows take no ids, and every call writes them under new ones. A document without a pricing table gets one, in a new section at the end. Bonsai prices every row at `quantity` × `rate`, adding tax from its `tax_rate_ids` when its package sets `includes_tax`; a row priced from an estimate loses its markup and discount, and the table stops following the estimate. Optional packages and optional rows are priced but left out of `subtotal`, `tax_total` and `total` until the client picks them, and each total is rounded to the cent on its own, so quote `total` rather than adding `subtotal` and `tax_total`. The table keeps its currency, and a new one takes the account's; the currency can only be changed in Bonsai. Pass the `updated_at` that `get_doc` or `create_doc` returned as `expected_updated_at`: if the document changed since, nothing is written — read it again with `get_doc`, rebuild the table from what it returns, and retry. Only drafts can be changed: a sent, signed or expired document is refused, and so is a document with more than one pricing table, which has to be changed in Bonsai. Nothing is sent to the client and the payment settings stay as they are; tell the user about every entry in `warnings`, which say when the new table changes what those settings collect. Anyone with the document open in the Bonsai editor has to reload it to see the new table, so tell the user to reload it if it is open.
List the taxes the account charges, such as a sales tax or VAT, ordered by name and paginated. This is where a tax name turns into the id a pricing table row takes: pass an entry's `id` in a row's `tax_rate_ids` on `set_doc_pricing_table`. When that call refuses a deleted tax rate, list your tax rates here and apply one still in use. Each entry has `id`, `name`, `rate` — the percentage charged, as a decimal string, so "8.25" means 8.25% — and `default`, whether Bonsai adds the rate to new invoice lines on its own; `default` has no effect on pricing table rows, which carry only the tax rates you send. Deleted tax rates are left out, so an id a `get_doc` row still carries may not be here.
One endpoint, the same key, whichever client you use.
~/Library/Application Support/Claude/claude_desktop_config.json (Mac) · %APPDATA%\Claude\claude_desktop_config.json (Windows)
Replace API_KEY with your own key.
Already have an "mcpServers" section in your config? Just add the server entry inside it.
Discovery, routing, credentials, tool scoping and execution logs all happen at the gateway→connections stay ACTIVE with no work from you
Bonsai MCP runs through a gateway that holds the credentials, scopes the access and records every call.
Managed auth, hosted MCP servers, and every Bonsai MCP tool your agent needs.
Free to start.
Get a standup ready view of what merged overnight and what is still open, grouped by author and age.