# Doing Work agent guide

This guide is made from the list of actions Doing Work actually runs, so it is always current. Each action works over REST (`/v1`) and, when agents may call it, as an MCP tool at `/mcp` and through A2A at `/a2a` (a SendMessage data part `{"op": "<id>", "input": {...}}`; card at `/.well-known/agent-card.json`). Send your key as `Authorization: Bearer <key>`.

## Bring in customers (imports)

Move a shop's customer list in from a CSV, including files exported from Jobber, Housecall Pro and CompanyCam. Ask imports.fields what can be filled, run imports.preview to see how each column maps and what would go wrong, show that to the owner, and only then call imports.run with the mapping they confirmed. Nothing is guessed: bad rows are reported, not fixed. Money is never imported.

### imports.fields

The customer import fields and the built-in presets (jobber, housecall_pro, companycam) with the export column each reads.

- REST: `GET /v1/imports/fields`
- MCP tool: `imports_fields`
- Input: No input.
- Read only. Agents can call this.

### imports.preview

Dry run of a customer CSV import. Writes nothing. Returns the headers, a suggested mapping (from a preset or the header names), and for every row the customer it would create or the problems found. Show the mapping to the owner before imports.run.

- REST: `POST /v1/imports/preview`
- MCP tool: `imports_preview`
- Input: `shop_id`, `csv`, `preset` (optional), `mapping` (optional)
- Read only. Agents can call this.

### imports.run

Owner only: import customers (and one service address each) from a CSV with the mapping the owner confirmed. mode all_or_nothing (default) writes nothing if any row fails; per_row writes the good rows and reports the rest. dry_run true checks without writing.

- REST: `POST /v1/imports/run`
- MCP tool: `imports_run`
- Input: `shop_id`, `csv`, `mapping`, `mode` (optional), `dry_run` (optional)
- Makes a change. Agents can call this.

## Take everything out (export)

The owner can download the whole shop as one ZIP of CSV and JSON files, for their own records or to move off Doing Work. Secrets such as keys and signing secrets are left out. shops.export returns download_url by default: a link that works for 60 minutes (link_minutes 5 to 1440) and opens at most 5 times, then the stored file is deleted. Anyone holding the link can download, so give it only to the owner. delivery "inline" returns zip_base64 for exports up to 10 MB.

### shops.export

Owner only: everything the shop holds as one ZIP: data/<table>.csv and data/<table>.json for every shop table, files/ with job photos and PDFs, and manifest.json. Signing secrets and token hashes are left out. By default you get download_url, a link that works for 60 minutes and up to 5 downloads; anyone with the link can download, so do not share it. delivery "inline" returns zip_base64 instead, only for exports up to 10 MB. Large shops: pass include_files false for data only.

- REST: `POST /v1/shops/export`
- MCP tool: `shops_export`
- Input: `shop_id`, `include_files` (optional), `delivery` (optional), `link_minutes` (optional)
- Read only. Agents can call this.

## Do one thing to many records (bulk)

Tag, untag, archive or unarchive up to 200 records in one call. Each record gets its own result, so one bad id does not stop the rest. The shop's approval rules still apply to each record.

### bulk.run

Run one safe action on up to 200 records: customers.addTags, customers.removeTags, customers.archive, customers.unarchive, jobs.addTags, jobs.removeTags, visits.assign. Each item goes through the shop's approval rules like a single call and is reported as done, approval_required or failed.

- REST: `POST /v1/bulk`
- MCP tool: `bulk_run`
- Input: `shop_id`, `action`, `ids`, `tags` (optional), `person_ids` (optional)
- Makes a change. Agents can call this.

## Checklists and the done gate (checklists)

