Estimates, invoices, clients and payments in Joist for contractors.
Maintain this server? Claim it
Call this server's tools from your browser. Nothing to install.
Requests go through mcptry because browsers can't call most MCP servers directly. Header values are sent to the server for that request only and are never stored or logged.
Tool list from our last check. Connect live to refresh it.
New client in your Clients list, ready for estimates and invoices. Creates a client card in Joist with name and optional email, phone, billing address and notes. It reaches no one. Joist does not refuse a duplicate name, so check list_clients first.
Client or company name.
Where documents are emailed.
Notes on the client card.
Billing address.
The server marks this tool as destructive. Running it may delete or overwrite data on the server or in any account you connected with your headers.
Pick your client. Values in capitals, like YOUR_TOKEN, are yours to fill in.
Run in a terminal
claude mcp add --transport http joist https://cotlet.dev/mcp/v1/joist/connect
New client in your Clients list, ready for estimates and invoices. Creates a client card in Joist with name and optional email, phone, billing address and notes. It reaches no one. Joist does not refuse a duplicate name, so check list_clients first.
| Argument | Type | Description |
|---|---|---|
| name* | string | Client or company name. |
| string | Where documents are emailed. | |
| notes | string | Notes on the client card. |
| phone | string | |
| address | object | Billing address. |
{
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"maxLength": 255,
"minLength": 1,
"description": "Client or company name."
},
"email": {
"type": "string",
"format": "email",
"description": "Where documents are emailed."
},
"notes": {
"type": "string",
"maxLength": 5000,
"description": "Notes on the client card."
},
"phone": {
"type": "string",
"maxLength": 30
},
"address": {
"type": "object",
"properties": {
"zip": {
"type": "string",
"maxLength": 20
},
"city": {
"type": "string",
"maxLength": 100
},
"state": {
"type": "string",
"maxLength": 100
},
"suite": {
"type": "string",
"maxLength": 100
},
"street": {
"type": "string",
"maxLength": 255
}
},
"description": "Billing address.",
"additionalProperties": false
}
},
"additionalProperties": false
}Save a service or material to your Items list. Saves an item with a name, rate and optional description to your Items list, so later estimates reuse it. It reaches no one.
| Argument | Type | Description |
|---|---|---|
| name* | string | Item name. |
| rate* | number | Price per unit, e.g. 350. |
| description | string | Shown on the line item. |
{
"type": "object",
"required": [
"name",
"rate"
],
"properties": {
"name": {
"type": "string",
"maxLength": 255,
"minLength": 1,
"description": "Item name."
},
"rate": {
"type": "number",
"minimum": 0,
"description": "Price per unit, e.g. 350."
},
"description": {
"type": "string",
"maxLength": 5000,
"description": "Shown on the line item."
}
},
"additionalProperties": false
}Generate an invoice with everything from the estimate. Nothing is sent to the client. Does what Generate Invoice does in Joist: a new invoice with the estimate's client, lines, tax, discount and deposit, and the estimate becomes approved. Joist allows more than one invoice from an estimate, so an estimate already invoiced is refused here. Nothing is sent until send_invoice.
| Argument | Type | Description |
|---|---|---|
| estimate_id* | string | From list_estimates. |
{
"type": "object",
"required": [
"estimate_id"
],
"properties": {
"estimate_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "From list_estimates."
}
},
"additionalProperties": false
}How to work in an app by clicking in the user's Cloud Browser with Cotlet's browser tools: which browser to use for this workspace and the sign-in link, the method step by step, the app's screens, menus and actions, notes, and the app's dedicated tools when Cotlet has them. Call it before the first browser step in an app. Works for any web app; one outside Cotlet's catalog gets the general method. Free, no browser session.
| Argument | Type | Description |
|---|---|---|
| app | string | The app's name or Cotlet slug. Default: Joist. |
| task | string | What the user wants done in the app, in their words. |
{
"type": "object",
"properties": {
"app": {
"type": "string",
"maxLength": 120,
"description": "The app's name or Cotlet slug. Default: Joist."
},
"task": {
"type": "string",
"maxLength": 500,
"description": "What the user wants done in the app, in their words."
}
},
"additionalProperties": false
}The apps of the user's workspace and whether each works now: the app account in the user's Cloud Browser (connected, signed out, signed in but not connected), and the one step the user has to take, with the console link. Free. Needs the user's Cotlet API key or sign-in.
| Argument | Type | Description |
|---|---|---|
| app | string | Only this app, e.g. skool. |
{
"type": "object",
"properties": {
"app": {
"type": "string",
"description": "Only this app, e.g. skool."
}
},
"additionalProperties": false
}Clicks element `element`, or point `x`, `y` of the screenshot; `double` double-clicks, `button: right` right-clicks. Answers the page after it: the elements and text in view, with the same numbers. Says when nothing changed. A link that would open a new tab opens in this one. Buttons that pay or buy, and sign-in and account pages, are refused.
| Argument | Type | Description |
|---|---|---|
| x | number | Instead of `element`: pixels from the left of the screenshot. |
| y | number | With `x`: pixels from the top of the screenshot. |
| button | string | right opens the page's own context menu. Default: left. |
| dialog | string | How to answer a native dialog (confirm, prompt, 'leave this page?') this step opens: accept presses OK, dismiss presses Cancel. Default: OK for an alert, Cancel for every other dialog. |
| double | boolean | Double-click. |
| element | integer | The element's number from the last read or step answer. |
| dialog_text | string | Text for a prompt dialog this step opens; implies accept. |
Ends the browser session when the task is done: the browser keeps its cookies and sleeps, and the per-minute charge stops. Answers the session's minutes and credits.
No arguments.
{
"type": "object",
"properties": {},
"additionalProperties": false
}Gets the file that clicking `element` (a download link or button) or opening `url` starts: answers its name, type, size and a link valid 15 minutes; CSV, JSON and other text files also as text.
| Argument | Type | Description |
|---|---|---|
| url | string | A public http or https address, e.g. https://example.com. |
| element | integer | The element's number from the last read or step answer. |
{
"type": "object",
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "A public http or https address, e.g. https://example.com."
},
"element": {
"type": "integer",
"minimum": 1,
"description": "The element's number from the last read or step answer."
}
},
"additionalProperties": false
}Drags from one element or point to another: a card between columns, a piece on a board, a slider's handle. Answers the page after it: the elements and text in view, with the same numbers.
| Argument | Type | Description |
|---|---|---|
| to* | object | An element number, or x and y of the screenshot. |
| from* | object | An element number, or x and y of the screenshot. |
| dialog | string | How to answer a native dialog (confirm, prompt, 'leave this page?') this step opens: accept presses OK, dismiss presses Cancel. Default: OK for an alert, Cancel for every other dialog. |
| dialog_text | string | Text for a prompt dialog this step opens; implies accept. |
{
"type": "object",
"required": [
"from",
"to"
],
"properties": {
"to": {
"type": "object",
"properties": {
"x": {
"type": "number",
"minimum": 0,
"description": "Instead of `element`: pixels from the left of the screenshot."
},
"y": {
"type": "number",
"minimum": 0,
"description": "With `x`: pixels from the top of the screenshot."
},
"element": {
"type": "integer",
"minimum": 1,
"description": "The element's number from the last read or step answer."
}
},
"description": "An element number, or x and y of the screenshot.",
"additionalProperties": false
},
"from": {
"type": "object",
"properties": {
"x": {
"type": "number",
"minimum": 0,
"description": "Instead of `element`: pixels from the left of the screenshot."
},
"y": {
"type": "number",
"minimum": 0,
"description": "With `x`: pixels from the top of the screenshot."
},
"element": {
"type": "integer",
"minimum": 1,
"description": "The element's number from the last read or step answer."
}
},
"description": "An element number, or x and y of the screenshot.",
"additionalProperties": false
},
"dialog": {
"enum": [
"accept",
"dismiss"
],
"type": "string",
"description": "How to answer a native dialog (confirm, prompt, 'leave this page?') this step opens: accept presses OK, dismiss presses Cancel. Default: OK for an alert, Cancel for every other dialog."
},
"dialog_text": {
"type": "string",
"maxLength": 1000,
"description": "Text for a prompt dialog this step opens; implies accept."
}
},
"additionalProperties": false
}Runs a JavaScript function body on the page and answers what it returns, as JSON. Use it to extract data the page holds (a table, a list, attributes); act with the other tools. It cannot send requests, click, submit, or read cookies or storage; a filled field's value reads [hidden by Cotlet]. Example: return [...document.querySelectorAll('h2')].map(h => h.innerText)
| Argument | Type | Description |
|---|---|---|
| script* | string | A function body that returns. |
{
"type": "object",
"required": [
"script"
],
"properties": {
"script": {
"type": "string",
"maxLength": 20000,
"description": "A function body that returns."
}
},
"additionalProperties": false
}Fills several fields in one step: text fields take text, drop-downs an option's label, checkboxes, radios and switches true or false. Stops at the first field that fails and names those filled. Does not send the form: click its button after. Answers the page after it: the elements and text in view, with the same numbers.
| Argument | Type | Description |
|---|---|---|
| dialog | string | How to answer a native dialog (confirm, prompt, 'leave this page?') this step opens: accept presses OK, dismiss presses Cancel. Default: OK for an alert, Cancel for every other dialog. |
| fields* | array | The fields in the order a person fills them. |
| dialog_text | string | Text for a prompt dialog this step opens; implies accept. |
{
"type": "object",
"required": [
"fields"
],
"properties": {
"dialog": {
"enum": [
"accept",
"dismiss"
],
"type": "string",
"description": "How to answer a native dialog (confirm, prompt, 'leave this page?') this step opens: accept presses OK, dismiss presses Cancel. Default: OK for an alert, Cancel for every other dialog."
},
"fields": {
"type": "array",
"items": {
"type": "object",
"required": [
"element",
"value"
],
"properties": {
"value": {
"type": [
"string",
"boolean"
],
"description": "Text, option, or true/false."
},
"element": {
"type": "integer",
"minimum": 1,
"description": "The element's number from the last read or step answer."
}
},
"additionalProperties": false
},
"maxItems": 30,
"minItems": 1,
"description": "The fields in the order a person fills them."
},
"dialog_text": {
"type": "string",
"maxLength": 1000,
"description": "Text for a prompt dialog this step opens; implies accept."
}
},
"additionalProperties": false
}Finds `text` on the whole current page, frames included: the elements whose label holds it, with their numbers, and each line of text that holds it. Use it instead of reading or scrolling a long page for one thing.
| Argument | Type | Description |
|---|---|---|
| kind | string | Only elements of this kind. Default: any. |
| text* | string | Words to look for, any case. |
{
"type": "object",
"required": [
"text"
],
"properties": {
"kind": {
"enum": [
"link",
"button",
"field",
"any"
],
"type": "string",
"description": "Only elements of this kind. Default: any."
},
"text": {
"type": "string",
"maxLength": 200,
"description": "Words to look for, any case."
}
},
"additionalProperties": false
}Rests the pointer on an element or a point, to open a hover menu, a tooltip or a preview. Answers the page after it: the elements and text in view, with the same numbers.
| Argument | Type | Description |
|---|---|---|
| x | number | Instead of `element`: pixels from the left of the screenshot. |
| y | number | With `x`: pixels from the top of the screenshot. |
| element | integer | The element's number from the last read or step answer. |
{
"type": "object",
"properties": {
"x": {
"type": "number",
"minimum": 0,
"description": "Instead of `element`: pixels from the left of the screenshot."
},
"y": {
"type": "number",
"minimum": 0,
"description": "With `x`: pixels from the top of the screenshot."
},
"element": {
"type": "integer",
"minimum": 1,
"description": "The element's number from the last read or step answer."
}
},
"additionalProperties": false
}Opens `url` in the current tab, or goes `history` back or forward. Answers the page after it: the elements and text in view, with the same numbers.
| Argument | Type | Description |
|---|---|---|
| url | string | A public http or https address, e.g. https://example.com. |
| dialog | string | How to answer a native dialog (confirm, prompt, 'leave this page?') this step opens: accept presses OK, dismiss presses Cancel. Default: OK for an alert, Cancel for every other dialog. |
| history | string | Instead of `url`. |
| dialog_text | string | Text for a prompt dialog this step opens; implies accept. |
{
"type": "object",
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "A public http or https address, e.g. https://example.com."
},
"dialog": {
"enum": [
"accept",
"dismiss"
],
"type": "string",
"description": "How to answer a native dialog (confirm, prompt, 'leave this page?') this step opens: accept presses OK, dismiss presses Cancel. Default: OK for an alert, Cancel for every other dialog."
},
"history": {
"enum": [
"back",
"forward"
],
"type": "string",
"description": "Instead of `url`."
},
"dialog_text": {
"type": "string",
"maxLength": 1000,
"description": "Text for a prompt dialog this step opens; implies accept."
}
},
"additionalProperties": false
}Starts the user's Joist Cloud Browser (a real Chrome on Cotlet, signed in to Joist as the user; use it for what the Joist tools do not cover) and opens `url`. Call it first. Only Joist pages open. Answers the page after it: the elements and text in view, with the same numbers.
| Argument | Type | Description |
|---|---|---|
| url | string | A public http or https address, e.g. https://example.com. |
{
"type": "object",
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "A public http or https address, e.g. https://example.com."
}
},
"additionalProperties": false
}Presses a key on the focused element: Enter to send, Tab to the next field, Escape to close a pop-up, arrows in a list or slider, or one character (a site's shortcut). Answers the page after it: the elements and text in view, with the same numbers.
| Argument | Type | Description |
|---|---|---|
| key* | string | One of Enter, Tab, Shift+Tab, Escape, Backspace, Delete, Space, ArrowDown, ArrowUp, ArrowLeft, ArrowRight, PageDown, PageUp, Home, End, or one character such as a or 5. |
| dialog | string | How to answer a native dialog (confirm, prompt, 'leave this page?') this step opens: accept presses OK, dismiss presses Cancel. Default: OK for an alert, Cancel for every other dialog. |
| repeat | integer | Times to press it. Default 1. |
| dialog_text | string | Text for a prompt dialog this step opens; implies accept. |
{
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"anyOf": [
{
"enum": [
"Enter",
"Tab",
"Shift+Tab",
"Escape",
"Backspace",
"Delete",
"Space",
"ArrowDown",
"ArrowUp",
"ArrowLeft",
"ArrowRight",
"PageDown",
"PageUp",
"Home",
"End"
]
},
{
"pattern": "^[ -~]$"
}
],
"description": "One of Enter, Tab, Shift+Tab, Escape, Backspace, Delete, Space, ArrowDown, ArrowUp, ArrowLeft, ArrowRight, PageDown, PageUp, Home, End, or one character such as a or 5."
},
"dialog": {
"enum": [
"accept",
"dismiss"
],
"type": "string",
"description": "How to answer a native dialog (confirm, prompt, 'leave this page?') this step opens: accept presses OK, dismiss presses Cancel. Default: OK for an alert, Cancel for every other dialog."
},
"repeat": {
"type": "integer",
"maximum": 20,
"minimum": 1,
"description": "Times to press it. Default 1."
},
"dialog_text": {
"type": "string",
"maxLength": 1000,
"description": "Text for a prompt dialog this step opens; implies accept."
}
},
"additionalProperties": false
}The whole current page: its text (up to 20,000 characters) and a numbered list of its links, buttons, fields and other controls, open dialogs first. Use it on a new page or when a step's answer is not enough; `element` reads only inside one element (a dialog, a form, a list). Never returns field values, cookies or storage.
| Argument | Type | Description |
|---|---|---|
| element | integer | Read only inside this element. |
{
"type": "object",
"properties": {
"element": {
"type": "integer",
"minimum": 1,
"description": "Read only inside this element."
}
},
"additionalProperties": false
}A JPEG of the visible part of the page. Use it for boards, canvases, maps, charts and layout that the text does not show, then click or drag by `x`, `y` of the image.
No arguments.
{
"type": "object",
"properties": {},
"additionalProperties": false
}Scrolls with the mouse wheel: `down` or `up` by `screens`, to `top` or `bottom`, until `element` is in view, or inside a panel or list (`inside`). Content that loads on scroll appears. Answers the page after it: the elements and text in view, with the same numbers.
| Argument | Type | Description |
|---|---|---|
| inside | integer | Scroll inside this element, not the page. |
| element | integer | Scroll until this element is in view. |
| screens | number | Viewport heights, for down and up. Default: 0.8. |
| direction | string | Default: down. |
{
"type": "object",
"properties": {
"inside": {
"type": "integer",
"minimum": 1,
"description": "Scroll inside this element, not the page."
},
"element": {
"type": "integer",
"minimum": 1,
"description": "Scroll until this element is in view."
},
"screens": {
"type": "number",
"maximum": 10,
"description": "Viewport heights, for down and up. Default: 0.8.",
"exclusiveMinimum": 0
},
"direction": {
"enum": [
"down",
"up",
"top",
"bottom"
],
"type": "string",
"description": "Default: down."
}
},
"additionalProperties": false
}Chooses `option` (its label as listed, or its value) in drop-down `element`. For a home-made drop-down, click it, then click the option. Answers the page after it: the elements and text in view, with the same numbers.
| Argument | Type | Description |
|---|---|---|
| dialog | string | How to answer a native dialog (confirm, prompt, 'leave this page?') this step opens: accept presses OK, dismiss presses Cancel. Default: OK for an alert, Cancel for every other dialog. |
| option* | string | The option. |
| element* | integer | The element's number from the last read or step answer. |
| dialog_text | string | Text for a prompt dialog this step opens; implies accept. |
{
"type": "object",
"required": [
"element",
"option"
],
"properties": {
"dialog": {
"enum": [
"accept",
"dismiss"
],
"type": "string",
"description": "How to answer a native dialog (confirm, prompt, 'leave this page?') this step opens: accept presses OK, dismiss presses Cancel. Default: OK for an alert, Cancel for every other dialog."
},
"option": {
"type": "string",
"maxLength": 300,
"description": "The option."
},
"element": {
"type": "integer",
"minimum": 1,
"description": "The element's number from the last read or step answer."
},
"dialog_text": {
"type": "string",
"maxLength": 1000,
"description": "Text for a prompt dialog this step opens; implies accept."
}
},
"additionalProperties": false
}Types `text` into field `element`, replacing what it held; `submit` presses Enter after it. For several fields use fill_form. Without `element` the text goes where the focus is, as a person types after clicking: for spreadsheet cells and editors that read does not number (Google Sheets, Docs), click the place first; there a tab in `text` presses Tab and a line break Enter, so one call fills a table. Answers the page after it: the elements and text in view, with the same numbers. Password, card and one-time code fields are refused: the user types them.
| Argument | Type | Description |
|---|---|---|
| text* | string | The text to type. |
| dialog | string | How to answer a native dialog (confirm, prompt, 'leave this page?') this step opens: accept presses OK, dismiss presses Cancel. Default: OK for an alert, Cancel for every other dialog. |
| slowly | boolean | Key by key, for a search or address field whose suggestions do not appear. |
| submit | boolean | Press Enter after typing, e.g. to search. |
| element | integer | The field's number from the last read. Leave it out to type where the focus is. |
| dialog_text | string | Text for a prompt dialog this step opens; implies accept. |
Puts files into file field or upload button `element`: each file is {"url": "https://..."} or {"file_id": "..."} of a file uploaded to Cotlet; up to 10 files, 20 MB each. Answers the page after it: the elements and text in view, with the same numbers.
| Argument | Type | Description |
|---|---|---|
| files* | array | The files. |
| element* | integer | The element's number from the last read or step answer. |
{
"type": "object",
"required": [
"element",
"files"
],
"properties": {
"files": {
"type": "array",
"items": {
"type": "object",
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "A public file URL."
},
"file_id": {
"type": "string",
"description": "A file uploaded to Cotlet."
}
},
"additionalProperties": false
},
"maxItems": 10,
"minItems": 1,
"description": "The files."
},
"element": {
"type": "integer",
"minimum": 1,
"description": "The element's number from the last read or step answer."
}
},
"additionalProperties": false
}Waits until `text` shows on the page (at most `seconds`, default 5, up to 20), or just `seconds`: for results that load slowly. Says whether the text appeared. Answers the page after it: the elements and text in view, with the same numbers.
| Argument | Type | Description |
|---|---|---|
| text | string | Text to wait for. |
| seconds | number | At most. |
{
"type": "object",
"properties": {
"text": {
"type": "string",
"maxLength": 200,
"description": "Text to wait for."
},
"seconds": {
"type": "number",
"maximum": 20,
"description": "At most.",
"exclusiveMinimum": 0
}
},
"additionalProperties": false
}Status of an earlier Cotlet call, by its call_id: pending (waits for the owner's approval in the Cotlet console), approved, running, success, rejected, expired, blocked or failed. For a call that answered with status pending or running.
| Argument | Type | Description |
|---|---|---|
| call_id* | string | call_id from the earlier call |
{
"type": "object",
"required": [
"call_id"
],
"properties": {
"call_id": {
"type": "string",
"description": "call_id from the earlier call"
}
},
"additionalProperties": false
}Sends a report to the Cotlet team: a tool that failed or returned wrong data, an error you could not get past, or a step the browser could not do, with the page, the step and what you saw. A missing dedicated tool is not a problem to report: do the task in the browser. A feature the user asks for goes as feature_request. A report promises nothing: tell the user it was sent, never that something will be built or fixed, or when. The report carries the error text and call_id when there is one, never passwords or tokens. Needs the user's Cotlet API key or sign-in.
| Argument | Type | Description |
|---|---|---|
| app | any | App slug the report is about, e.g. 'skool', or for a site in the web browser its host, e.g. 'app.example.com'. |
| kind* | string | bug: a tool did the wrong thing or returned wrong data. error: a call failed or returned an error you could not get past. feature_request: something new the user asked for. missing_feature: a step the task needed that you tried and could not do, in the browser or with a tool. other: anything else. |
| tool | any | Tool or endpoint name, e.g. 'skool_list_members'. |
| title* | string | One line, e.g. 'skool_list_members stops after 50 members'. |
| client | any | Agent or tool sending this, e.g. 'Claude Code'. |
| call_id | any | call_id of the call that went wrong, if any. |
Requests access to apps for the user's workspace, as Request access in the console: every app is granted at once. Not needed to work in an app: the user's AI works in any web app in their Web browser with cotlet_app_guide. An app with dedicated tools gets them after the user signs in to it in their Cloud Browser. Access says nothing about whether a task works there: check that in the browser. Takes app slugs as listed at https://cotlet.dev/api/v1/apps. Free. Needs the user's Cotlet API key or sign-in.
| Argument | Type | Description |
|---|---|---|
| apps* | array | App slugs, e.g. ["skool", "substack"]. |
| use_case | string | What the user wants the agent to do in these apps, in their words. |
{
"type": "object",
"required": [
"apps"
],
"properties": {
"apps": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 10,
"minItems": 1,
"description": "App slugs, e.g. [\"skool\", \"substack\"]."
},
"use_case": {
"type": "string",
"maxLength": 1000,
"description": "What the user wants the agent to do in these apps, in their words."
}
},
"additionalProperties": false
}Returns a one-time upload_url and a file_id for a local file (an image for a post, an attachment). The file is sent to upload_url with an HTTP PUT; an argument that takes a file accepts {"file_id": "..."}. Up to 25 MB, the URL works once. Free. Needs the user's Cotlet API key or sign-in.
| Argument | Type | Description |
|---|---|---|
| name | string | File name, e.g. photo.jpg. |
| content_type | string | MIME type, e.g. image/jpeg. |
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "File name, e.g. photo.jpg."
},
"content_type": {
"type": "string",
"description": "MIME type, e.g. image/jpeg."
}
},
"additionalProperties": false
}The Cotlet account this connection acts as: email, workspace, access status, the API key or OAuth client in use with its scope, the apps the workspace may call and its credit balance. Free. Needs the user's Cotlet API key or sign-in.
No arguments.
{
"type": "object",
"properties": {},
"additionalProperties": false
}New saved estimate with line items, tax, discount, deposit and expiry. Nothing is sent to the client. Creates an estimate for a client with the next estimate number, copying the client's name, email, phone and address as Joist does. tax_rate applies one percent to every line, reusing your saved rate with that percent or adding one; tax_id picks a saved rate. A deposit shows as Deposit Due. expires_on cannot be in the past. Nothing is sent: the client sees it only after send_estimate. Joist computes the total.
| Argument | Type | Description |
|---|---|---|
| notes | string | Notes shown on the document. |
| tax_id | string | A saved tax rate from list_items (taxes) instead of tax_rate. |
| deposit | object | Requested deposit, shown as Deposit Due: percent or amount. |
| discount | object | Discount on the subtotal: percent or amount. |
| tax_rate | number | Tax percent on every line, e.g. 7.25. Uses your tax rate with that percent, or adds one. |
| client_id* | string | From list_clients or add_client. |
| po_number | string | PO number. |
New saved invoice for a client without an estimate. Nothing is sent to the client. Creates an invoice with the next invoice number, the client's details, line items, tax, discount, deposit and payment terms (due date = invoice date + payment_terms_days). For work you estimated, use convert_estimate_to_invoice instead. Nothing is sent: the client sees it only after send_invoice.
| Argument | Type | Description |
|---|---|---|
| notes | string | Notes shown on the document. |
| tax_id | string | A saved tax rate from list_items (taxes) instead of tax_rate. |
| deposit | object | Requested deposit, shown as Deposit Due: percent or amount. |
| discount | object | Discount on the subtotal: percent or amount. |
| tax_rate | number | Tax percent on every line, e.g. 7.25. Uses your tax rate with that percent, or adds one. |
| client_id* | string | From list_clients or add_client. |
| po_number | string | PO number. |
| line_items* | array |
Delete an estimate for good. Deletes the estimate from Joist; it cannot be undone and the client's link stops working. An invoice made from it stays.
| Argument | Type | Description |
|---|---|---|
| estimate_id* | string | From list_estimates. |
{
"type": "object",
"required": [
"estimate_id"
],
"properties": {
"estimate_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "From list_estimates."
}
},
"additionalProperties": false
}Delete an invoice that has no payments, for good. Deletes the invoice from Joist; it cannot be undone and the client's link stops working. An invoice with payments recorded is refused, so your payment history stays.
| Argument | Type | Description |
|---|---|---|
| invoice_id* | string | From list_invoices. |
{
"type": "object",
"required": [
"invoice_id"
],
"properties": {
"invoice_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "From list_invoices."
}
},
"additionalProperties": false
}Joist's monthly CSV of estimates or invoices: number, client, subtotal, tax, total, dates, payments. Runs the same export as the Export button on Joist's Estimates or Invoices page for one month, by date issued: number, client name, subtotal, one column per tax name, total, date issued, date created and, for invoices, payment received less refunds. Returns the columns, the rows and the CSV text as Joist made it, at most 2000 rows parsed. Line items are not in the export; use get_estimate or get_invoice.
| Argument | Type | Description |
|---|---|---|
| type* | string | |
| month* | string | Month as YYYY-MM. |
{
"type": "object",
"required": [
"type",
"month"
],
"properties": {
"type": {
"enum": [
"estimates",
"invoices"
],
"type": "string"
},
"month": {
"type": "string",
"pattern": "^[0-9]{4}-(0[1-9]|1[0-2])$",
"description": "Month as YYYY-MM."
}
},
"additionalProperties": false
}One client card: every email and phone, billing and service address, notes and tags. Returns one client with all emails and phones, the billing and service address, notes and tags.
| Argument | Type | Description |
|---|---|---|
| client_id* | string | From list_clients. |
{
"type": "object",
"required": [
"client_id"
],
"properties": {
"client_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "From list_clients."
}
},
"additionalProperties": false
}Full estimate: client, line items, tax, discount, deposit, expiry, approval and signature. Returns one estimate with its client, every line item with its taxes, subtotal, discount, tax, total, requested deposit, expiry, approval status, signature date and the client's link. Read it before changing or sending the estimate.
| Argument | Type | Description |
|---|---|---|
| estimate_id* | string | From list_estimates. |
{
"type": "object",
"required": [
"estimate_id"
],
"properties": {
"estimate_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "From list_estimates."
}
},
"additionalProperties": false
}Full invoice: client, line items, tax, discount, deposit, due date, balance and every payment. Returns one invoice with its client, every line item with its taxes, subtotal, discount, tax, total, requested deposit, payment terms and due date, amount paid, balance due, overdue, the estimate it came from, and the payments and refunds recorded on it. Check the payments before recording another.
| Argument | Type | Description |
|---|---|---|
| invoice_id* | string | From list_invoices. |
{
"type": "object",
"required": [
"invoice_id"
],
"properties": {
"invoice_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "From list_invoices."
}
},
"additionalProperties": false
}A Joist report for a date range: revenue, invoices, estimates or sales tax, with the period total. Returns one of the reports on Joist's Reports page for a date range, optionally for one client: revenue received per invoice (partial payments included), invoice statuses and balances, estimate statuses, or sales tax collected per tax. total is the report's total for the period. Joist updates report numbers once a day, so today's documents and payments show tomorrow. Reports are part of Joist Elite (and the trial). Pass next_cursor back as cursor.
| Argument | Type | Description |
|---|---|---|
| to* | string | Last day, up to a year after from. |
| from* | string | First day of the period. |
| limit | integer | |
| client | string | Only this client, by exact name. |
| cursor | string | next_cursor from the previous call. |
| report* | string | revenue (payments received per invoice, with balances at the start and end of the period), invoices (status, total, balance and due date), estimates (status, total, expiry) or sales_tax (tax collected per tax name and invoice). |
| tax_name | string |
Clients in your account with name, email, phone and address. Lists your Joist clients A to Z, one Joist page per call, or the matches of search (name, email, phone or address). Pass next_cursor back as cursor; an empty page ends the list. client_id goes to create_estimate, create_invoice and the client filter of list_estimates and list_invoices.
| Argument | Type | Description |
|---|---|---|
| cursor | string | next_cursor from the previous call. |
| search | string | Part of a name, email, phone or address. |
{
"type": "object",
"required": [],
"properties": {
"cursor": {
"type": "string",
"description": "next_cursor from the previous call."
},
"search": {
"type": "string",
"maxLength": 100,
"description": "Part of a name, email, phone or address."
}
},
"additionalProperties": false
}Estimates with number, client, total, expiry, status, approval, and whether it was emailed or signed. Lists estimates newest first, from one of the panel's tabs (approval pending, approved, declined or all), optionally searched by client name or number or limited to one client. status is Joist's: draft, issued (sent), invoiced (an invoice was made from it). email_status says whether it was emailed (unsent, sent) and signed whether the client signed it. Pass next_cursor back as cursor.
| Argument | Type | Description |
|---|---|---|
| limit | integer | |
| cursor | string | next_cursor from the previous call. |
| search | string | Client name or estimate number. |
| approval | string | The panel's tabs: pending, approved, declined, or all (default). |
| client_id | string | Only this client's estimates (list_clients). |
{
"type": "object",
"required": [],
"properties": {
"limit": {
"type": "integer",
"default": 40,
"maximum": 100,
"minimum": 1
},
"cursor": {
"type": "string",
"description": "next_cursor from the previous call."
},
"search": {
"type": "string",
"maxLength": 100,
"description": "Client name or estimate number."
},
"approval": {
"enum": [
"all",
"pending",
"approved",
"declined"
],
"type": "string",
"default": "all",
"description": "The panel's tabs: pending, approved, declined, or all (default)."
},
"client_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "Only this client's estimates (list_clients)."
}
},
"additionalProperties": false
}Invoices with client, total, amount paid, balance due, due date and overdue. Active, paid or all. Lists invoices newest first from the panel's Active tab (not paid in full; default) or Paid tab, or all. overdue keeps the active ones past their due date, which is the invoice date plus its payment terms. status is Joist's: draft, issued (sent), partial or paid. Search matches client name or number. Each page of overdue may hold fewer rows than limit; follow next_cursor until it is null.
| Argument | Type | Description |
|---|---|---|
| view | string | active (not paid in full; default), overdue (active and past due), paid, or all. |
| limit | integer | |
| cursor | string | next_cursor from the previous call. |
| search | string | Client name or invoice number. |
| client_id | string | Only this client's invoices (list_clients). |
{
"type": "object",
"required": [],
"properties": {
"view": {
"enum": [
"active",
"overdue",
"paid",
"all"
],
"type": "string",
"default": "active",
"description": "active (not paid in full; default), overdue (active and past due), paid, or all."
},
"limit": {
"type": "integer",
"default": 25,
"maximum": 50,
"minimum": 1
},
"cursor": {
"type": "string",
"description": "next_cursor from the previous call."
},
"search": {
"type": "string",
"maxLength": 100,
"description": "Client name or invoice number."
},
"client_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "Only this client's invoices (list_clients)."
}
},
"additionalProperties": false
}Saved items from your Items list with rate and description, plus your tax rates. Lists the items you saved in Joist, one Joist page per call, or the matches of search on the name. Reuse their names and rates as line items (item_id) instead of guessing prices. The first page also lists your tax rates (tax_id for create_estimate and create_invoice); later pages leave taxes empty. Pass next_cursor back as cursor; an empty page ends the list.
| Argument | Type | Description |
|---|---|---|
| cursor | string | next_cursor from the previous call. |
| search | string | Part of the item name. |
{
"type": "object",
"required": [],
"properties": {
"cursor": {
"type": "string",
"description": "next_cursor from the previous call."
},
"search": {
"type": "string",
"maxLength": 100,
"description": "Part of the item name."
}
},
"additionalProperties": false
}Record a cash, check, credit card or bank transfer payment on an invoice; paid in full moves to Paid. Records a payment received outside Joist Payments, as Record Payment does in Joist. Partial payments are allowed and an invoice can hold several; an amount above the balance due or a date in the future is refused. Once paid in full the invoice moves from Active to Paid. Check get_invoice first so the same check is not entered twice. By default you confirm each call, and you can switch that off for this tool.
| Argument | Type | Description |
|---|---|---|
| amount* | number | Amount received, e.g. 1200.50. |
| method* | string | How the client paid. |
| paid_on | string | Payment date. Default: today. |
| invoice_id* | string | From list_invoices. |
{
"type": "object",
"required": [
"invoice_id",
"amount",
"method"
],
"properties": {
"amount": {
"type": "number",
"description": "Amount received, e.g. 1200.50.",
"exclusiveMinimum": 0
},
"method": {
"enum": [
"cash",
"check",
"credit_card",
"bank_transfer"
],
"type": "string",
"description": "How the client paid."
},
"paid_on": {
"type": "string",
"format": "date",
"description": "Payment date. Default: today."
},
"invoice_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "From list_invoices."
}
},
"additionalProperties": false
}Email the estimate to the client from Joist with its link and PDF. Emails the estimate from Joist, as its Send button does: the client gets a link to view and sign it and the PDF. Without to it goes to the client email on the document; subject and message default to Joist's subject and your saved estimate message. A draft becomes issued; a sent estimate can be sent again. By default you confirm each call, and you can switch that off for this tool. Waits for the owner's approval.
| Argument | Type | Description |
|---|---|---|
| to | array | Recipients. Default: the client email on the document. |
| message | string | Default: your estimate email message. |
| subject | string | Default: Joist's subject. |
| estimate_id* | string | From list_estimates. |
{
"type": "object",
"required": [
"estimate_id"
],
"properties": {
"to": {
"type": "array",
"items": {
"type": "string",
"format": "email"
},
"maxItems": 10,
"minItems": 1,
"description": "Recipients. Default: the client email on the document."
},
"message": {
"type": "string",
"maxLength": 5000,
"description": "Default: your estimate email message."
},
"subject": {
"type": "string",
"maxLength": 200,
"description": "Default: Joist's subject."
},
"estimate_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "From list_estimates."
}
},
"additionalProperties": false
}Email the invoice to the client from Joist with its link and PDF. Emails the invoice from Joist, as its Send button does: the client gets a link to view and pay it and the PDF. Without to it goes to the client email on the document; subject and message default to Joist's subject and your saved invoice message. A draft becomes issued; a sent invoice can be sent again. By default you confirm each call, and you can switch that off for this tool. Waits for the owner's approval.
| Argument | Type | Description |
|---|---|---|
| to | array | Recipients. Default: the client email on the document. |
| message | string | Default: your invoice email message. |
| subject | string | Default: Joist's subject. |
| invoice_id* | string | From list_invoices. |
{
"type": "object",
"required": [
"invoice_id"
],
"properties": {
"to": {
"type": "array",
"items": {
"type": "string",
"format": "email"
},
"maxItems": 10,
"minItems": 1,
"description": "Recipients. Default: the client email on the document."
},
"message": {
"type": "string",
"maxLength": 5000,
"description": "Default: your invoice email message."
},
"subject": {
"type": "string",
"maxLength": 200,
"description": "Default: Joist's subject."
},
"invoice_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "From list_invoices."
}
},
"additionalProperties": false
}Change an estimate's lines, tax, discount, deposit, expiry, notes or PO number. Nothing is sent. Changes only the fields you pass. line_items replaces every line (the tax stays unless you pass tax_rate, tax_id or remove_tax). tax_rate or tax_id alone re-taxes the current lines. discount or deposit null removes it. Joist recomputes the total. Nothing is sent; a client who already got the estimate sees the change behind the same link.
| Argument | Type | Description |
|---|---|---|
| notes | string | Notes shown on the document. |
| tax_id | string | A saved tax rate from list_items (taxes) instead of tax_rate. |
| deposit | object | null | null removes it. |
| discount | object | null | null removes it. |
| tax_rate | number | Tax percent on every line, e.g. 7.25. Uses your tax rate with that percent, or adds one. |
| po_number | string | PO number. |
| expires_on | string | New last valid day. |
| line_items | array | Replaces every line. Leave out to keep the lines. |
We read every tool's name, description and inputs the way the model will, looking for instructions hidden from you. A clean result means no warning signs, not a guarantee. Scanned Nov 9, 2026.
{
"type": "object",
"properties": {
"x": {
"type": "number",
"minimum": 0,
"description": "Instead of `element`: pixels from the left of the screenshot."
},
"y": {
"type": "number",
"minimum": 0,
"description": "With `x`: pixels from the top of the screenshot."
},
"button": {
"enum": [
"left",
"right"
],
"type": "string",
"description": "right opens the page's own context menu. Default: left."
},
"dialog": {
"enum": [
"accept",
"dismiss"
],
"type": "string",
"description": "How to answer a native dialog (confirm, prompt, 'leave this page?') this step opens: accept presses OK, dismiss presses Cancel. Default: OK for an alert, Cancel for every other dialog."
},
"double": {
"type": "boolean",
"description": "Double-click."
},
"element": {
"type": "integer",
"minimum": 1,
"description": "The element's number from the last read or step answer."
},
"dialog_text": {
"type": "string",
"maxLength": 1000,
"description": "Text for a prompt dialog this step opens; implies accept."
}
},
"additionalProperties": false
}{
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string",
"maxLength": 5000,
"description": "The text to type."
},
"dialog": {
"enum": [
"accept",
"dismiss"
],
"type": "string",
"description": "How to answer a native dialog (confirm, prompt, 'leave this page?') this step opens: accept presses OK, dismiss presses Cancel. Default: OK for an alert, Cancel for every other dialog."
},
"slowly": {
"type": "boolean",
"description": "Key by key, for a search or address field whose suggestions do not appear."
},
"submit": {
"type": "boolean",
"description": "Press Enter after typing, e.g. to search."
},
"element": {
"type": "integer",
"minimum": 1,
"description": "The field's number from the last read. Leave it out to type where the focus is."
},
"dialog_text": {
"type": "string",
"maxLength": 1000,
"description": "Text for a prompt dialog this step opens; implies accept."
}
},
"additionalProperties": false
}| description* |
| string |
| What the user asked for, what you tried, what you expected and what happened, with the error text if there was one. No passwords, tokens or other secrets. |
{
"type": "object",
"title": "AgentReportCreate",
"required": [
"kind",
"title",
"description"
],
"properties": {
"app": {
"anyOf": [
{
"type": "string",
"maxLength": 80
},
{
"type": "null"
}
],
"title": "App",
"default": null,
"description": "App slug the report is about, e.g. 'skool', or for a site in the web browser its host, e.g. 'app.example.com'."
},
"kind": {
"enum": [
"bug",
"error",
"feature_request",
"missing_feature",
"other"
],
"type": "string",
"title": "Kind",
"description": "bug: a tool did the wrong thing or returned wrong data. error: a call failed or returned an error you could not get past. feature_request: something new the user asked for. missing_feature: a step the task needed that you tried and could not do, in the browser or with a tool. other: anything else."
},
"tool": {
"anyOf": [
{
"type": "string",
"maxLength": 160
},
{
"type": "null"
}
],
"title": "Tool",
"default": null,
"description": "Tool or endpoint name, e.g. 'skool_list_members'."
},
"title": {
"type": "string",
"title": "Title",
"maxLength": 200,
"minLength": 3,
"description": "One line, e.g. 'skool_list_members stops after 50 members'."
},
"client": {
"anyOf": [
{
"type": "string",
"maxLength": 80
},
{
"type": "null"
}
],
"title": "Client",
"default": null,
"description": "Agent or tool sending this, e.g. 'Claude Code'."
},
"call_id": {
"anyOf": [
{
"type": "string",
"format": "uuid"
},
{
"type": "null"
}
],
"title": "Call Id",
"default": null,
"description": "call_id of the call that went wrong, if any."
},
"description": {
"type": "string",
"title": "Description",
"maxLength": 5000,
"minLength": 10,
"description": "What the user asked for, what you tried, what you expected and what happened, with the error text if there was one. No passwords, tokens or other secrets."
}
},
"description": "Body of POST /api/v1/reports and the arguments of the MCP tool cotlet_report_issue.",
"additionalProperties": false
}| expires_on |
| string |
| Last day the estimate is valid. |
| line_items* | array | Lines: name, rate, optional quantity, description, item_id. |
| document_date | string | Estimate date. Default: today. |
{
"type": "object",
"required": [
"client_id",
"line_items"
],
"properties": {
"notes": {
"type": "string",
"maxLength": 5000,
"description": "Notes shown on the document."
},
"tax_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "A saved tax rate from list_items (taxes) instead of tax_rate."
},
"deposit": {
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"enum": [
"percent",
"amount"
],
"type": "string"
},
"value": {
"type": "number",
"minimum": 0
}
},
"description": "Requested deposit, shown as Deposit Due: percent or amount.",
"additionalProperties": false
},
"discount": {
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"enum": [
"percent",
"amount"
],
"type": "string"
},
"value": {
"type": "number",
"minimum": 0
}
},
"description": "Discount on the subtotal: percent or amount.",
"additionalProperties": false
},
"tax_rate": {
"type": "number",
"maximum": 100,
"minimum": 0,
"description": "Tax percent on every line, e.g. 7.25. Uses your tax rate with that percent, or adds one."
},
"client_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "From list_clients or add_client."
},
"po_number": {
"type": "string",
"maxLength": 100,
"description": "PO number."
},
"expires_on": {
"type": "string",
"format": "date",
"description": "Last day the estimate is valid."
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"required": [
"name",
"rate"
],
"properties": {
"name": {
"type": "string",
"maxLength": 255,
"minLength": 1
},
"rate": {
"type": "number",
"minimum": 0,
"description": "Price per unit."
},
"item_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "Saved item from list_items, kept as the line's source."
},
"quantity": {
"type": "number",
"default": 1,
"exclusiveMinimum": 0
},
"description": {
"type": "string",
"maxLength": 5000
}
},
"additionalProperties": false
},
"maxItems": 100,
"minItems": 1,
"description": "Lines: name, rate, optional quantity, description, item_id."
},
"document_date": {
"type": "string",
"format": "date",
"description": "Estimate date. Default: today."
}
},
"additionalProperties": false
}| Lines: name, rate, optional quantity, description, item_id. |
| document_date | string | Invoice date. Default: today. |
| payment_terms_days | integer | Days after the invoice date the payment is due (0: due on receipt). |
{
"type": "object",
"required": [
"client_id",
"line_items"
],
"properties": {
"notes": {
"type": "string",
"maxLength": 5000,
"description": "Notes shown on the document."
},
"tax_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "A saved tax rate from list_items (taxes) instead of tax_rate."
},
"deposit": {
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"enum": [
"percent",
"amount"
],
"type": "string"
},
"value": {
"type": "number",
"minimum": 0
}
},
"description": "Requested deposit, shown as Deposit Due: percent or amount.",
"additionalProperties": false
},
"discount": {
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"enum": [
"percent",
"amount"
],
"type": "string"
},
"value": {
"type": "number",
"minimum": 0
}
},
"description": "Discount on the subtotal: percent or amount.",
"additionalProperties": false
},
"tax_rate": {
"type": "number",
"maximum": 100,
"minimum": 0,
"description": "Tax percent on every line, e.g. 7.25. Uses your tax rate with that percent, or adds one."
},
"client_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "From list_clients or add_client."
},
"po_number": {
"type": "string",
"maxLength": 100,
"description": "PO number."
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"required": [
"name",
"rate"
],
"properties": {
"name": {
"type": "string",
"maxLength": 255,
"minLength": 1
},
"rate": {
"type": "number",
"minimum": 0,
"description": "Price per unit."
},
"item_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "Saved item from list_items, kept as the line's source."
},
"quantity": {
"type": "number",
"default": 1,
"exclusiveMinimum": 0
},
"description": {
"type": "string",
"maxLength": 5000
}
},
"additionalProperties": false
},
"maxItems": 100,
"minItems": 1,
"description": "Lines: name, rate, optional quantity, description, item_id."
},
"document_date": {
"type": "string",
"format": "date",
"description": "Invoice date. Default: today."
},
"payment_terms_days": {
"type": "integer",
"default": 0,
"maximum": 365,
"minimum": 0,
"description": "Days after the invoice date the payment is due (0: due on receipt)."
}
},
"additionalProperties": false
}| sales_tax: only this tax, by name. |
{
"type": "object",
"required": [
"report",
"from",
"to"
],
"properties": {
"to": {
"type": "string",
"format": "date",
"description": "Last day, up to a year after from."
},
"from": {
"type": "string",
"format": "date",
"description": "First day of the period."
},
"limit": {
"type": "integer",
"default": 50,
"maximum": 50,
"minimum": 1
},
"client": {
"type": "string",
"maxLength": 255,
"description": "Only this client, by exact name."
},
"cursor": {
"type": "string",
"description": "next_cursor from the previous call."
},
"report": {
"enum": [
"revenue",
"invoices",
"estimates",
"sales_tax"
],
"type": "string",
"description": "revenue (payments received per invoice, with balances at the start and end of the period), invoices (status, total, balance and due date), estimates (status, total, expiry) or sales_tax (tax collected per tax name and invoice)."
},
"tax_name": {
"type": "string",
"maxLength": 255,
"description": "sales_tax: only this tax, by name."
}
},
"additionalProperties": false
}| remove_tax | boolean | true takes the tax off every line. |
| estimate_id* | string | From list_estimates. |
{
"type": "object",
"required": [
"estimate_id"
],
"properties": {
"notes": {
"type": "string",
"maxLength": 5000,
"description": "Notes shown on the document."
},
"tax_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "A saved tax rate from list_items (taxes) instead of tax_rate."
},
"deposit": {
"type": [
"object",
"null"
],
"required": [
"type",
"value"
],
"properties": {
"type": {
"enum": [
"percent",
"amount"
],
"type": "string"
},
"value": {
"type": "number",
"minimum": 0
}
},
"description": "null removes it.",
"additionalProperties": false
},
"discount": {
"type": [
"object",
"null"
],
"required": [
"type",
"value"
],
"properties": {
"type": {
"enum": [
"percent",
"amount"
],
"type": "string"
},
"value": {
"type": "number",
"minimum": 0
}
},
"description": "null removes it.",
"additionalProperties": false
},
"tax_rate": {
"type": "number",
"maximum": 100,
"minimum": 0,
"description": "Tax percent on every line, e.g. 7.25. Uses your tax rate with that percent, or adds one."
},
"po_number": {
"type": "string",
"maxLength": 100,
"description": "PO number."
},
"expires_on": {
"type": "string",
"format": "date",
"description": "New last valid day."
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"required": [
"name",
"rate"
],
"properties": {
"name": {
"type": "string",
"maxLength": 255,
"minLength": 1
},
"rate": {
"type": "number",
"minimum": 0,
"description": "Price per unit."
},
"item_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "Saved item from list_items, kept as the line's source."
},
"quantity": {
"type": "number",
"default": 1,
"exclusiveMinimum": 0
},
"description": {
"type": "string",
"maxLength": 5000
}
},
"additionalProperties": false
},
"maxItems": 100,
"minItems": 1,
"description": "Replaces every line. Leave out to keep the lines."
},
"remove_tax": {
"type": "boolean",
"description": "true takes the tax off every line."
},
"estimate_id": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "From list_estimates."
}
},
"additionalProperties": false
}