Keep checklist templates (checklists.saveTemplate) and put a checklist on a job or on one visit (checklists.create). Required items gate done: visits.done refuses a visit with open required items or a missing sign-off, and jobs.complete refuses a job while its own checklists, or those of visits not yet done, are open. The error code is checklist_open; checklists.list shows what is left. Workers on the visit tick items with checklists.checkItem. Only the owner (or the owner's agent) can push past with override_reason; the override, the reason and the open items are recorded and audited, so ask the owner before using it. For a sign-off, finish the required items and call checklists.requestSignOff; it returns a sign_url on the hosted pages (the W1 signature primitive). A payment recorded with invoices.markPaidOutside never fails on a checklist; the job just stays open.

### checklists.saveTemplate

Create a checklist template, or replace one (give checklist_template_id). Items are in order; required items (the default) must be ticked before the visit or job can be marked done. Checklists already made from it keep their own copy.

- REST: `POST /v1/checklist-templates`
- MCP tool: `checklists_saveTemplate`
- Input: `shop_id`, `checklist_template_id` (optional), `name`, `description` (optional), `signoff_required` (optional), `items`
- Makes a change. Agents can call this.

### checklists.listTemplates

The shop's checklist templates with their items. Archived ones only with include_archived.

- REST: `GET /v1/checklist-templates`
- MCP tool: `checklists_listTemplates`
- Input: `shop_id`, `include_archived` (optional)
- Read only. Agents can call this.

### checklists.archiveTemplate

Archive a checklist template so it is no longer offered. Checklists made from it are unchanged.

- REST: `POST /v1/checklist-templates/archive`
- MCP tool: `checklists_archiveTemplate`
- Input: `checklist_template_id`
- Makes a change. Agents can call this.

### checklists.create

Put a checklist on a job, or on one visit of the job (visit_id). Copy a template (checklist_template_id) or give items. A visit checklist gates that visit's done; a job checklist gates the job's done (jobs.complete), and the job also waits on checklists of visits not yet done.

- REST: `POST /v1/checklists`
- MCP tool: `checklists_create`
- Input: `job_id`, `visit_id` (optional), `checklist_template_id` (optional), `name` (optional), `signoff_required` (optional), `items` (optional)
- Makes a change. Agents can call this.

### checklists.list

Checklists on a job (all of them, job-level and per visit) or on one visit, with counts of open required items and sign-off state. Workers see the checklists of the jobs they can see.

- REST: `GET /v1/checklists`
- MCP tool: `checklists_list`
- Input: `job_id` (optional), `visit_id` (optional)
- Read only. Agents can call this.

### checklists.get

One checklist with every item (removed items included and marked), who ticked what and when, and its sign-off.

- REST: `GET /v1/checklists/get`
- MCP tool: `checklists_get`
- Input: `checklist_id`
- Read only. Agents can call this.

### checklists.checkItem

Tick (done true) or untick an item, with an optional note. The worker on the visit (for a job checklist, on any visit of the job) or the office can do this. Locked once the visit or job is done, while a sign-off is waiting, and after sign-off.

- REST: `POST /v1/checklists/check`
- MCP tool: `checklists_checkItem`
- Input: `checklist_item_id`, `done`, `note` (optional)
- Makes a change. Agents can call this.

### checklists.addItems

Add items to the end of a checklist (owner or office). Same locks as ticking.

- REST: `POST /v1/checklists/items`
- MCP tool: `checklists_addItems`
- Input: `checklist_id`, `items`
- Makes a change. Agents can call this.

### checklists.removeItem

Remove an item from a checklist (it stays in the history, marked removed). Owner or office; a required item only by the owner. Audited.

- REST: `POST /v1/checklists/items/remove`
- MCP tool: `checklists_removeItem`
- Input: `checklist_item_id`
- Makes a change. Agents can call this.

### checklists.requestSignOff

Ask for a sign-off on a finished checklist and get the signing link. Every required item must be ticked. The signer (the customer by default, or a shop member) signs the checklist exactly as it stands; items lock while the sign-off waits (signatures.cancel unlocks).

- REST: `POST /v1/checklists/sign-off`
- MCP tool: `checklists_requestSignOff`
- Input: `checklist_id`, `signer_role` (optional), `signer_email` (optional), `link_days` (optional)
- Makes a change. Agents can call this.

### jobs.complete

Mark a job done (owner or office). Refused with checklist_open while a job checklist, a checklist on a visit not yet done, or a sign-off checklist on a cancelled visit, has required items open or a sign-off missing; the owner, signed in on the hosted override page (/p/checklist/override), may pass override_reason to mark it done anyway (recorded and audited). Agents, keys and office staff cannot override.

- REST: `POST /v1/jobs/complete`
- MCP tool: `jobs_complete`
- Input: `job_id`, `override_reason` (optional)
- Makes a change. Agents can call this.

## Change orders (changes)

A change order is a priced change to a job's scope: extra work, or a credit for work taken out. A worker writes the lines (changes.create; never fill prices in yourself). The owner or office sends it (changes.send), which returns a sign_url on the hosted pages; deliver it to the customer. When the customer signs, the change order becomes approved (event change_order.approved) and adds to the job's revised total; changes.list shows signed quotes, approved and pending changes, what is invoiced and what is not. Bill approved change orders with changes.invoice. A change order's lines never change after it is written; cancel it (changes.cancel) and write a new one. Every op here needs the see_prices permission, and owner approval rules with an amount floor use the change order's total.

### changes.create

Write a change order on a job: a priced change to its scope, from lines a worker wrote (never filled in by AI). Discount lines take work out; a change order may total less than 0 (a credit). Tax: the shop default rate unless tax_rate_id is given (null for no tax). It starts as a draft; send it to the customer with changes.send.

- REST: `POST /v1/change-orders`
- MCP tool: `changes_create`
- Input: `job_id`, `title`, `description` (optional), `lines`, `tax_rate_id` (optional)
- Makes a change. Agents can call this.

### changes.list

A job's change orders and its money picture: signed quotes (contract_minor) + approved change orders = revised_total_minor; pending (sent, not yet signed); invoiced on live invoices; unbilled; and approved change orders not on an invoice yet.

- REST: `GET /v1/change-orders`
- MCP tool: `changes_list`
- Input: `job_id`
- Read only. Agents can call this.

### changes.get

One change order with its lines, totals, status, signature and invoice.

- REST: `GET /v1/change-orders/get`
- MCP tool: `changes_get`
- Input: `change_order_id`
- Read only. Agents can call this.

### changes.send

Owner or office: ask the customer to approve a change order. Doing Work writes the document (the lines, totals and the job's total before and after) and returns a single-use sign_url for the agent to deliver; the customer signs it on a hosted page and the change order becomes approved. To send again after the request was cancelled, call this again; for a fresh link on an open request use signatures.newLink.

- REST: `POST /v1/change-orders/send`
- MCP tool: `changes_send`
- Input: `change_order_id`, `signer_email` (optional), `link_days` (optional)
- Makes a change. Agents can call this.

### changes.cancel

Cancel a draft or sent change order (its open signature request is cancelled too, so the link stops working). An approved change order cannot be cancelled; write a new change order that takes the work back out.

- REST: `POST /v1/change-orders/cancel`
- MCP tool: `changes_cancel`
- Input: `change_order_id`, `reason` (optional)
- Makes a change. Agents can call this.

### changes.invoice

Bill approved change orders on a normal invoice (draft; send it with invoices.send). Each change order's lines are copied, labelled with its number. Leave out change_order_ids to bill every approved change order on the job not yet invoiced. They must share one tax rate. Credits (change orders under 0) come off as discounts and need priced work on the same invoice.

- REST: `POST /v1/change-orders/invoice`
- MCP tool: `changes_invoice`
- Input: `job_id`, `change_order_ids` (optional), `due_on` (optional), `memo` (optional)
- Makes a change. Agents can call this.

## Time clock and timesheets (timesheets)

Keep the crew's time: timesheets.clockIn starts a clock on a job (category work) or for drive, shop or break time, and timesheets.clockOut stops it; one clock runs per person per shop (pass stop_running to switch). Forgotten time goes in with timesheets.add (start and end, at most 24 hours per entry); time never overlaps the same person's other time. A field worker keeps only their own time, and on a visit only when assigned to it. Stopped time waits for the owner or office (timesheets.decide approves or rejects up to 200 at once); approved time is locked, and only the owner can reopen it (timesheets.reopen, with a reason). timesheets.list gives minutes by category and person for a date range in the shop's time zone. Labor cost rates (timesheets.setRate) are copied onto time when it is kept, so a new rate never rewrites old time; rates and labor cost show only to members with the see_costs permission and are null otherwise. Events carry minutes, never amounts.

### timesheets.clockIn

Start a person's clock: on a job (work), or drive, shop or break time. A field worker clocks only themself, and on a visit only when assigned to it; the owner or office can clock in anyone. One running clock per person per shop: pass stop_running to stop the running one first (for example to switch from drive to work).

- REST: `POST /v1/timesheets/clock-in`
- MCP tool: `timesheets_clockIn`
- Input: `shop_id` (optional), `job_id` (optional), `visit_id` (optional), `person_id` (optional), `category` (optional), `notes` (optional), `stop_running` (optional)
- Makes a change. Agents can call this.

### timesheets.clockOut

Stop a person's running clock. The entry then waits for the owner or office to approve it. Forgot to clock out? Give ended_at (in the past, at most 24 hours after the start).

- REST: `POST /v1/timesheets/clock-out`
- MCP tool: `timesheets_clockOut`
- Input: `shop_id`, `person_id` (optional), `ended_at` (optional), `notes` (optional)
- Makes a change. Agents can call this.

### timesheets.current

A person's running clock in a shop, or null when they are not clocked in. A field worker sees only their own.

- REST: `GET /v1/timesheets/current`
- MCP tool: `timesheets_current`
- Input: `shop_id`, `person_id` (optional)
- Read only. Agents can call this.

### timesheets.add

Add time after the fact (a manual entry with a start and an end, at most 24 hours). It waits for approval like clocked time. It cannot overlap the person's other time.

- REST: `POST /v1/timesheets`
- MCP tool: `timesheets_add`
- Input: `shop_id` (optional), `job_id` (optional), `visit_id` (optional), `person_id` (optional), `category` (optional), `started_at`, `ended_at`, `notes` (optional)
- Makes a change. Agents can call this.

### timesheets.update

Fix a time entry that is not approved: its start, end, category, job, visit or notes. A rejected entry goes back to waiting for approval. A running clock's end is set with timesheets.clockOut. Approved time is locked; the owner can reopen it with timesheets.reopen.

- REST: `POST /v1/timesheets/update`
- MCP tool: `timesheets_update`
- Input: `time_entry_id`, `started_at` (optional), `ended_at` (optional), `category` (optional), `job_id` (optional), `visit_id` (optional), `notes` (optional)
- Makes a change. Agents can call this.

### timesheets.void

Take back a time entry that was never approved (kept in history, no longer counted). A running clock is stopped and voided. The person, the owner or office can.

- REST: `POST /v1/timesheets/void`
- MCP tool: `timesheets_void`
- Input: `time_entry_id`, `reason` (optional)
- Makes a change. Agents can call this.

### timesheets.decide

Owner or office: approve or reject stopped time entries (up to 200 at once, one shop). Office staff cannot decide their own time; only the owner can. Approved time is locked and counts for payroll and job costing; rejected time goes back to the worker to fix (any fix puts it back to waiting). A note is kept with the decision.

- REST: `POST /v1/timesheets/decide`
- MCP tool: `timesheets_decide`
- Input: `time_entry_ids`, `decision`, `note` (optional)
- Makes a change. Agents can call this.

### timesheets.reopen

Owner only: reopen approved time so it can be fixed (it goes back to waiting for approval). Use this when approved time was wrong; the reason is audited.

- REST: `POST /v1/timesheets/reopen`
- MCP tool: `timesheets_reopen`
- Input: `time_entry_id`, `reason`
- Makes a change. Agents can call this.

### timesheets.list

Time entries in a shop between dates (the shop's time zone), or on one job, with minutes by category and by person, and approved minutes. Owner and office see everyone; a field worker sees only their own time. Labor cost shows only for members who may see costs (see_costs). Voided entries are left out unless status is void.

- REST: `GET /v1/timesheets`
- MCP tool: `timesheets_list`
- Input: `shop_id` (optional), `job_id` (optional), `person_id` (optional), `from` (optional), `to` (optional), `status` (optional), `category` (optional)
- Read only. Agents can call this.

### timesheets.setRate

Owner or office: set what an hour of a worker's time costs the shop (wage plus burden), or the shop default when person_id is left out. It applies from effective_from (default now) to time kept after that; time already kept keeps the rate it was kept at. null cost_per_hour_minor on a worker puts them back on the shop default. Needs see_costs.

- REST: `POST /v1/timesheets/rates`
- MCP tool: `timesheets_setRate`
- Input: `shop_id`, `person_id` (optional), `cost_per_hour_minor`, `effective_from` (optional)
- Makes a change. Agents can call this.

### timesheets.listRates

Labor cost rates in force now: the shop default and each worker's own, plus upcoming ones. Needs see_costs. A field worker sees only their own and the shop default.

- REST: `GET /v1/timesheets/rates`
- MCP tool: `timesheets_listRates`
- Input: `shop_id`
- Read only. Agents can call this.

## Job notes and voice memos (notes)

Keep the shop's own notes on a job or one visit (notes.add). For a voice memo, transcribe it with your own speech-to-text and save the words with notes.addVoiceMemo (transcript, duration_seconds, and optionally recorded_at, transcribed_with and language); core runs no speech model and does not store the audio. Notes are internal and never shown to the customer. Any member writes team notes; owner or office can write office-only notes (audience office, hidden from field workers) and pin notes to the top. Only the writer changes a note's words (notes.update keeps every earlier wording, see notes.get history); the writer, owner or office can remove one (notes.remove, with a reason), and it stays in history. notes.list returns pinned notes first, then newest. Events carry ids, never the words.

### notes.add

Write a note on a job, or on one visit of it (give visit_id; job_id is then optional). Any shop member can. Notes are internal to the shop and never shown to the customer. Set audience office to keep it to owner and office (owner or office only), pinned to keep it at the top (owner or office only). Attach photos already on the job with media_ids.

- REST: `POST /v1/notes`
- MCP tool: `notes_add`
- Input: `job_id` (optional), `visit_id` (optional), `body`, `audience` (optional), `pinned` (optional), `media_ids` (optional)
- Makes a change. Agents can call this.

### notes.addVoiceMemo

Save a voice memo as a note: send the transcript your agent made, the memo's length in seconds, when it was recorded, and what transcribed it. Core runs no speech-to-text and does not keep the audio. Same rules as notes.add for who sees and pins it.

- REST: `POST /v1/notes/voice-memo`
- MCP tool: `notes_addVoiceMemo`
- Input: `job_id` (optional), `visit_id` (optional), `transcript`, `duration_seconds`, `recorded_at` (optional), `transcribed_with` (optional), `language` (optional), `audience` (optional), `pinned` (optional), `media_ids` (optional)
- Makes a change. Agents can call this.

### notes.update

Change a note. Only the writer changes body or media_ids (each earlier wording is kept, up to 50 edits). Owner or office can pin or unpin it and change audience. A removed note cannot change.

- REST: `POST /v1/notes/update`
- MCP tool: `notes_update`
- Input: `note_id`, `body` (optional), `media_ids` (optional), `pinned` (optional), `audience` (optional)
- Makes a change. Agents can call this.

### notes.remove

Take a note off the job. The writer, owner or office can. It stays in history with who removed it and why.

- REST: `POST /v1/notes/remove`
- MCP tool: `notes_remove`
- Input: `note_id`, `reason` (optional)
- Makes a change. Agents can call this.

### notes.get

One note you may see, with every earlier wording (history, oldest first).

- REST: `GET /v1/notes/get`
- MCP tool: `notes_get`
- Input: `note_id`
- Read only. Agents can call this.

### notes.list

Notes you may see on a job (job-level and every visit) or on one visit: pinned first, then newest first. Filter by source (typed or voice_memo); removed notes are left out unless include_removed is true.

- REST: `GET /v1/notes`
- MCP tool: `notes_list`
- Input: `job_id` (optional), `visit_id` (optional), `source` (optional), `include_removed` (optional)
- Read only. Agents can call this.

## Custom forms and inspections (forms)

Keep form templates (forms.saveTemplate: ordered fields of type heading, text, long_text, number, yes_no, choice, multi_choice, date or photos, each optionally required). Start a form on a job or one visit (forms.start) from a template, from an earlier form (copy_from_form_id, which also carries its answers), or with fields given on the spot; the form keeps its own copy of the fields. The owner, office, or a worker assigned on the job (on that visit for a visit form) fills it with forms.fill (answers by field key; null clears one) and submits it with forms.submit, which refuses while required fields are missing, renders the PDF, stores it on the job as a document of kind form with its sha256, and makes the form final. To correct a submitted form, start a copy and submit that. forms.requestSignOff (owner or office) returns a sign_url where the customer or a member signs the stored PDF exactly as submitted. Text the PDF cannot print yet (Chinese, Japanese, Korean, Arabic, Hebrew, Indic scripts) is refused with unprintable_text when it is saved, not at submit.

### forms.saveTemplate

Create a form template (an inspection, a safety sheet, a start-up report), or replace one (give form_template_id). Fields are in order; types: heading, text, long_text, number, yes_no, choice, multi_choice, date, photos. Forms already started keep their own copy. Owner or office.

- REST: `POST /v1/forms/templates`
- MCP tool: `forms_saveTemplate`
- Input: `shop_id` (optional), `form_template_id` (optional), `name`, `description` (optional), `fields`, `signoff_required` (optional)
- Makes a change. Agents can call this.

### forms.listTemplates

The shop's form templates with their fields. Archived ones only with include_archived.

- REST: `GET /v1/forms/templates`
- MCP tool: `forms_listTemplates`
- Input: `shop_id`, `include_archived` (optional)
- Read only. Agents can call this.

### forms.archiveTemplate

Archive a form template so it is no longer offered. Forms made from it are unchanged.

- REST: `POST /v1/forms/templates/archive`
- MCP tool: `forms_archiveTemplate`
- Input: `form_template_id`
- Makes a change. Agents can call this.

### forms.start

Start a form on a job, or on one visit of the job (visit_id). Copy a template (form_template_id), copy an earlier form with its answers (copy_from_form_id: the way to correct a submitted form), or give name and fields. The owner, office, or a worker assigned on the job can start one. It starts as a draft.

- REST: `POST /v1/forms`
- MCP tool: `forms_start`
- Input: `job_id`, `visit_id` (optional), `form_template_id` (optional), `copy_from_form_id` (optional), `name` (optional), `fields` (optional), `signoff_required` (optional)
- Makes a change. Agents can call this.

### forms.fill

Save answers on a draft form. answers is an object by field key; only the keys given change, and null clears one. Values: text for text/long_text, a number, true/false for yes_no, one option for choice, a list of options for multi_choice, YYYY-MM-DD for date, photo ids already on the job for photos.

- REST: `POST /v1/forms/fill`
- MCP tool: `forms_fill`
- Input: `form_id`, `answers`
- Makes a change. Agents can call this.

### forms.submit

Submit a draft form: every required field must be answered (forms.get lists what is missing). Core renders the PDF, stores it on the job (a document of kind form, with its sha256) and freezes the form. Returns the form with the PDF (base64).

- REST: `POST /v1/forms/submit`
- MCP tool: `forms_submit`
- Input: `form_id`
- Makes a change. Agents can call this.

### forms.cancel

Cancel a draft form (kept in history). A submitted form cannot be cancelled; start a corrected copy with forms.start copy_from_form_id.

- REST: `POST /v1/forms/cancel`
- MCP tool: `forms_cancel`
- Input: `form_id`
- Makes a change. Agents can call this.

### forms.get

One form with its fields, answers, the required fields still missing, its stored PDF id and hash once submitted, and its sign-off.

- REST: `GET /v1/forms/get`
- MCP tool: `forms_get`
- Input: `form_id`
- Read only. Agents can call this.

### forms.list

Forms on a job (job-level and per visit), or on one visit, newest first, without fields and answers. Filter by status.

- REST: `GET /v1/forms`
- MCP tool: `forms_list`
- Input: `job_id` (optional), `visit_id` (optional), `status` (optional)
- Read only. Agents can call this.

### forms.requestSignOff

Ask for a signature on a submitted form and get the signing link (owner or office). The signer (the customer by default, or a shop member) signs the stored PDF exactly as submitted.

- REST: `POST /v1/forms/sign-off`
- MCP tool: `forms_requestSignOff`
- Input: `form_id`, `signer_role` (optional), `signer_email` (optional), `link_days` (optional)
- Makes a change. Agents can call this.

## Materials used on jobs (materials)

Record what went into a job: materials.add from the price book (pricebook_item_id; its name, unit, price, cost and taxable are copied as they are today) or typed in, optionally on one visit. Afterwards only the quantity, visit and notes change (materials.update); for anything else remove it (materials.remove keeps it in history with who and why) and add it again. A field worker changes only what they entered. materials.list totals price and cost for the job. Prices show only to members with see_prices and costs only with see_costs (null otherwise), so any worker can record materials without seeing what they cost.

### materials.add

Record a material used on a job (optionally on one of its visits): from the price book (pricebook_item_id; its name, unit, price, cost and taxable are copied as they are now) or typed in (name, unit, and optionally the price and cost per unit). Any worker of the shop can record materials, including one who may not see prices.

- REST: `POST /v1/materials`
- MCP tool: `materials_add`
- Input: `job_id`, `visit_id` (optional), `pricebook_item_id` (optional), `name` (optional), `unit` (optional), `quantity`, `unit_price_minor` (optional), `unit_cost_minor` (optional), `taxable` (optional), `notes` (optional)
- Makes a change. Agents can call this.

### materials.update

Change a recorded material's quantity, visit or notes. Anything else (a different item or price): remove it and add it again. A field worker changes only materials they entered.

- REST: `POST /v1/materials/update`
- MCP tool: `materials_update`
- Input: `material_id`, `quantity` (optional), `visit_id` (optional), `notes` (optional)
- Makes a change. Agents can call this.

### materials.remove

Remove a recorded material (recorded in error or returned). It stays in the history with who removed it and why, and no longer counts. A field worker removes only materials they entered.

- REST: `POST /v1/materials/remove`
- MCP tool: `materials_remove`
- Input: `material_id`, `reason` (optional)
- Makes a change. Agents can call this.

### materials.list

Materials recorded on a job (or one visit), with the price and cost totals of those still counted. Prices show only with see_prices, costs only with see_costs; lines with no price or cost set are counted in unpriced_count and uncosted_count.

- REST: `GET /v1/materials`
- MCP tool: `materials_list`
- Input: `job_id`, `visit_id` (optional), `include_removed` (optional)
- Read only. Agents can call this.

## Get a document signed (signatures)

Ask a customer, or someone in the shop, to sign a document: contract text you supply (for example a service agreement) or a PDF already stored on the job. signatures.request returns a sign_url on the hosted pages; send it to the signer yourself. Only a person signed in on that page can sign. Agents can never sign, and the database refuses it. The signature keeps the document sha256, the signer, the consent words and their sha256, and the time. Watch for the document.signed event.

### signatures.request

Ask someone to sign a document on a hosted page, and get the signing link to give them. kind contract: text is the agreement exactly as the shop wrote it (Doing Work never writes contract or legal wording). kind job_document: a stored job PDF (documents.list). The signer signs in, reads it, types their name and agrees to sign electronically. Agents can never sign.

- REST: `POST /v1/signatures`
- MCP tool: `signatures_request`
- Input: `kind`, `job_id` (optional), `customer_id` (optional), `title`, `text` (optional), `document_id` (optional), `signer_role` (optional), `signer_email` (optional), `link_days` (optional)
- Makes a change. Agents can call this.

### signatures.newLink

A new signing link for an open signature request. Earlier links for it stop working.

- REST: `POST /v1/signatures/link`
- MCP tool: `signatures_newLink`
- Input: `signature_request_id`, `link_days` (optional)
- Makes a change. Agents can call this.

### signatures.cancel

Cancel an open signature request. Its links stop working. A signed document stays signed.

- REST: `POST /v1/signatures/cancel`
- MCP tool: `signatures_cancel`
- Input: `signature_request_id`
- Makes a change. Agents can call this.

### signatures.get

One signature request: the document text (or stored PDF id) and its sha256, and the signature when signed (who, typed name, consent text and its sha256, surface, time).

- REST: `GET /v1/signatures/get`
- MCP tool: `signatures_get`
- Input: `signature_request_id`
- Read only. Agents can call this.

### signatures.list

Signature requests for a job, a customer or a whole shop, newest first, with status and who signed. Filter by kind, status or subject_id.

- REST: `GET /v1/signatures`
- MCP tool: `signatures_list`
- Input: `shop_id` (optional), `job_id` (optional), `customer_id` (optional), `kind` (optional), `subject_id` (optional), `status` (optional), `limit` (optional)
- Read only. Agents can call this.

### signatures.sign

Sign a document after reading it, typing your name and agreeing to sign electronically. Hosted page only; agents can never sign.

- REST: `POST /v1/signatures/sign`
- Input: `t`, `document_sha256`, `signer_name`, `esign_consent`
- Makes a change. The person does this on a page they open; agents cannot.

## Ask the owner first (approvals)

An owner can say which actions an agent must ask about first, optionally only above an amount. When a rule matches, your call returns an approval id instead of running. The owner approves or denies it; on approve it runs then. A worker acting with their own sign-in is never held.

### approvals.setRule

Owner only, not an agent: require owner approval before an agent can run an action. op is an operation id (invoices.send), an area (invoices.*) or * for every write. min_amount_minor limits the rule to actions moving or billing at least that much.

- REST: `POST /v1/approvals/rules`
- Input: `shop_id`, `op`, `min_amount_minor` (optional), `note` (optional)
- Makes a change. Needs the person's own sign-in; an agent key is refused.

### approvals.removeRule

Owner only, not an agent: stop requiring approval for a rule. Pending requests under it stay pending.

- REST: `POST /v1/approvals/rules/remove`
- Input: `rule_id`
- Makes a change. Needs the person's own sign-in; an agent key is refused.

### approvals.listRules

The shop's approval rules: which agent actions wait for the owner.

- REST: `GET /v1/approvals/rules`
- MCP tool: `approvals_listRules`
- Input: `shop_id`
- Read only. Agents can call this.

### approvals.list

Approval requests for a shop, newest first, optionally by status (pending, approved, denied, failed, expired). Requests past their expiry read as expired. Owner and office see every request; a field worker sees only the ones they or their agents asked for.

- REST: `GET /v1/approvals`
- MCP tool: `approvals_list`
- Input: `shop_id`, `status` (optional)
- Read only. Agents can call this.

### approvals.get

One approval request with its status and, once approved and run, the action's result. Owner and office see any; a field worker sees only their own.

- REST: `GET /v1/approvals/get`
- MCP tool: `approvals_get`
- Input: `approval_id`
- Read only. Agents can call this.

### approvals.decide

Owner only, not an agent: approve or deny a pending agent action. On approve it runs now, as the owner, and the result is saved on the request.

- REST: `POST /v1/approvals/decide`
- Input: `approval_id`, `decision`, `note` (optional)
- Makes a change. Needs the person's own sign-in; an agent key is refused.

## Pick an audience (customers.audience)

Find the customers a message, campaign or call list is for: customers.audience takes filters (tags, when their last job was, kind of work done, an open quote, money owed, maintenance plan status and renewal dates, city or postal code, has an email or phone) and returns the counts and a page of matching customers. All filters must match. It sends nothing and changes nothing; show the owner the count and a sample before doing anything with the list. Owner or office only. Money filters need the see_prices permission and money columns are null without it. has_email only says an address exists; it is not consent to marketing email, and Doing Work keeps no marketing opt-out list yet, so ask the owner before emailing a list.

### customers.audience

Pick a customer audience by filters (tags, last job dates, kind of work, open quotes, money owed, maintenance plans, city or postal code, has email or phone) and get the counts and a page of matching customers. Read only: it sends nothing and changes nothing. Owner or office only; money filters need see_prices.

- REST: `POST /v1/customers/audience`
- MCP tool: `customers_audience`
- Input: `shop_id`, `tags_any` (optional), `tags_all` (optional), `tags_none` (optional), `lead_sources` (optional), `kind` (optional), `autopay` (optional), `created_from` (optional), `created_to` (optional), `has_jobs` (optional), `last_job_from` (optional), `last_job_to` (optional), `job_kinds` (optional), `job_tags_any` (optional), `job_summary_contains` (optional), `jobs_from` (optional), `jobs_to` (optional), `has_open_quote` (optional), `plan_status` (optional), `plan_term_ends_from` (optional), `plan_term_ends_to` (optional), `cities` (optional), `postal_codes` (optional), `has_email` (optional), `has_phone` (optional), `owes_money` (optional), `overdue` (optional), `min_owed_minor` (optional), `min_spent_minor` (optional), `max_spent_minor` (optional), `include_archived` (optional), `sort` (optional), `limit` (optional), `cursor` (optional)
- Read only. Agents can call this.

## Customer email wording (messages templates)

The shop can set its own wording for the automatic customer emails: invoice reminders, quote follow-ups and review requests. Read messages.templates for the wording in use and the placeholders each kind takes. Try new wording with messages.previewTemplate (sends nothing), then save it with messages.saveTemplate. The body must contain the kind's link placeholder ({pay_link}, {quote_link} or {review_link}) exactly once; Doing Work fills in the link and the "<shop> via Doing Work" sender line, and neither can be changed. messages.resetTemplate goes back to the built-in wording. Owner or office only.

### messages.templates

The shop's customer message templates, one per kind (invoice_reminder, quote_followup, review_request): the wording in use (custom or built-in), the placeholders each kind takes, and the locked link placeholder that must stay in the body. Owner or office only.

- REST: `GET /v1/messages/templates`
- MCP tool: `messages_templates`
- Input: `shop_id`
- Read only. Agents can call this.

### messages.saveTemplate

Set the subject and body wording for one kind of automatic customer email. Use the kind's placeholders ({first_name}, {shop}, {job} and so on). The body must contain the kind's link placeholder ({pay_link}, {quote_link} or {review_link}) exactly once; Doing Work fills in the link and the sender line, and those cannot be changed. Replaces any earlier template for that kind.

- REST: `POST /v1/messages/templates`
- MCP tool: `messages_saveTemplate`
- Input: `shop_id`, `kind`, `subject`, `body`
- Makes a change. Agents can call this.

### messages.resetTemplate

Go back to the built-in wording for one kind of automatic customer email.

- REST: `POST /v1/messages/templates/reset`
- MCP tool: `messages_resetTemplate`
- Input: `shop_id`, `kind`
- Makes a change. Agents can call this.

### messages.previewTemplate

Show how one kind of automatic customer email reads, filled with sample customer details and the shop's name. Send subject and body to preview unsaved wording (checked the same way as saving); leave them out to preview the wording in use. Sends nothing.

- REST: `POST /v1/messages/templates/preview`
- MCP tool: `messages_previewTemplate`
- Input: `shop_id`, `kind`, `subject` (optional), `body` (optional)
- Read only. Agents can call this.

## The customer hub (hub links and visit change requests)

Give a customer one page with everything they have with the shop: hub.createLink makes a link for one customer (shown once; send it yourself, Doing Work sends nothing). On it the customer sees their quotes, jobs, invoices with what is still owed, upcoming visits and requests, opens a quote, pays an invoice, asks to move or cancel a visit, and asks for new work (it arrives as a portal request). They must sign in with an email the shop has for them, so a forwarded link shows nothing to anyone else. The hub.view, hub.open and hub.request* ops are the customer's own and work on the hosted pages only; agents can never act as the customer. Customers ask and the office answers: list open asks with visits.changeRequests and answer each with visits.answerChange (accept moves or cancels the visit through visits.update or visits.cancel; decline leaves it). Revoke a link with hub.revokeLink. Owner or office only.

### hub.createLink

Make a customer hub link: one page where this customer sees their quotes, jobs, invoices, upcoming visits and requests with you, pays, asks to move or cancel a visit, and asks for new work. The customer must sign in with an email you have for them (the customer's email or a contact's), so a forwarded link shows nothing to anyone else. The url is shown once; send it to the customer yourself. Lasts expires_in_days (default 90, up to 365) until revoked. Owner or office only.

- REST: `POST /v1/hub/createLink`
- MCP tool: `hub_createLink`
- Input: `customer_id`, `expires_in_days` (optional)
- Makes a change. Agents can call this.

### hub.links

A customer's hub links, newest first: when each was made, when it expires or was revoked, and how often and when it was last opened. The link itself is never shown again. Owner or office only.

- REST: `GET /v1/hub/links`
- MCP tool: `hub_links`
- Input: `customer_id`
- Read only. Agents can call this.

### hub.revokeLink

Stop a customer hub link working, for good. Page links the customer already opened from it run out on their own within a day. Owner or office only.

- REST: `POST /v1/hub/revokeLink`
- MCP tool: `hub_revokeLink`
- Input: `link_id`
- Makes a change. Agents can call this.

### visits.changeRequests

Customers' requests to move or cancel a visit, from the customer hub, newest first. Defaults to open ones. Owner or office only; answer with visits.answerChange.

- REST: `GET /v1/visits/changeRequests`
- MCP tool: `visits_changeRequests`
- Input: `shop_id`, `status` (optional), `visit_id` (optional), `limit` (optional)
- Read only. Agents can call this.

### visits.answerChange

Answer a customer's request to move or cancel a visit. decline leaves the visit as it is. accept on a cancel request cancels the visit (visits.cancel); accept on a reschedule moves it to starts_at (and ends_at, or the same length as before) through visits.update. Pass apply false to mark it accepted without changing the visit (you already moved it). The customer sees the answer and your note on their hub. If a shop approval rule holds the visit change, the request stays open and the approval id comes back. Owner or office only.

- REST: `POST /v1/visits/answerChange`
- MCP tool: `visits_answerChange`
- Input: `change_request_id`, `decision`, `note` (optional), `starts_at` (optional), `ends_at` (optional), `apply` (optional)
- Makes a change. Agents can call this.

### hub.view

The customer hub for the signed-in customer: their quotes, jobs, invoices with what is still owed, upcoming visits with any change request, and requests with this shop. state is ok, gone (revoked, expired or unknown) or wrong_person (signed in with an email the shop does not have for this customer). Hosted page only.

- REST: `GET /v1/hub/view`
- Input: `k`
- Read only. The person does this on a page they open; agents cannot.

### hub.open

Open a sent quote or an unpaid invoice from the customer hub: returns a fresh page link for that one item, for the signed-in customer only, good for a day. Hosted page only.

- REST: `POST /v1/hub/open`
- Input: `k`, `kind`, `id`
- Makes a change. The person does this on a page they open; agents cannot.

### hub.requestVisitChange

From the customer hub, ask the shop to move (reschedule, with the times that work better) or cancel an upcoming visit. The visit stays as it is until the shop answers. One open request per visit. Hosted page only.

- REST: `POST /v1/hub/requestVisitChange`
- Input: `k`, `visit_id`, `kind`, `preferred_times` (optional), `note` (optional)
- Makes a change. The person does this on a page they open; agents cannot.

### hub.withdrawVisitChange

From the customer hub, take back your own open request to move or cancel a visit. Hosted page only.

- REST: `POST /v1/hub/withdrawVisitChange`
- Input: `k`, `change_request_id`
- Makes a change. The person does this on a page they open; agents cannot.

### hub.requestWork

From the customer hub, ask the shop for new work. It lands in the shop's requests (source portal) on this customer, with their contact details already filled. Up to 5 a day. Hosted page only.

- REST: `POST /v1/hub/requestWork`
- Input: `k`, `title`, `details` (optional), `preferred_times` (optional), `property_id` (optional)
- Makes a change. The person does this on a page they open; agents cannot.

## Find things (search)

Search across customers, jobs, quotes, invoices and requests in one call, or look in one place when you know where it is.

### customers.search

Find customers by name, company, email, phone or property address.

- REST: `GET /v1/customers/search`
- MCP tool: `customers_search`
- Input: `shop_id`, `q`, `include_archived` (optional), `limit` (optional)
- Read only. Agents can call this.

### tags.list

Every tag used in the shop, with how many photos and jobs carry it.

- REST: `GET /v1/tags`
- MCP tool: `tags_list`
- Input: `shop_id`
- Read only. Agents can call this.

### media.search

Find photos across the shop that carry all the given tags. Narrow by job, stage, or upload dates (shop time zone). Newest first, up to 200.

- REST: `GET /v1/media/search`
- MCP tool: `media_search`
- Input: `shop_id`, `tags` (optional), `job_id` (optional), `stage` (optional), `from` (optional), `to` (optional), `limit` (optional)
- Read only. Agents can call this.

### jobs.findByTag

Jobs in the shop that carry all the given tags (set with jobs.setTags), newest first, up to 200.

- REST: `GET /v1/jobs/by-tag`
- MCP tool: `jobs_findByTag`
- Input: `shop_id`, `tags`
- Read only. Agents can call this.

### search.global

Search one shop for customers (name, email, phone), properties (address), jobs (summary) and requests (title). Case-insensitive substring match; you see only what your role can read.

- REST: `GET /v1/search`
- MCP tool: `search_global`
- Input: `shop_id`, `q`, `limit` (optional)
- Read only. Agents can call this.

## Hear about changes (webhooks and the audit log)

Add an https endpoint and Doing Work posts signed events to it as things happen. The signing secret is shown once. Failed deliveries retry on their own and can be sent again by hand. The owner can read the audit log of who did what.

### webhooks.create

Add a webhook endpoint for a shop (owner or office). Returns the signing secret once; it is never shown again.

- REST: `POST /v1/webhooks`
- Input: `shop_id`, `url`, `events` (optional)
- Makes a change. Needs the person's own sign-in; an agent key is refused.

### webhooks.list

List a shop's webhook endpoints and their recent delivery counts. Secrets are not returned.

- REST: `GET /v1/webhooks`
- MCP tool: `webhooks_list`
- Input: `shop_id`
- Read only. Agents can call this.

### webhooks.delete

Remove a webhook endpoint. Pending deliveries to it stop.

- REST: `POST /v1/webhooks/delete`
- MCP tool: `webhooks_delete`
- Input: `endpoint_id`
- Makes a change. Agents can call this.

### audit.list

Read a shop's audit log, newest first (owner only).

- REST: `GET /v1/audit`
- MCP tool: `audit_list`
- Input: `shop_id`, `limit` (optional), `before_id` (optional)
- Read only. Agents can call this.

### events.catalog

Every event a webhook can receive, with its payload keys and types. Approved agent actions add requested_by_token_id to any event.

- REST: `GET /v1/events/catalog`
- MCP tool: `events_catalog`
- Input: No input.
- Read only. Agents can call this.

### webhooks.deliveries

List deliveries to one webhook endpoint, newest first, with event, status, attempts and last HTTP status.

- REST: `GET /v1/webhooks/deliveries`
- MCP tool: `webhooks_deliveries`
- Input: `endpoint_id`, `status` (optional), `limit` (optional), `before_id` (optional)
- Read only. Agents can call this.

### webhooks.retry

Send a failed or dead delivery again from attempt 0 (owner or office).

- REST: `POST /v1/webhooks/retry`
- MCP tool: `webhooks_retry`
- Input: `delivery_id`
- Makes a change. Agents can call this.

## Book the work (schedule, visits and customer notices)

Read schedule.settings first: it has the shop's time zone, working hours, arrival window and buffer. Use schedule.findTime to get open times that fit the shop's hours, each worker's hours and time off, and visits already booked with the buffer around them, then book with visits.create. Booking outside shop hours or over a conflict is allowed but comes back as a warning to show the office. In the field, the worker taps visits.onTheWay, visits.arrived, visits.start and visits.done. Doing Work sends the customer an appointment reminder before the visit and an on my way email when the worker heads out; visits.notices shows what was planned, sent or dropped.

### visits.createRecurring

Book a repeating visit (daily, weekly or monthly, every n) for a job. All visits are booked now, up to 104. Times repeat in the shop's local time zone. Overlaps and unavailable workers come back as warnings, never blocks.

- REST: `POST /v1/visits/recurring`
- MCP tool: `visits_createRecurring`
- Input: `job_id`, `starts_at`, `ends_at`, `freq`, `every` (optional), `occurrences` (optional), `until` (optional), `title` (optional), `notes` (optional), `assignee_ids` (optional), `arrival_window_minutes` (optional)
- Makes a change. Agents can call this.

### visits.cancelSeries

Cancel the visits of a repeating series that have not started yet, from a time onward (default now). Past, started and done visits stay.

- REST: `POST /v1/visits/recurring/cancel`
- MCP tool: `visits_cancelSeries`
- Input: `series_id`, `from` (optional), `reason` (optional)
- Makes a change. Agents can call this.

### visits.start

Mark a visit started (from scheduled, on the way, or arrived). The worker on the visit (or the office) can do this.

- REST: `POST /v1/visits/start`
- MCP tool: `visits_start`
- Input: `visit_id`
- Makes a change. Agents can call this.

### visits.done

Mark a visit done, with an optional note. The worker on the visit (or the office) can do this. A visit not yet started is started too. Refused with checklist_open while a checklist on the visit has required items open or a sign-off missing; the owner, signed in on the hosted override page (/p/checklist/override), may pass override_reason to mark it done anyway (recorded and audited). Agents, keys and office staff cannot override.

- REST: `POST /v1/visits/done`
- MCP tool: `visits_done`
- Input: `visit_id`, `note` (optional), `override_reason` (optional)
- Makes a change. Agents can call this.

### visits.create

Book a visit to a job at a time, optionally with the workers going. Overlaps for those workers come back in conflicts.

- REST: `POST /v1/visits`
- MCP tool: `visits_create`
- Input: `job_id`, `starts_at`, `ends_at`, `title` (optional), `notes` (optional), `assignee_ids` (optional), `arrival_window_minutes` (optional)
- Makes a change. Agents can call this.

### visits.update

Move a visit or change its title, notes or arrival window. Overlaps for its workers come back in conflicts. Moving it re-plans the appointment reminder.

- REST: `POST /v1/visits/update`
- MCP tool: `visits_update`
- Input: `visit_id`, `starts_at` (optional), `ends_at` (optional), `title` (optional), `notes` (optional), `arrival_window_minutes` (optional)
- Makes a change. Agents can call this.

### visits.assign

Set who is going on a visit. The list replaces the current one; an empty list leaves it unassigned.

- REST: `POST /v1/visits/assign`
- MCP tool: `visits_assign`
- Input: `visit_id`, `person_ids`
- Makes a change. Agents can call this.

### visits.cancel

Cancel a visit that is scheduled, on the way, or arrived. It stays on record with the reason, and customer notices not yet sent for it are dropped.

- REST: `POST /v1/visits/cancel`
- MCP tool: `visits_cancel`
- Input: `visit_id`, `reason` (optional)
- Makes a change. Agents can call this.

### visits.get

One visit with its job and workers.

- REST: `GET /v1/visits/get`
- MCP tool: `visits_get`
- Input: `visit_id`
- Read only. Agents can call this.

### schedule.list

A shop's visits that overlap a time window, earliest first. Filter by worker, job, or only mine. Field workers only ever see their own.

- REST: `GET /v1/schedule`
- MCP tool: `schedule_list`
- Input: `shop_id`, `from`, `to`, `person_id` (optional), `job_id` (optional), `mine` (optional), `unassigned` (optional), `include_cancelled` (optional)
- Read only. Agents can call this.

### availability.setHours

Set a worker's weekly hours, replacing what was there. Days are 0 (Sunday) to 6, times are shop local HH:MM. An empty list means no set hours (always available).

- REST: `POST /v1/availability/hours`
- MCP tool: `availability_setHours`
- Input: `shop_id`, `person_id` (optional), `hours`
- Makes a change. Agents can call this.

### availability.addTimeOff

Add time off for a worker (vacation, sick, appointment).

- REST: `POST /v1/availability/time-off`
- MCP tool: `availability_addTimeOff`
- Input: `shop_id`, `person_id` (optional), `starts_at`, `ends_at`, `reason` (optional)
- Makes a change. Agents can call this.

### availability.removeTimeOff

Remove a time-off entry.

- REST: `POST /v1/availability/time-off/remove`
- MCP tool: `availability_removeTimeOff`
- Input: `time_off_id`
- Makes a change. Agents can call this.

### availability.get

A worker's weekly hours, time off, and booked visits in a window (at most 62 days). Defaults to me.

- REST: `GET /v1/availability`
- MCP tool: `availability_get`
- Input: `shop_id`, `person_id` (optional), `from`, `to`
- Read only. Agents can call this.

### schedule.settings

The shop's scheduling setup: time zone, booking hours (or the default when none are set), arrival window, travel buffer, reminder lead time, and whether on-my-way notices go out. Any worker in the shop can read it.

- REST: `GET /v1/schedule/settings`
- MCP tool: `schedule_settings`
- Input: `shop_id`
- Read only. Agents can call this.

### schedule.setShopHours

Set the shop's booking hours, replacing what was there (owner or office). Days are 0 (Sunday) to 6, times are shop local HH:MM, several blocks a day are fine. An empty list clears them, and find-a-time falls back to Monday to Friday 08:00 to 17:00. Visits outside these hours are still allowed and come back with outside_shop_hours.

- REST: `POST /v1/schedule/hours`
- MCP tool: `schedule_setShopHours`
- Input: `shop_id`, `hours`
- Makes a change. Agents can call this.

### schedule.setRules

Change the shop's scheduling rules (owner or office; the time zone is owner only). arrival_window_minutes is the default window customers are told for new visits (0 means an exact time). buffer_minutes is travel and setup time find-a-time keeps clear around booked visits. reminder_hours is how long before a visit the customer gets an appointment reminder email (0 turns reminders off). on_my_way_notices emails the customer when a worker taps on the way. Only the fields you send change. Visits already booked keep their window and reminder.

- REST: `POST /v1/schedule/rules`
- MCP tool: `schedule_setRules`
- Input: `shop_id`, `arrival_window_minutes` (optional), `buffer_minutes` (optional), `reminder_hours` (optional), `on_my_way_notices` (optional), `time_zone` (optional)
- Makes a change. Agents can call this.

### schedule.findTime

Find open times for a visit of a given length. Looks inside shop hours (or Monday to Friday 08:00 to 17:00 when none are set), skips worker time off and times outside a worker's own hours, and keeps the shop's travel buffer clear around booked visits. Give person_ids to check those workers (together: all of them free at once, the default; or any one of them). Leave it out to check every worker in the shop and get back who is free. Field workers can only check themselves. Nothing is booked; call visits.create with a slot.

- REST: `POST /v1/schedule/find-time`
- MCP tool: `schedule_findTime`
- Input: `shop_id`, `duration_minutes`, `from` (optional), `to` (optional), `person_ids` (optional), `together` (optional), `step_minutes` (optional), `limit` (optional)
- Read only. Agents can call this.

### visits.onTheWay

Mark that the worker is on the way to a visit, with an optional ETA (eta_at, or eta_minutes from now). The worker on the visit (or the office) can do this. When the shop has on-my-way notices on, the customer gets one email saying who is coming and when.

- REST: `POST /v1/visits/on-the-way`
- MCP tool: `visits_onTheWay`
- Input: `visit_id`, `eta_at` (optional), `eta_minutes` (optional)
- Makes a change. Agents can call this.

### visits.arrived

Mark that the worker arrived at a visit. The worker on the visit (or the office) can do this. Any on-my-way email not yet sent is dropped.

- REST: `POST /v1/visits/arrived`
- MCP tool: `visits_arrived`
- Input: `visit_id`
- Makes a change. Agents can call this.

### visits.notices

Customer notices planned or handled for a visit (appointment reminder, on my way), with status pending, queued (handed to email), cancelled or skipped and why. Owner or office.

- REST: `GET /v1/visits/notices`
- MCP tool: `visits_notices`
- Input: `visit_id`
- Read only. Agents can call this.

### visits.changeRequests

Customers' requests to move or cancel a visit, from the customer hub, newest first. Defaults to open ones. Owner or office only; answer with visits.answerChange.

- REST: `GET /v1/visits/changeRequests`
- MCP tool: `visits_changeRequests`
- Input: `shop_id`, `status` (optional), `visit_id` (optional), `limit` (optional)
- Read only. Agents can call this.

### visits.answerChange

Answer a customer's request to move or cancel a visit. decline leaves the visit as it is. accept on a cancel request cancels the visit (visits.cancel); accept on a reschedule moves it to starts_at (and ends_at, or the same length as before) through visits.update. Pass apply false to mark it accepted without changing the visit (you already moved it). The customer sees the answer and your note on their hub. If a shop approval rule holds the visit change, the request stays open and the approval id comes back. Owner or office only.

- REST: `POST /v1/visits/answerChange`
- MCP tool: `visits_answerChange`
- Input: `change_request_id`, `decision`, `note` (optional), `starts_at` (optional), `ends_at` (optional), `apply` (optional)
- Makes a change. Agents can call this.

## Named agent keys

The owner makes a key for each agent they use, with a name they will recognize. The key is shown once and never again, so save it when it is made. A key can be narrowed: read_only makes no changes, no_money cannot charge, refund or change payments. A key never does more than the owner can, and only in that one shop. Revoking a key stops it at once.

### keys.create

Make a named agent key for one shop (owner only). The owner accepts the agent key terms by passing accept_terms_version. Optional scopes narrow the key: read_only (no changes) and no_money (no charges, refunds or payment changes). A key never does more than the owner can. The key is shown once, here, and never again.

- REST: `POST /v1/keys`
- Input: `shop_id`, `label`, `scopes` (optional), `audience` (optional), `accept_terms_version`
- Makes a change. Needs the person's own sign-in; an agent key is refused.

### keys.list

List a shop's named agent keys (owner only): label, scopes, audience, when made, last used and revoked. The key itself is never returned. Filter by audience (any or mcp) or include revoked keys.

- REST: `GET /v1/keys`
- MCP tool: `keys_list`
- Input: `shop_id`, `audience` (optional), `include_revoked` (optional)
- Read only. Agents can call this.

### keys.revoke

Revoke a named agent key (owner only). It stops working at once and cannot be turned back on; make a new key instead.

- REST: `POST /v1/keys/revoke`
- MCP tool: `keys_revoke`
- Input: `key_id`, `reason` (optional)
- Makes a change. Agents can call this.

## Every action

All actions, in the order Doing Work lists them.

- `auth.requestLink`: Email a one-time sign-in link.
- `auth.redeemLink`: Exchange a sign-in link token for a browser session.
- `auth.oauthRegister`: Register an OAuth client (RFC 7591 dynamic registration).
- `auth.oauthAuthorize`: A signed-in person approves an agent client.
- `auth.oauthToken`: Exchange an authorization code and PKCE verifier, or a refresh token, for a short-lived agent access token and a new refresh token.
- `me.get`: The signed-in person.
- `shops.create`: Create a shop.
- `members.invite`: Invite a person by email as office or field.
- `members.accept`: Accept an invite as the signed-in person.
- `members.list`: Current members of a shop with role and start date.
- `members.end`: End a membership: the owner removes a worker, or a worker leaves.
- `members.changeRole`: Owner changes a worker between office and field.
- `jobs.create`: Start a job at a home (the home is found or created from the address).
- `jobs.list`: Jobs of a shop, newest first.
- `jobs.get`: One job with its home address.
- `media.requestUpload`: Get a signed upload URL for a job photo.
- `media.requestCapture`: Hosted photo capture: a signed upload URL for a worker taking a photo in the browser.
- `media.list`: List the photos on a job that you are allowed to see.
- `media.get`: One photo's details.
- `jobs.requestConfirmation`: Get a single-use link the customer opens to confirm the work was done (or flag a problem).
- `jobs.confirm`: Customer confirms the work or flags a problem (hosted page).
- `quotes.create`: Create a quote from lines the worker wrote.
- `quotes.send`: Lock the quote and get a single-use signing link for the customer.
- `quotes.sign`: Sign a quote after accepting electronic signing.
- `quotes.revise`: Replace an unsigned quote with a new version.
- `quotes.withdraw`: Withdraw a draft or sent quote the customer has not signed, with an optional reason.
- `subcontractors.get`: The Idaho 45-525(3) subcontractor and supplier statement for a job: the latest signed one, any prepared draft, and whether final payment needs one.
- `subcontractors.prepare`: Prepare the Idaho 45-525(3) statement for the shop to sign: a list of subcontractors, materialmen and rental equipment providers over $500, or none_attestation "yes" for "No subcontractors or suppliers over $500 on this job".
- `subcontractors.sign`: The shop owner or office signs the Idaho 45-525(3) statement on a hosted page: a list (entries JSON), the "No subcontractors or suppliers over $500 on this job" attestation (none_attestation "yes"), or with neither, the prepared draft.
- `payments.connectStart`: Start payment setup for a shop (owner, hosted page).
- `invoices.createFromQuote`: Create an invoice for a signed quote, for the total of the option and optional lines the customer picked.
- `invoices.send`: Send an invoice (or the next part of it) and get a single-use pay link.
- `invoices.pay`: Start paying an invoice (hosted page).
- `invoices.markPaidOutside`: Record that the customer paid an invoice outside Doing Work (check, cash, financing, insurance or other).
- `invoices.refund`: Refund part or all of a payment (owner or office).
- `fees.schedule`: Doing Work's fee schedule: one stepped rate on each shop's calendar month of card and bank payments, taken out of the payments.
- `fees.preview`: What Doing Work would take on a payment of this size right now, the effective rate on it, the month's volume so far (all locations under the same owner), and how far the shop is from the next, cheaper band (owner or office).
- `fees.statement`: A month's Doing Work fees for a shop: card and bank volume, fees by kind, the effective rate, each line, and how far the shop is from the next band this month (owner or office).
- `records.keys`: Public keys that verify signed job records.
- `jobs.record`: The latest signed record for a job and its level (logged, paid, verified).
- `webhooks.create`: Add a webhook endpoint for a shop (owner or office).
- `webhooks.list`: List a shop's webhook endpoints and their recent delivery counts.
- `webhooks.delete`: Remove a webhook endpoint.
- `audit.list`: Read a shop's audit log, newest first (owner only).
- `platforms.create`: Create a developer platform account (private beta).
- `platforms.list`: List the platform accounts you own and the shops each one manages.
- `platforms.grantManage`: Let a platform manage this shop.
- `platforms.revokeManage`: Stop a platform from managing this shop.
- `customers.create`: Add a customer to a shop, with kind, company, email, phone, billing address, lead source, tags and notes in one call.
- `customers.get`: One customer with contacts, properties, custom fields and open balance.
- `customers.list`: Customers of a shop, newest first, filtered by tag, lead source or archived.
- `customers.search`: Find customers by name, company, email, phone or property address.
- `customers.update`: Change a customer's name, kind, company, email, phone, billing address, lead source, tags or notes.
- `customers.archive`: Archive a customer (hidden from lists, history kept) or bring one back.
- `customers.duplicates`: Likely duplicate customers in a shop: same email, same phone digits, or same name.
- `customers.merge`: Merge a duplicate customer into another: jobs, requests, contacts, properties and fields move over.
- `customers.contactAdd`: Add a contact person to a customer (tenant, manager, billing, spouse).
- `customers.contactUpdate`: Change or remove one contact of a customer.
- `properties.add`: Add a service address to a customer, with optional label and access notes.
- `properties.list`: Service addresses of a customer, or every property of a shop.
- `properties.update`: Change a property's label or access notes, or archive it.
- `equipment.add`: Record equipment at a property: kind, make, model, serial, install date and warranty.
- `equipment.list`: Equipment at a property, or warranties ending soon across a shop.
- `equipment.update`: Change or archive an equipment record.
- `jobs.setTags`: Set the tags and lead source on a job.
- `customers.timeline`: Everything that happened with a customer, newest first: requests, jobs, quotes, signatures, invoices and payments.
- `customers.statement`: A customer's account statement: every invoice with total, paid and balance, and the total owed.
- `customers.privacyExport`: Everything the shop holds about one customer, as one JSON document, for a privacy request.
- `customers.privacyDelete`: Erase a customer's personal details for a privacy request.
- `fields.define`: Add a custom field to customers, properties, jobs, quotes or invoices for a shop.
- `fields.list`: Custom fields a shop has defined, optionally for one kind of record.
- `fields.set`: Set one custom field value on a customer, property, job, quote or invoice.
- `fields.get`: Custom field values on one record, with any required fields still empty.
- `requests.create`: Log a request for work from a customer or a new lead.
- `requests.list`: Requests of a shop, newest first, filtered by stage, customer or tag.
- `requests.pipeline`: How many open requests a shop has in each stage, and how many were won or lost in the last 30 days.
- `requests.get`: One request with its customer and the job it became.
- `requests.update`: Move a request to another stage or change its details, customer or property.
- `requests.convert`: Turn a request into a job at its property or address; the worker then writes the quote or books a visit.
- `requests.formCreate`: Turn on the shop's public request form and get its key.
- `requests.submitForm`: Send a shop a request through its public form.
- `visits.createRecurring`: Book a repeating visit (daily, weekly or monthly, every n) for a job.
- `visits.cancelSeries`: Cancel the visits of a repeating series that have not started yet, from a time onward (default now).
- `visits.start`: Mark a visit started (from scheduled, on the way, or arrived).
- `visits.done`: Mark a visit done, with an optional note.
- `visits.create`: Book a visit to a job at a time, optionally with the workers going.
- `visits.update`: Move a visit or change its title, notes or arrival window.
- `visits.assign`: Set who is going on a visit.
- `visits.cancel`: Cancel a visit that is scheduled, on the way, or arrived.
- `visits.get`: One visit with its job and workers.
- `schedule.list`: A shop's visits that overlap a time window, earliest first.
- `availability.setHours`: Set a worker's weekly hours, replacing what was there.
- `availability.addTimeOff`: Add time off for a worker (vacation, sick, appointment).
- `availability.removeTimeOff`: Remove a time-off entry.
- `availability.get`: A worker's weekly hours, time off, and booked visits in a window (at most 62 days).
- `schedule.settings`: The shop's scheduling setup: time zone, booking hours (or the default when none are set), arrival window, travel buffer, reminder lead time, and whether on-my-way notices go out.
- `schedule.setShopHours`: Set the shop's booking hours, replacing what was there (owner or office).
- `schedule.setRules`: Change the shop's scheduling rules (owner or office; the time zone is owner only).
- `schedule.findTime`: Find open times for a visit of a given length.
- `visits.onTheWay`: Mark that the worker is on the way to a visit, with an optional ETA (eta_at, or eta_minutes from now).
- `visits.arrived`: Mark that the worker arrived at a visit.
- `visits.notices`: Customer notices planned or handled for a visit (appointment reminder, on my way), with status pending, queued (handed to email), cancelled or skipped and why.
- `shops.setInvoiceReminders`: Turn the automatic unpaid-invoice reminder on or off for the whole shop (owner only).
- `shops.setContact`: Set the phone and email customers see when they open a canceled invoice or quote link (owner only).
- `shops.setFieldCosts`: Let field workers in the shop see job costs (labor cost, material cost, expenses others entered, price book cost), or hide them again (owner only).
- `shops.setHubLinkInEmails`: Turn the customer hub link in quote and invoice emails on or off for the whole shop (owner only).
- `invoices.setReminder`: Turn the automatic unpaid reminder on or off for one invoice (owner or office).
- `pricebook.saveItem`: Add a price book item, or change one when item_id is given (owner or office).
- `pricebook.list`: List the shop's price book items, by name.
- `pricebook.archiveItem`: Archive a price book item, or bring it back with archived false (owner or office).
- `templates.save`: Save a quote template: worker-written lines (and good, better, best options) to start quotes from.
- `templates.list`: List the shop's saved quote templates with their lines.
- `templates.archive`: Archive a quote template, or bring it back with archived false (owner or office).
- `quotes.respond`: Customer declines a quote or asks for changes (hosted page).
- `quotes.activity`: What the customer has done with a quote: when each version was first opened and how often, and any decline or change request with its note.
- `shops.setQuoteFollowup`: Set how many days after a quote is sent the customer gets one friendly follow-up email if they have not answered (owner only).
- `invoices.create`: Create an invoice without a quote, from lines the worker wrote (never filled in by AI).
- `invoices.createBatch`: Create many invoices at once (up to 100), each from a job and the lines the worker wrote.
- `invoices.get`: One invoice with its lines, what has been paid, the balance, the due date, and whether a bank payment is still clearing.
- `invoices.pdf`: The invoice as a PDF (base64), with shop, customer, lines, paid so far and balance.
- `invoices.aging`: Accounts receivable aging: every open invoice's balance by how late it is (current, 1-30, 31-60, 61-90, over 90 days past due), with totals per customer (owner or office).
- `shops.setTips`: Turn tips on or off on this shop's invoice pay page (owner only).
- `shops.setReviewRequest`: Set the review link (for example your Google review page, https only) and how many days after an invoice is paid the customer gets one email asking for a review (owner only).
- `tips.report`: Tips customers paid, by invoice and in total, between two dates (owner or office).
- `cards.setupLink`: Get a secure page where the customer saves a card with the shop's payment processor (owner or office).
- `cards.resave`: Open the card saving page from a save-your-card-again email link (hosted page only).
- `cards.get`: The card a customer has on file (brand, last 4, expiry) and whether auto-pay is on (owner or office).
- `cards.remove`: Remove a customer's saved card (owner or office).
- `customers.setAutopay`: Turn auto-pay on or off for a customer (owner or office).
- `maintenance.createPlan`: Start a maintenance plan for a job's customer (owner or office).
- `maintenance.getPlan`: One maintenance plan: price per term, current term, renewal date, and each term's invoice.
- `maintenance.listPlans`: A shop's maintenance plans, optionally for one customer or by status, soonest renewal first.
- `maintenance.setPrice`: Replace a plan's priced lines (owner or office; you write them, never AI), and optionally its tax rate.
- `maintenance.cancelPlan`: Cancel a maintenance plan (owner or office).
- `taxes.saveRate`: Add or change a sales tax rate the shop charges in one jurisdiction (owner or office).
- `taxes.listRates`: The shop's tax rates.
- `taxes.archiveRate`: Stop using a tax rate (owner or office).
- `shops.setTaxReportBasis`: Choose how the sales tax report counts a period (owner only).
- `taxes.salesReport`: Sales tax report for a period (owner or office), shop local dates, from and to inclusive.
- `quotes.addAttachment`: Show an uploaded photo or file of the same job with a quote, with an optional caption.
- `quotes.removeAttachment`: Take a photo or file off a quote.
- `quotes.listAttachments`: Photos and files shown with a quote, in order.
- `media.tag`: Add or remove tags on a photo (for example: leak, panel, permit).
- `tags.list`: Every tag used in the shop, with how many photos and jobs carry it.
- `media.search`: Find photos across the shop that carry all the given tags.
- `jobs.findByTag`: Jobs in the shop that carry all the given tags (set with jobs.setTags), newest first, up to 200.
- `media.saveMarkup`: Save markup over a photo as a new layer version: arrows, lines, boxes, circles, text and freehand strokes in 0..1 photo coordinates (0,0 is top left).
- `media.getMarkup`: A photo's markup: the latest version unless one is named, with an SVG overlay to lay over the original photo.
- `shares.create`: Make a link to a gallery of a job's photos that anyone with the link can open without signing in.
- `shares.list`: Photo share links on a job, newest first, with whether each is still active and how often it was opened.
- `shares.revoke`: Turn a photo share link off now.
- `logs.add`: Write a daily log entry on a job: what happened on site that day, in the worker's own words.
- `logs.list`: Daily log entries on a job, oldest first, optionally between two dates (inclusive).
- `logs.remove`: Take a daily log entry out of the log.
- `documents.photoReport`: Render a photo report PDF for a job: a title and the worker's own notes, then the job's photos in time order with stage and tags.
- `documents.dailyLog`: Render the daily log for a job as a PDF: each day's entries in the workers' own words with who wrote them, and the photos they attached.
- `documents.list`: PDF documents core made for a job (photo reports, daily logs and submitted forms), newest first, with each file's sha256.
- `documents.get`: One stored job PDF (base64) with its sha256, byte for byte as it was made.
- `expenses.add`: Record money a worker spent on a job, with the receipt photo.
- `expenses.update`: Change an expense: date, vendor, category, amount, tax, receipt photo or note.
- `expenses.remove`: Take an expense off the books (kept in history).
- `expenses.list`: Expenses on a job, or across the shop between dates, with totals by category.
- `search.global`: Search one shop for customers (name, email, phone), properties (address), jobs (summary) and requests (title).
- `events.catalog`: Every event a webhook can receive, with its payload keys and types.
- `webhooks.deliveries`: List deliveries to one webhook endpoint, newest first, with event, status, attempts and last HTTP status.
- `webhooks.retry`: Send a failed or dead delivery again from attempt 0 (owner or office).
- `imports.fields`: The customer import fields and the built-in presets (jobber, housecall_pro, companycam) with the export column each reads.
- `imports.preview`: Dry run of a customer CSV import.
- `imports.run`: Owner only: import customers (and one service address each) from a CSV with the mapping the owner confirmed.
- `shops.export`: Owner only: everything the shop holds as one ZIP: data/<table>.csv and data/<table>.json for every shop table, files/ with job photos and PDFs, and manifest.json.
- `keys.create`: Make a named agent key for one shop (owner only).
- `keys.list`: List a shop's named agent keys (owner only): label, scopes, audience, when made, last used and revoked.
- `keys.revoke`: Revoke a named agent key (owner only).
- `agents.register`: auth.md agent registration.
- `agents.claim`: auth.md claim.
- `agents.approve`: Hosted claim page only: the shop owner whose email the claim names types the agent's 6 digit code, picks one of their shops and accepts the agent key terms.
- `agents.deny`: Hosted claim page only: the person turns the agent down.
- `reports.revenue`: Money collected between two dates (inclusive, UTC), by month: card and bank payments through Doing Work less refunds, and payments recorded as taken outside Doing Work, shown apart.
- `reports.owed`: Money customers still owe on sent invoices, grouped by how long ago each invoice went out: 0 to 30, 31 to 60, 61 to 90 and over 90 days.
- `reports.quoteWinRate`: Of the quotes made between two dates (inclusive, UTC) and sent, how many were signed, declined, or are still waiting.
- `members.permissions`: What each current worker in a shop may do beyond their role: see_prices, see_costs, take_money, delete and manage_schedule.
- `members.setPermissions`: Owner only, not an agent: turn a worker's permissions on or off.
- `legal.documents`: The legal documents Doing Work asks people to accept, with the current version and a link to the text.
- `legal.status`: What the signed-in person has accepted: for each document that is accepted here, the current version, the versions accepted and when, and whether the current version still needs accepting.
- `legal.accept`: Record that the signed-in person accepts the current version of a legal document (clickwrap).
- `signatures.request`: Ask someone to sign a document on a hosted page, and get the signing link to give them.
- `signatures.newLink`: A new signing link for an open signature request.
- `signatures.cancel`: Cancel an open signature request.
- `signatures.get`: One signature request: the document text (or stored PDF id) and its sha256, and the signature when signed (who, typed name, consent text and its sha256, surface, time).
- `signatures.list`: Signature requests for a job, a customer or a whole shop, newest first, with status and who signed.
- `signatures.sign`: Sign a document after reading it, typing your name and agreeing to sign electronically.
- `checklists.saveTemplate`: Create a checklist template, or replace one (give checklist_template_id).
- `checklists.listTemplates`: The shop's checklist templates with their items.
- `checklists.archiveTemplate`: Archive a checklist template so it is no longer offered.
- `checklists.create`: Put a checklist on a job, or on one visit of the job (visit_id).
- `checklists.list`: Checklists on a job (all of them, job-level and per visit) or on one visit, with counts of open required items and sign-off state.
- `checklists.get`: One checklist with every item (removed items included and marked), who ticked what and when, and its sign-off.
- `checklists.checkItem`: Tick (done true) or untick an item, with an optional note.
- `checklists.addItems`: Add items to the end of a checklist (owner or office).
- `checklists.removeItem`: Remove an item from a checklist (it stays in the history, marked removed).
- `checklists.requestSignOff`: Ask for a sign-off on a finished checklist and get the signing link.
- `jobs.complete`: Mark a job done (owner or office).
- `forms.saveTemplate`: Create a form template (an inspection, a safety sheet, a start-up report), or replace one (give form_template_id).
- `forms.listTemplates`: The shop's form templates with their fields.
- `forms.archiveTemplate`: Archive a form template so it is no longer offered.
- `forms.start`: Start a form on a job, or on one visit of the job (visit_id).
- `forms.fill`: Save answers on a draft form.
- `forms.submit`: Submit a draft form: every required field must be answered (forms.get lists what is missing).
- `forms.cancel`: Cancel a draft form (kept in history).
- `forms.get`: One form with its fields, answers, the required fields still missing, its stored PDF id and hash once submitted, and its sign-off.
- `forms.list`: Forms on a job (job-level and per visit), or on one visit, newest first, without fields and answers.
- `forms.requestSignOff`: Ask for a signature on a submitted form and get the signing link (owner or office).
- `notes.add`: Write a note on a job, or on one visit of it (give visit_id; job_id is then optional).
- `notes.addVoiceMemo`: Save a voice memo as a note: send the transcript your agent made, the memo's length in seconds, when it was recorded, and what transcribed it.
- `notes.update`: Change a note.
- `notes.remove`: Take a note off the job.
- `notes.get`: One note you may see, with every earlier wording (history, oldest first).
- `notes.list`: Notes you may see on a job (job-level and every visit) or on one visit: pinned first, then newest first.
- `costs.job`: Job costing for one job: contract, invoiced, collected and revenue against labor (approved and pending time at each entry's cost rate), materials at cost, expenses and Doing Work fees, with profit and margin.
- `costs.jobs`: Job costing across jobs dated between two days (inclusive, in the shop's time zone), with totals.
- `reports.today`: The shop's today queues in one call: visits today, visits in the next 7 days with nobody assigned, new requests, quotes and change orders waiting on the customer, overdue and draft invoices, done jobs not invoiced, time waiting for approval and approvals waiting on the owner.
- `reports.week`: How the shop did in one week (Monday to Sunday in the shop's time zone) against the week before: money collected, refunded, paid outside and invoiced; quotes sent and signed; jobs, visits, customers and requests; time logged and expenses.
- `customers.audience`: Pick a customer audience by filters (tags, last job dates, kind of work, open quotes, money owed, maintenance plans, city or postal code, has email or phone) and get the counts and a page of matching customers.
- `messages.templates`: The shop's customer message templates, one per kind (invoice_reminder, quote_followup, review_request): the wording in use (custom or built-in), the placeholders each kind takes, and the locked link placeholder that must stay in the body.
- `messages.saveTemplate`: Set the subject and body wording for one kind of automatic customer email.
- `messages.resetTemplate`: Go back to the built-in wording for one kind of automatic customer email.
- `messages.previewTemplate`: Show how one kind of automatic customer email reads, filled with sample customer details and the shop's name.
- `changes.create`: Write a change order on a job: a priced change to its scope, from lines a worker wrote (never filled in by AI).
- `changes.list`: A job's change orders and its money picture: signed quotes (contract_minor) + approved change orders = revised_total_minor; pending (sent, not yet signed); invoiced on live invoices; unbilled; and approved change orders not on an invoice yet.
- `changes.get`: One change order with its lines, totals, status, signature and invoice.
- `changes.send`: Owner or office: ask the customer to approve a change order.
- `changes.cancel`: Cancel a draft or sent change order (its open signature request is cancelled too, so the link stops working).
- `changes.invoice`: Bill approved change orders on a normal invoice (draft; send it with invoices.send).
- `timesheets.clockIn`: Start a person's clock: on a job (work), or drive, shop or break time.
- `timesheets.clockOut`: Stop a person's running clock.
- `timesheets.current`: A person's running clock in a shop, or null when they are not clocked in.
- `timesheets.add`: Add time after the fact (a manual entry with a start and an end, at most 24 hours).
- `timesheets.update`: Fix a time entry that is not approved: its start, end, category, job, visit or notes.
- `timesheets.void`: Take back a time entry that was never approved (kept in history, no longer counted).
- `timesheets.decide`: Owner or office: approve or reject stopped time entries (up to 200 at once, one shop).
- `timesheets.reopen`: Owner only: reopen approved time so it can be fixed (it goes back to waiting for approval).
- `timesheets.list`: Time entries in a shop between dates (the shop's time zone), or on one job, with minutes by category and by person, and approved minutes.
- `timesheets.setRate`: Owner or office: set what an hour of a worker's time costs the shop (wage plus burden), or the shop default when person_id is left out.
- `timesheets.listRates`: Labor cost rates in force now: the shop default and each worker's own, plus upcoming ones.
- `materials.add`: Record a material used on a job (optionally on one of its visits): from the price book (pricebook_item_id; its name, unit, price, cost and taxable are copied as they are now) or typed in (name, unit, and optionally the price and cost per unit).
- `materials.update`: Change a recorded material's quantity, visit or notes.
- `materials.remove`: Remove a recorded material (recorded in error or returned).
- `materials.list`: Materials recorded on a job (or one visit), with the price and cost totals of those still counted.
- `shops.transferStart`: Start handing the shop to a current worker (owner, on a hosted page).
- `shops.transferAccept`: Accept a hand-off and become the shop's owner (the person asked, on a hosted page).
- `shops.transferCancel`: Stop an open hand-off: the owner cancels it, or the person asked declines it.
- `shops.transfers`: Hand-offs of a shop, newest first.
- `shops.closeCheck`: Whether the shop can close now (owner only).
- `shops.close`: Close the shop for good (owner, on a hosted page, typing the shop name).
- `shops.reopen`: Reopen a closed shop (only the owner who closed it, on a hosted page, within 90 days of closing).
- `subs.add`: Add a subcontractor to a job: a company and the one email they will sign in with.
- `subs.list`: Subcontractors on a job, or on every job in the shop: status invited, joined, expired or removed, when they last opened their page.
- `subs.update`: Change a subcontractor's contact name, trade or scope note (what they see on their page).
- `subs.remove`: Take a subcontractor off a job, for good: their link stops working at once.
- `subs.notes`: Notes subcontractors left on a job, newest first.
- `subs.view`: The subcontractor's page for one job: the shop, job summary, work address, scope note, upcoming visit times and their notes.
- `subs.addNote`: Leave a note for the shop on this job, as the subcontractor (up to 20 a day).
- `invoices.void`: Void an invoice nobody has paid yet (owner or office), with a reason.
- `bulk.run`: Run one safe action on up to 200 records: customers.addTags, customers.removeTags, customers.archive, customers.unarchive, jobs.addTags, jobs.removeTags, visits.assign.
- `reports.save`: Save a report to run again later: a name, one report operation (reports.revenue, reports.owed, reports.quoteWinRate, reports.today, reports.week, costs.jobs), its settings, and an optional moving date range (today, this_week, last_week, this_month, last_month, last_7_days, last_30_days, last_90_days, this_quarter, last_quarter, this_year, last_year) worked out on the shop's calendar each run.
- `reports.listSaved`: The shop's saved reports, by name.
- `reports.removeSaved`: Remove a saved report (kept for the record; its name is free to use again).
- `reports.run`: Run a saved report now: its date range is worked out on the shop's calendar (or around as_of), and the report operation runs with the caller's own permissions.
- `hub.createLink`: Make a customer hub link: one page where this customer sees their quotes, jobs, invoices, upcoming visits and requests with you, pays, asks to move or cancel a visit, and asks for new work.
- `hub.links`: A customer's hub links, newest first: when each was made, when it expires or was revoked, and how often and when it was last opened.
- `hub.revokeLink`: Stop a customer hub link working, for good.
- `visits.changeRequests`: Customers' requests to move or cancel a visit, from the customer hub, newest first.
- `visits.answerChange`: Answer a customer's request to move or cancel a visit.
- `hub.view`: The customer hub for the signed-in customer: their quotes, jobs, invoices with what is still owed, upcoming visits with any change request, and requests with this shop.
- `hub.open`: Open a sent quote or an unpaid invoice from the customer hub: returns a fresh page link for that one item, for the signed-in customer only, good for a day.
- `hub.requestVisitChange`: From the customer hub, ask the shop to move (reschedule, with the times that work better) or cancel an upcoming visit.
- `hub.withdrawVisitChange`: From the customer hub, take back your own open request to move or cancel a visit.
- `hub.requestWork`: From the customer hub, ask the shop for new work.
- `approvals.setRule`: Owner only, not an agent: require owner approval before an agent can run an action.
- `approvals.removeRule`: Owner only, not an agent: stop requiring approval for a rule.
- `approvals.listRules`: The shop's approval rules: which agent actions wait for the owner.
- `approvals.list`: Approval requests for a shop, newest first, optionally by status (pending, approved, denied, failed, expired).
- `approvals.get`: One approval request with its status and, once approved and run, the action's result.
- `approvals.decide`: Owner only, not an agent: approve or deny a pending agent action.
