# Doing Work: full documentation for AI agents

Every page of https://doingwork.work in one file. Generated 2026-10-31.

# Field service software your AI agent can run.

For home service shops and the AI agents that work for them.

Doing Work is a complete field service platform built for agents. Requests, scheduling, jobs, photos, quotes, invoices, card and bank payments, time, costs and reports. Your agent does the busywork. You make the calls. Workers, jobs, quotes and invoices are unlimited for every shop.

[Start free](https://doingwork.work/start/) · [Docs for agents](https://doingwork.work/docs/)

## Things you can ask your agent

- “Start a job at 412 Elm St for Dana Ruiz and add these photos.”
- “Write up a quote: 40 gallon gas heater installed, $1,850. Haul away, $95. Send it to Dana.”
- “Dana signed. Invoice her with a $500 deposit.”
- “Which jobs are still waiting on payment?”
- “Invite Sam as a field worker.”
- “How far are we from the next fee band this month?”

Your agent works through Doing Work's MCP server, REST API or A2A. [How agents connect](https://doingwork.work/docs/quickstart/)

## Your system of record

Your AI agent is good at talking. It's bad at remembering. Doing Work is where the shop's records live: every customer, job, photo, quote, signature, invoice and payment. Your agent reads and writes those records. When you switch agents, or run two, the records stay put.

- One customer history, whichever agent or worker touched it.
- Signatures and payments recorded on the job, from the page the customer used.
- Export your records whenever you want.

Coming soon: bring your own Twilio number for texts and AI phone answering, billed by Twilio and your voice provider at their rates, with nothing added by us. QuickBooks Online sync is coming soon too.

## Their app gives you more to do. Ours does it.

Other field service software sells screens: forms to fill in, schedules to drag around, invoices to click send on. Those screens are where people do work an agent can do. Doing Work has no app to learn. You tell your agent what happened and it does the work in Doing Work.

## What you still do

You still take the photo. Use your phone's camera and send it to your agent, and it goes on the job. We don't have our own camera app, offline mode or GPS stamped photos. If your work depends on those, keep your photo app.

You or your customer also open a page for anything that needs a person: signing, paying, connecting Stripe and refunds.

## One job, start to finish

1. **Start the job**: Your agent adds the customer and the address. Every job at the same home is linked.
2. **Photos**: Before and after photos. Location and camera data are removed before storage.
3. **Quote**: The worker gives the lines and prices. The agent writes them up exactly and sends a signing link.
4. **Invoice and paid**: The agent makes the invoice from the signed quote. The customer pays by card or bank into the shop's own Stripe account.

## You keep the decisions

Your agent can't sign a quote, take a payment, connect Stripe or give a refund. Those happen on a page that you or your customer opens. The agent sends the link and tells you what it's for.

[What stays with people](https://doingwork.work/docs/human-steps/)

## Workers write the prices

Doing Work never fills in priced quote lines. The worker says what the job costs and the agent writes it up exactly. In Idaho, quotes over $2,000 carry the state's required disclosure.

## The whole shop, not just the paperwork

- **Requests and scheduling**: A request form for your website, a pipeline, visits with arrival windows, a find-a-time search, and on the way and arrived notices by email.
- **Quotes your workers write**: Good, better and best options, optional lines, a price book, templates, photos on the quote, change orders and e-signatures.
- **Invoices and getting paid**: Deposits, card and bank payments into your own Stripe account, saved cards and auto-pay, reminders, maintenance plans and tips.
- **On the job**: Checklists, custom forms and inspections, notes, daily logs, materials, time clock and timesheets, photo markup and subcontractors.
- **Your customers**: Multiple properties and contacts, equipment and warranties, custom fields, and a customer hub where they see quotes, invoices and visits.
- **Knowing where you stand**: Job costing, expenses with receipts, money owed, revenue, win rate, sales tax and a daily list of what needs attention.

Coming soon: QuickBooks Online sync and Google Ads conversion uploads. Text messages aren't part of launch; customer notices go by email. [See everything](https://doingwork.work/platform/)

## You only pay when you get paid

There's nothing to pay up front, no monthly fee and no card on file. Deposits count as getting paid. Our fee comes out of the payments your customers make, before the money reaches you. No payments in a month means no fee.

## One price that drops as you grow

- **First $5,000 a month**: 1% of each card or bank payment.
- **$5,000 to $40,000**: 0.25% on the dollars in this band.
- **Above $40,000**: 0.04% on the rest of the month. No cap.

No plans to pick. Every shop gets every feature, with unlimited workers, jobs, customers, quotes and invoices. Heavy AI use is metered the same way for everyone. [See pricing](https://doingwork.work/pricing/)

## If you're an AI agent

Start with [llms.txt](https://doingwork.work/llms.txt) or the [docs](https://doingwork.work/docs/). There are 284 operations in the [API reference](https://doingwork.work/docs/reference/), 250 of them open to agents. The same actions run over REST, MCP and A2A, with the same permissions and owner approvals. The [OpenAPI file](https://doingwork.work/openapi.json), [MCP server card](https://doingwork.work/.well-known/mcp/server-card.json) and [A2A agent card](https://doingwork.work/.well-known/agent-card.json) are generated from that one list. REST is at https://doingwork.work/v1, MCP at https://doingwork.work/mcp and A2A at https://doingwork.work/a2a, all with the same operations and permissions.

## Run your next job through Doing Work.

[Start free](https://doingwork.work/start/)

Questions? Call [(208) 274-3273](tel:+12082743273).


---

# Everything a home service shop runs on, built for agents

Doing Work is a complete field service platform. Your AI agent runs it; you make the calls. Every shop gets every feature, with unlimited workers, jobs, customers, quotes and invoices.

## Available now

What's built today, with every operation an agent can call. Each links to its reference page.

### Sign-in and agent access

- [Email a one-time sign-in link](https://doingwork.work/docs/reference/auth-requestlink/) (agent)
- [Exchange a sign-in link token for a browser session](https://doingwork.work/docs/reference/auth-redeemlink/) (hosted page)
- [Register an OAuth client (RFC 7591 dynamic registration)](https://doingwork.work/docs/reference/auth-oauthregister/) (agent)
- [A signed-in person approves an agent client](https://doingwork.work/docs/reference/auth-oauthauthorize/) (agent)
- [Exchange an authorization code and PKCE verifier, or a refresh token, for a short-lived agent access token and a new refresh token](https://doingwork.work/docs/reference/auth-oauthtoken/) (agent)
- [The signed-in person](https://doingwork.work/docs/reference/me-get/) (agent)
- [Make a named agent key for one shop (owner only)](https://doingwork.work/docs/reference/keys-create/) (person only)
- [List a shop's named agent keys (owner only): label, scopes, audience, when made, last used and revoked](https://doingwork.work/docs/reference/keys-list/) (agent)
- [Revoke a named agent key (owner only)](https://doingwork.work/docs/reference/keys-revoke/) (agent)
- [auth.md agent registration](https://doingwork.work/docs/reference/agents-register/) (agent)
- [auth.md claim](https://doingwork.work/docs/reference/agents-claim/) (agent)
- [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](https://doingwork.work/docs/reference/agents-approve/) (hosted page)
- [Hosted claim page only: the person turns the agent down](https://doingwork.work/docs/reference/agents-deny/) (hosted page)
- [The legal documents Doing Work asks people to accept, with the current version and a link to the text](https://doingwork.work/docs/reference/legal-documents/) (agent)
- [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](https://doingwork.work/docs/reference/legal-status/) (agent)
- [Record that the signed-in person accepts the current version of a legal document (clickwrap)](https://doingwork.work/docs/reference/legal-accept/) (person only)

### Shop

- [Create a shop](https://doingwork.work/docs/reference/shops-create/) (agent)
- [Withdraw a draft or sent quote the customer has not signed, with an optional reason](https://doingwork.work/docs/reference/quotes-withdraw/) (agent)
- [Doing Work's fee schedule: one stepped rate on each shop's calendar month of card and bank payments, taken out of the payments](https://doingwork.work/docs/reference/fees-schedule/) (agent)
- [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)](https://doingwork.work/docs/reference/fees-preview/) (agent)
- [Read a shop's audit log, newest first (owner only)](https://doingwork.work/docs/reference/audit-list/) (agent)
- [One customer with contacts, properties, custom fields and open balance](https://doingwork.work/docs/reference/customers-get/) (agent)
- [Customers of a shop, newest first, filtered by tag, lead source or archived](https://doingwork.work/docs/reference/customers-list/) (agent)
- [Find customers by name, company, email, phone or property address](https://doingwork.work/docs/reference/customers-search/) (agent)
- [Change a customer's name, kind, company, email, phone, billing address, lead source, tags or notes](https://doingwork.work/docs/reference/customers-update/) (agent)
- [Archive a customer (hidden from lists, history kept) or bring one back](https://doingwork.work/docs/reference/customers-archive/) (agent)
- [Likely duplicate customers in a shop: same email, same phone digits, or same name](https://doingwork.work/docs/reference/customers-duplicates/) (agent)
- [Merge a duplicate customer into another: jobs, requests, contacts, properties and fields move over](https://doingwork.work/docs/reference/customers-merge/) (agent)
- [Add a contact person to a customer (tenant, manager, billing, spouse)](https://doingwork.work/docs/reference/customers-contactadd/) (agent)
- [Change or remove one contact of a customer](https://doingwork.work/docs/reference/customers-contactupdate/) (agent)
- [Set the tags and lead source on a job](https://doingwork.work/docs/reference/jobs-settags/) (agent)
- [Everything that happened with a customer, newest first: requests, jobs, quotes, signatures, invoices and payments](https://doingwork.work/docs/reference/customers-timeline/) (agent)
- [A customer's account statement: every invoice with total, paid and balance, and the total owed](https://doingwork.work/docs/reference/customers-statement/) (agent)
- [Everything the shop holds about one customer, as one JSON document, for a privacy request](https://doingwork.work/docs/reference/customers-privacyexport/) (agent)
- [Erase a customer's personal details for a privacy request](https://doingwork.work/docs/reference/customers-privacydelete/) (person only)
- [Turn the automatic unpaid-invoice reminder on or off for the whole shop (owner only)](https://doingwork.work/docs/reference/shops-setinvoicereminders/) (agent)
- [Set the phone and email customers see when they open a canceled invoice or quote link (owner only)](https://doingwork.work/docs/reference/shops-setcontact/) (agent)
- [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)](https://doingwork.work/docs/reference/shops-setfieldcosts/) (agent)
- [Turn the customer hub link in quote and invoice emails on or off for the whole shop (owner only)](https://doingwork.work/docs/reference/shops-sethublinkinemails/) (agent)
- [Turn the automatic unpaid reminder on or off for one invoice (owner or office)](https://doingwork.work/docs/reference/invoices-setreminder/) (agent)
- [Customer declines a quote or asks for changes (hosted page)](https://doingwork.work/docs/reference/quotes-respond/) (hosted page)
- [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](https://doingwork.work/docs/reference/quotes-activity/) (agent)
- [Set how many days after a quote is sent the customer gets one friendly follow-up email if they have not answered (owner only)](https://doingwork.work/docs/reference/shops-setquotefollowup/) (agent)
- [Create an invoice without a quote, from lines the worker wrote (never filled in by AI)](https://doingwork.work/docs/reference/invoices-create/) (agent)
- [Create many invoices at once (up to 100), each from a job and the lines the worker wrote](https://doingwork.work/docs/reference/invoices-createbatch/) (agent)
- [One invoice with its lines, what has been paid, the balance, the due date, and whether a bank payment is still clearing](https://doingwork.work/docs/reference/invoices-get/) (agent)
- [The invoice as a PDF (base64), with shop, customer, lines, paid so far and balance](https://doingwork.work/docs/reference/invoices-pdf/) (agent)
- [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)](https://doingwork.work/docs/reference/invoices-aging/) (agent)
- [Turn tips on or off on this shop's invoice pay page (owner only)](https://doingwork.work/docs/reference/shops-settips/) (agent)
- [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)](https://doingwork.work/docs/reference/shops-setreviewrequest/) (agent)
- [Turn auto-pay on or off for a customer (owner or office)](https://doingwork.work/docs/reference/customers-setautopay/) (agent)
- [Choose how the sales tax report counts a period (owner only)](https://doingwork.work/docs/reference/shops-settaxreportbasis/) (agent)
- [Show an uploaded photo or file of the same job with a quote, with an optional caption](https://doingwork.work/docs/reference/quotes-addattachment/) (agent)
- [Take a photo or file off a quote](https://doingwork.work/docs/reference/quotes-removeattachment/) (agent)
- [Photos and files shown with a quote, in order](https://doingwork.work/docs/reference/quotes-listattachments/) (agent)
- [Add or remove tags on a photo (for example: leak, panel, permit)](https://doingwork.work/docs/reference/media-tag/) (agent)
- [Find photos across the shop that carry all the given tags](https://doingwork.work/docs/reference/media-search/) (agent)
- [Jobs in the shop that carry all the given tags (set with jobs.setTags), newest first, up to 200](https://doingwork.work/docs/reference/jobs-findbytag/) (agent)
- [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)](https://doingwork.work/docs/reference/media-savemarkup/) (agent)
- [A photo's markup: the latest version unless one is named, with an SVG overlay to lay over the original photo](https://doingwork.work/docs/reference/media-getmarkup/) (agent)
- [List deliveries to one webhook endpoint, newest first, with event, status, attempts and last HTTP status](https://doingwork.work/docs/reference/webhooks-deliveries/) (agent)
- [Send a failed or dead delivery again from attempt 0 (owner or office)](https://doingwork.work/docs/reference/webhooks-retry/) (agent)
- [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](https://doingwork.work/docs/reference/shops-export/) (agent)
- [What each current worker in a shop may do beyond their role: see_prices, see_costs, take_money, delete and manage_schedule](https://doingwork.work/docs/reference/members-permissions/) (agent)
- [Owner only, not an agent: turn a worker's permissions on or off](https://doingwork.work/docs/reference/members-setpermissions/) (person only)
- [Mark a job done (owner or office)](https://doingwork.work/docs/reference/jobs-complete/) (agent)
- [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](https://doingwork.work/docs/reference/customers-audience/) (agent)
- [Start handing the shop to a current worker (owner, on a hosted page)](https://doingwork.work/docs/reference/shops-transferstart/) (hosted page)
- [Accept a hand-off and become the shop's owner (the person asked, on a hosted page)](https://doingwork.work/docs/reference/shops-transferaccept/) (hosted page)
- [Stop an open hand-off: the owner cancels it, or the person asked declines it](https://doingwork.work/docs/reference/shops-transfercancel/) (agent)
- [Hand-offs of a shop, newest first](https://doingwork.work/docs/reference/shops-transfers/) (agent)
- [Whether the shop can close now (owner only)](https://doingwork.work/docs/reference/shops-closecheck/) (agent)
- [Close the shop for good (owner, on a hosted page, typing the shop name)](https://doingwork.work/docs/reference/shops-close/) (hosted page)
- [Reopen a closed shop (only the owner who closed it, on a hosted page, within 90 days of closing)](https://doingwork.work/docs/reference/shops-reopen/) (hosted page)
- [Void an invoice nobody has paid yet (owner or office), with a reason](https://doingwork.work/docs/reference/invoices-void/) (agent)

### Team (unlimited workers)

- [Invite a person by email as office or field](https://doingwork.work/docs/reference/members-invite/) (agent)
- [Accept an invite as the signed-in person](https://doingwork.work/docs/reference/members-accept/) (agent)
- [Current members of a shop with role and start date](https://doingwork.work/docs/reference/members-list/) (agent)
- [End a membership: the owner removes a worker, or a worker leaves](https://doingwork.work/docs/reference/members-end/) (agent)
- [Owner changes a worker between office and field](https://doingwork.work/docs/reference/members-changerole/) (agent)
- [Owner only, not an agent: require owner approval before an agent can run an action](https://doingwork.work/docs/reference/approvals-setrule/) (person only)
- [Owner only, not an agent: stop requiring approval for a rule](https://doingwork.work/docs/reference/approvals-removerule/) (person only)
- [The shop's approval rules: which agent actions wait for the owner](https://doingwork.work/docs/reference/approvals-listrules/) (agent)
- [Approval requests for a shop, newest first, optionally by status (pending, approved, denied, failed, expired)](https://doingwork.work/docs/reference/approvals-list/) (agent)
- [One approval request with its status and, once approved and run, the action's result](https://doingwork.work/docs/reference/approvals-get/) (agent)
- [Owner only, not an agent: approve or deny a pending agent action](https://doingwork.work/docs/reference/approvals-decide/) (person only)

### Customers

- [Add a customer to a shop, with kind, company, email, phone, billing address, lead source, tags and notes in one call](https://doingwork.work/docs/reference/customers-create/) (agent)
- [Add a service address to a customer, with optional label and access notes](https://doingwork.work/docs/reference/properties-add/) (agent)
- [Service addresses of a customer, or every property of a shop](https://doingwork.work/docs/reference/properties-list/) (agent)
- [Change a property's label or access notes, or archive it](https://doingwork.work/docs/reference/properties-update/) (agent)
- [Record equipment at a property: kind, make, model, serial, install date and warranty](https://doingwork.work/docs/reference/equipment-add/) (agent)
- [Equipment at a property, or warranties ending soon across a shop](https://doingwork.work/docs/reference/equipment-list/) (agent)
- [Change or archive an equipment record](https://doingwork.work/docs/reference/equipment-update/) (agent)

### Jobs (unlimited)

- [Start a job at a home (the home is found or created from the address)](https://doingwork.work/docs/reference/jobs-create/) (agent)
- [Jobs of a shop, newest first](https://doingwork.work/docs/reference/jobs-list/) (agent)
- [One job with its home address](https://doingwork.work/docs/reference/jobs-get/) (agent)

### Job photos

- [Get a signed upload URL for a job photo](https://doingwork.work/docs/reference/media-requestupload/) (agent)
- [Hosted photo capture: a signed upload URL for a worker taking a photo in the browser](https://doingwork.work/docs/reference/media-requestcapture/) (hosted page)
- [List the photos on a job that you are allowed to see](https://doingwork.work/docs/reference/media-list/) (agent)
- [One photo's details](https://doingwork.work/docs/reference/media-get/) (agent)
- [Make a link to a gallery of a job's photos that anyone with the link can open without signing in](https://doingwork.work/docs/reference/shares-create/) (agent)
- [Photo share links on a job, newest first, with whether each is still active and how often it was opened](https://doingwork.work/docs/reference/shares-list/) (agent)
- [Turn a photo share link off now](https://doingwork.work/docs/reference/shares-revoke/) (agent)

### Quotes (unlimited)

- [Create a quote from lines the worker wrote](https://doingwork.work/docs/reference/quotes-create/) (agent)
- [Lock the quote and get a single-use signing link for the customer](https://doingwork.work/docs/reference/quotes-send/) (agent)
- [Sign a quote after accepting electronic signing](https://doingwork.work/docs/reference/quotes-sign/) (hosted page)
- [Replace an unsigned quote with a new version](https://doingwork.work/docs/reference/quotes-revise/) (agent)
- [Add a price book item, or change one when item_id is given (owner or office)](https://doingwork.work/docs/reference/pricebook-saveitem/) (agent)
- [List the shop's price book items, by name](https://doingwork.work/docs/reference/pricebook-list/) (agent)
- [Archive a price book item, or bring it back with archived false (owner or office)](https://doingwork.work/docs/reference/pricebook-archiveitem/) (agent)

### Invoices and payments (unlimited)

- [Create an invoice for a signed quote, for the total of the option and optional lines the customer picked](https://doingwork.work/docs/reference/invoices-createfromquote/) (agent)
- [Send an invoice (or the next part of it) and get a single-use pay link](https://doingwork.work/docs/reference/invoices-send/) (agent)
- [Start paying an invoice (hosted page)](https://doingwork.work/docs/reference/invoices-pay/) (hosted page)
- [Record that the customer paid an invoice outside Doing Work (check, cash, financing, insurance or other)](https://doingwork.work/docs/reference/invoices-markpaidoutside/) (person only)
- [Refund part or all of a payment (owner or office)](https://doingwork.work/docs/reference/invoices-refund/) (person only)
- [Add or change a sales tax rate the shop charges in one jurisdiction (owner or office)](https://doingwork.work/docs/reference/taxes-saverate/) (agent)
- [The shop's tax rates](https://doingwork.work/docs/reference/taxes-listrates/) (agent)
- [Stop using a tax rate (owner or office)](https://doingwork.work/docs/reference/taxes-archiverate/) (agent)
- [Sales tax report for a period (owner or office), shop local dates, from and to inclusive](https://doingwork.work/docs/reference/taxes-salesreport/) (agent)

### Stripe connection

- [Start payment setup for a shop (owner, hosted page)](https://doingwork.work/docs/reference/payments-connectstart/) (hosted page)
- [Get a secure page where the customer saves a card with the shop's payment processor (owner or office)](https://doingwork.work/docs/reference/cards-setuplink/) (agent)
- [Open the card saving page from a save-your-card-again email link (hosted page only)](https://doingwork.work/docs/reference/cards-resave/) (hosted page)
- [The card a customer has on file (brand, last 4, expiry) and whether auto-pay is on (owner or office)](https://doingwork.work/docs/reference/cards-get/) (agent)
- [Remove a customer's saved card (owner or office)](https://doingwork.work/docs/reference/cards-remove/) (agent)

### Idaho compliance

- [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](https://doingwork.work/docs/reference/subcontractors-get/) (agent)
- [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"](https://doingwork.work/docs/reference/subcontractors-prepare/) (agent)
- [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](https://doingwork.work/docs/reference/subcontractors-sign/) (hosted page)

### Signed job records

- [Get a single-use link the customer opens to confirm the work was done (or flag a problem)](https://doingwork.work/docs/reference/jobs-requestconfirmation/) (agent)
- [Customer confirms the work or flags a problem (hosted page)](https://doingwork.work/docs/reference/jobs-confirm/) (hosted page)
- [Public keys that verify signed job records](https://doingwork.work/docs/reference/records-keys/) (agent)
- [The latest signed record for a job and its level (logged, paid, verified)](https://doingwork.work/docs/reference/jobs-record/) (agent)
- [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](https://doingwork.work/docs/reference/documents-photoreport/) (agent)
- [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](https://doingwork.work/docs/reference/documents-dailylog/) (agent)
- [PDF documents core made for a job (photo reports, daily logs and submitted forms), newest first, with each file's sha256](https://doingwork.work/docs/reference/documents-list/) (agent)
- [One stored job PDF (base64) with its sha256, byte for byte as it was made](https://doingwork.work/docs/reference/documents-get/) (agent)

### Plans and fees

- [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)](https://doingwork.work/docs/reference/fees-statement/) (agent)

### Developers and platforms

- [Add a webhook endpoint for a shop (owner or office)](https://doingwork.work/docs/reference/webhooks-create/) (person only)
- [List a shop's webhook endpoints and their recent delivery counts](https://doingwork.work/docs/reference/webhooks-list/) (agent)
- [Remove a webhook endpoint](https://doingwork.work/docs/reference/webhooks-delete/) (agent)
- [Create a developer platform account (private beta)](https://doingwork.work/docs/reference/platforms-create/) (agent)
- [List the platform accounts you own and the shops each one manages](https://doingwork.work/docs/reference/platforms-list/) (agent)
- [Let a platform manage this shop](https://doingwork.work/docs/reference/platforms-grantmanage/) (hosted page)
- [Stop a platform from managing this shop](https://doingwork.work/docs/reference/platforms-revokemanage/) (agent)
- [Every event a webhook can receive, with its payload keys and types](https://doingwork.work/docs/reference/events-catalog/) (agent)

### Scheduling and visits

- [Book a repeating visit (daily, weekly or monthly, every n) for a job](https://doingwork.work/docs/reference/visits-createrecurring/) (agent)
- [Cancel the visits of a repeating series that have not started yet, from a time onward (default now)](https://doingwork.work/docs/reference/visits-cancelseries/) (agent)
- [Mark a visit started (from scheduled, on the way, or arrived)](https://doingwork.work/docs/reference/visits-start/) (agent)
- [Mark a visit done, with an optional note](https://doingwork.work/docs/reference/visits-done/) (agent)
- [Book a visit to a job at a time, optionally with the workers going](https://doingwork.work/docs/reference/visits-create/) (agent)
- [Move a visit or change its title, notes or arrival window](https://doingwork.work/docs/reference/visits-update/) (agent)
- [Set who is going on a visit](https://doingwork.work/docs/reference/visits-assign/) (agent)
- [Cancel a visit that is scheduled, on the way, or arrived](https://doingwork.work/docs/reference/visits-cancel/) (agent)
- [One visit with its job and workers](https://doingwork.work/docs/reference/visits-get/) (agent)
- [A shop's visits that overlap a time window, earliest first](https://doingwork.work/docs/reference/schedule-list/) (agent)
- [Set a worker's weekly hours, replacing what was there](https://doingwork.work/docs/reference/availability-sethours/) (agent)
- [Add time off for a worker (vacation, sick, appointment)](https://doingwork.work/docs/reference/availability-addtimeoff/) (agent)
- [Remove a time-off entry](https://doingwork.work/docs/reference/availability-removetimeoff/) (agent)
- [A worker's weekly hours, time off, and booked visits in a window (at most 62 days)](https://doingwork.work/docs/reference/availability-get/) (agent)
- [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](https://doingwork.work/docs/reference/schedule-settings/) (agent)
- [Set the shop's booking hours, replacing what was there (owner or office)](https://doingwork.work/docs/reference/schedule-setshophours/) (agent)
- [Change the shop's scheduling rules (owner or office; the time zone is owner only)](https://doingwork.work/docs/reference/schedule-setrules/) (agent)
- [Find open times for a visit of a given length](https://doingwork.work/docs/reference/schedule-findtime/) (agent)
- [Mark that the worker is on the way to a visit, with an optional ETA (eta_at, or eta_minutes from now)](https://doingwork.work/docs/reference/visits-ontheway/) (agent)
- [Mark that the worker arrived at a visit](https://doingwork.work/docs/reference/visits-arrived/) (agent)
- [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](https://doingwork.work/docs/reference/visits-notices/) (agent)
- [Start a maintenance plan for a job's customer (owner or office)](https://doingwork.work/docs/reference/maintenance-createplan/) (agent)
- [One maintenance plan: price per term, current term, renewal date, and each term's invoice](https://doingwork.work/docs/reference/maintenance-getplan/) (agent)
- [A shop's maintenance plans, optionally for one customer or by status, soonest renewal first](https://doingwork.work/docs/reference/maintenance-listplans/) (agent)
- [Replace a plan's priced lines (owner or office; you write them, never AI), and optionally its tax rate](https://doingwork.work/docs/reference/maintenance-setprice/) (agent)
- [Cancel a maintenance plan (owner or office)](https://doingwork.work/docs/reference/maintenance-cancelplan/) (agent)
- [Customers' requests to move or cancel a visit, from the customer hub, newest first](https://doingwork.work/docs/reference/visits-changerequests/) (agent)
- [Answer a customer's request to move or cancel a visit](https://doingwork.work/docs/reference/visits-answerchange/) (agent)

### Field work (checklists, forms, time, notes, change orders, subcontractors)

- [Write a daily log entry on a job: what happened on site that day, in the worker's own words](https://doingwork.work/docs/reference/logs-add/) (agent)
- [Daily log entries on a job, oldest first, optionally between two dates (inclusive)](https://doingwork.work/docs/reference/logs-list/) (agent)
- [Take a daily log entry out of the log](https://doingwork.work/docs/reference/logs-remove/) (agent)
- [Ask someone to sign a document on a hosted page, and get the signing link to give them](https://doingwork.work/docs/reference/signatures-request/) (agent)
- [A new signing link for an open signature request](https://doingwork.work/docs/reference/signatures-newlink/) (agent)
- [Cancel an open signature request](https://doingwork.work/docs/reference/signatures-cancel/) (agent)
- [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)](https://doingwork.work/docs/reference/signatures-get/) (agent)
- [Signature requests for a job, a customer or a whole shop, newest first, with status and who signed](https://doingwork.work/docs/reference/signatures-list/) (agent)
- [Sign a document after reading it, typing your name and agreeing to sign electronically](https://doingwork.work/docs/reference/signatures-sign/) (hosted page)
- [Create a checklist template, or replace one (give checklist_template_id)](https://doingwork.work/docs/reference/checklists-savetemplate/) (agent)
- [The shop's checklist templates with their items](https://doingwork.work/docs/reference/checklists-listtemplates/) (agent)
- [Archive a checklist template so it is no longer offered](https://doingwork.work/docs/reference/checklists-archivetemplate/) (agent)
- [Put a checklist on a job, or on one visit of the job (visit_id)](https://doingwork.work/docs/reference/checklists-create/) (agent)
- [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](https://doingwork.work/docs/reference/checklists-list/) (agent)
- [One checklist with every item (removed items included and marked), who ticked what and when, and its sign-off](https://doingwork.work/docs/reference/checklists-get/) (agent)
- [Tick (done true) or untick an item, with an optional note](https://doingwork.work/docs/reference/checklists-checkitem/) (agent)
- [Add items to the end of a checklist (owner or office)](https://doingwork.work/docs/reference/checklists-additems/) (agent)
- [Remove an item from a checklist (it stays in the history, marked removed)](https://doingwork.work/docs/reference/checklists-removeitem/) (agent)
- [Ask for a sign-off on a finished checklist and get the signing link](https://doingwork.work/docs/reference/checklists-requestsignoff/) (agent)
- [Create a form template (an inspection, a safety sheet, a start-up report), or replace one (give form_template_id)](https://doingwork.work/docs/reference/forms-savetemplate/) (agent)
- [The shop's form templates with their fields](https://doingwork.work/docs/reference/forms-listtemplates/) (agent)
- [Archive a form template so it is no longer offered](https://doingwork.work/docs/reference/forms-archivetemplate/) (agent)
- [Start a form on a job, or on one visit of the job (visit_id)](https://doingwork.work/docs/reference/forms-start/) (agent)
- [Save answers on a draft form](https://doingwork.work/docs/reference/forms-fill/) (agent)
- [Submit a draft form: every required field must be answered (forms.get lists what is missing)](https://doingwork.work/docs/reference/forms-submit/) (agent)
- [Cancel a draft form (kept in history)](https://doingwork.work/docs/reference/forms-cancel/) (agent)
- [One form with its fields, answers, the required fields still missing, its stored PDF id and hash once submitted, and its sign-off](https://doingwork.work/docs/reference/forms-get/) (agent)
- [Forms on a job (job-level and per visit), or on one visit, newest first, without fields and answers](https://doingwork.work/docs/reference/forms-list/) (agent)
- [Ask for a signature on a submitted form and get the signing link (owner or office)](https://doingwork.work/docs/reference/forms-requestsignoff/) (agent)
- [Write a note on a job, or on one visit of it (give visit_id; job_id is then optional)](https://doingwork.work/docs/reference/notes-add/) (agent)
- [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](https://doingwork.work/docs/reference/notes-addvoicememo/) (agent)
- [Change a note](https://doingwork.work/docs/reference/notes-update/) (agent)
- [Take a note off the job](https://doingwork.work/docs/reference/notes-remove/) (agent)
- [One note you may see, with every earlier wording (history, oldest first)](https://doingwork.work/docs/reference/notes-get/) (agent)
- [Notes you may see on a job (job-level and every visit) or on one visit: pinned first, then newest first](https://doingwork.work/docs/reference/notes-list/) (agent)
- [Write a change order on a job: a priced change to its scope, from lines a worker wrote (never filled in by AI)](https://doingwork.work/docs/reference/changes-create/) (agent)
- [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](https://doingwork.work/docs/reference/changes-list/) (agent)
- [One change order with its lines, totals, status, signature and invoice](https://doingwork.work/docs/reference/changes-get/) (agent)
- [Owner or office: ask the customer to approve a change order](https://doingwork.work/docs/reference/changes-send/) (agent)
- [Cancel a draft or sent change order (its open signature request is cancelled too, so the link stops working)](https://doingwork.work/docs/reference/changes-cancel/) (agent)
- [Bill approved change orders on a normal invoice (draft; send it with invoices.send)](https://doingwork.work/docs/reference/changes-invoice/) (agent)
- [Start a person's clock: on a job (work), or drive, shop or break time](https://doingwork.work/docs/reference/timesheets-clockin/) (agent)
- [Stop a person's running clock](https://doingwork.work/docs/reference/timesheets-clockout/) (agent)
- [A person's running clock in a shop, or null when they are not clocked in](https://doingwork.work/docs/reference/timesheets-current/) (agent)
- [Add time after the fact (a manual entry with a start and an end, at most 24 hours)](https://doingwork.work/docs/reference/timesheets-add/) (agent)
- [Fix a time entry that is not approved: its start, end, category, job, visit or notes](https://doingwork.work/docs/reference/timesheets-update/) (agent)
- [Take back a time entry that was never approved (kept in history, no longer counted)](https://doingwork.work/docs/reference/timesheets-void/) (agent)
- [Owner or office: approve or reject stopped time entries (up to 200 at once, one shop)](https://doingwork.work/docs/reference/timesheets-decide/) (agent)
- [Owner only: reopen approved time so it can be fixed (it goes back to waiting for approval)](https://doingwork.work/docs/reference/timesheets-reopen/) (agent)
- [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](https://doingwork.work/docs/reference/timesheets-list/) (agent)
- [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](https://doingwork.work/docs/reference/timesheets-setrate/) (agent)
- [Labor cost rates in force now: the shop default and each worker's own, plus upcoming ones](https://doingwork.work/docs/reference/timesheets-listrates/) (agent)
- [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)](https://doingwork.work/docs/reference/materials-add/) (agent)
- [Change a recorded material's quantity, visit or notes](https://doingwork.work/docs/reference/materials-update/) (agent)
- [Remove a recorded material (recorded in error or returned)](https://doingwork.work/docs/reference/materials-remove/) (agent)
- [Materials recorded on a job (or one visit), with the price and cost totals of those still counted](https://doingwork.work/docs/reference/materials-list/) (agent)
- [Add a subcontractor to a job: a company and the one email they will sign in with](https://doingwork.work/docs/reference/subs-add/) (agent)
- [Subcontractors on a job, or on every job in the shop: status invited, joined, expired or removed, when they last opened their page](https://doingwork.work/docs/reference/subs-list/) (agent)
- [Change a subcontractor's contact name, trade or scope note (what they see on their page)](https://doingwork.work/docs/reference/subs-update/) (agent)
- [Take a subcontractor off a job, for good: their link stops working at once](https://doingwork.work/docs/reference/subs-remove/) (agent)
- [Notes subcontractors left on a job, newest first](https://doingwork.work/docs/reference/subs-notes/) (agent)
- [The subcontractor's page for one job: the shop, job summary, work address, scope note, upcoming visit times and their notes](https://doingwork.work/docs/reference/subs-view/) (hosted page)
- [Leave a note for the shop on this job, as the subcontractor (up to 20 a day)](https://doingwork.work/docs/reference/subs-addnote/) (hosted page)

### Customer hub, requests and customer emails

- [Log a request for work from a customer or a new lead](https://doingwork.work/docs/reference/requests-create/) (agent)
- [Requests of a shop, newest first, filtered by stage, customer or tag](https://doingwork.work/docs/reference/requests-list/) (agent)
- [How many open requests a shop has in each stage, and how many were won or lost in the last 30 days](https://doingwork.work/docs/reference/requests-pipeline/) (agent)
- [One request with its customer and the job it became](https://doingwork.work/docs/reference/requests-get/) (agent)
- [Move a request to another stage or change its details, customer or property](https://doingwork.work/docs/reference/requests-update/) (agent)
- [Turn a request into a job at its property or address; the worker then writes the quote or books a visit](https://doingwork.work/docs/reference/requests-convert/) (agent)
- [Turn on the shop's public request form and get its key](https://doingwork.work/docs/reference/requests-formcreate/) (agent)
- [Send a shop a request through its public form](https://doingwork.work/docs/reference/requests-submitform/) (agent)
- [Save a quote template: worker-written lines (and good, better, best options) to start quotes from](https://doingwork.work/docs/reference/templates-save/) (agent)
- [List the shop's saved quote templates with their lines](https://doingwork.work/docs/reference/templates-list/) (agent)
- [Archive a quote template, or bring it back with archived false (owner or office)](https://doingwork.work/docs/reference/templates-archive/) (agent)
- [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](https://doingwork.work/docs/reference/messages-templates/) (agent)
- [Set the subject and body wording for one kind of automatic customer email](https://doingwork.work/docs/reference/messages-savetemplate/) (agent)
- [Go back to the built-in wording for one kind of automatic customer email](https://doingwork.work/docs/reference/messages-resettemplate/) (agent)
- [Show how one kind of automatic customer email reads, filled with sample customer details and the shop's name](https://doingwork.work/docs/reference/messages-previewtemplate/) (agent)
- [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](https://doingwork.work/docs/reference/hub-createlink/) (agent)
- [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](https://doingwork.work/docs/reference/hub-links/) (agent)
- [Stop a customer hub link working, for good](https://doingwork.work/docs/reference/hub-revokelink/) (agent)
- [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](https://doingwork.work/docs/reference/hub-view/) (hosted page)
- [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](https://doingwork.work/docs/reference/hub-open/) (hosted page)
- [From the customer hub, ask the shop to move (reschedule, with the times that work better) or cancel an upcoming visit](https://doingwork.work/docs/reference/hub-requestvisitchange/) (hosted page)
- [From the customer hub, take back your own open request to move or cancel a visit](https://doingwork.work/docs/reference/hub-withdrawvisitchange/) (hosted page)
- [From the customer hub, ask the shop for new work](https://doingwork.work/docs/reference/hub-requestwork/) (hosted page)

### Costs, expenses and reports

- [Tips customers paid, by invoice and in total, between two dates (owner or office)](https://doingwork.work/docs/reference/tips-report/) (agent)
- [Record money a worker spent on a job, with the receipt photo](https://doingwork.work/docs/reference/expenses-add/) (agent)
- [Change an expense: date, vendor, category, amount, tax, receipt photo or note](https://doingwork.work/docs/reference/expenses-update/) (agent)
- [Take an expense off the books (kept in history)](https://doingwork.work/docs/reference/expenses-remove/) (agent)
- [Expenses on a job, or across the shop between dates, with totals by category](https://doingwork.work/docs/reference/expenses-list/) (agent)
- [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](https://doingwork.work/docs/reference/reports-revenue/) (agent)
- [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](https://doingwork.work/docs/reference/reports-owed/) (agent)
- [Of the quotes made between two dates (inclusive, UTC) and sent, how many were signed, declined, or are still waiting](https://doingwork.work/docs/reference/reports-quotewinrate/) (agent)
- [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](https://doingwork.work/docs/reference/costs-job/) (agent)
- [Job costing across jobs dated between two days (inclusive, in the shop's time zone), with totals](https://doingwork.work/docs/reference/costs-jobs/) (agent)
- [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](https://doingwork.work/docs/reference/reports-today/) (agent)
- [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](https://doingwork.work/docs/reference/reports-week/) (agent)
- [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](https://doingwork.work/docs/reference/reports-save/) (agent)
- [The shop's saved reports, by name](https://doingwork.work/docs/reference/reports-listsaved/) (agent)
- [Remove a saved report (kept for the record; its name is free to use again)](https://doingwork.work/docs/reference/reports-removesaved/) (agent)
- [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](https://doingwork.work/docs/reference/reports-run/) (agent)

### Imports, custom fields, tags and search

- [Add a custom field to customers, properties, jobs, quotes or invoices for a shop](https://doingwork.work/docs/reference/fields-define/) (agent)
- [Custom fields a shop has defined, optionally for one kind of record](https://doingwork.work/docs/reference/fields-list/) (agent)
- [Set one custom field value on a customer, property, job, quote or invoice](https://doingwork.work/docs/reference/fields-set/) (agent)
- [Custom field values on one record, with any required fields still empty](https://doingwork.work/docs/reference/fields-get/) (agent)
- [Every tag used in the shop, with how many photos and jobs carry it](https://doingwork.work/docs/reference/tags-list/) (agent)
- [Search one shop for customers (name, email, phone), properties (address), jobs (summary) and requests (title)](https://doingwork.work/docs/reference/search-global/) (agent)
- [The customer import fields and the built-in presets (jobber, housecall_pro, companycam) with the export column each reads](https://doingwork.work/docs/reference/imports-fields/) (agent)
- [Dry run of a customer CSV import](https://doingwork.work/docs/reference/imports-preview/) (agent)
- [Owner only: import customers (and one service address each) from a CSV with the mapping the owner confirmed](https://doingwork.work/docs/reference/imports-run/) (agent)
- [Run one safe action on up to 200 records: customers.addTags, customers.removeTags, customers.archive, customers.unarchive, jobs.addTags, jobs.removeTags, visits.assign](https://doingwork.work/docs/reference/bulk-run/) (agent)

## Coming soon

Not available yet, so agents shouldn't offer these until they appear in the [API reference](https://doingwork.work/docs/reference/). Text messages aren't part of launch either; customer notices go by email.

### Integrations

- QuickBooks Online sync
- Google Ads conversion uploads


---

# You only pay when you get paid

No plans and no monthly fee. Deposits count as getting paid. Every shop gets every feature, with unlimited workers, jobs, quotes and invoices. Our share of each payment drops as your month grows.

- No plans, no monthly fee and no card to enter. Every shop gets every feature, with unlimited workers, jobs, customers, quotes and invoices.
- Doing Work takes a fee out of each card or bank (ACH) payment, set by the shop's total for the calendar month: 1% of the first $5,000, 0.25% from $5,000 to $40,000, and 0.04% above $40,000, with no cap. The shop only pays when it gets paid, and deposits count.
- Each rate applies only to the dollars in its band. A payment that crosses a band is split across the two rates. Cards and bank payments cost the same, with no cap per payment.
- Locations under one owner count their payments together. The month starts over on the 1st.
- Fees come out of the shop's customer payments before payout. On the 1st each shop gets a fee statement and how far it is from the next band.
- A refund returns our fee on the refunded part at the rate it was charged. Jobs paid outside Doing Work are unlimited and cost nothing.
- Heavy AI use is metered the same way for every shop.
- Requests and quotes are free for shops and homeowners. We will never charge lead or referral fees.
- Stripe's processing fee is separate from our fee.

Each rate applies only to the dollars in its band, counted per calendar month. Locations under one owner count together. You never enter a card with us; fees come out of your customers' payments. Stripe's processing fee is separate. Prices in US dollars.

## What you'd pay in a month

Type in a month of card and bank payments. Doing Work fees only; Stripe's processing fee is extra.

## What a month costs, side by side

Four example shops. These are our assumptions, not customer data. Card processing is left out because it's about the same everywhere: 2.9% plus 30 cents online.

| Example shop | Doing Work fee | Doing Work with texts and AI phone answering | Jobber Grow plus Receptionist | Housecall Pro | Jobber Grow, Receptionist and CompanyCam | Housecall Pro and CompanyCam |
|---|---|---|---|---|---|---|
| Solo, 1 worker, $8,000 a month | $57.50 | about $77 | $228 | $79 plus AI answering (price not published) | $307 | $158 plus AI answering |
| Crew of 5, $40,000 a month | $137.50 | about $214 | $328 | $189 plus AI answering (price not published) | $545 | $406 plus AI answering |
| Crew of 12, $120,000 a month | $169.50 | about $379 | $486 | $469 plus AI answering (price not published) | $941 | $924 plus AI answering |
| Crew of 25, $300,000 a month | $241.50 | about $667 | $818 | $924 plus AI answering (price not published) | $1,715 | $1,821 plus AI answering |

Assumes texts out and in of 300, 1,500, 4,000 and 8,000 a month and 100, 400, 1,200 and 2,500 minutes of AI phone answering, at Twilio and Grok Voice list prices. Texting and phone answering through your own Twilio are coming soon. Competitor prices are monthly list prices from [getjobber.com](https://www.getjobber.com/pricing/), [housecallpro.com](https://www.housecallpro.com/llm-info/) and [CompanyCam's plans page](https://help.companycam.com/en/articles/14477655-companycam-plans-explained), checked October 31, 2026. CompanyCam is Core $79 for 1 worker and Crew $149 plus $34 for each worker over 3. Housecall Pro sells AI answering separately and doesn't publish the price. Your own AI agent's subscription isn't counted in any column. Prices change, so check theirs before you decide.

## Questions

### Do you charge for leads?

No. Requests and quotes are free for shops and homeowners. We will never charge lead or referral fees. Our only fee is on payments, and Stripe's processing fee is separate from our fee.

### Do I enter a card with Doing Work?

No. Shops never enter payment details with us. Our fee comes out of your customers' card and bank payments before the payout reaches you. There's no monthly fee, so a month with no payments costs nothing.

### How does the fee drop as I grow?

We add up your card and bank payments for the calendar month. The first $5,000 is at 1%, the next $35,000 at 0.25% and anything above $40,000 at 0.04%. There is no cap. Each rate applies only to the dollars in its band, like tax brackets. A payment that crosses a band is split across the two rates.

### When does the month start over?

On the 1st, in your shop's time zone. On the 1st you get a fee statement for the month before, and Doing Work tells you how far you are from the next band.

### I have more than one location. Do they count together?

Yes. Locations under the same owner add their payments together for the month, so each one gets the lower rates sooner.

### What happens to the fee if I refund a customer?

We give back our fee on the refunded part, at the rate it was charged. A refund in the same month also lowers that month's total. A refund in a later month doesn't reopen the earlier month. Lost disputes work the same way.

### What does Stripe charge?

Stripe's processing fee is separate and comes out of each payment on your Stripe account. Our fee is on top of that.

### Do bank payments cost the same as cards?

Yes. Card and bank (ACH) payments count the same and pay the same rates, with no cap per payment.

### What counts as heavy AI use?

Normal use is included. Heavy AI use is metered the same way for every shop.

### Is anything limited?

No. Workers, jobs, customers, quotes and invoices are unlimited, and you can mark as many jobs paid outside Doing Work as you like, at no cost.

### Do I need my own Stripe account?

Yes. You connect a Stripe account, or make one, the first time you take a card or bank payment. Customers pay you directly.


---

# About Doing Work

Doing Work LLC is a small software company in Coeur d'Alene, Idaho. We build field service software for home service shops, made so an AI agent can run it.

Most of running a shop is paperwork: the job, the photos, the quote, the invoice, chasing the payment. An agent can do that if the software lets it. So every action in Doing Work is open to agents, except the few that need a person's signature or card.

We charge a share of card and bank payments that drops as a shop grows, with no monthly fee. We don't sell your data or your customers' data.

[Get in touch](https://doingwork.work/contact/)


---

# Contact us

Phone: [(208) 274-3273](tel:+12082743273)

Email: [contact@doingwork.work](mailto:contact@doingwork.work)

Mail: Doing Work LLC, 212 W Ironwood Dr Ste D PMB 466, Coeur d'Alene, ID 83814


---

# Consent to Electronic Signatures and Records

Effective October 31, 2026. Version id: esign-2026-10-31.

Doing Work lets a business ("the shop") send you quotes, change orders, documents, invoices and job records, and lets you sign them online. Before you sign, please read this. A link to it appears next to the consent box. It applies each time you check the box that says you agree to sign and get the document electronically.

## What you agree to

By checking that box and typing your name, you agree that:

- you are signing this document electronically, and your electronic signature has the same effect as signing it on paper;
- the shop and Doing Work LLC may give you this document, and the signed copy, electronically instead of on paper.

This consent covers only the document you are signing on that page. A new document will ask you again.

## How signing works

You sign on a page Doing Work hosts, after you sign in with your own email. You type your full name and check the consent box. Doing Work records what you signed, the exact text, your name, your sign-in and the time. No one at the shop, and no software acting for the shop, can sign as you. Each signature is recorded with the email of the person who signed in. For a change order, only the person whose email the shop entered for you can sign. A quote, and some other documents, can be signed by anyone who is signed in and opens the link, so keep your link private.

## Paper copies and not signing

You don't have to sign electronically. If you would rather sign on paper, or want a paper copy of anything you signed, ask the shop before or after you sign. Paper copies are free. Neither the shop nor Doing Work charges you for a paper copy or for choosing paper.

## Withdrawing consent

You can withdraw this consent at any time by telling the shop, or by emailing contact@doingwork.work and we will tell the shop. Withdrawing doesn't undo a document you already signed. It means the shop must use paper for future documents that need your signature, unless you agree again.

## What you need

A device with a current web browser, an internet connection, and access to the email account you sign in with. To keep a copy, you need to be able to save or print a web page or a PDF. If these needs change in a way that could stop you from opening or keeping your records, the shop or Doing Work will tell you, and you can withdraw consent at no cost.

## Keeping your copy

After you sign, the page shows the signed document. Save or print it now. You can view it again from your link while the shop's account is open, and in the shop's customer page if the shop gave you one. If you need a copy later, ask the shop, or email contact@doingwork.work and we will help.

## Updating your email

If your email changes, tell the shop so it can send future documents to the right address.

## Contact

The shop that sent you the document is your first contact. You can also reach Doing Work LLC at contact@doingwork.work, (208) 274-3273, or 212 W Ironwood Dr Ste D PMB 466, Coeur d'Alene, ID 83814.


---

# Agent key terms

Effective October 31, 2026. Version id: agent-keys-2026-10-31b.

These are the agent key terms from section 8 of the Doing Work [Terms of Service](https://doingwork.work/terms/). The owner accepts them each time they make a named agent key or approve an agent claim.

- **You are responsible for your agent.** Your shop is responsible for every agent that works in it, however the agent got access. Anything an agent does with a key, a token or an approval counts as done by your shop, as if the person who approved it did it. Only give access to an agent you trust. We are not responsible for an agent's choices, mistakes or outputs, even when they follow your instructions.
- **A key never does more than the owner can.** It works in one shop. The owner can narrow it with scopes: read_only makes no changes, and no_money cannot take money actions: taking or refunding a payment, recording a payment taken outside Doing Work, voiding an invoice, turning auto-pay on or off, saving or removing a customer's card, connecting Stripe, setting up or changing maintenance plans, or turning tips on or off. A narrowed key also cannot manage keys. A scope only narrows a key. It never adds to what the owner can do.
- **Approvals still apply.** Money actions and other writes go through the same approval rules and role limits your shop sets for workers. A key cannot do steps that need a person, such as signing, paying, connecting Stripe, accepting these terms, approving a claim, handing off or closing a shop, refunds, recording outside payments or adding webhooks.
- **Revoking.** The owner can revoke a key at any time in Doing Work, or ask us to. It stops working at once and cannot be turned back on. An owner's keys also stop when that person stops being the owner, including on a hand-off or when the shop closes.
- **Connected agents.** Any worker can also connect an outside agent through OAuth. It acts as that worker, with that worker's role and limits. The owner can see every connected agent and every key in the shop, and can revoke any of them at any time. A revoked connection stops at once.
- **Register, then claim.** An agent that registers itself can do nothing in a shop until the owner signs in on our page, types the agent's 6 digit code, picks the shop and accepts these terms.
- **We log use.** We keep a record of each key's use, including when it was last used and what it changed.
- **Keep keys secret.** Treat a key like a password. Until you revoke it, actions with a leaked key count as yours, unless the key leaked because of a security failure on our side.
- **We may limit or stop a key** used in a way that breaks the Terms, harms the service or risks other shops' data.
- **These terms can change.** The owner must accept a new version before making another key or approving another claim.

Questions: contact@doingwork.work, (208) 274-3273, or 212 W Ironwood Dr Ste D PMB 466, Coeur d'Alene, ID 83814.


---

# Doing Work docs for AI agents

Doing Work is field service software for home service shops: plumbers, electricians, HVAC, roofers, handymen and the rest. It's built for an AI agent to run on the shop's behalf. These docs tell you what you can do, when to do it, and how to explain it to the people you work for.

> REST is at https://doingwork.work/v1, MCP at https://doingwork.work/mcp and A2A at https://doingwork.work/a2a, all with the same operations and permissions.

## Read these first

- [Connect an agent](https://doingwork.work/docs/quickstart/): discovery, OAuth with PKCE, MCP and REST.
- [Onboard a shop](https://doingwork.work/docs/onboarding/): from no account to the first invoice.
- [Job to paid](https://doingwork.work/docs/job-to-paid/): the core loop, step by step.
- [Guide the people](https://doingwork.work/docs/guiding-people/): how to talk to owners, workers and customers.
- [Consent and human steps](https://doingwork.work/docs/human-steps/): what you must never do yourself.
- [Conventions](https://doingwork.work/docs/conventions/), [errors](https://doingwork.work/docs/errors/), [webhooks](https://doingwork.work/docs/webhooks/), and the [API reference](https://doingwork.work/docs/reference/).

## Rules that always apply

- Workers write their own quote lines and prices. Write exactly what they tell you. Never invent, estimate or fill in a price.
- Signing, paying, connecting Stripe and granting platform access happen on hosted pages. Send the link; never try to do it yourself.
- Recording outside payments, refunds and adding webhooks need the person's own sign-in. Agent tokens are refused.
- Send an Idempotency-Key on every write so a retry never doubles anything.

## Machine-readable files

- [llms.txt](https://doingwork.work/llms.txt) and [llms-full.txt](https://doingwork.work/llms-full.txt)
- [OpenAPI 3.1](https://doingwork.work/openapi.json), generated from the core's operation list
- [MCP server card](https://doingwork.work/.well-known/mcp/server-card.json), [A2A agent card](https://doingwork.work/.well-known/agent-card.json) and [API catalog](https://doingwork.work/.well-known/api-catalog)
- [Agent guide](https://doingwork.work/agent-guide.md): every action grouped by area, generated by the core
- [Agent skills](https://doingwork.work/.well-known/agent-skills/index.json)
- [auth.md](https://doingwork.work/auth.md), [OAuth server metadata](https://doingwork.work/.well-known/oauth-authorization-server) and [protected resource metadata](https://doingwork.work/.well-known/oauth-protected-resource)
- Every page as Markdown: add `.md` to the path, or send `Accept: text/markdown`.


---

# Connect an agent

> REST is at https://doingwork.work/v1, MCP at https://doingwork.work/mcp and A2A at https://doingwork.work/a2a, all with the same operations and permissions.

## 1. Discover

Read `https://doingwork.work/.well-known/mcp/server-card.json` for the MCP endpoint, or `https://doingwork.work/openapi.json` for REST. The API sends a 401 with `WWW-Authenticate: Bearer resource_metadata=...` when you need to sign in.

## 2. Register your client

Dynamic client registration (RFC 7591). Public clients only, no secret. Redirect URIs must be https or loopback.

```http
POST https://doingwork.work/v1/oauth/register
content-type: application/json

{"client_name": "My shop assistant", "redirect_uris": ["http://127.0.0.1:8976/callback"]}
```

## 3. The person approves you

Send the person to the `authorization_endpoint` from [OAuth server metadata](https://doingwork.work/.well-known/oauth-authorization-server) with `client_id`, `redirect_uri`, `code_challenge` (S256), `state`, and `resource=https://doingwork.work/mcp`. They sign in with an email link and approve. You get a one-time code at your redirect.

## 4. Get a token

```http
POST https://doingwork.work/v1/oauth/token
content-type: application/json

{"grant_type": "authorization_code", "client_id": "...", "code": "...", "code_verifier": "...", "redirect_uri": "http://127.0.0.1:8976/callback", "resource": "https://doingwork.work/mcp"}
```

Access tokens are short-lived. Refresh tokens rotate: each works once, and reusing one revokes the whole chain. Store the new one every time.

## 5. Call tools

MCP: streamable HTTP at `https://doingwork.work/mcp`, `Authorization: Bearer <token>`. Tool names are operation ids with dots as underscores, for example `quotes_create`. Hosted-page and person-only operations are not listed as tools.

```json
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"jobs_list","arguments":{"shop_id":"..."},"_meta":{"idempotencyKey":"..."}}}
```

REST: the same operations under `https://doingwork.work/v1`. GET operations take query parameters; POST operations take a JSON body. See [conventions](https://doingwork.work/docs/conventions/).

## Other ways in

- **Named agent key.** The shop owner makes a key for you with their own sign-in ([keys.create](https://doingwork.work/docs/reference/keys-create/)). It starts with `dwk_`, is shown once, works in one shop, and can be narrowed to `read_only` or `no_money`. It works on REST, MCP and A2A.
- **Register and get claimed.** With no key yet, [agents.register](https://doingwork.work/docs/reference/agents-register/) gives you a claim token. [agents.claim](https://doingwork.work/docs/reference/agents-claim/) with the owner's email returns a 6 digit code the owner types on a hosted page. Poll the token endpoint until they approve. Details in [auth.md](https://doingwork.work/auth.md).
- **A2A.** `POST https://doingwork.work/a2a` (A2A 1.0 JSON-RPC) with a named agent key. Send `SendMessage` with one data part, `{"op": "<op id>", "input": {...}}`. There's no model behind it: the op runs exactly as on REST and MCP. A write the owner must approve comes back as an input required task. Card: [agent-card.json](https://doingwork.work/.well-known/agent-card.json).

## 6. Start with who you are

Call [me.get](https://doingwork.work/docs/reference/me-get/) to see the person and their shops. Then follow [Onboard a shop](https://doingwork.work/docs/onboarding/) or [Job to paid](https://doingwork.work/docs/job-to-paid/).


---

# Onboard a shop

Use this when the person you work for runs a home service business and doesn't have Doing Work yet. Ask one thing at a time and keep it short.

1. **Account.** Ask their email. Call [auth.requestLink](https://doingwork.work/docs/reference/auth-requestlink/). Tell them: "I sent a sign-in link to your email. Open it on this device." Then complete [Connect an agent](https://doingwork.work/docs/quickstart/) so they approve you.
2. **Shop.** Ask the business name and the state or province it works in. Call [shops.create](https://doingwork.work/docs/reference/shops-create/) with `jurisdiction` like `US-ID`. They're the owner.
3. **Team.** Ask if anyone else works with them. For each, ask the email and whether they're office or field. Call [members.invite](https://doingwork.work/docs/reference/members-invite/). Workers are unlimited for every shop, so never suggest leaving someone out to save money.
4. **Pricing.** There are no plans to pick. Doing Work takes 1% of the first $5,000 in card and bank payments each month, then 0.25% up to $40,000 and 0.04% above, with no monthly fee and no cap. The shop only pays when it gets paid. If they ask, read the rates from [fees.schedule](https://doingwork.work/docs/reference/fees-schedule/).
5. **First job.** Ask for the customer name and the job address. Call [customers.create](https://doingwork.work/docs/reference/customers-create/) then [jobs.create](https://doingwork.work/docs/reference/jobs-create/).
6. **Stripe.** Before the first card or bank payment, the owner connects their own Stripe account on a hosted page. Tell them: "To take card and bank payments, open this link and connect your Stripe account. Customers then pay you directly."
7. **Wrap up.** Tell them what's set up and what's next, in one or two sentences.

## Don't

- Don't make up a business name, address, price or email.
- Don't send anything to a customer until the owner says so.


---

# Job to paid

| Step | Operation | Who |
|---|---|---|
| Start the job | [jobs.create](https://doingwork.work/docs/reference/jobs-create/) | Agent |
| Add photos | [media.requestUpload](https://doingwork.work/docs/reference/media-requestupload/), then PUT the bytes | Agent |
| Write the quote from the worker's lines | [quotes.create](https://doingwork.work/docs/reference/quotes-create/) | Agent |
| Owner checks it, then send | [quotes.send](https://doingwork.work/docs/reference/quotes-send/) | Agent, after the owner says go |
| Customer signs | [quotes.sign](https://doingwork.work/docs/reference/quotes-sign/) | Customer, hosted page |
| Make the invoice | [invoices.createFromQuote](https://doingwork.work/docs/reference/invoices-createfromquote/) | Agent |
| Idaho jobs: subcontractor statement | [subcontractors.prepare](https://doingwork.work/docs/reference/subcontractors-prepare/), then the owner signs | Agent, then owner |
| Send the invoice | [invoices.send](https://doingwork.work/docs/reference/invoices-send/) | Agent |
| Customer pays by card or bank (ACH) | [invoices.pay](https://doingwork.work/docs/reference/invoices-pay/) | Customer, hosted page |
| Or paid by cash or check | [invoices.markPaidOutside](https://doingwork.work/docs/reference/invoices-markpaidoutside/) | Owner or office, own sign-in |
| Customer confirms the work | [jobs.requestConfirmation](https://doingwork.work/docs/reference/jobs-requestconfirmation/) | Agent sends the link |
| Show the signed record | [jobs.record](https://doingwork.work/docs/reference/jobs-record/) | Agent |

## Quote lines

Each line has a description, a quantity and a unit price in cents (`unit_price_minor`). Take these from the worker word for word. If a price is missing, ask. Read the total back before sending.

## Bank payments

A bank (ACH) payment takes a few business days to clear. While it clears, the invoice isn't paid yet, and paying again answers `payment_pending`. Tell the owner: "The customer paid by bank. It takes a few business days to clear." When it clears you get `invoice.paid`. If it fails you get `invoice.payment_failed`, the owner gets an email, and the same pay link works again.

## Waiting

Signing and paying happen when the customer gets to it. Subscribe to [webhooks](https://doingwork.work/docs/webhooks/) such as `quote.signed` and `invoice.paid`, or check with the read operations, then tell the owner.


---

# Guide the people

The people you work for are busy and often on a job site. Keep it short, plain and specific.

## With the owner

- Say what you did and what happens next, in one or two sentences.
- Ask before anything a customer will see: a sent quote, an invoice, a confirmation request.
- Read money back exactly: "2 lines, $1,945.00."
- When a step needs them, send the link and say what it's for and how long it takes.
- Use "worker" for people on the team.

## With workers

- Ask for their lines and prices. Never suggest a price.
- Field workers see what their role allows. If something is refused, say who can do it.

## With customers

Sending a quote or invoice returns a single-use link for the customer. Give it to the owner to send, or send it the way the owner tells you. Each link works once, and a newer one replaces it.

## Money and fees

- No plans, no monthly fee and no card to enter. Every shop gets every feature, with unlimited workers, jobs, customers, quotes and invoices.
- Doing Work takes a fee out of each card or bank (ACH) payment, set by the shop's total for the calendar month: 1% of the first $5,000, 0.25% from $5,000 to $40,000, and 0.04% above $40,000, with no cap. The shop only pays when it gets paid, and deposits count.
- Each rate applies only to the dollars in its band. A payment that crosses a band is split across the two rates. Cards and bank payments cost the same, with no cap per payment.
- Locations under one owner count their payments together. The month starts over on the 1st.
- Fees come out of the shop's customer payments before payout. On the 1st each shop gets a fee statement and how far it is from the next band.
- A refund returns our fee on the refunded part at the rate it was charged. Jobs paid outside Doing Work are unlimited and cost nothing.
- Heavy AI use is metered the same way for every shop.
- Requests and quotes are free for shops and homeowners. We will never charge lead or referral fees.
- Stripe's processing fee is separate from our fee.
- Use [fees.preview](https://doingwork.work/docs/reference/fees-preview/) to say what a payment would cost and how far the shop is from the next band. Mention the next band only when the owner asks about fees.

## What to say for each action

Every page in the [API reference](https://doingwork.work/docs/reference/) has a "What to tell the owner" line.


---

# Consent and human steps

Some actions need a person: a signature, a card, consent to renewal terms, or a decision about money. Doing Work enforces this. Hosted-page operations return `human_only` (403) on the API and aren't MCP tools. Person-only operations refuse agent tokens with `forbidden`.

| Action | Who does it | What you do |
|---|---|---|
| [auth.redeemLink](https://doingwork.work/docs/reference/auth-redeemlink/) | Hosted page only (a person does it in the browser) | Never call it yourself. The person's browser does this when they open the sign-in link. |
| [media.requestCapture](https://doingwork.work/docs/reference/media-requestcapture/) | Hosted page only (a person does it in the browser) | Never call it yourself. Used by the hosted camera page when a worker takes a photo in the browser. |
| [jobs.confirm](https://doingwork.work/docs/reference/jobs-confirm/) | Hosted page only (a person does it in the browser) | Never call it. The customer confirms or flags a problem on the hosted page. |
| [quotes.sign](https://doingwork.work/docs/reference/quotes-sign/) | Hosted page only (a person does it in the browser) | Never call it. The customer signs on the hosted page after agreeing to sign electronically. |
| [subcontractors.sign](https://doingwork.work/docs/reference/subcontractors-sign/) | Hosted page only (a person does it in the browser) | Never call it. The owner or office signs on the hosted page. |
| [payments.connectStart](https://doingwork.work/docs/reference/payments-connectstart/) | Hosted page only (a person does it in the browser) | Never call it. The owner connects their own Stripe account on the hosted page. |
| [invoices.pay](https://doingwork.work/docs/reference/invoices-pay/) | Hosted page only (a person does it in the browser) | Never call it. The customer pays on the hosted page. |
| [invoices.markPaidOutside](https://doingwork.work/docs/reference/invoices-markpaidoutside/) | A person's own sign-in only (agent tokens are refused) | Agents can't call this. Tell the owner or office to record a cash, check, financing or insurance payment with their own sign-in. |
| [invoices.refund](https://doingwork.work/docs/reference/invoices-refund/) | A person's own sign-in only (agent tokens are refused) | Agents can't call this. The owner or office refunds with their own sign-in. |
| [webhooks.create](https://doingwork.work/docs/reference/webhooks-create/) | A person's own sign-in only (agent tokens are refused) | Agents can't call this. A developer adds an endpoint with their own sign-in. |
| [platforms.grantManage](https://doingwork.work/docs/reference/platforms-grantmanage/) | Hosted page only (a person does it in the browser) | Never call it. The shop owner allows a platform on the hosted page. |
| [customers.privacyDelete](https://doingwork.work/docs/reference/customers-privacydelete/) | A person's own sign-in only (agent tokens are refused) | Erase a customer's personal details for a privacy request. Money and job records stay, unnamed. |
| [quotes.respond](https://doingwork.work/docs/reference/quotes-respond/) | Hosted page only (a person does it in the browser) | Customer declines a quote or asks for changes (hosted page). Asking for changes needs a note and puts the quote back to draft for the shop to revise. |
| [cards.resave](https://doingwork.work/docs/reference/cards-resave/) | Hosted page only (a person does it in the browser) | Open the card saving page from a save-your-card-again email link (hosted page only). Returns the processor URL where the customer saves the card on the shop's current payments account. |
| [keys.create](https://doingwork.work/docs/reference/keys-create/) | A person's own sign-in only (agent tokens are refused) | 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. |
| [agents.approve](https://doingwork.work/docs/reference/agents-approve/) | Hosted page only (a person does it in the browser) | 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. Makes a named agent key for that shop. |
| [agents.deny](https://doingwork.work/docs/reference/agents-deny/) | Hosted page only (a person does it in the browser) | Hosted claim page only: the person turns the agent down. The registration closes and the agent's poll gets access_denied. |
| [members.setPermissions](https://doingwork.work/docs/reference/members-setpermissions/) | A person's own sign-in only (agent tokens are refused) | Owner only, not an agent: turn a worker's permissions on or off. Give only the flags to change (see_prices, see_costs, take_money, delete, manage_schedule). |
| [legal.accept](https://doingwork.work/docs/reference/legal-accept/) | A person's own sign-in only (agent tokens are refused) | Record that the signed-in person accepts the current version of a legal document (clickwrap). The person does this themselves; an agent cannot accept for them. |
| [signatures.sign](https://doingwork.work/docs/reference/signatures-sign/) | Hosted page only (a person does it in the browser) | 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. |
| [shops.transferStart](https://doingwork.work/docs/reference/shops-transferstart/) | Hosted page only (a person does it in the browser) | Start handing the shop to a current worker (owner, on a hosted page). They accept on their own hosted page within 14 days, and ownership moves at once. |
| [shops.transferAccept](https://doingwork.work/docs/reference/shops-transferaccept/) | Hosted page only (a person does it in the browser) | Accept a hand-off and become the shop's owner (the person asked, on a hosted page). The person agrees to the Terms of Service by ticking a box (accept_terms_version); the record keeps the version, time, IP address and browser. |
| [shops.close](https://doingwork.work/docs/reference/shops-close/) | Hosted page only (a person does it in the browser) | Close the shop for good (owner, on a hosted page, typing the shop name). Everyone loses access, agent connections and webhooks stop, every customer link stops working, and the payments account only settles what is in flight. |
| [shops.reopen](https://doingwork.work/docs/reference/shops-reopen/) | Hosted page only (a person does it in the browser) | Reopen a closed shop (only the owner who closed it, on a hosted page, within 90 days of closing). You come back as owner. |
| [subs.view](https://doingwork.work/docs/reference/subs-view/) | Hosted page only (a person does it in the browser) | The subcontractor's page for one job: the shop, job summary, work address, scope note, upcoming visit times and their notes. state is ok, gone (removed, expired, job cancelled or shop closed) or wrong_person (signed in with a different email, or someone else opened it first). |
| [subs.addNote](https://doingwork.work/docs/reference/subs-addnote/) | Hosted page only (a person does it in the browser) | Leave a note for the shop on this job, as the subcontractor (up to 20 a day). Hosted page only. |
| [hub.view](https://doingwork.work/docs/reference/hub-view/) | Hosted page only (a person does it in the browser) | 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). |
| [hub.open](https://doingwork.work/docs/reference/hub-open/) | Hosted page only (a person does it in the browser) | 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). |
| [hub.requestVisitChange](https://doingwork.work/docs/reference/hub-requestvisitchange/) | Hosted page only (a person does it in the browser) | 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). |
| [hub.withdrawVisitChange](https://doingwork.work/docs/reference/hub-withdrawvisitchange/) | Hosted page only (a person does it in the browser) | 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). |
| [hub.requestWork](https://doingwork.work/docs/reference/hub-requestwork/) | Hosted page only (a person does it in the browser) | 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). |
| [approvals.setRule](https://doingwork.work/docs/reference/approvals-setrule/) | A person's own sign-in only (agent tokens are refused) | 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. |
| [approvals.removeRule](https://doingwork.work/docs/reference/approvals-removerule/) | A person's own sign-in only (agent tokens are refused) | 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. |
| [approvals.decide](https://doingwork.work/docs/reference/approvals-decide/) | A person's own sign-in only (agent tokens are refused) | 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. |

## Rules

- Never try another route around a refusal. Send the link or ask the person.
- Never tick a consent box, sign, or enter a card for someone.
- Say what the person is agreeing to before you send the link, for example the quote total and the e-sign consent.


---

# Conventions

- **Base**: `https://doingwork.work/v1` for REST, `https://doingwork.work/mcp` for MCP. REST is at https://doingwork.work/v1, MCP at https://doingwork.work/mcp and A2A at https://doingwork.work/a2a, all with the same operations and permissions.
- **Auth**: `Authorization: Bearer <token>` from [OAuth](https://doingwork.work/docs/quickstart/).
- **Version**: send `Doing-Work-Version: 2026-11-01`. Tokens are pinned to a version, and every response echoes the one used.
- **Idempotency**: send `Idempotency-Key` on every POST that writes (in MCP, `_meta.idempotencyKey`). A retry with the same key and body returns the first result. A different body gives `idempotency_key_reused`.
- **Money**: integers in cents, in fields ending `_minor`.
- **Pagination**: list operations take `limit` (1 to 200) and `cursor`, and return `next_cursor`.
- **Rate limits**: 429 `rate_limited` with `Retry-After` in seconds.
- **Errors**: JSON `{code, message, status}`. See [errors](https://doingwork.work/docs/errors/).
- **Unknown fields are rejected**, so a typo fails loudly instead of being ignored.
- **Events**: every write emits one event per resource it changes. See [webhooks](https://doingwork.work/docs/webhooks/).


---

# Errors

Errors are JSON: `{"code": "...", "message": "...", "status": 400}`. The message is for you; the code is stable.

| Code | Status | Meaning and what to do |
|---|---|---|
| `invalid_input` | 400 | A field is missing, the wrong type, or unknown. The message names the field. Fix the input; don't retry as is. |
| `disclosure_ack_required` | 400 | The customer must acknowledge the Idaho disclosure on the hosted signing page. |
| `terms_not_accepted` | 400 |  |
| `terms_not_published` | 503 |  |
| `consent_required` | 400 | A person must tick a consent box on a hosted page (for example e-sign). Send them the link. |
| `unsupported_version` | 400 | The Doing-Work-Version header isn't one the API knows. Use 2026-11-01. |
| `bad_signature` | 400 | A signature didn't verify. |
| `invalid_link` | 400 | The link was used, expired, or replaced by a newer one. Get a new link. |
| `invalid_request` | 400 | An OAuth request is malformed. |
| `invalid_grant` | 400 | The code, verifier or refresh token is wrong, used or expired. Start sign-in again. |
| `unsupported_grant_type` | 400 | Use authorization_code or refresh_token. |
| `invalid_target` | 400 | The resource parameter doesn't match this server. |
| `invalid_scope` | 400 | The requested scope isn't offered. |
| `invalid_client_metadata` | 400 | The client registration is invalid, for example a redirect URI that isn't https or loopback. |
| `invalid_claim_token` | 400 |  |
| `issuer_not_enabled` | 400 |  |
| `authorization_pending` | 400 |  |
| `slow_down` | 400 |  |
| `expired_token` | 400 |  |
| `access_denied` | 400 |  |
| `claimed_or_in_flight` | 409 |  |
| `claim_expired` | 410 |  |
| `invalid_token` | 401 | The token is missing, expired, or for a different resource. Refresh it or sign in again. |
| `session_required` | 403 | A person has to be signed in on a browser for this step. |
| `owner_cannot_leave` | 409 | The owner can't leave a shop. Ownership moves only by transfer. |
| `owner_role_fixed` | 409 | The owner's role can't be changed. |
| `unauthenticated` | 401 | No token was sent. Sign in first. |
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `human_only` | 403 | This step happens on a hosted page the person opens themselves. Send them the link; don't try another route. |
| `feature_unavailable` | 403 | This isn't available for this shop or place yet. |
| `private_beta` | 403 | This is invite only for now. Tell the person to email contact@doingwork.work. |
| `not_found` | 404 | No such record, or this worker can't see it. |
| `conflict` | 409 | The record is in the wrong state for this, for example sending a quote that's already signed. Read it again and pick the next step. |
| `checklist_open` | 409 |  |
| `payment_pending` | 409 | A bank (ACH) payment for this invoice is still clearing, which takes a few business days. Don't ask the customer to pay again. Wait for invoice.paid or invoice.payment_failed. |
| `payments_not_set_up` | 409 | The shop hasn't connected Stripe. The owner does that on a hosted page. |
| `payments_paused` | 409 |  |
| `idempotency_in_progress` | 409 | A request with this Idempotency-Key is still running. Wait and retry with the same key. |
| `idempotency_key_reused` | 422 | This Idempotency-Key was used with a different body. Use a new key for a new request. |
| `unprintable_text` | 422 |  |
| `document_too_large` | 422 |  |
| `rate_limited` | 429 | Too many requests. Wait for the Retry-After time, then retry. |
| `payload_too_large` | 413 |  |
| `internal` | 500 | Something broke on our side. Retry later with the same Idempotency-Key. |
| `not_supported` | 501 | Not supported yet. |


---

# Webhooks and events

A developer adds an endpoint with their own sign-in ([webhooks.create](https://doingwork.work/docs/reference/webhooks-create/)); the secret is shown once. Deliveries are POSTs with `DW-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of t.body>`. Any non-2xx is retried with backoff. Delivery is at least once, so dedupe on the event `id`.

| Event | What happened |
|---|---|
| `shop.created` | A shop was created. |
| `membership.invited` | A person was invited to a shop. |
| `membership.role_changed` | A worker's role in a shop changed. The old membership ended and a new one started. |
| `quote.revised` | An unsigned quote was replaced by a new version. |
| `quote.withdrawn` | The shop took back an unsigned quote; its signing links stopped working. |
| `charge.succeeded` | A payment succeeded. |
| `dispute.closed` | A payment dispute closed (won, lost or closed). |
| `plan.changed` | A shop changed plan. |
| `subcontractor_list.signed` | A shop signed the Idaho 45-525(3) subcontractor list for a job. |
| `subcontractor_list.prepared` | An agent prepared the Idaho 45-525(3) subcontractor list for a person to sign. |
| `plan.change_scheduled` | A shop scheduled a move down; it starts on the first day of effective_period (shop time). |
| `fee.owed_to_shop` | A fee that was credited against a plan price is owed back to the shop after a refund or lost dispute. |
| `plan.billed` | Not sent. Doing Work has no plans or monthly fee; kept so old subscriptions stay valid. |
| `membership.started` | A person joined a shop. |
| `membership.ended` | A person's membership in a shop ended. |
| `membership.permissions_changed` | The owner changed what a worker may do: see_prices, see_costs, take_money, delete and manage_schedule. |
| `ownership.transfer_started` | The owner started handing the shop to someone else. |
| `ownership.transferred` | Shop ownership moved to a new owner. |
| `ownership.transfer_cancelled` | An open hand-off was stopped: the owner cancelled it or the person asked declined. |
| `shop.closed` | The owner closed the shop. Everyone lost access and every link stopped; records are kept. |
| `job_subcontractor.added` | A subcontractor was added to a job. |
| `job_subcontractor.updated` | A subcontractor's contact name, trade or scope note changed. |
| `job_subcontractor.removed` | A subcontractor was taken off a job; their link stopped. |
| `job_subcontractor.joined` | A subcontractor opened their link for the first time, signed in. |
| `job_subcontractor.note_added` | A subcontractor left a note on a job. |
| `recovery.opened` | Someone asked to take over a shop whose owner cannot be reached. |
| `recovery.decided` | A recovery request was decided. |
| `home.claimed` | A person claimed a home. |
| `home_role.ended` | A person's role at a home ended. |
| `job.created` | A job was started at a home. |
| `job.completed` | A job was marked done with jobs.complete (W2). override_id is set when the owner overrode open checklist work. |
| `media.uploaded` | A photo was added to a job. |
| `media.tagged` | Tags on a photo changed. |
| `photo_share.created` | A share link for job photos was made. It expires on its own and can be revoked. |
| `photo_share.revoked` | A photo share link was turned off before it expired. |
| `daily_log.added` | A worker wrote a daily log entry on a job. |
| `daily_log.removed` | A daily log entry was taken out (it stays in history). |
| `job_document.created` | Core rendered a PDF for a job (photo report or daily log) and stored it with its sha256. |
| `signature.requested` | A shop asked someone to sign a document (contract text or a stored job PDF) on a hosted page. |
| `signature.link_issued` | A new signing link was made for an open signature request; earlier links stopped working. |
| `signature.cancelled` | An open signature request was cancelled. |
| `document.signed` | A person signed a document on a hosted page. The signature holds the document sha256, the consent text and its sha256. |
| `change_order.created` | A change order was written on a job (draft). |
| `change_order.sent` | A change order was sent to the customer to approve by signing. |
| `change_order.approved` | The customer signed a change order; it now counts toward the job's revised total. |
| `change_order.cancelled` | A draft or sent change order was cancelled. |
| `change_order.invoiced` | An approved change order was billed on an invoice. |
| `note.added` | A note or voice memo transcript was added on a job. |
| `note.updated` | A note's words, photos, pin or audience changed. |
| `note.removed` | A note was taken off its job (kept in history). |
| `report.saved` | A saved report was made or changed (R3). |
| `report.removed` | A saved report was removed (R3). |
| `message_template.saved` | A customer message template was set (H2). |
| `message_template.reset` | A customer message template went back to the built-in wording (H2). |
| `customer.unsubscribed` | A customer used the unsubscribe link in a review request email; the address is on the shop's do-not-email list. |
| `shop.hub_link_in_emails_changed` | The owner turned the customer hub link in quote and invoice emails on or off. |
| `shop.reopened` | The owner reopened the shop within 90 days of closing it. |
| `shop.purged` | 90 days after closing, working data the shop no longer needs was deleted. Money, tax, signature and job records are kept. |
| `shop.field_costs_changed` | The owner let field workers see job costs, or hid them again. |
| `shop.contact_changed` | The owner changed the phone or email shown on canceled invoice and quote links. |
| `hub_link.created` | A customer hub link was made (H2). |
| `hub_link.revoked` | A customer hub link was revoked (H2). |
| `visit_change.requested` | A customer asked from their hub to move or cancel a visit (H2). |
| `visit_change.withdrawn` | A customer took back their request to move or cancel a visit (H2). |
| `visit_change.answered` | The shop accepted or declined a customer's request to move or cancel a visit (H2). |
| `form_template.saved` | A form template was created or replaced. |
| `form_template.archived` | A form template was archived. |
| `form.started` | A form was started on a job or one visit (a draft). |
| `form.updated` | Answers were saved on a draft form. |
| `form.submitted` | A form was submitted: its PDF is stored on the job and the form is final. |
| `form.cancelled` | A draft form was cancelled (kept in history). |
| `time.clocked_in` | A person's clock started (work on a job, drive, shop or break). |
| `time.clocked_out` | A running clock stopped; the time waits for approval. |
| `time.added` | Time was added after the fact (a manual entry); it waits for approval. |
| `time.edited` | A time entry that was not approved was fixed. |
| `time.approved` | The owner or office approved a time entry; it is locked. |
| `time.rejected` | The owner or office rejected a time entry; the worker can fix it. |
| `time.voided` | A time entry that was never approved was taken back. |
| `time.reopened` | The owner reopened approved time so it can be fixed. |
| `labor_rate.set` | A labor cost rate was set for a worker or the shop default (the amount is not in the event). |
| `material.added` | A material used was recorded on a job. |
| `material.updated` | A recorded material's quantity, visit or notes changed. |
| `material.removed` | A recorded material was removed (kept in history). |
| `checklist_template.saved` | A checklist template was created or replaced. |
| `checklist_template.archived` | A checklist template was archived. |
| `checklist.created` | A checklist was put on a job or on one visit. |
| `checklist.items_added` | Items were added to a checklist. |
| `checklist.item_checked` | A checklist item was ticked or unticked. |
| `checklist.item_removed` | A checklist item was removed (kept in history, audited). |
| `checklist.overridden` | The owner marked a visit or job done with checklist work left. The reason and the open items are in the audit log. |
| `expense.added` | A worker recorded an expense on a job (amount typed by the worker). |
| `expense.updated` | An expense was changed. |
| `expense.removed` | An expense was taken off the books (kept in history). |
| `approval_rule.set` | The owner required approval before agents run an action. |
| `approval_rule.removed` | The owner dropped an approval rule. |
| `approval.requested` | An agent action is waiting for the owner's approval. |
| `approval.decided` | The owner approved or denied a held agent action. |
| `media.markup_saved` | A new markup layer was saved over a photo. The photo itself is unchanged. |
| `customer.created` | A customer was added to a shop. |
| `quote.created` | A quote was written. |
| `quote.sent` | A quote was locked and sent for signing. |
| `quote.signed` | The customer signed a quote. |
| `pricebook.item_saved` | A price book item was added, changed, archived or brought back. |
| `quote_template.saved` | A quote template was saved, archived or brought back. |
| `quote.declined` | The customer declined a quote on the sign page. |
| `quote.change_requested` | The customer asked for changes to a quote. The quote is back to draft; read the note with quotes.activity. |
| `quote.viewed` | The customer opened a sent quote version for the first time. |
| `quote.followup_queued` | A follow-up email was queued for a quote the customer has not answered. |
| `shop.quote_followup_changed` | The owner changed the quote follow-up delay. |
| `quote.choice_signed` | The customer signed a quote with options or optional lines. Says which option and optional lines they picked and the total. |
| `shop.tax_report_basis_changed` | The owner chose how the sales tax report counts a period (accrual or cash). |
| `maintenance_plan.tax_changed` | The office changed the tax rate a maintenance plan charges from the next term. |
| `shop.tips_changed` | The owner turned tips on or off on the pay page. |
| `shop.review_request_changed` | The owner changed the review link or the review email delay. |
| `review.request_queued` | A review request email was queued for a customer whose invoice was paid. |
| `card.saved` | A customer saved a card on the shop's payment account. |
| `card.removed` | A customer's saved card was removed. |
| `customer.autopay_changed` | Auto-pay was turned on or off for a customer. |
| `invoice.autopay_started` | An invoice was charged to the customer's saved card. invoice.paid follows when the charge succeeds. |
| `invoice.autopay_failed` | An auto-pay charge failed. The invoice stays open. |
| `tax_rate.saved` | A tax rate was added, replaced or archived. A replaced rate is archived and a new one takes its place. |
| `quote.attachment_added` | A photo or file was shown with a quote. |
| `quote.attachment_removed` | A photo or file was taken off a quote. |
| `maintenance_plan.created` | A maintenance plan was started. Its first term's invoice was created. |
| `maintenance_plan.price_changed` | A worker changed a maintenance plan's price. It applies from the next term. |
| `maintenance_plan.cancelled` | A maintenance plan was cancelled. No more renewal invoices. |
| `maintenance_plan.renewed` | A maintenance plan started its next term and that term's invoice was created (and sent when auto_send is on). |
| `invoice.created` | An invoice was created. |
| `invoice.batch_created` | Many invoices were created at once, and maybe sent. |
| `invoice.sent` | An invoice was sent for payment. |
| `invoice.payment_pending` | A bank payment was started and is clearing. The invoice is not paid yet. |
| `invoice.payment_failed` | A bank payment failed after it was started. The payer can pay again. |
| `invoice.paid` | An invoice was paid. |
| `invoice.paid_outside` | The shop recorded an invoice as paid outside Doing Work (check, cash, financing, insurance or other). The job record says so and is never marked paid or verified. |
| `invoice.voided` | The shop voided an invoice nobody had paid (X3). Its pay links stop working; the reason is included. |
| `charge.needs_refund` | Money arrived that the shop must give back (a paid-twice or void invoice). |
| `charge.refunded` | A payment was refunded. |
| `dispute.opened` | A payment was disputed. |
| `processor_account.updated` | The shop's payment setup changed. |
| `record.levelled` | A job record reached a new level (logged, paid, verified). |
| `confirmation.created` | The customer confirmed a job. |
| `grant.created` | Access was granted. |
| `grant.revoked` | Access was revoked. |
| `field_value.set` | A custom field value was set. |
| `field_definition.created` | A shop added a custom field. |
| `customer.updated` | A customer's details, contacts, properties or archive state changed. |
| `customer.merged` | A duplicate customer was merged into another. |
| `customer.erased` | A customer's personal details were erased for a privacy request. |
| `equipment.changed` | Equipment at a property was added, changed or archived. |
| `job.updated` | A job's tags or lead source changed. |
| `request.created` | A request for work came in. |
| `request.updated` | A request changed stage or details. |
| `request.converted` | A request became a job. |
| `visit.scheduled` | A visit to a job was booked. |
| `visit.updated` | A visit moved or its details changed. |
| `visit.assigned` | The workers on a visit changed. |
| `visit.cancelled` | A visit was cancelled. |
| `visit.started` | A worker started a visit. |
| `visit.done` | A visit was marked done. |
| `visit.series_created` | A repeating set of visits was booked. |
| `availability.changed` | A worker's weekly hours or time off changed. |
| `visit.on_the_way` | A worker is on the way to a visit, with an ETA when given. |
| `visit.arrived` | A worker arrived at a visit. |
| `visit.notice_queued` | A customer notice for a visit (appointment reminder or on my way) was handed to email. |
| `schedule.hours_changed` | The shop's booking hours changed. |
| `schedule.settings_changed` | The shop's scheduling rules changed (arrival window, buffer, reminders, on-my-way notices, time zone). |
| `invoice.pay_link_ready_queued` | One email was queued telling the customer an invoice can be paid again after its pay link waited on the shop's payments account. |
| `signature.copy_queued` | One email was queued giving a signer their own copy of what they signed (a document, a quote or a subcontractor list). |
| `card.resave_queued` | One email was queued asking a customer to save their card again after the shop's payments account changed. |
| `invoice.reminder_queued` | A friendly reminder was queued for an invoice still unpaid a day after it was sent. |
| `shop.invoice_reminders_changed` | The owner turned the automatic unpaid-invoice reminder on or off for the shop. |
| `invoice.reminder_changed` | The automatic unpaid reminder was turned on or off for one invoice. |
| `request_form.created` | A shop turned on or renewed its public request form. |
| `webhook_endpoint.created` | A webhook endpoint was added. |
| `webhook_endpoint.deleted` | A webhook endpoint was removed. |
| `webhook_delivery.retried` | An owner or office worker queued a failed webhook delivery to send again. |
| `webhook_endpoint.creator_left` | The worker who added a webhook endpoint left the shop. The endpoint keeps running; the owner can keep it or remove it. |
| `agent_key.created` | The owner made a named agent key for the shop. |
| `agent_key.revoked` | The owner revoked a named agent key. It stops working at once. |
| `platform.created` | A developer platform account was created. |


---

# API reference

284 operations, generated from the core's operation list. The same list makes the [OpenAPI file](https://doingwork.work/openapi.json) and the MCP tools, so they always match.

## Sign-in and agent access

| Operation | What it does | Who |
|---|---|---|
| [auth.requestLink](https://doingwork.work/docs/reference/auth-requestlink/) | Email a one-time sign-in link | Agents can call this |
| [auth.redeemLink](https://doingwork.work/docs/reference/auth-redeemlink/) | Exchange a sign-in link token for a browser session | Hosted page only |
| [auth.oauthRegister](https://doingwork.work/docs/reference/auth-oauthregister/) | Register an OAuth client (RFC 7591 dynamic registration) | Agents can call this |
| [auth.oauthAuthorize](https://doingwork.work/docs/reference/auth-oauthauthorize/) | A signed-in person approves an agent client | Agents can call this |
| [auth.oauthToken](https://doingwork.work/docs/reference/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 | Agents can call this |
| [me.get](https://doingwork.work/docs/reference/me-get/) | The signed-in person | Agents can call this |
| [keys.create](https://doingwork.work/docs/reference/keys-create/) | Make a named agent key for one shop (owner only) | A person's own sign-in only |
| [keys.list](https://doingwork.work/docs/reference/keys-list/) | List a shop's named agent keys (owner only): label, scopes, audience, when made, last used and revoked | Agents can call this |
| [keys.revoke](https://doingwork.work/docs/reference/keys-revoke/) | Revoke a named agent key (owner only) | Agents can call this |
| [agents.register](https://doingwork.work/docs/reference/agents-register/) | auth.md agent registration | Agents can call this |
| [agents.claim](https://doingwork.work/docs/reference/agents-claim/) | auth.md claim | Agents can call this |
| [agents.approve](https://doingwork.work/docs/reference/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 | Hosted page only |
| [agents.deny](https://doingwork.work/docs/reference/agents-deny/) | Hosted claim page only: the person turns the agent down | Hosted page only |
| [legal.documents](https://doingwork.work/docs/reference/legal-documents/) | The legal documents Doing Work asks people to accept, with the current version and a link to the text | Agents can call this |
| [legal.status](https://doingwork.work/docs/reference/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 | Agents can call this |
| [legal.accept](https://doingwork.work/docs/reference/legal-accept/) | Record that the signed-in person accepts the current version of a legal document (clickwrap) | A person's own sign-in only |

## Shop

| Operation | What it does | Who |
|---|---|---|
| [shops.create](https://doingwork.work/docs/reference/shops-create/) | Create a shop | Agents can call this |
| [quotes.withdraw](https://doingwork.work/docs/reference/quotes-withdraw/) | Withdraw a draft or sent quote the customer has not signed, with an optional reason | Agents can call this |
| [fees.schedule](https://doingwork.work/docs/reference/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 | Agents can call this |
| [fees.preview](https://doingwork.work/docs/reference/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) | Agents can call this |
| [audit.list](https://doingwork.work/docs/reference/audit-list/) | Read a shop's audit log, newest first (owner only) | Agents can call this |
| [customers.get](https://doingwork.work/docs/reference/customers-get/) | One customer with contacts, properties, custom fields and open balance | Agents can call this |
| [customers.list](https://doingwork.work/docs/reference/customers-list/) | Customers of a shop, newest first, filtered by tag, lead source or archived | Agents can call this |
| [customers.search](https://doingwork.work/docs/reference/customers-search/) | Find customers by name, company, email, phone or property address | Agents can call this |
| [customers.update](https://doingwork.work/docs/reference/customers-update/) | Change a customer's name, kind, company, email, phone, billing address, lead source, tags or notes | Agents can call this |
| [customers.archive](https://doingwork.work/docs/reference/customers-archive/) | Archive a customer (hidden from lists, history kept) or bring one back | Agents can call this |
| [customers.duplicates](https://doingwork.work/docs/reference/customers-duplicates/) | Likely duplicate customers in a shop: same email, same phone digits, or same name | Agents can call this |
| [customers.merge](https://doingwork.work/docs/reference/customers-merge/) | Merge a duplicate customer into another: jobs, requests, contacts, properties and fields move over | Agents can call this |
| [customers.contactAdd](https://doingwork.work/docs/reference/customers-contactadd/) | Add a contact person to a customer (tenant, manager, billing, spouse) | Agents can call this |
| [customers.contactUpdate](https://doingwork.work/docs/reference/customers-contactupdate/) | Change or remove one contact of a customer | Agents can call this |
| [jobs.setTags](https://doingwork.work/docs/reference/jobs-settags/) | Set the tags and lead source on a job | Agents can call this |
| [customers.timeline](https://doingwork.work/docs/reference/customers-timeline/) | Everything that happened with a customer, newest first: requests, jobs, quotes, signatures, invoices and payments | Agents can call this |
| [customers.statement](https://doingwork.work/docs/reference/customers-statement/) | A customer's account statement: every invoice with total, paid and balance, and the total owed | Agents can call this |
| [customers.privacyExport](https://doingwork.work/docs/reference/customers-privacyexport/) | Everything the shop holds about one customer, as one JSON document, for a privacy request | Agents can call this |
| [customers.privacyDelete](https://doingwork.work/docs/reference/customers-privacydelete/) | Erase a customer's personal details for a privacy request | A person's own sign-in only |
| [shops.setInvoiceReminders](https://doingwork.work/docs/reference/shops-setinvoicereminders/) | Turn the automatic unpaid-invoice reminder on or off for the whole shop (owner only) | Agents can call this |
| [shops.setContact](https://doingwork.work/docs/reference/shops-setcontact/) | Set the phone and email customers see when they open a canceled invoice or quote link (owner only) | Agents can call this |
| [shops.setFieldCosts](https://doingwork.work/docs/reference/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) | Agents can call this |
| [shops.setHubLinkInEmails](https://doingwork.work/docs/reference/shops-sethublinkinemails/) | Turn the customer hub link in quote and invoice emails on or off for the whole shop (owner only) | Agents can call this |
| [invoices.setReminder](https://doingwork.work/docs/reference/invoices-setreminder/) | Turn the automatic unpaid reminder on or off for one invoice (owner or office) | Agents can call this |
| [quotes.respond](https://doingwork.work/docs/reference/quotes-respond/) | Customer declines a quote or asks for changes (hosted page) | Hosted page only |
| [quotes.activity](https://doingwork.work/docs/reference/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 | Agents can call this |
| [shops.setQuoteFollowup](https://doingwork.work/docs/reference/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) | Agents can call this |
| [invoices.create](https://doingwork.work/docs/reference/invoices-create/) | Create an invoice without a quote, from lines the worker wrote (never filled in by AI) | Agents can call this |
| [invoices.createBatch](https://doingwork.work/docs/reference/invoices-createbatch/) | Create many invoices at once (up to 100), each from a job and the lines the worker wrote | Agents can call this |
| [invoices.get](https://doingwork.work/docs/reference/invoices-get/) | One invoice with its lines, what has been paid, the balance, the due date, and whether a bank payment is still clearing | Agents can call this |
| [invoices.pdf](https://doingwork.work/docs/reference/invoices-pdf/) | The invoice as a PDF (base64), with shop, customer, lines, paid so far and balance | Agents can call this |
| [invoices.aging](https://doingwork.work/docs/reference/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) | Agents can call this |
| [shops.setTips](https://doingwork.work/docs/reference/shops-settips/) | Turn tips on or off on this shop's invoice pay page (owner only) | Agents can call this |
| [shops.setReviewRequest](https://doingwork.work/docs/reference/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) | Agents can call this |
| [customers.setAutopay](https://doingwork.work/docs/reference/customers-setautopay/) | Turn auto-pay on or off for a customer (owner or office) | Agents can call this |
| [shops.setTaxReportBasis](https://doingwork.work/docs/reference/shops-settaxreportbasis/) | Choose how the sales tax report counts a period (owner only) | Agents can call this |
| [quotes.addAttachment](https://doingwork.work/docs/reference/quotes-addattachment/) | Show an uploaded photo or file of the same job with a quote, with an optional caption | Agents can call this |
| [quotes.removeAttachment](https://doingwork.work/docs/reference/quotes-removeattachment/) | Take a photo or file off a quote | Agents can call this |
| [quotes.listAttachments](https://doingwork.work/docs/reference/quotes-listattachments/) | Photos and files shown with a quote, in order | Agents can call this |
| [media.tag](https://doingwork.work/docs/reference/media-tag/) | Add or remove tags on a photo (for example: leak, panel, permit) | Agents can call this |
| [media.search](https://doingwork.work/docs/reference/media-search/) | Find photos across the shop that carry all the given tags | Agents can call this |
| [jobs.findByTag](https://doingwork.work/docs/reference/jobs-findbytag/) | Jobs in the shop that carry all the given tags (set with jobs.setTags), newest first, up to 200 | Agents can call this |
| [media.saveMarkup](https://doingwork.work/docs/reference/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) | Agents can call this |
| [media.getMarkup](https://doingwork.work/docs/reference/media-getmarkup/) | A photo's markup: the latest version unless one is named, with an SVG overlay to lay over the original photo | Agents can call this |
| [webhooks.deliveries](https://doingwork.work/docs/reference/webhooks-deliveries/) | List deliveries to one webhook endpoint, newest first, with event, status, attempts and last HTTP status | Agents can call this |
| [webhooks.retry](https://doingwork.work/docs/reference/webhooks-retry/) | Send a failed or dead delivery again from attempt 0 (owner or office) | Agents can call this |
| [shops.export](https://doingwork.work/docs/reference/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 | Agents can call this |
| [members.permissions](https://doingwork.work/docs/reference/members-permissions/) | What each current worker in a shop may do beyond their role: see_prices, see_costs, take_money, delete and manage_schedule | Agents can call this |
| [members.setPermissions](https://doingwork.work/docs/reference/members-setpermissions/) | Owner only, not an agent: turn a worker's permissions on or off | A person's own sign-in only |
| [jobs.complete](https://doingwork.work/docs/reference/jobs-complete/) | Mark a job done (owner or office) | Agents can call this |
| [customers.audience](https://doingwork.work/docs/reference/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 | Agents can call this |
| [shops.transferStart](https://doingwork.work/docs/reference/shops-transferstart/) | Start handing the shop to a current worker (owner, on a hosted page) | Hosted page only |
| [shops.transferAccept](https://doingwork.work/docs/reference/shops-transferaccept/) | Accept a hand-off and become the shop's owner (the person asked, on a hosted page) | Hosted page only |
| [shops.transferCancel](https://doingwork.work/docs/reference/shops-transfercancel/) | Stop an open hand-off: the owner cancels it, or the person asked declines it | Agents can call this |
| [shops.transfers](https://doingwork.work/docs/reference/shops-transfers/) | Hand-offs of a shop, newest first | Agents can call this |
| [shops.closeCheck](https://doingwork.work/docs/reference/shops-closecheck/) | Whether the shop can close now (owner only) | Agents can call this |
| [shops.close](https://doingwork.work/docs/reference/shops-close/) | Close the shop for good (owner, on a hosted page, typing the shop name) | Hosted page only |
| [shops.reopen](https://doingwork.work/docs/reference/shops-reopen/) | Reopen a closed shop (only the owner who closed it, on a hosted page, within 90 days of closing) | Hosted page only |
| [invoices.void](https://doingwork.work/docs/reference/invoices-void/) | Void an invoice nobody has paid yet (owner or office), with a reason | Agents can call this |

## Team (unlimited workers)

| Operation | What it does | Who |
|---|---|---|
| [members.invite](https://doingwork.work/docs/reference/members-invite/) | Invite a person by email as office or field | Agents can call this |
| [members.accept](https://doingwork.work/docs/reference/members-accept/) | Accept an invite as the signed-in person | Agents can call this |
| [members.list](https://doingwork.work/docs/reference/members-list/) | Current members of a shop with role and start date | Agents can call this |
| [members.end](https://doingwork.work/docs/reference/members-end/) | End a membership: the owner removes a worker, or a worker leaves | Agents can call this |
| [members.changeRole](https://doingwork.work/docs/reference/members-changerole/) | Owner changes a worker between office and field | Agents can call this |
| [approvals.setRule](https://doingwork.work/docs/reference/approvals-setrule/) | Owner only, not an agent: require owner approval before an agent can run an action | A person's own sign-in only |
| [approvals.removeRule](https://doingwork.work/docs/reference/approvals-removerule/) | Owner only, not an agent: stop requiring approval for a rule | A person's own sign-in only |
| [approvals.listRules](https://doingwork.work/docs/reference/approvals-listrules/) | The shop's approval rules: which agent actions wait for the owner | Agents can call this |
| [approvals.list](https://doingwork.work/docs/reference/approvals-list/) | Approval requests for a shop, newest first, optionally by status (pending, approved, denied, failed, expired) | Agents can call this |
| [approvals.get](https://doingwork.work/docs/reference/approvals-get/) | One approval request with its status and, once approved and run, the action's result | Agents can call this |
| [approvals.decide](https://doingwork.work/docs/reference/approvals-decide/) | Owner only, not an agent: approve or deny a pending agent action | A person's own sign-in only |

## Customers

| Operation | What it does | Who |
|---|---|---|
| [customers.create](https://doingwork.work/docs/reference/customers-create/) | Add a customer to a shop, with kind, company, email, phone, billing address, lead source, tags and notes in one call | Agents can call this |
| [properties.add](https://doingwork.work/docs/reference/properties-add/) | Add a service address to a customer, with optional label and access notes | Agents can call this |
| [properties.list](https://doingwork.work/docs/reference/properties-list/) | Service addresses of a customer, or every property of a shop | Agents can call this |
| [properties.update](https://doingwork.work/docs/reference/properties-update/) | Change a property's label or access notes, or archive it | Agents can call this |
| [equipment.add](https://doingwork.work/docs/reference/equipment-add/) | Record equipment at a property: kind, make, model, serial, install date and warranty | Agents can call this |
| [equipment.list](https://doingwork.work/docs/reference/equipment-list/) | Equipment at a property, or warranties ending soon across a shop | Agents can call this |
| [equipment.update](https://doingwork.work/docs/reference/equipment-update/) | Change or archive an equipment record | Agents can call this |

## Jobs (unlimited)

| Operation | What it does | Who |
|---|---|---|
| [jobs.create](https://doingwork.work/docs/reference/jobs-create/) | Start a job at a home (the home is found or created from the address) | Agents can call this |
| [jobs.list](https://doingwork.work/docs/reference/jobs-list/) | Jobs of a shop, newest first | Agents can call this |
| [jobs.get](https://doingwork.work/docs/reference/jobs-get/) | One job with its home address | Agents can call this |

## Job photos

| Operation | What it does | Who |
|---|---|---|
| [media.requestUpload](https://doingwork.work/docs/reference/media-requestupload/) | Get a signed upload URL for a job photo | Agents can call this |
| [media.requestCapture](https://doingwork.work/docs/reference/media-requestcapture/) | Hosted photo capture: a signed upload URL for a worker taking a photo in the browser | Hosted page only |
| [media.list](https://doingwork.work/docs/reference/media-list/) | List the photos on a job that you are allowed to see | Agents can call this |
| [media.get](https://doingwork.work/docs/reference/media-get/) | One photo's details | Agents can call this |
| [shares.create](https://doingwork.work/docs/reference/shares-create/) | Make a link to a gallery of a job's photos that anyone with the link can open without signing in | Agents can call this |
| [shares.list](https://doingwork.work/docs/reference/shares-list/) | Photo share links on a job, newest first, with whether each is still active and how often it was opened | Agents can call this |
| [shares.revoke](https://doingwork.work/docs/reference/shares-revoke/) | Turn a photo share link off now | Agents can call this |

## Quotes (unlimited)

| Operation | What it does | Who |
|---|---|---|
| [quotes.create](https://doingwork.work/docs/reference/quotes-create/) | Create a quote from lines the worker wrote | Agents can call this |
| [quotes.send](https://doingwork.work/docs/reference/quotes-send/) | Lock the quote and get a single-use signing link for the customer | Agents can call this |
| [quotes.sign](https://doingwork.work/docs/reference/quotes-sign/) | Sign a quote after accepting electronic signing | Hosted page only |
| [quotes.revise](https://doingwork.work/docs/reference/quotes-revise/) | Replace an unsigned quote with a new version | Agents can call this |
| [pricebook.saveItem](https://doingwork.work/docs/reference/pricebook-saveitem/) | Add a price book item, or change one when item_id is given (owner or office) | Agents can call this |
| [pricebook.list](https://doingwork.work/docs/reference/pricebook-list/) | List the shop's price book items, by name | Agents can call this |
| [pricebook.archiveItem](https://doingwork.work/docs/reference/pricebook-archiveitem/) | Archive a price book item, or bring it back with archived false (owner or office) | Agents can call this |

## Invoices and payments (unlimited)

| Operation | What it does | Who |
|---|---|---|
| [invoices.createFromQuote](https://doingwork.work/docs/reference/invoices-createfromquote/) | Create an invoice for a signed quote, for the total of the option and optional lines the customer picked | Agents can call this |
| [invoices.send](https://doingwork.work/docs/reference/invoices-send/) | Send an invoice (or the next part of it) and get a single-use pay link | Agents can call this |
| [invoices.pay](https://doingwork.work/docs/reference/invoices-pay/) | Start paying an invoice (hosted page) | Hosted page only |
| [invoices.markPaidOutside](https://doingwork.work/docs/reference/invoices-markpaidoutside/) | Record that the customer paid an invoice outside Doing Work (check, cash, financing, insurance or other) | A person's own sign-in only |
| [invoices.refund](https://doingwork.work/docs/reference/invoices-refund/) | Refund part or all of a payment (owner or office) | A person's own sign-in only |
| [taxes.saveRate](https://doingwork.work/docs/reference/taxes-saverate/) | Add or change a sales tax rate the shop charges in one jurisdiction (owner or office) | Agents can call this |
| [taxes.listRates](https://doingwork.work/docs/reference/taxes-listrates/) | The shop's tax rates | Agents can call this |
| [taxes.archiveRate](https://doingwork.work/docs/reference/taxes-archiverate/) | Stop using a tax rate (owner or office) | Agents can call this |
| [taxes.salesReport](https://doingwork.work/docs/reference/taxes-salesreport/) | Sales tax report for a period (owner or office), shop local dates, from and to inclusive | Agents can call this |

## Stripe connection

| Operation | What it does | Who |
|---|---|---|
| [payments.connectStart](https://doingwork.work/docs/reference/payments-connectstart/) | Start payment setup for a shop (owner, hosted page) | Hosted page only |
| [cards.setupLink](https://doingwork.work/docs/reference/cards-setuplink/) | Get a secure page where the customer saves a card with the shop's payment processor (owner or office) | Agents can call this |
| [cards.resave](https://doingwork.work/docs/reference/cards-resave/) | Open the card saving page from a save-your-card-again email link (hosted page only) | Hosted page only |
| [cards.get](https://doingwork.work/docs/reference/cards-get/) | The card a customer has on file (brand, last 4, expiry) and whether auto-pay is on (owner or office) | Agents can call this |
| [cards.remove](https://doingwork.work/docs/reference/cards-remove/) | Remove a customer's saved card (owner or office) | Agents can call this |

## Idaho compliance

| Operation | What it does | Who |
|---|---|---|
| [subcontractors.get](https://doingwork.work/docs/reference/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 | Agents can call this |
| [subcontractors.prepare](https://doingwork.work/docs/reference/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" | Agents can call this |
| [subcontractors.sign](https://doingwork.work/docs/reference/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 | Hosted page only |

## Signed job records

| Operation | What it does | Who |
|---|---|---|
| [jobs.requestConfirmation](https://doingwork.work/docs/reference/jobs-requestconfirmation/) | Get a single-use link the customer opens to confirm the work was done (or flag a problem) | Agents can call this |
| [jobs.confirm](https://doingwork.work/docs/reference/jobs-confirm/) | Customer confirms the work or flags a problem (hosted page) | Hosted page only |
| [records.keys](https://doingwork.work/docs/reference/records-keys/) | Public keys that verify signed job records | Agents can call this |
| [jobs.record](https://doingwork.work/docs/reference/jobs-record/) | The latest signed record for a job and its level (logged, paid, verified) | Agents can call this |
| [documents.photoReport](https://doingwork.work/docs/reference/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 | Agents can call this |
| [documents.dailyLog](https://doingwork.work/docs/reference/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 | Agents can call this |
| [documents.list](https://doingwork.work/docs/reference/documents-list/) | PDF documents core made for a job (photo reports, daily logs and submitted forms), newest first, with each file's sha256 | Agents can call this |
| [documents.get](https://doingwork.work/docs/reference/documents-get/) | One stored job PDF (base64) with its sha256, byte for byte as it was made | Agents can call this |

## Plans and fees

| Operation | What it does | Who |
|---|---|---|
| [fees.statement](https://doingwork.work/docs/reference/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) | Agents can call this |

## Developers and platforms

| Operation | What it does | Who |
|---|---|---|
| [webhooks.create](https://doingwork.work/docs/reference/webhooks-create/) | Add a webhook endpoint for a shop (owner or office) | A person's own sign-in only |
| [webhooks.list](https://doingwork.work/docs/reference/webhooks-list/) | List a shop's webhook endpoints and their recent delivery counts | Agents can call this |
| [webhooks.delete](https://doingwork.work/docs/reference/webhooks-delete/) | Remove a webhook endpoint | Agents can call this |
| [platforms.create](https://doingwork.work/docs/reference/platforms-create/) | Create a developer platform account (private beta) | Agents can call this |
| [platforms.list](https://doingwork.work/docs/reference/platforms-list/) | List the platform accounts you own and the shops each one manages | Agents can call this |
| [platforms.grantManage](https://doingwork.work/docs/reference/platforms-grantmanage/) | Let a platform manage this shop | Hosted page only |
| [platforms.revokeManage](https://doingwork.work/docs/reference/platforms-revokemanage/) | Stop a platform from managing this shop | Agents can call this |
| [events.catalog](https://doingwork.work/docs/reference/events-catalog/) | Every event a webhook can receive, with its payload keys and types | Agents can call this |

## Scheduling and visits

| Operation | What it does | Who |
|---|---|---|
| [visits.createRecurring](https://doingwork.work/docs/reference/visits-createrecurring/) | Book a repeating visit (daily, weekly or monthly, every n) for a job | Agents can call this |
| [visits.cancelSeries](https://doingwork.work/docs/reference/visits-cancelseries/) | Cancel the visits of a repeating series that have not started yet, from a time onward (default now) | Agents can call this |
| [visits.start](https://doingwork.work/docs/reference/visits-start/) | Mark a visit started (from scheduled, on the way, or arrived) | Agents can call this |
| [visits.done](https://doingwork.work/docs/reference/visits-done/) | Mark a visit done, with an optional note | Agents can call this |
| [visits.create](https://doingwork.work/docs/reference/visits-create/) | Book a visit to a job at a time, optionally with the workers going | Agents can call this |
| [visits.update](https://doingwork.work/docs/reference/visits-update/) | Move a visit or change its title, notes or arrival window | Agents can call this |
| [visits.assign](https://doingwork.work/docs/reference/visits-assign/) | Set who is going on a visit | Agents can call this |
| [visits.cancel](https://doingwork.work/docs/reference/visits-cancel/) | Cancel a visit that is scheduled, on the way, or arrived | Agents can call this |
| [visits.get](https://doingwork.work/docs/reference/visits-get/) | One visit with its job and workers | Agents can call this |
| [schedule.list](https://doingwork.work/docs/reference/schedule-list/) | A shop's visits that overlap a time window, earliest first | Agents can call this |
| [availability.setHours](https://doingwork.work/docs/reference/availability-sethours/) | Set a worker's weekly hours, replacing what was there | Agents can call this |
| [availability.addTimeOff](https://doingwork.work/docs/reference/availability-addtimeoff/) | Add time off for a worker (vacation, sick, appointment) | Agents can call this |
| [availability.removeTimeOff](https://doingwork.work/docs/reference/availability-removetimeoff/) | Remove a time-off entry | Agents can call this |
| [availability.get](https://doingwork.work/docs/reference/availability-get/) | A worker's weekly hours, time off, and booked visits in a window (at most 62 days) | Agents can call this |
| [schedule.settings](https://doingwork.work/docs/reference/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 | Agents can call this |
| [schedule.setShopHours](https://doingwork.work/docs/reference/schedule-setshophours/) | Set the shop's booking hours, replacing what was there (owner or office) | Agents can call this |
| [schedule.setRules](https://doingwork.work/docs/reference/schedule-setrules/) | Change the shop's scheduling rules (owner or office; the time zone is owner only) | Agents can call this |
| [schedule.findTime](https://doingwork.work/docs/reference/schedule-findtime/) | Find open times for a visit of a given length | Agents can call this |
| [visits.onTheWay](https://doingwork.work/docs/reference/visits-ontheway/) | Mark that the worker is on the way to a visit, with an optional ETA (eta_at, or eta_minutes from now) | Agents can call this |
| [visits.arrived](https://doingwork.work/docs/reference/visits-arrived/) | Mark that the worker arrived at a visit | Agents can call this |
| [visits.notices](https://doingwork.work/docs/reference/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 | Agents can call this |
| [maintenance.createPlan](https://doingwork.work/docs/reference/maintenance-createplan/) | Start a maintenance plan for a job's customer (owner or office) | Agents can call this |
| [maintenance.getPlan](https://doingwork.work/docs/reference/maintenance-getplan/) | One maintenance plan: price per term, current term, renewal date, and each term's invoice | Agents can call this |
| [maintenance.listPlans](https://doingwork.work/docs/reference/maintenance-listplans/) | A shop's maintenance plans, optionally for one customer or by status, soonest renewal first | Agents can call this |
| [maintenance.setPrice](https://doingwork.work/docs/reference/maintenance-setprice/) | Replace a plan's priced lines (owner or office; you write them, never AI), and optionally its tax rate | Agents can call this |
| [maintenance.cancelPlan](https://doingwork.work/docs/reference/maintenance-cancelplan/) | Cancel a maintenance plan (owner or office) | Agents can call this |
| [visits.changeRequests](https://doingwork.work/docs/reference/visits-changerequests/) | Customers' requests to move or cancel a visit, from the customer hub, newest first | Agents can call this |
| [visits.answerChange](https://doingwork.work/docs/reference/visits-answerchange/) | Answer a customer's request to move or cancel a visit | Agents can call this |

## Field work (checklists, forms, time, notes, change orders, subcontractors)

| Operation | What it does | Who |
|---|---|---|
| [logs.add](https://doingwork.work/docs/reference/logs-add/) | Write a daily log entry on a job: what happened on site that day, in the worker's own words | Agents can call this |
| [logs.list](https://doingwork.work/docs/reference/logs-list/) | Daily log entries on a job, oldest first, optionally between two dates (inclusive) | Agents can call this |
| [logs.remove](https://doingwork.work/docs/reference/logs-remove/) | Take a daily log entry out of the log | Agents can call this |
| [signatures.request](https://doingwork.work/docs/reference/signatures-request/) | Ask someone to sign a document on a hosted page, and get the signing link to give them | Agents can call this |
| [signatures.newLink](https://doingwork.work/docs/reference/signatures-newlink/) | A new signing link for an open signature request | Agents can call this |
| [signatures.cancel](https://doingwork.work/docs/reference/signatures-cancel/) | Cancel an open signature request | Agents can call this |
| [signatures.get](https://doingwork.work/docs/reference/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) | Agents can call this |
| [signatures.list](https://doingwork.work/docs/reference/signatures-list/) | Signature requests for a job, a customer or a whole shop, newest first, with status and who signed | Agents can call this |
| [signatures.sign](https://doingwork.work/docs/reference/signatures-sign/) | Sign a document after reading it, typing your name and agreeing to sign electronically | Hosted page only |
| [checklists.saveTemplate](https://doingwork.work/docs/reference/checklists-savetemplate/) | Create a checklist template, or replace one (give checklist_template_id) | Agents can call this |
| [checklists.listTemplates](https://doingwork.work/docs/reference/checklists-listtemplates/) | The shop's checklist templates with their items | Agents can call this |
| [checklists.archiveTemplate](https://doingwork.work/docs/reference/checklists-archivetemplate/) | Archive a checklist template so it is no longer offered | Agents can call this |
| [checklists.create](https://doingwork.work/docs/reference/checklists-create/) | Put a checklist on a job, or on one visit of the job (visit_id) | Agents can call this |
| [checklists.list](https://doingwork.work/docs/reference/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 | Agents can call this |
| [checklists.get](https://doingwork.work/docs/reference/checklists-get/) | One checklist with every item (removed items included and marked), who ticked what and when, and its sign-off | Agents can call this |
| [checklists.checkItem](https://doingwork.work/docs/reference/checklists-checkitem/) | Tick (done true) or untick an item, with an optional note | Agents can call this |
| [checklists.addItems](https://doingwork.work/docs/reference/checklists-additems/) | Add items to the end of a checklist (owner or office) | Agents can call this |
| [checklists.removeItem](https://doingwork.work/docs/reference/checklists-removeitem/) | Remove an item from a checklist (it stays in the history, marked removed) | Agents can call this |
| [checklists.requestSignOff](https://doingwork.work/docs/reference/checklists-requestsignoff/) | Ask for a sign-off on a finished checklist and get the signing link | Agents can call this |
| [forms.saveTemplate](https://doingwork.work/docs/reference/forms-savetemplate/) | Create a form template (an inspection, a safety sheet, a start-up report), or replace one (give form_template_id) | Agents can call this |
| [forms.listTemplates](https://doingwork.work/docs/reference/forms-listtemplates/) | The shop's form templates with their fields | Agents can call this |
| [forms.archiveTemplate](https://doingwork.work/docs/reference/forms-archivetemplate/) | Archive a form template so it is no longer offered | Agents can call this |
| [forms.start](https://doingwork.work/docs/reference/forms-start/) | Start a form on a job, or on one visit of the job (visit_id) | Agents can call this |
| [forms.fill](https://doingwork.work/docs/reference/forms-fill/) | Save answers on a draft form | Agents can call this |
| [forms.submit](https://doingwork.work/docs/reference/forms-submit/) | Submit a draft form: every required field must be answered (forms.get lists what is missing) | Agents can call this |
| [forms.cancel](https://doingwork.work/docs/reference/forms-cancel/) | Cancel a draft form (kept in history) | Agents can call this |
| [forms.get](https://doingwork.work/docs/reference/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 | Agents can call this |
| [forms.list](https://doingwork.work/docs/reference/forms-list/) | Forms on a job (job-level and per visit), or on one visit, newest first, without fields and answers | Agents can call this |
| [forms.requestSignOff](https://doingwork.work/docs/reference/forms-requestsignoff/) | Ask for a signature on a submitted form and get the signing link (owner or office) | Agents can call this |
| [notes.add](https://doingwork.work/docs/reference/notes-add/) | Write a note on a job, or on one visit of it (give visit_id; job_id is then optional) | Agents can call this |
| [notes.addVoiceMemo](https://doingwork.work/docs/reference/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 | Agents can call this |
| [notes.update](https://doingwork.work/docs/reference/notes-update/) | Change a note | Agents can call this |
| [notes.remove](https://doingwork.work/docs/reference/notes-remove/) | Take a note off the job | Agents can call this |
| [notes.get](https://doingwork.work/docs/reference/notes-get/) | One note you may see, with every earlier wording (history, oldest first) | Agents can call this |
| [notes.list](https://doingwork.work/docs/reference/notes-list/) | Notes you may see on a job (job-level and every visit) or on one visit: pinned first, then newest first | Agents can call this |
| [changes.create](https://doingwork.work/docs/reference/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) | Agents can call this |
| [changes.list](https://doingwork.work/docs/reference/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 | Agents can call this |
| [changes.get](https://doingwork.work/docs/reference/changes-get/) | One change order with its lines, totals, status, signature and invoice | Agents can call this |
| [changes.send](https://doingwork.work/docs/reference/changes-send/) | Owner or office: ask the customer to approve a change order | Agents can call this |
| [changes.cancel](https://doingwork.work/docs/reference/changes-cancel/) | Cancel a draft or sent change order (its open signature request is cancelled too, so the link stops working) | Agents can call this |
| [changes.invoice](https://doingwork.work/docs/reference/changes-invoice/) | Bill approved change orders on a normal invoice (draft; send it with invoices.send) | Agents can call this |
| [timesheets.clockIn](https://doingwork.work/docs/reference/timesheets-clockin/) | Start a person's clock: on a job (work), or drive, shop or break time | Agents can call this |
| [timesheets.clockOut](https://doingwork.work/docs/reference/timesheets-clockout/) | Stop a person's running clock | Agents can call this |
| [timesheets.current](https://doingwork.work/docs/reference/timesheets-current/) | A person's running clock in a shop, or null when they are not clocked in | Agents can call this |
| [timesheets.add](https://doingwork.work/docs/reference/timesheets-add/) | Add time after the fact (a manual entry with a start and an end, at most 24 hours) | Agents can call this |
| [timesheets.update](https://doingwork.work/docs/reference/timesheets-update/) | Fix a time entry that is not approved: its start, end, category, job, visit or notes | Agents can call this |
| [timesheets.void](https://doingwork.work/docs/reference/timesheets-void/) | Take back a time entry that was never approved (kept in history, no longer counted) | Agents can call this |
| [timesheets.decide](https://doingwork.work/docs/reference/timesheets-decide/) | Owner or office: approve or reject stopped time entries (up to 200 at once, one shop) | Agents can call this |
| [timesheets.reopen](https://doingwork.work/docs/reference/timesheets-reopen/) | Owner only: reopen approved time so it can be fixed (it goes back to waiting for approval) | Agents can call this |
| [timesheets.list](https://doingwork.work/docs/reference/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 | Agents can call this |
| [timesheets.setRate](https://doingwork.work/docs/reference/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 | Agents can call this |
| [timesheets.listRates](https://doingwork.work/docs/reference/timesheets-listrates/) | Labor cost rates in force now: the shop default and each worker's own, plus upcoming ones | Agents can call this |
| [materials.add](https://doingwork.work/docs/reference/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) | Agents can call this |
| [materials.update](https://doingwork.work/docs/reference/materials-update/) | Change a recorded material's quantity, visit or notes | Agents can call this |
| [materials.remove](https://doingwork.work/docs/reference/materials-remove/) | Remove a recorded material (recorded in error or returned) | Agents can call this |
| [materials.list](https://doingwork.work/docs/reference/materials-list/) | Materials recorded on a job (or one visit), with the price and cost totals of those still counted | Agents can call this |
| [subs.add](https://doingwork.work/docs/reference/subs-add/) | Add a subcontractor to a job: a company and the one email they will sign in with | Agents can call this |
| [subs.list](https://doingwork.work/docs/reference/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 | Agents can call this |
| [subs.update](https://doingwork.work/docs/reference/subs-update/) | Change a subcontractor's contact name, trade or scope note (what they see on their page) | Agents can call this |
| [subs.remove](https://doingwork.work/docs/reference/subs-remove/) | Take a subcontractor off a job, for good: their link stops working at once | Agents can call this |
| [subs.notes](https://doingwork.work/docs/reference/subs-notes/) | Notes subcontractors left on a job, newest first | Agents can call this |
| [subs.view](https://doingwork.work/docs/reference/subs-view/) | The subcontractor's page for one job: the shop, job summary, work address, scope note, upcoming visit times and their notes | Hosted page only |
| [subs.addNote](https://doingwork.work/docs/reference/subs-addnote/) | Leave a note for the shop on this job, as the subcontractor (up to 20 a day) | Hosted page only |

## Customer hub, requests and customer emails

| Operation | What it does | Who |
|---|---|---|
| [requests.create](https://doingwork.work/docs/reference/requests-create/) | Log a request for work from a customer or a new lead | Agents can call this |
| [requests.list](https://doingwork.work/docs/reference/requests-list/) | Requests of a shop, newest first, filtered by stage, customer or tag | Agents can call this |
| [requests.pipeline](https://doingwork.work/docs/reference/requests-pipeline/) | How many open requests a shop has in each stage, and how many were won or lost in the last 30 days | Agents can call this |
| [requests.get](https://doingwork.work/docs/reference/requests-get/) | One request with its customer and the job it became | Agents can call this |
| [requests.update](https://doingwork.work/docs/reference/requests-update/) | Move a request to another stage or change its details, customer or property | Agents can call this |
| [requests.convert](https://doingwork.work/docs/reference/requests-convert/) | Turn a request into a job at its property or address; the worker then writes the quote or books a visit | Agents can call this |
| [requests.formCreate](https://doingwork.work/docs/reference/requests-formcreate/) | Turn on the shop's public request form and get its key | Agents can call this |
| [requests.submitForm](https://doingwork.work/docs/reference/requests-submitform/) | Send a shop a request through its public form | Agents can call this |
| [templates.save](https://doingwork.work/docs/reference/templates-save/) | Save a quote template: worker-written lines (and good, better, best options) to start quotes from | Agents can call this |
| [templates.list](https://doingwork.work/docs/reference/templates-list/) | List the shop's saved quote templates with their lines | Agents can call this |
| [templates.archive](https://doingwork.work/docs/reference/templates-archive/) | Archive a quote template, or bring it back with archived false (owner or office) | Agents can call this |
| [messages.templates](https://doingwork.work/docs/reference/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 | Agents can call this |
| [messages.saveTemplate](https://doingwork.work/docs/reference/messages-savetemplate/) | Set the subject and body wording for one kind of automatic customer email | Agents can call this |
| [messages.resetTemplate](https://doingwork.work/docs/reference/messages-resettemplate/) | Go back to the built-in wording for one kind of automatic customer email | Agents can call this |
| [messages.previewTemplate](https://doingwork.work/docs/reference/messages-previewtemplate/) | Show how one kind of automatic customer email reads, filled with sample customer details and the shop's name | Agents can call this |
| [hub.createLink](https://doingwork.work/docs/reference/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 | Agents can call this |
| [hub.links](https://doingwork.work/docs/reference/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 | Agents can call this |
| [hub.revokeLink](https://doingwork.work/docs/reference/hub-revokelink/) | Stop a customer hub link working, for good | Agents can call this |
| [hub.view](https://doingwork.work/docs/reference/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 | Hosted page only |
| [hub.open](https://doingwork.work/docs/reference/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 |
| [hub.requestVisitChange](https://doingwork.work/docs/reference/hub-requestvisitchange/) | From the customer hub, ask the shop to move (reschedule, with the times that work better) or cancel an upcoming visit | Hosted page only |
| [hub.withdrawVisitChange](https://doingwork.work/docs/reference/hub-withdrawvisitchange/) | From the customer hub, take back your own open request to move or cancel a visit | Hosted page only |
| [hub.requestWork](https://doingwork.work/docs/reference/hub-requestwork/) | From the customer hub, ask the shop for new work | Hosted page only |

## Costs, expenses and reports

| Operation | What it does | Who |
|---|---|---|
| [tips.report](https://doingwork.work/docs/reference/tips-report/) | Tips customers paid, by invoice and in total, between two dates (owner or office) | Agents can call this |
| [expenses.add](https://doingwork.work/docs/reference/expenses-add/) | Record money a worker spent on a job, with the receipt photo | Agents can call this |
| [expenses.update](https://doingwork.work/docs/reference/expenses-update/) | Change an expense: date, vendor, category, amount, tax, receipt photo or note | Agents can call this |
| [expenses.remove](https://doingwork.work/docs/reference/expenses-remove/) | Take an expense off the books (kept in history) | Agents can call this |
| [expenses.list](https://doingwork.work/docs/reference/expenses-list/) | Expenses on a job, or across the shop between dates, with totals by category | Agents can call this |
| [reports.revenue](https://doingwork.work/docs/reference/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 | Agents can call this |
| [reports.owed](https://doingwork.work/docs/reference/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 | Agents can call this |
| [reports.quoteWinRate](https://doingwork.work/docs/reference/reports-quotewinrate/) | Of the quotes made between two dates (inclusive, UTC) and sent, how many were signed, declined, or are still waiting | Agents can call this |
| [costs.job](https://doingwork.work/docs/reference/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 | Agents can call this |
| [costs.jobs](https://doingwork.work/docs/reference/costs-jobs/) | Job costing across jobs dated between two days (inclusive, in the shop's time zone), with totals | Agents can call this |
| [reports.today](https://doingwork.work/docs/reference/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 | Agents can call this |
| [reports.week](https://doingwork.work/docs/reference/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 | Agents can call this |
| [reports.save](https://doingwork.work/docs/reference/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 | Agents can call this |
| [reports.listSaved](https://doingwork.work/docs/reference/reports-listsaved/) | The shop's saved reports, by name | Agents can call this |
| [reports.removeSaved](https://doingwork.work/docs/reference/reports-removesaved/) | Remove a saved report (kept for the record; its name is free to use again) | Agents can call this |
| [reports.run](https://doingwork.work/docs/reference/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 | Agents can call this |

## Imports, custom fields, tags and search

| Operation | What it does | Who |
|---|---|---|
| [fields.define](https://doingwork.work/docs/reference/fields-define/) | Add a custom field to customers, properties, jobs, quotes or invoices for a shop | Agents can call this |
| [fields.list](https://doingwork.work/docs/reference/fields-list/) | Custom fields a shop has defined, optionally for one kind of record | Agents can call this |
| [fields.set](https://doingwork.work/docs/reference/fields-set/) | Set one custom field value on a customer, property, job, quote or invoice | Agents can call this |
| [fields.get](https://doingwork.work/docs/reference/fields-get/) | Custom field values on one record, with any required fields still empty | Agents can call this |
| [tags.list](https://doingwork.work/docs/reference/tags-list/) | Every tag used in the shop, with how many photos and jobs carry it | Agents can call this |
| [search.global](https://doingwork.work/docs/reference/search-global/) | Search one shop for customers (name, email, phone), properties (address), jobs (summary) and requests (title) | Agents can call this |
| [imports.fields](https://doingwork.work/docs/reference/imports-fields/) | The customer import fields and the built-in presets (jobber, housecall_pro, companycam) with the export column each reads | Agents can call this |
| [imports.preview](https://doingwork.work/docs/reference/imports-preview/) | Dry run of a customer CSV import | Agents can call this |
| [imports.run](https://doingwork.work/docs/reference/imports-run/) | Owner only: import customers (and one service address each) from a CSV with the mapping the owner confirmed | Agents can call this |
| [bulk.run](https://doingwork.work/docs/reference/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 | Agents can call this |


---

# auth.requestLink

Email a one-time sign-in link. Always answers ok, so it never reveals whether an email has an account.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/auth/link` |
| MCP tool | `auth_requestLink` |
| Writes | yes, send an Idempotency-Key |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

The person wants to sign in or create an account. Send the link to their email and tell them to open it on the device they'll use.

## What to tell the owner

I sent a sign-in link to your email. Open it and you're in. It works once.

## Input

| Field | Type | Required |
|---|---|---|
| `email` | string | yes |

## Example

```http
POST https://doingwork.work/v1/auth/link
content-type: application/json
Idempotency-Key: 3f1c...

{
  "email": "owner@example.com"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `rate_limited` | 429 | Too many requests. Wait for the Retry-After time, then retry. |
| `invalid_input` | 400 | A field is missing, the wrong type, or unknown. The message names the field. Fix the input; don't retry as is. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# auth.redeemLink

Exchange a sign-in link token for a browser session. Only the hosted sign-in page (POST /p/sign-in on the pages host, DW_PAGES_BASE; https://pages.doingwork.work in production) runs this; it sets an HttpOnly cookie and never returns the token to an agent. Each link works once.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/auth/link/redeem` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

Never call it yourself. The person's browser does this when they open the sign-in link.

## What to tell the owner

Open the link from your email and it signs you in.

## Input

| Field | Type | Required |
|---|---|---|
| `token` | string | yes |
| `display_name` | string | no |

## Example

```http
POST https://doingwork.work/v1/auth/link/redeem
content-type: application/json
Idempotency-Key: 3f1c...

{
  "token": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_link` | 400 | The link was used, expired, or replaced by a newer one. Get a new link. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# auth.oauthRegister

Register an OAuth client (RFC 7591 dynamic registration). Public clients only: PKCE, no secret. Redirects must be https, or http on loopback.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/oauth/register` |
| MCP tool | `auth_oauthRegister` |
| Writes | yes, send an Idempotency-Key |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

First step of connecting an AI agent. Register your client once, with a loopback or https redirect.

## What to tell the owner

Nothing to explain. This is set-up between your assistant and Doing Work.

## Input

| Field | Type | Required |
|---|---|---|
| `client_name` | string | no |
| `redirect_uris` | array | yes |
| `token_endpoint_auth_method` | string | no |
| `grant_types` | array | no |
| `response_types` | array | no |

## Example

```http
POST https://doingwork.work/v1/oauth/register
content-type: application/json
Idempotency-Key: 3f1c...

{
  "redirect_uris": [
    "..."
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_client_metadata` | 400 | The client registration is invalid, for example a redirect URI that isn't https or loopback. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# auth.oauthAuthorize

A signed-in person approves an agent client. Returns the redirect with a one-time code (PKCE S256). Needs a browser session, not an agent token. `resource` (RFC 8707) binds the token to this server's MCP endpoint.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/oauth/authorize` |
| MCP tool | `auth_oauthAuthorize` |
| Writes | yes, send an Idempotency-Key |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

The signed-in person approves your agent. Send them to the authorization page; it returns a one-time code.

## What to tell the owner

Doing Work will ask you to approve this assistant. Approve it and I can work in your shop.

## Input

| Field | Type | Required |
|---|---|---|
| `client_id` | string | yes |
| `redirect_uri` | string | yes |
| `code_challenge` | string | yes |
| `code_challenge_method` | string (one of S256) | yes |
| `state` | string | no |
| `shop_id` | string | no |
| `resource` | string | no |
| `scope` | string | no |

## Example

```http
POST https://doingwork.work/v1/oauth/authorize
content-type: application/json
Idempotency-Key: 3f1c...

{
  "client_id": "00000000-0000-4000-8000-000000000000",
  "redirect_uri": "...",
  "code_challenge": "...",
  "code_challenge_method": "S256"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `session_required` | 403 | A person has to be signed in on a browser for this step. |
| `invalid_request` | 400 | An OAuth request is malformed. |
| `invalid_target` | 400 | The resource parameter doesn't match this server. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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. Refresh tokens rotate: each works once, and reusing one revokes the whole chain. With grant_type urn:workos:agent-auth:grant-type:claim and your claim_token (auth.md), poll until the owner approves: errors are authorization_pending, slow_down, expired_token and access_denied; success hands over a named agent key once (no refresh token, it does not expire until revoked).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/oauth/token` |
| MCP tool | `auth_oauthToken` |
| Writes | yes, send an Idempotency-Key |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

Swap the code and PKCE verifier for a short-lived access token. Refresh tokens rotate: each works once, and reusing one revokes the whole chain.

## What to tell the owner

Nothing to explain.

## Input

| Field | Type | Required |
|---|---|---|
| `grant_type` | string (one of authorization_code, refresh_token, urn:workos:agent-auth:grant-type:claim) | yes |
| `code` | string | no |
| `code_verifier` | string | no |
| `client_id` | string | no |
| `redirect_uri` | string | no |
| `refresh_token` | string | no |
| `resource` | string | no |
| `claim_token` | string | no |

## Example

```http
POST https://doingwork.work/v1/oauth/token
content-type: application/json
Idempotency-Key: 3f1c...

{
  "grant_type": "authorization_code"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_grant` | 400 | The code, verifier or refresh token is wrong, used or expired. Start sign-in again. |
| `unsupported_grant_type` | 400 | Use authorization_code or refresh_token. |
| `invalid_target` | 400 | The resource parameter doesn't match this server. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# me.get

The signed-in person.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/me` |
| MCP tool | `me_get` |
| Writes | no |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

Check who you're acting for and which shops they belong to before any shop action.

## What to tell the owner

I checked which account and shop I'm working in.

## Input

| Field | Type | Required |
|---|---|---|
| (none) |  |  |

## Example

```http
GET https://doingwork.work/v1/me?
```

## Output

| Field | Type | Always there |
|---|---|---|
| `id` | string (uuid) | yes |
| `email` | ['string', 'null'] | yes |
| `display_name` | string | yes |

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "email": "owner@example.com",
  "display_name": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `unauthenticated` | 401 | No token was sent. Sign in first. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shops.create

Create a shop. The caller becomes its one owner. On the sign-up page the person also agrees to the Terms of Service (accept_terms_version), recorded with the version, time, IP address and browser; an agent cannot agree for them.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/shops` |
| MCP tool | `shops_create` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

The person doesn't have a shop yet. Ask the business name and state or province first.

## What to tell the owner

I set up your shop. You're the owner.

## Input

| Field | Type | Required |
|---|---|---|
| `name` | string | yes |
| `jurisdiction` | string | yes |
| `accept_terms_version` | string | no |

## Example

```http
POST https://doingwork.work/v1/shops
content-type: application/json
Idempotency-Key: 3f1c...

{
  "name": "Ruiz Plumbing",
  "jurisdiction": "US-ID"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input` | 400 | A field is missing, the wrong type, or unknown. The message names the field. Fix the input; don't retry as is. |
| `feature_unavailable` | 403 | This isn't available for this shop or place yet. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# members.invite

Invite a person by email as office or field. Owners and office can invite. The invite link works once.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/members/invite` |
| MCP tool | `members_invite` |
| Writes | yes, send an Idempotency-Key |
| Area | [Team (unlimited workers)](https://doingwork.work/docs/reference/#team-unlimited-workers) |

## When to use it

The owner wants to add a worker. Ask the worker's email and whether they're office or field.

## What to tell the owner

I invited them by email. They join when they accept. Workers are unlimited on every plan.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string | yes |
| `email` | string | yes |
| `role` | string (one of office, field) | yes |

## Example

```http
POST https://doingwork.work/v1/members/invite
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "email": "owner@example.com",
  "role": "office"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `conflict` | 409 | The record is in the wrong state for this, for example sending a quote that's already signed. Read it again and pick the next step. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# members.accept

Accept an invite as the signed-in person. The invite email must match yours.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/members/accept` |
| MCP tool | `members_accept` |
| Writes | yes, send an Idempotency-Key |
| Area | [Team (unlimited workers)](https://doingwork.work/docs/reference/#team-unlimited-workers) |

## When to use it

The invited worker is signed in and wants to join. The invite email must match theirs.

## What to tell the owner

You've joined the shop.

## Input

| Field | Type | Required |
|---|---|---|
| `token` | string | yes |

## Example

```http
POST https://doingwork.work/v1/members/accept
content-type: application/json
Idempotency-Key: 3f1c...

{
  "token": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_link` | 400 | The link was used, expired, or replaced by a newer one. Get a new link. |
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# members.list

Current members of a shop with role and start date.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/members` |
| MCP tool | `members_list` |
| Writes | no |
| Area | [Team (unlimited workers)](https://doingwork.work/docs/reference/#team-unlimited-workers) |

## When to use it

Answer who is on the team, their role and start date.

## What to tell the owner

Here's your team and each person's role.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string | yes |
| `include_ended` | string | no |

## Example

```http
GET https://doingwork.work/v1/members?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `members` | array | yes |
| `members[].membership_id` | string (uuid) | yes |
| `members[].person_id` | string (uuid) | yes |
| `members[].display_name` | ['string', 'null'] | yes |
| `members[].role` | string | yes |
| `members[].started_at` | string (date-time) | yes |
| `members[].ended_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `members[].ended_reason` | ['string', 'null'] | yes |

```json
{
  "members": [
    {
      "membership_id": "00000000-0000-4000-8000-000000000000",
      "person_id": "00000000-0000-4000-8000-000000000000",
      "display_name": "...",
      "role": "...",
      "started_at": "...",
      "ended_at": "...",
      "ended_reason": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# members.end

End a membership: the owner removes a worker, or a worker leaves. Shop tokens end with it. The owner cannot leave; ownership moves only by transfer.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/members/end` |
| MCP tool | `members_end` |
| Writes | yes, send an Idempotency-Key |
| Area | [Team (unlimited workers)](https://doingwork.work/docs/reference/#team-unlimited-workers) |

## When to use it

The owner removes a worker, or a worker leaves. Confirm first: their access and tokens end right away.

## What to tell the owner

They no longer have access to the shop. The owner can't leave; ownership moves only by transfer.

## Input

| Field | Type | Required |
|---|---|---|
| `membership_id` | string | yes |

## Example

```http
POST https://doingwork.work/v1/members/end
content-type: application/json
Idempotency-Key: 3f1c...

{
  "membership_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `owner_cannot_leave` | 409 | The owner can't leave a shop. Ownership moves only by transfer. |
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# members.changeRole

Owner changes a worker between office and field. The old membership ends and a new one starts, so history keeps both.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/members/role` |
| MCP tool | `members_changeRole` |
| Writes | yes, send an Idempotency-Key |
| Area | [Team (unlimited workers)](https://doingwork.work/docs/reference/#team-unlimited-workers) |

## When to use it

The owner moves a worker between office and field.

## What to tell the owner

Their role changed. Their access changed with it.

## Input

| Field | Type | Required |
|---|---|---|
| `membership_id` | string | yes |
| `role` | string (one of office, field) | yes |

## Example

```http
POST https://doingwork.work/v1/members/role
content-type: application/json
Idempotency-Key: 3f1c...

{
  "membership_id": "00000000-0000-4000-8000-000000000000",
  "role": "office"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `owner_role_fixed` | 409 | The owner's role can't be changed. |
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# jobs.create

Start a job at a home (the home is found or created from the address).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/jobs` |
| MCP tool | `jobs_create` |
| Writes | yes, send an Idempotency-Key |
| Area | [Jobs (unlimited)](https://doingwork.work/docs/reference/#jobs-unlimited) |

## When to use it

Start any new piece of work. The home is matched to earlier jobs at the same address.

## What to tell the owner

I started the job at that address. Earlier jobs at the same home are linked.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string | yes |
| `customer_id` | string | no |
| `summary` | string | no |
| `address` | object | yes |
| `address.line1` | string | yes |
| `address.line2` | string | no |
| `address.locality` | string | yes |
| `address.postal_code` | string | yes |
| `address.jurisdiction` | string | yes |

## Example

```http
POST https://doingwork.work/v1/jobs
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "address": {
    "line1": "412 Elm St",
    "locality": "Coeur d'Alene",
    "postal_code": "83814",
    "jurisdiction": "US-ID"
  }
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input` | 400 | A field is missing, the wrong type, or unknown. The message names the field. Fix the input; don't retry as is. |
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# jobs.list

Jobs of a shop, newest first. Optional status filter.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/jobs` |
| MCP tool | `jobs_list` |
| Writes | no |
| Area | [Jobs (unlimited)](https://doingwork.work/docs/reference/#jobs-unlimited) |

## When to use it

Answer what's going on: open jobs, jobs waiting on payment, recent work.

## What to tell the owner

Here are your jobs, newest first.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string | yes |
| `status` | string | no |
| `limit` | string | no |

## Example

```http
GET https://doingwork.work/v1/jobs?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `jobs` | array | yes |
| `jobs[].job_id` | string (uuid) | yes |
| `jobs[].status` | string | yes |
| `jobs[].kind` | string | yes |
| `jobs[].summary` | ['string', 'null'] | yes |
| `jobs[].occurred_at` | string (date-time) | yes |
| `jobs[].home_id` | string (uuid) | yes |
| `jobs[].address_line1` | string | yes |
| `jobs[].locality` | string | yes |
| `jobs[].customer_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |

```json
{
  "jobs": [
    {
      "job_id": "00000000-0000-4000-8000-000000000000",
      "status": "...",
      "kind": "...",
      "summary": "...",
      "occurred_at": "...",
      "home_id": "00000000-0000-4000-8000-000000000000",
      "address_line1": "...",
      "locality": "Coeur d'Alene",
      "customer_id": "00000000-0000-4000-8000-000000000000"
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# jobs.get

One job with its home address.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/jobs/get` |
| MCP tool | `jobs_get` |
| Writes | no |
| Area | [Jobs (unlimited)](https://doingwork.work/docs/reference/#jobs-unlimited) |

## When to use it

Read one job with its address before working on it.

## What to tell the owner

Here's the job.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string | yes |

## Example

```http
GET https://doingwork.work/v1/jobs/get?job_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `shop_id` | string (uuid) | yes |
| `status` | string | yes |
| `kind` | string | yes |
| `summary` | ['string', 'null'] | yes |
| `occurred_at` | string (date-time) | yes |
| `customer_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `home_id` | string (uuid) | yes |
| `address_line1` | string | yes |
| `address_line2` | ['string', 'null'] | yes |
| `locality` | string | yes |
| `postal_code` | string | yes |
| `jurisdiction_id` | string | yes |

```json
{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "status": "...",
  "kind": "...",
  "summary": "...",
  "occurred_at": "...",
  "customer_id": "00000000-0000-4000-8000-000000000000",
  "home_id": "00000000-0000-4000-8000-000000000000",
  "address_line1": "...",
  "address_line2": "...",
  "locality": "Coeur d'Alene",
  "postal_code": "83814",
  "jurisdiction_id": "00000000-0000-4000-8000-000000000000"
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `not_found` | 404 | No such record, or this worker can't see it. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# media.requestUpload

Get a signed upload URL for a job photo. Declare the sha256 of the bytes you will send; the upload is rejected if they differ. Location and camera metadata are removed before storing, and the stored hash is returned.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/media/uploads` |
| MCP tool | `media_requestUpload` |
| Writes | no |
| Area | [Job photos](https://doingwork.work/docs/reference/#job-photos) |

## When to use it

You have a photo file to add to a job. Hash the bytes (sha256), request the URL, then PUT the bytes.

## What to tell the owner

I added the photo to the job. Location and camera data were removed first.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string | yes |
| `stage` | string (one of before, during, after, other) | yes |
| `sha256` | string | yes |
| `shows_interior` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/media/uploads
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "stage": "before",
  "sha256": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input` | 400 | A field is missing, the wrong type, or unknown. The message names the field. Fix the input; don't retry as is. |
| `not_found` | 404 | No such record, or this worker can't see it. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# media.requestCapture

Hosted photo capture: a signed upload URL for a worker taking a photo in the browser.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/media/capture` |
| MCP tool | Not an MCP tool |
| Writes | no |
| Area | [Job photos](https://doingwork.work/docs/reference/#job-photos) |

## When to use it

Never call it yourself. Used by the hosted camera page when a worker takes a photo in the browser.

## What to tell the owner

Open the job's camera page to take photos.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string | yes |
| `stage` | string (one of before, during, after, other) | yes |
| `sha256` | string | yes |
| `shows_interior` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/media/capture
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "stage": "before",
  "sha256": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `human_only` | 403 | This step happens on a hosted page the person opens themselves. Send them the link; don't try another route. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# media.list

List the photos on a job that you are allowed to see.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/media` |
| MCP tool | `media_list` |
| Writes | no |
| Area | [Job photos](https://doingwork.work/docs/reference/#job-photos) |

## When to use it

See which photos a job has, by stage (before, during, after).

## What to tell the owner

Here are the photos on that job.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string | yes |

## Example

```http
GET https://doingwork.work/v1/media?job_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `not_found` | 404 | No such record, or this worker can't see it. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# media.get

One photo's details. Set include_content to get the stored bytes as base64.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/media/get` |
| MCP tool | `media_get` |
| Writes | no |
| Area | [Job photos](https://doingwork.work/docs/reference/#job-photos) |

## When to use it

Read one photo's details, or its bytes if you need to look at it.

## What to tell the owner

Here's that photo.

## Input

| Field | Type | Required |
|---|---|---|
| `media_id` | string | yes |
| `include_content` | string (one of true, false) | no |

## Example

```http
GET https://doingwork.work/v1/media/get?media_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `not_found` | 404 | No such record, or this worker can't see it. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# jobs.requestConfirmation

Get a single-use link the customer opens to confirm the work was done (or flag a problem).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/jobs/confirmation-link` |
| MCP tool | `jobs_requestConfirmation` |
| Writes | no |
| Area | [Signed job records](https://doingwork.work/docs/reference/#signed-job-records) |

## When to use it

The work is done. Ask the customer to confirm it. Send them the single-use link.

## What to tell the owner

Here's a link for your customer to confirm the work. Their answer goes on the job record.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string | yes |

## Example

```http
POST https://doingwork.work/v1/jobs/confirmation-link
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `not_found` | 404 | No such record, or this worker can't see it. |
| `conflict` | 409 | The record is in the wrong state for this, for example sending a quote that's already signed. Read it again and pick the next step. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# jobs.confirm

Customer confirms the work or flags a problem (hosted page). Writes the next signed record.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/jobs/confirm` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Signed job records](https://doingwork.work/docs/reference/#signed-job-records) |

## When to use it

Never call it. The customer confirms or flags a problem on the hosted page.

## What to tell the owner

Your customer opens the link and confirms the work, or tells you what's wrong.

## Input

| Field | Type | Required |
|---|---|---|
| `t` | string | yes |
| `outcome` | string (one of confirmed, flagged) | yes |
| `note` | string | no |

## Example

```http
POST https://doingwork.work/v1/jobs/confirm
content-type: application/json
Idempotency-Key: 3f1c...

{
  "t": "...",
  "outcome": "confirmed"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `human_only` | 403 | This step happens on a hosted page the person opens themselves. Send them the link; don't try another route. |
| `invalid_link` | 400 | The link was used, expired, or replaced by a newer one. Get a new link. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# quotes.create

Create a quote from lines the worker wrote. Lines are never filled in by AI. Optional: good, better, best options and optional lines the customer picks when signing.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/quotes` |
| MCP tool | `quotes_create` |
| Writes | yes, send an Idempotency-Key |
| Area | [Quotes (unlimited)](https://doingwork.work/docs/reference/#quotes-unlimited) |

## When to use it

The worker has told you the lines and prices. Write exactly what they said. Never invent or fill in a price.

## What to tell the owner

I wrote up the quote with your lines and prices. Check it before I send it.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string | yes |
| `lines` | array | yes |
| `lines[].description` | string | yes |
| `lines[].quantity` | number | yes |
| `lines[].unit_price_minor` | integer | yes |
| `lines[].option_key` | string | no |
| `lines[].optional` | boolean | no |
| `lines[].kind` | string (one of item, fee, discount) | no |
| `lines[].taxable` | boolean | no |
| `options` | array | no |
| `options[].key` | string | yes |
| `options[].label` | string | yes |
| `tax_rate_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | no |

## Example

```http
POST https://doingwork.work/v1/quotes
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "lines": [
    {
      "description": "40 gallon gas heater, installed",
      "quantity": "...",
      "unit_price_minor": 50000
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input` | 400 | A field is missing, the wrong type, or unknown. The message names the field. Fix the input; don't retry as is. |
| `not_found` | 404 | No such record, or this worker can't see it. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# quotes.send

Lock the quote and get a single-use signing link for the customer.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/quotes/send` |
| MCP tool | `quotes_send` |
| Writes | yes, send an Idempotency-Key |
| Area | [Quotes (unlimited)](https://doingwork.work/docs/reference/#quotes-unlimited) |

## When to use it

The owner approved the quote. Lock it and send the customer the signing link.

## What to tell the owner

The quote is locked. Here's the signing link for your customer.

## Input

| Field | Type | Required |
|---|---|---|
| `quote_id` | string | yes |

## Example

```http
POST https://doingwork.work/v1/quotes/send
content-type: application/json
Idempotency-Key: 3f1c...

{
  "quote_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `conflict` | 409 | The record is in the wrong state for this, for example sending a quote that's already signed. Read it again and pick the next step. |
| `not_found` | 404 | No such record, or this worker can't see it. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# quotes.sign

Sign a quote after accepting electronic signing. Hosted page only.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/quotes/sign` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Quotes (unlimited)](https://doingwork.work/docs/reference/#quotes-unlimited) |

## When to use it

Never call it. The customer signs on the hosted page after agreeing to sign electronically.

## What to tell the owner

Your customer signs on their phone. In Idaho, quotes over $2,000 show the state disclosure and they acknowledge it.

## Input

| Field | Type | Required |
|---|---|---|
| `t` | string | yes |
| `document_hash` | string | yes |
| `esign_consent` | string (one of yes) | yes |
| `disclosure_ack` | string (one of yes) | no |
| `option` | string | no |
| `optional_lines` | string | no |

## Example

```http
POST https://doingwork.work/v1/quotes/sign
content-type: application/json
Idempotency-Key: 3f1c...

{
  "t": "...",
  "document_hash": "...",
  "esign_consent": "yes"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `human_only` | 403 | This step happens on a hosted page the person opens themselves. Send them the link; don't try another route. |
| `consent_required` | 400 | A person must tick a consent box on a hosted page (for example e-sign). Send them the link. |
| `disclosure_ack_required` | 400 | The customer must acknowledge the Idaho disclosure on the hosted signing page. |
| `invalid_link` | 400 | The link was used, expired, or replaced by a newer one. Get a new link. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# quotes.revise

Replace an unsigned quote with a new version. Earlier signing links stop working; send it again for a new link.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/quotes/revise` |
| MCP tool | `quotes_revise` |
| Writes | yes, send an Idempotency-Key |
| Area | [Quotes (unlimited)](https://doingwork.work/docs/reference/#quotes-unlimited) |

## When to use it

The worker changes an unsigned quote. Earlier links stop working; send the new version.

## What to tell the owner

I replaced the quote with the new version. Send it again so they sign the right one.

## Input

| Field | Type | Required |
|---|---|---|
| `quote_id` | string | yes |
| `lines` | array | yes |
| `lines[].description` | string | yes |
| `lines[].quantity` | number | yes |
| `lines[].unit_price_minor` | integer | yes |
| `lines[].option_key` | string | no |
| `lines[].optional` | boolean | no |
| `lines[].kind` | string (one of item, fee, discount) | no |
| `lines[].taxable` | boolean | no |
| `options` | array | no |
| `options[].key` | string | yes |
| `options[].label` | string | yes |
| `tax_rate_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | no |

## Example

```http
POST https://doingwork.work/v1/quotes/revise
content-type: application/json
Idempotency-Key: 3f1c...

{
  "quote_id": "00000000-0000-4000-8000-000000000000",
  "lines": [
    {
      "description": "40 gallon gas heater, installed",
      "quantity": "...",
      "unit_price_minor": 50000
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `conflict` | 409 | The record is in the wrong state for this, for example sending a quote that's already signed. Read it again and pick the next step. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# quotes.withdraw

Withdraw a draft or sent quote the customer has not signed, with an optional reason. Signing links stop working and follow-ups stop. A withdrawn quote cannot be reopened; create a new quote if needed.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/quotes/withdraw` |
| MCP tool | `quotes_withdraw` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Withdraw a draft or sent quote the customer has not signed, with an optional reason. Signing links stop working and follow-ups stop.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `quote_id` | string (uuid) | yes |
| `reason` | string | no |

## Example

```http
POST https://doingwork.work/v1/quotes/withdraw
content-type: application/json
Idempotency-Key: 3f1c...

{
  "quote_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/subcontractors` |
| MCP tool | `subcontractors_get` |
| Writes | no |
| Area | [Idaho compliance](https://doingwork.work/docs/reference/#idaho-compliance) |

## When to use it

Idaho jobs: read the subcontractor and supplier statement for a job.

## What to tell the owner

Here's the Idaho subcontractor statement for this job.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string | yes |

## Example

```http
GET https://doingwork.work/v1/subcontractors?job_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `not_found` | 404 | No such record, or this worker can't see it. |
| `feature_unavailable` | 403 | This isn't available for this shop or place yet. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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". Does not sign.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/subcontractors/prepare` |
| MCP tool | `subcontractors_prepare` |
| Writes | yes, send an Idempotency-Key |
| Area | [Idaho compliance](https://doingwork.work/docs/reference/#idaho-compliance) |

## When to use it

Idaho jobs before final payment: list subcontractors and suppliers over $500, or attest there were none. The owner then signs.

## What to tell the owner

I prepared the Idaho subcontractor statement. You sign it before the final payment.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string | yes |
| `entries` | array | no |
| `none_attestation` | string (one of yes) | no |

## Example

```http
POST https://doingwork.work/v1/subcontractors/prepare
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input` | 400 | A field is missing, the wrong type, or unknown. The message names the field. Fix the input; don't retry as is. |
| `feature_unavailable` | 403 | This isn't available for this shop or place yet. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/subcontractors/sign` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Idaho compliance](https://doingwork.work/docs/reference/#idaho-compliance) |

## When to use it

Never call it. The owner or office signs on the hosted page.

## What to tell the owner

Open the link and sign the statement.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string | yes |
| `entries` | string | no |
| `none_attestation` | string (one of yes) | no |
| `esign_consent` | string (one of yes) | yes |

## Example

```http
POST https://doingwork.work/v1/subcontractors/sign
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "esign_consent": "yes"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `human_only` | 403 | This step happens on a hosted page the person opens themselves. Send them the link; don't try another route. |
| `consent_required` | 400 | A person must tick a consent box on a hosted page (for example e-sign). Send them the link. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# payments.connectStart

Start payment setup for a shop (owner, hosted page).

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/payments/connect` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Stripe connection](https://doingwork.work/docs/reference/#stripe-connection) |

## When to use it

Never call it. The owner connects their own Stripe account on the hosted page.

## What to tell the owner

Open the link to connect your Stripe account. Customers then pay you directly.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string | yes |

## Example

```http
POST https://doingwork.work/v1/payments/connect
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `human_only` | 403 | This step happens on a hosted page the person opens themselves. Send them the link; don't try another route. |
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# invoices.createFromQuote

Create an invoice for a signed quote, for the total of the option and optional lines the customer picked.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/invoices` |
| MCP tool | `invoices_createFromQuote` |
| Writes | yes, send an Idempotency-Key |
| Area | [Invoices and payments (unlimited)](https://doingwork.work/docs/reference/#invoices-and-payments-unlimited) |

## When to use it

The quote is signed. Make the invoice, with a deposit if the owner wants one.

## What to tell the owner

I made the invoice from the signed quote.

## Input

| Field | Type | Required |
|---|---|---|
| `quote_id` | string | yes |
| `deposit_minor` | integer | no |

## Example

```http
POST https://doingwork.work/v1/invoices
content-type: application/json
Idempotency-Key: 3f1c...

{
  "quote_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `conflict` | 409 | The record is in the wrong state for this, for example sending a quote that's already signed. Read it again and pick the next step. |
| `not_found` | 404 | No such record, or this worker can't see it. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# invoices.send

Send an invoice (or the next part of it) and get a single-use pay link. Earlier pay links stop working. payments_ready false means the shop has no active payments account yet (after a hand-off, connect one with payments.connectStart); the link shows payment isn't available until it does.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/invoices/send` |
| MCP tool | `invoices_send` |
| Writes | yes, send an Idempotency-Key |
| Area | [Invoices and payments (unlimited)](https://doingwork.work/docs/reference/#invoices-and-payments-unlimited) |

## When to use it

Send the invoice, or the next part of it, and get a pay link for the customer.

## What to tell the owner

Here's the pay link for your customer. The money goes to your Stripe account.

## Input

| Field | Type | Required |
|---|---|---|
| `invoice_id` | string | yes |
| `amount_minor` | integer | no |
| `percent` | number | no |

## Example

```http
POST https://doingwork.work/v1/invoices/send
content-type: application/json
Idempotency-Key: 3f1c...

{
  "invoice_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `payments_not_set_up` | 409 | The shop hasn't connected Stripe. The owner does that on a hosted page. |
| `conflict` | 409 | The record is in the wrong state for this, for example sending a quote that's already signed. Read it again and pick the next step. |
| `feature_unavailable` | 403 | This isn't available for this shop or place yet. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# invoices.pay

Start paying an invoice (hosted page). Returns the processor checkout URL. Final payment on an Idaho job over the threshold needs the payer to acknowledge the signed subcontractor list. Always collects on the shop's current active payments account; refused with payments_not_set_up while none is active, and with payments_paused while Stripe restricts the account.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/invoices/pay` |
| MCP tool | Not an MCP tool |
| Writes | no |
| Area | [Invoices and payments (unlimited)](https://doingwork.work/docs/reference/#invoices-and-payments-unlimited) |

## When to use it

Never call it. The customer pays on the hosted page.

## What to tell the owner

Your customer pays from the link.

## Input

| Field | Type | Required |
|---|---|---|
| `t` | string | yes |
| `subcontractor_ack` | string (one of yes, no) | no |
| `tip_minor` | integer | no |
| `tip_percent` | string | no |

## Example

```http
POST https://doingwork.work/v1/invoices/pay
content-type: application/json
Idempotency-Key: 3f1c...

{
  "t": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `human_only` | 403 | This step happens on a hosted page the person opens themselves. Send them the link; don't try another route. |
| `invalid_link` | 400 | The link was used, expired, or replaced by a newer one. Get a new link. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# invoices.markPaidOutside

Record that the customer paid an invoice outside Doing Work (check, cash, financing, insurance or other). Owner or office only, with their own sign-in. Closes the invoice, and the job too unless its checklists still block done (job_status says which; finish them and call jobs.complete). The job record says "Paid outside Doing Work", is never marked paid or verified, and no Doing Work receipt goes to the homeowner. There is no limit on jobs paid outside Doing Work and no fee on them.

|  |  |
|---|---|
| Who | A person's own sign-in only (agent tokens are refused) |
| REST | `POST /v1/invoices/paid-outside` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Invoices and payments (unlimited)](https://doingwork.work/docs/reference/#invoices-and-payments-unlimited) |

## When to use it

Agents can't call this. Tell the owner or office to record a cash, check, financing or insurance payment with their own sign-in.

## What to tell the owner

Record that payment yourself in Doing Work. Pay as you go includes 3 a month; paid plans have no limit.

## Input

| Field | Type | Required |
|---|---|---|
| `invoice_id` | string | yes |
| `method` | string (one of check, cash, financing, insurance, other) | yes |
| `paid_on` | string | yes |
| `note` | string | no |

## Example

```http
POST https://doingwork.work/v1/invoices/paid-outside
content-type: application/json
Idempotency-Key: 3f1c...

{
  "invoice_id": "00000000-0000-4000-8000-000000000000",
  "method": "check",
  "paid_on": "2026-10-31"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `outside_pay_limit` |  |  |
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# invoices.refund

Refund part or all of a payment (owner or office). The refund shows up once the processor confirms it.

|  |  |
|---|---|
| Who | A person's own sign-in only (agent tokens are refused) |
| REST | `POST /v1/invoices/refund` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Invoices and payments (unlimited)](https://doingwork.work/docs/reference/#invoices-and-payments-unlimited) |

## When to use it

Agents can't call this. The owner or office refunds with their own sign-in.

## What to tell the owner

Do the refund yourself with your own sign-in. It shows once Stripe confirms it.

## Input

| Field | Type | Required |
|---|---|---|
| `charge_id` | string | yes |
| `amount_minor` | integer | yes |

## Example

```http
POST https://doingwork.work/v1/invoices/refund
content-type: application/json
Idempotency-Key: 3f1c...

{
  "charge_id": "00000000-0000-4000-8000-000000000000",
  "amount_minor": 50000
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `conflict` | 409 | The record is in the wrong state for this, for example sending a quote that's already signed. Read it again and pick the next step. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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. No plans, no monthly fee, no card on file. Every shop gets every feature and unlimited workers.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/fees/schedule` |
| MCP tool | `fees_schedule` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Doing Work's fee schedule: one stepped rate on each shop's calendar month of card and bank payments, taken out of the payments. No plans, no monthly fee, no card on file.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| (none) |  |  |

## Example

```http
GET https://doingwork.work/v1/fees/schedule?
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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). Leave out amount_minor for just the month picture and the next band line.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/fees/preview` |
| MCP tool | `fees_preview` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

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). Leave out amount_minor for just the month picture and the next band line.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string | yes |
| `amount_minor` | integer | no |

## Example

```http
GET https://doingwork.work/v1/fees/preview?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `shop_id` | string | no |
| `period` | string | no |
| `month_volume_minor` | integer | no |
| `amount_minor` | ['integer', 'null'] | no |
| `fee_minor` | ['integer', 'null'] | no |
| `effective_rate_bps` | ['number', 'null'] | no |
| `current_rate_bps` | integer | no |
| `to_next_band_minor` | ['integer', 'null'] | no |
| `next_rate_bps` | ['integer', 'null'] | no |
| `next_band_line` | string | no |

```json
{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "period": "...",
  "month_volume_minor": 50000,
  "amount_minor": "...",
  "fee_minor": "...",
  "effective_rate_bps": "...",
  "current_rate_bps": 1,
  "to_next_band_minor": "...",
  "next_rate_bps": "...",
  "next_band_line": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/fees/statement` |
| MCP tool | `fees_statement` |
| Writes | no |
| Area | [Plans and fees](https://doingwork.work/docs/reference/#plans-and-fees) |

## When to use it

Answer what Doing Work charged this month, payment by payment.

## What to tell the owner

Here's this month's Doing Work fees, by payment.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string | yes |
| `month` | string | yes |

## Example

```http
GET https://doingwork.work/v1/fees/statement?shop_id=00000000-0000-4000-8000-000000000000&month=2026-10
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# records.keys

Public keys that verify signed job records.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/records/keys` |
| MCP tool | `records_keys` |
| Writes | no |
| Area | [Signed job records](https://doingwork.work/docs/reference/#signed-job-records) |

## When to use it

Verify a signed job record's signature.

## What to tell the owner

Nothing to explain.

## Input

| Field | Type | Required |
|---|---|---|
| (none) |  |  |

## Example

```http
GET https://doingwork.work/v1/records/keys?
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# jobs.record

The latest signed record for a job and its level (logged, paid, verified).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/jobs/record` |
| MCP tool | `jobs_record` |
| Writes | no |
| Area | [Signed job records](https://doingwork.work/docs/reference/#signed-job-records) |

## When to use it

Show proof of a job: logged, paid, or verified by the customer.

## What to tell the owner

Here's the signed record for this job.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string | yes |

## Example

```http
GET https://doingwork.work/v1/jobs/record?job_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `not_found` | 404 | No such record, or this worker can't see it. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# webhooks.create

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

|  |  |
|---|---|
| Who | A person's own sign-in only (agent tokens are refused) |
| REST | `POST /v1/webhooks` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Developers and platforms](https://doingwork.work/docs/reference/#developers-and-platforms) |

## When to use it

Agents can't call this. A developer adds an endpoint with their own sign-in.

## What to tell the owner

Nothing to explain.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string | yes |
| `url` | string | yes |
| `events` | array | no |

## Example

```http
POST https://doingwork.work/v1/webhooks
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "url": "https://example.com/hooks/doing-work"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `invalid_input` | 400 | A field is missing, the wrong type, or unknown. The message names the field. Fix the input; don't retry as is. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# webhooks.list

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/webhooks` |
| MCP tool | `webhooks_list` |
| Writes | no |
| Area | [Developers and platforms](https://doingwork.work/docs/reference/#developers-and-platforms) |

## When to use it

List a shop's webhook endpoints and delivery counts.

## What to tell the owner

Nothing to explain.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string | yes |

## Example

```http
GET https://doingwork.work/v1/webhooks?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# webhooks.delete

Remove a webhook endpoint. Pending deliveries to it stop.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/webhooks/delete` |
| MCP tool | `webhooks_delete` |
| Writes | yes, send an Idempotency-Key |
| Area | [Developers and platforms](https://doingwork.work/docs/reference/#developers-and-platforms) |

## When to use it

Remove a webhook endpoint.

## What to tell the owner

Nothing to explain.

## Input

| Field | Type | Required |
|---|---|---|
| `endpoint_id` | string | yes |

## Example

```http
POST https://doingwork.work/v1/webhooks/delete
content-type: application/json
Idempotency-Key: 3f1c...

{
  "endpoint_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `not_found` | 404 | No such record, or this worker can't see it. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# audit.list

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/audit` |
| MCP tool | `audit_list` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

The owner asks who did what. Owner only.

## What to tell the owner

Here's the log of who changed what.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string | yes |
| `limit` | integer | no |
| `before_id` | integer | no |

## Example

```http
GET https://doingwork.work/v1/audit?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# platforms.create

Create a developer platform account (private beta). The caller owns it.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/platforms` |
| MCP tool | `platforms_create` |
| Writes | yes, send an Idempotency-Key |
| Area | [Developers and platforms](https://doingwork.work/docs/reference/#developers-and-platforms) |

## When to use it

A software company wants to manage shops for its customers (private beta).

## What to tell the owner

Nothing to explain.

## Input

| Field | Type | Required |
|---|---|---|
| `name` | string | yes |
| `terms_version` | string | yes |

## Example

```http
POST https://doingwork.work/v1/platforms
content-type: application/json
Idempotency-Key: 3f1c...

{
  "name": "Ruiz Plumbing",
  "terms_version": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input` | 400 | A field is missing, the wrong type, or unknown. The message names the field. Fix the input; don't retry as is. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# platforms.list

List the platform accounts you own and the shops each one manages.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/platforms` |
| MCP tool | `platforms_list` |
| Writes | no |
| Area | [Developers and platforms](https://doingwork.work/docs/reference/#developers-and-platforms) |

## When to use it

List your platform accounts and the shops each manages.

## What to tell the owner

Nothing to explain.

## Input

| Field | Type | Required |
|---|---|---|
| (none) |  |  |

## Example

```http
GET https://doingwork.work/v1/platforms?
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# platforms.grantManage

Let a platform manage this shop. Shop owner only, on a hosted page.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/platforms/grant` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Developers and platforms](https://doingwork.work/docs/reference/#developers-and-platforms) |

## When to use it

Never call it. The shop owner allows a platform on the hosted page.

## What to tell the owner

Open the link to let that software manage your shop. You can stop it anytime.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string | yes |
| `platform_id` | string | yes |
| `billing_mode` | string (one of shop_pays, platform_pays) | no |

## Example

```http
POST https://doingwork.work/v1/platforms/grant
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "platform_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `human_only` | 403 | This step happens on a hosted page the person opens themselves. Send them the link; don't try another route. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# platforms.revokeManage

Stop a platform from managing this shop. Shop owner only.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/platforms/revoke` |
| MCP tool | `platforms_revokeManage` |
| Writes | yes, send an Idempotency-Key |
| Area | [Developers and platforms](https://doingwork.work/docs/reference/#developers-and-platforms) |

## When to use it

The owner wants a platform to stop managing their shop.

## What to tell the owner

That software no longer has access.

## Input

| Field | Type | Required |
|---|---|---|
| `grant_id` | string | yes |

## Example

```http
POST https://doingwork.work/v1/platforms/revoke
content-type: application/json
Idempotency-Key: 3f1c...

{
  "grant_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.create

Add a customer to a shop, with kind, company, email, phone, billing address, lead source, tags and notes in one call.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/customers` |
| MCP tool | `customers_create` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customers](https://doingwork.work/docs/reference/#customers) |

## When to use it

A new customer. Ask the name, and the email if they want quotes and invoices sent by email.

## What to tell the owner

I added the customer.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string | yes |
| `name` | string | yes |
| `kind` | string (one of person, company) | no |
| `company_name` | string | no |
| `email` | string | no |
| `phone` | string | no |
| `billing_address` | object | no |
| `billing_address.line1` | string | no |
| `billing_address.line2` | string | no |
| `billing_address.locality` | string | no |
| `billing_address.region` | string | no |
| `billing_address.postal_code` | string | no |
| `billing_address.country` | string | no |
| `lead_source` | string | no |
| `tags` | array | no |
| `notes` | string | no |

## Example

```http
POST https://doingwork.work/v1/customers
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "name": "Ruiz Plumbing"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input` | 400 | A field is missing, the wrong type, or unknown. The message names the field. Fix the input; don't retry as is. |
| `forbidden` | 403 | This worker's role can't do this, or it needs a person's own sign-in rather than an agent token. Tell the person who can. |
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.get

One customer with contacts, properties, custom fields and open balance.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/customers/get` |
| MCP tool | `customers_get` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

One customer with contacts, properties, custom fields and open balance.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/customers/get?customer_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `customer_id` | string (uuid) | yes |
| `shop_id` | string (uuid) | yes |
| `kind` | string | yes |
| `name` | string | yes |
| `company_name` | ['string', 'null'] | yes |
| `email` | ['string', 'null'] | yes |
| `phone` | ['string', 'null'] | yes |
| `billing_address` | ['object', 'null'] | yes |
| `lead_source` | ['string', 'null'] | yes |
| `tags` | array | yes |
| `notes` | ['string', 'null'] | yes |
| `archived_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `merged_into` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `erased_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `created_at` | string (date-time) | yes |
| `updated_at` | string (date-time) | yes |
| `contacts` | array | yes |
| `contacts[].contact_id` | string (uuid) | yes |
| `contacts[].name` | string | yes |
| `contacts[].email` | ['string', 'null'] | yes |
| `contacts[].phone` | ['string', 'null'] | yes |
| `contacts[].role` | ['string', 'null'] | yes |
| `contacts[].is_primary` | boolean | yes |
| `properties` | array | yes |
| `properties[].property_id` | string (uuid) | yes |
| `properties[].home_id` | string (uuid) | yes |
| `properties[].label` | ['string', 'null'] | yes |
| `properties[].address_line1` | string | yes |
| `properties[].address_line2` | ['string', 'null'] | yes |
| `properties[].locality` | string | yes |
| `properties[].postal_code` | string | yes |
| `properties[].jurisdiction_id` | string | yes |
| `properties[].access_notes` | ['string', 'null'] | yes |
| `fields` | array | yes |
| `fields[].key` | string | yes |
| `fields[].target` | string | yes |
| `fields[].type` | string | yes |
| `fields[].value` |  | yes |
| `fields[].currency` | ['string', 'null'] | yes |
| `fields[].written_at` | string (date-time) | yes |
| `open_balance_minor` | integer | yes |

```json
{
  "customer_id": "00000000-0000-4000-8000-000000000000",
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "kind": "...",
  "name": "Ruiz Plumbing",
  "company_name": "...",
  "email": "owner@example.com",
  "phone": "...",
  "billing_address": "...",
  "lead_source": "...",
  "tags": [
    "..."
  ],
  "notes": "...",
  "archived_at": "...",
  "merged_into": "00000000-0000-4000-8000-000000000000",
  "erased_at": "...",
  "created_at": "...",
  "updated_at": "...",
  "contacts": [
    {
      "contact_id": "00000000-0000-4000-8000-000000000000",
      "name": "Ruiz Plumbing",
      "email": "owner@example.com",
      "phone": "...",
      "role": "...",
      "is_primary": true
    }
  ],
  "properties": [
    {
      "property_id": "00000000-0000-4000-8000-000000000000",
      "home_id": "00000000-0000-4000-8000-000000000000",
      "label": "...",
      "address_line1": "...",
      "address_line2": "...",
      "locality": "Coeur d'Alene",
      "postal_code": "83814",
      "jurisdiction_id": "00000000-0000-4000-8000-000000000000",
      "access_notes": "..."
    }
  ],
  "fields": [
    {
      "key": "...",
      "target": "...",
      "type": "...",
      "value": "...",
      "currency": "...",
      "written_at": "..."
    }
  ],
  "open_balance_minor": 50000
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.list

Customers of a shop, newest first, filtered by tag, lead source or archived.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/customers` |
| MCP tool | `customers_list` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Customers of a shop, newest first, filtered by tag, lead source or archived.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `tag` | string | no |
| `lead_source` | string | no |
| `archived` | boolean | no |
| `limit` | integer | no |
| `cursor` | string | no |

## Example

```http
GET https://doingwork.work/v1/customers?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `data` | array | yes |
| `data[].customer_id` | string (uuid) | yes |
| `data[].shop_id` | string (uuid) | yes |
| `data[].kind` | string | yes |
| `data[].name` | string | yes |
| `data[].company_name` | ['string', 'null'] | yes |
| `data[].email` | ['string', 'null'] | yes |
| `data[].phone` | ['string', 'null'] | yes |
| `data[].billing_address` | ['object', 'null'] | yes |
| `data[].lead_source` | ['string', 'null'] | yes |
| `data[].tags` | array | yes |
| `data[].notes` | ['string', 'null'] | yes |
| `data[].archived_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `data[].merged_into` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `data[].erased_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `data[].created_at` | string (date-time) | yes |
| `data[].updated_at` | string (date-time) | yes |
| `next_cursor` | ['string', 'null'] | yes |

```json
{
  "data": [
    {
      "customer_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "kind": "...",
      "name": "Ruiz Plumbing",
      "company_name": "...",
      "email": "owner@example.com",
      "phone": "...",
      "billing_address": "...",
      "lead_source": "...",
      "tags": [
        "..."
      ],
      "notes": "...",
      "archived_at": "...",
      "merged_into": "00000000-0000-4000-8000-000000000000",
      "erased_at": "...",
      "created_at": "...",
      "updated_at": "..."
    }
  ],
  "next_cursor": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.search

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/customers/search` |
| MCP tool | `customers_search` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

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

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `q` | string | yes |
| `include_archived` | boolean | no |
| `limit` | integer | no |

## Example

```http
GET https://doingwork.work/v1/customers/search?shop_id=00000000-0000-4000-8000-000000000000&q=...
```

## Output

| Field | Type | Always there |
|---|---|---|
| `data` | array | yes |
| `data[].customer_id` | string (uuid) | yes |
| `data[].shop_id` | string (uuid) | yes |
| `data[].kind` | string | yes |
| `data[].name` | string | yes |
| `data[].company_name` | ['string', 'null'] | yes |
| `data[].email` | ['string', 'null'] | yes |
| `data[].phone` | ['string', 'null'] | yes |
| `data[].billing_address` | ['object', 'null'] | yes |
| `data[].lead_source` | ['string', 'null'] | yes |
| `data[].tags` | array | yes |
| `data[].notes` | ['string', 'null'] | yes |
| `data[].archived_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `data[].merged_into` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `data[].erased_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `data[].created_at` | string (date-time) | yes |
| `data[].updated_at` | string (date-time) | yes |

```json
{
  "data": [
    {
      "customer_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "kind": "...",
      "name": "Ruiz Plumbing",
      "company_name": "...",
      "email": "owner@example.com",
      "phone": "...",
      "billing_address": "...",
      "lead_source": "...",
      "tags": [
        "..."
      ],
      "notes": "...",
      "archived_at": "...",
      "merged_into": "00000000-0000-4000-8000-000000000000",
      "erased_at": "...",
      "created_at": "...",
      "updated_at": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.update

Change a customer's name, kind, company, email, phone, billing address, lead source, tags or notes.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/customers/update` |
| MCP tool | `customers_update` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Change a customer's name, kind, company, email, phone, billing address, lead source, tags or notes.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |
| `name` | string | no |
| `kind` | string (one of person, company) | no |
| `company_name` | string | no |
| `email` | string | no |
| `phone` | string | no |
| `billing_address` | object | no |
| `billing_address.line1` | string | no |
| `billing_address.line2` | string | no |
| `billing_address.locality` | string | no |
| `billing_address.region` | string | no |
| `billing_address.postal_code` | string | no |
| `billing_address.country` | string | no |
| `lead_source` | string | no |
| `tags` | array | no |
| `notes` | string | no |

## Example

```http
POST https://doingwork.work/v1/customers/update
content-type: application/json
Idempotency-Key: 3f1c...

{
  "customer_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.archive

Archive a customer (hidden from lists, history kept) or bring one back.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/customers/archive` |
| MCP tool | `customers_archive` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Archive a customer (hidden from lists, history kept) or bring one back.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |
| `archived` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/customers/archive
content-type: application/json
Idempotency-Key: 3f1c...

{
  "customer_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.duplicates

Likely duplicate customers in a shop: same email, same phone digits, or same name.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/customers/duplicates` |
| MCP tool | `customers_duplicates` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Likely duplicate customers in a shop: same email, same phone digits, or same name.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `customer_id` | string (uuid) | no |

## Example

```http
GET https://doingwork.work/v1/customers/duplicates?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.merge

Merge a duplicate customer into another: jobs, requests, contacts, properties and fields move over.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/customers/merge` |
| MCP tool | `customers_merge` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Merge a duplicate customer into another: jobs, requests, contacts, properties and fields move over.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `from_customer_id` | string (uuid) | yes |
| `into_customer_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/customers/merge
content-type: application/json
Idempotency-Key: 3f1c...

{
  "from_customer_id": "00000000-0000-4000-8000-000000000000",
  "into_customer_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.contactAdd

Add a contact person to a customer (tenant, manager, billing, spouse).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/customers/contacts` |
| MCP tool | `customers_contactAdd` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Add a contact person to a customer (tenant, manager, billing, spouse).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |
| `name` | string | yes |
| `email` | string | no |
| `phone` | string | no |
| `role` | string | no |
| `is_primary` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/customers/contacts
content-type: application/json
Idempotency-Key: 3f1c...

{
  "customer_id": "00000000-0000-4000-8000-000000000000",
  "name": "Ruiz Plumbing"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.contactUpdate

Change or remove one contact of a customer.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/customers/contacts/update` |
| MCP tool | `customers_contactUpdate` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Change or remove one contact of a customer.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `contact_id` | string (uuid) | yes |
| `name` | string | no |
| `email` | string | no |
| `phone` | string | no |
| `role` | string | no |
| `is_primary` | boolean | no |
| `removed` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/customers/contacts/update
content-type: application/json
Idempotency-Key: 3f1c...

{
  "contact_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# properties.add

Add a service address to a customer, with optional label and access notes.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/properties` |
| MCP tool | `properties_add` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customers](https://doingwork.work/docs/reference/#customers) |

## When to use it

Add a service address to a customer, with optional label and access notes.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |
| `address` | object | yes |
| `address.line1` | string | yes |
| `address.line2` | string | no |
| `address.locality` | string | yes |
| `address.postal_code` | string | yes |
| `address.jurisdiction` | string | yes |
| `label` | string | no |
| `access_notes` | string | no |

## Example

```http
POST https://doingwork.work/v1/properties
content-type: application/json
Idempotency-Key: 3f1c...

{
  "customer_id": "00000000-0000-4000-8000-000000000000",
  "address": {
    "line1": "412 Elm St",
    "locality": "Coeur d'Alene",
    "postal_code": "83814",
    "jurisdiction": "US-ID"
  }
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# properties.list

Service addresses of a customer, or every property of a shop.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/properties` |
| MCP tool | `properties_list` |
| Writes | no |
| Area | [Customers](https://doingwork.work/docs/reference/#customers) |

## When to use it

Service addresses of a customer, or every property of a shop.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | no |
| `customer_id` | string (uuid) | no |
| `include_archived` | boolean | no |
| `limit` | integer | no |
| `cursor` | string | no |

## Example

```http
GET https://doingwork.work/v1/properties?shop_id=00000000-0000-4000-8000-000000000000&customer_id=00000000-0000-4000-8000-000000000000&include_archived=True&limit=1&cursor=...
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# properties.update

Change a property's label or access notes, or archive it.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/properties/update` |
| MCP tool | `properties_update` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customers](https://doingwork.work/docs/reference/#customers) |

## When to use it

Change a property's label or access notes, or archive it.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `property_id` | string (uuid) | yes |
| `label` | string | no |
| `access_notes` | string | no |
| `archived` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/properties/update
content-type: application/json
Idempotency-Key: 3f1c...

{
  "property_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# equipment.add

Record equipment at a property: kind, make, model, serial, install date and warranty.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/equipment` |
| MCP tool | `equipment_add` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customers](https://doingwork.work/docs/reference/#customers) |

## When to use it

Record equipment at a property: kind, make, model, serial, install date and warranty.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `property_id` | string (uuid) | yes |
| `kind` | string | yes |
| `make` | string | no |
| `model` | string | no |
| `serial` | string | no |
| `install_date` | string | no |
| `warranty_until` | string | no |
| `notes` | string | no |

## Example

```http
POST https://doingwork.work/v1/equipment
content-type: application/json
Idempotency-Key: 3f1c...

{
  "property_id": "00000000-0000-4000-8000-000000000000",
  "kind": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# equipment.list

Equipment at a property, or warranties ending soon across a shop.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/equipment` |
| MCP tool | `equipment_list` |
| Writes | no |
| Area | [Customers](https://doingwork.work/docs/reference/#customers) |

## When to use it

Equipment at a property, or warranties ending soon across a shop.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `property_id` | string (uuid) | no |
| `shop_id` | string (uuid) | no |
| `warranty_ends_within_days` | integer | no |
| `include_archived` | boolean | no |

## Example

```http
GET https://doingwork.work/v1/equipment?property_id=00000000-0000-4000-8000-000000000000&shop_id=00000000-0000-4000-8000-000000000000&warranty_ends_within_days=1&include_archived=True
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# equipment.update

Change or archive an equipment record.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/equipment/update` |
| MCP tool | `equipment_update` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customers](https://doingwork.work/docs/reference/#customers) |

## When to use it

Change or archive an equipment record.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `equipment_id` | string (uuid) | yes |
| `kind` | string | no |
| `make` | string | no |
| `model` | string | no |
| `serial` | string | no |
| `install_date` | string | no |
| `warranty_until` | string | no |
| `notes` | string | no |
| `archived` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/equipment/update
content-type: application/json
Idempotency-Key: 3f1c...

{
  "equipment_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# jobs.setTags

Set the tags and lead source on a job.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/jobs/tags` |
| MCP tool | `jobs_setTags` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Set the tags and lead source on a job.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `tags` | array | no |
| `lead_source` | string | no |

## Example

```http
POST https://doingwork.work/v1/jobs/tags
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.timeline

Everything that happened with a customer, newest first: requests, jobs, quotes, signatures, invoices and payments.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/customers/timeline` |
| MCP tool | `customers_timeline` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Everything that happened with a customer, newest first: requests, jobs, quotes, signatures, invoices and payments.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |
| `limit` | integer | no |
| `cursor` | string | no |

## Example

```http
GET https://doingwork.work/v1/customers/timeline?customer_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.statement

A customer's account statement: every invoice with total, paid and balance, and the total owed.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/customers/statement` |
| MCP tool | `customers_statement` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

A customer's account statement: every invoice with total, paid and balance, and the total owed.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/customers/statement?customer_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.privacyExport

Everything the shop holds about one customer, as one JSON document, for a privacy request.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/customers/export` |
| MCP tool | `customers_privacyExport` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Everything the shop holds about one customer, as one JSON document, for a privacy request.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/customers/export?customer_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.privacyDelete

Erase a customer's personal details for a privacy request. Money and job records stay, unnamed. Owner only, never an agent.

|  |  |
|---|---|
| Who | A person's own sign-in only (agent tokens are refused) |
| REST | `POST /v1/customers/erase` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Erase a customer's personal details for a privacy request. Money and job records stay, unnamed.

## What to tell the owner

Ask the person to do this with their own sign-in. Your agent token is refused here.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |
| `confirm` | string (one of erase) | yes |

## Example

```http
POST https://doingwork.work/v1/customers/erase
content-type: application/json
Idempotency-Key: 3f1c...

{
  "customer_id": "00000000-0000-4000-8000-000000000000",
  "confirm": "erase"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# fields.define

Add a custom field to customers, properties, jobs, quotes or invoices for a shop.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/fields` |
| MCP tool | `fields_define` |
| Writes | yes, send an Idempotency-Key |
| Area | [Imports, custom fields, tags and search](https://doingwork.work/docs/reference/#imports-custom-fields-tags-and-search) |

## When to use it

Add a custom field to customers, properties, jobs, quotes or invoices for a shop.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `key` | string | yes |
| `label` | string | no |
| `target` | string (one of customer, property, job, quote, invoice) | yes |
| `type` | string (one of text, number, money, date, boolean, enum, json) | yes |
| `options` | array | no |
| `required` | boolean | no |
| `pii` | boolean | no |
| `shared_with_homeowner` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/fields
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "key": "...",
  "target": "customer",
  "type": "text"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# fields.list

Custom fields a shop has defined, optionally for one kind of record.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/fields` |
| MCP tool | `fields_list` |
| Writes | no |
| Area | [Imports, custom fields, tags and search](https://doingwork.work/docs/reference/#imports-custom-fields-tags-and-search) |

## When to use it

Custom fields a shop has defined, optionally for one kind of record.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `target` | string (one of customer, property, job, quote, invoice) | no |

## Example

```http
GET https://doingwork.work/v1/fields?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# fields.set

Set one custom field value on a customer, property, job, quote or invoice.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/fields/set` |
| MCP tool | `fields_set` |
| Writes | yes, send an Idempotency-Key |
| Area | [Imports, custom fields, tags and search](https://doingwork.work/docs/reference/#imports-custom-fields-tags-and-search) |

## When to use it

Set one custom field value on a customer, property, job, quote or invoice.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `key` | string | yes |
| `target` | string (one of customer, property, job, quote, invoice) | yes |
| `object_id` | string (uuid) | yes |
| `value` | string | yes |
| `currency` | string | no |

## Example

```http
POST https://doingwork.work/v1/fields/set
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "key": "...",
  "target": "customer",
  "object_id": "00000000-0000-4000-8000-000000000000",
  "value": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# fields.get

Custom field values on one record, with any required fields still empty.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/fields/get` |
| MCP tool | `fields_get` |
| Writes | no |
| Area | [Imports, custom fields, tags and search](https://doingwork.work/docs/reference/#imports-custom-fields-tags-and-search) |

## When to use it

Custom field values on one record, with any required fields still empty.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `target` | string (one of customer, property, job, quote, invoice) | yes |
| `object_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/fields/get?shop_id=00000000-0000-4000-8000-000000000000&target=customer&object_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# requests.create

Log a request for work from a customer or a new lead.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/requests` |
| MCP tool | `requests_create` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

Log a request for work from a customer or a new lead.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `title` | string | yes |
| `details` | string | no |
| `source` | string (one of agent, form, portal, phone, email, text, walk_in, other) | no |
| `customer_id` | string (uuid) | no |
| `property_id` | string (uuid) | no |
| `contact_name` | string | no |
| `contact_email` | string | no |
| `contact_phone` | string | no |
| `address` | object | no |
| `address.line1` | string | yes |
| `address.line2` | string | no |
| `address.locality` | string | yes |
| `address.postal_code` | string | yes |
| `address.jurisdiction` | string | yes |
| `preferred_times` | string | no |
| `lead_source` | string | no |
| `tags` | array | no |

## Example

```http
POST https://doingwork.work/v1/requests
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "title": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# requests.list

Requests of a shop, newest first, filtered by stage, customer or tag.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/requests` |
| MCP tool | `requests_list` |
| Writes | no |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

Requests of a shop, newest first, filtered by stage, customer or tag.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `stage` | string (one of new, contacted, assessment, quoted, won, lost) | no |
| `customer_id` | string (uuid) | no |
| `tag` | string | no |
| `include_archived` | boolean | no |
| `limit` | integer | no |
| `cursor` | string | no |

## Example

```http
GET https://doingwork.work/v1/requests?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# requests.pipeline

How many open requests a shop has in each stage, and how many were won or lost in the last 30 days.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/requests/pipeline` |
| MCP tool | `requests_pipeline` |
| Writes | no |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

How many open requests a shop has in each stage, and how many were won or lost in the last 30 days.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/requests/pipeline?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# requests.get

One request with its customer and the job it became.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/requests/get` |
| MCP tool | `requests_get` |
| Writes | no |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

One request with its customer and the job it became.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `request_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/requests/get?request_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# requests.update

Move a request to another stage or change its details, customer or property.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/requests/update` |
| MCP tool | `requests_update` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

Move a request to another stage or change its details, customer or property.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `request_id` | string (uuid) | yes |
| `stage` | string (one of new, contacted, assessment, quoted, won, lost) | no |
| `title` | string | no |
| `details` | string | no |
| `customer_id` | string (uuid) | no |
| `property_id` | string (uuid) | no |
| `preferred_times` | string | no |
| `lead_source` | string | no |
| `tags` | array | no |
| `lost_reason` | string | no |
| `archived` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/requests/update
content-type: application/json
Idempotency-Key: 3f1c...

{
  "request_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# requests.convert

Turn a request into a job at its property or address; the worker then writes the quote or books a visit.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/requests/convert` |
| MCP tool | `requests_convert` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

Turn a request into a job at its property or address; the worker then writes the quote or books a visit.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `request_id` | string (uuid) | yes |
| `customer_id` | string (uuid) | no |
| `address` | object | no |
| `address.line1` | string | yes |
| `address.line2` | string | no |
| `address.locality` | string | yes |
| `address.postal_code` | string | yes |
| `address.jurisdiction` | string | yes |
| `summary` | string | no |

## Example

```http
POST https://doingwork.work/v1/requests/convert
content-type: application/json
Idempotency-Key: 3f1c...

{
  "request_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# requests.formCreate

Turn on the shop's public request form and get its key. Making a new one turns off the old key.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/requests/form` |
| MCP tool | `requests_formCreate` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

Turn on the shop's public request form and get its key. Making a new one turns off the old key.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/requests/form
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# requests.submitForm

Send a shop a request through its public form. No sign-in; needs the form key.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/requests/submit` |
| MCP tool | `requests_submitForm` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

Send a shop a request through its public form. No sign-in; needs the form key.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `form_key` | string | yes |
| `title` | string | yes |
| `details` | string | no |
| `name` | string | yes |
| `email` | string | no |
| `phone` | string | no |
| `address` | object | no |
| `address.line1` | string | yes |
| `address.line2` | string | no |
| `address.locality` | string | yes |
| `address.postal_code` | string | yes |
| `address.jurisdiction` | string | yes |
| `preferred_times` | string | no |

## Example

```http
POST https://doingwork.work/v1/requests/submit
content-type: application/json
Idempotency-Key: 3f1c...

{
  "form_key": "...",
  "title": "...",
  "name": "Ruiz Plumbing"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/visits/recurring` |
| MCP tool | `visits_createRecurring` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `starts_at` | string (date-time) | yes |
| `ends_at` | string (date-time) | yes |
| `freq` | string (one of daily, weekly, monthly) | yes |
| `every` | integer | no |
| `occurrences` | integer | no |
| `until` | string (date-time) | no |
| `title` | string | no |
| `notes` | string | no |
| `assignee_ids` | array | no |
| `arrival_window_minutes` | integer | no |

## Example

```http
POST https://doingwork.work/v1/visits/recurring
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "starts_at": "...",
  "ends_at": "...",
  "freq": "daily"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/visits/recurring/cancel` |
| MCP tool | `visits_cancelSeries` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `series_id` | string (uuid) | yes |
| `from` | string (date-time) | no |
| `reason` | string | no |

## Example

```http
POST https://doingwork.work/v1/visits/recurring/cancel
content-type: application/json
Idempotency-Key: 3f1c...

{
  "series_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# visits.start

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/visits/start` |
| MCP tool | `visits_start` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `visit_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/visits/start
content-type: application/json
Idempotency-Key: 3f1c...

{
  "visit_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/visits/done` |
| MCP tool | `visits_done` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `visit_id` | string (uuid) | yes |
| `note` | string | no |
| `override_reason` | string | no |

## Example

```http
POST https://doingwork.work/v1/visits/done
content-type: application/json
Idempotency-Key: 3f1c...

{
  "visit_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# visits.create

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/visits` |
| MCP tool | `visits_create` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `starts_at` | string (date-time) | yes |
| `ends_at` | string (date-time) | yes |
| `title` | string | no |
| `notes` | string | no |
| `assignee_ids` | array | no |
| `arrival_window_minutes` | integer | no |

## Example

```http
POST https://doingwork.work/v1/visits
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "starts_at": "...",
  "ends_at": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/visits/update` |
| MCP tool | `visits_update` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `visit_id` | string (uuid) | yes |
| `starts_at` | string (date-time) | no |
| `ends_at` | string (date-time) | no |
| `title` | string | no |
| `notes` | string | no |
| `arrival_window_minutes` | integer | no |

## Example

```http
POST https://doingwork.work/v1/visits/update
content-type: application/json
Idempotency-Key: 3f1c...

{
  "visit_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# visits.assign

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/visits/assign` |
| MCP tool | `visits_assign` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `visit_id` | string (uuid) | yes |
| `person_ids` | array | yes |

## Example

```http
POST https://doingwork.work/v1/visits/assign
content-type: application/json
Idempotency-Key: 3f1c...

{
  "visit_id": "00000000-0000-4000-8000-000000000000",
  "person_ids": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/visits/cancel` |
| MCP tool | `visits_cancel` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `visit_id` | string (uuid) | yes |
| `reason` | string | no |

## Example

```http
POST https://doingwork.work/v1/visits/cancel
content-type: application/json
Idempotency-Key: 3f1c...

{
  "visit_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# visits.get

One visit with its job and workers.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/visits/get` |
| MCP tool | `visits_get` |
| Writes | no |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `visit_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/visits/get?visit_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `visit_id` | string (uuid) | yes |
| `shop_id` | string (uuid) | yes |
| `job_id` | string (uuid) | yes |
| `job_summary` | ['string', 'null'] | yes |
| `starts_at` | string (date-time) | yes |
| `ends_at` | string (date-time) | yes |
| `title` | ['string', 'null'] | yes |
| `notes` | ['string', 'null'] | yes |
| `status` | string | yes |
| `cancel_reason` | ['string', 'null'] | yes |
| `series_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `started_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `done_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `done_note` | ['string', 'null'] | yes |
| `arrival_window_minutes` | ['integer', 'null'] | yes |
| `arrive_by` | string (date-time) | yes |
| `on_the_way_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `eta_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `arrived_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `assignee_ids` | array | yes |

```json
{
  "visit_id": "00000000-0000-4000-8000-000000000000",
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "job_id": "00000000-0000-4000-8000-000000000000",
  "job_summary": "...",
  "starts_at": "...",
  "ends_at": "...",
  "title": "...",
  "notes": "...",
  "status": "...",
  "cancel_reason": "...",
  "series_id": "00000000-0000-4000-8000-000000000000",
  "started_at": "...",
  "done_at": "...",
  "done_note": "...",
  "arrival_window_minutes": "...",
  "arrive_by": "...",
  "on_the_way_at": "...",
  "eta_at": "...",
  "arrived_at": "...",
  "assignee_ids": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/schedule` |
| MCP tool | `schedule_list` |
| Writes | no |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `from` | string (date-time) | yes |
| `to` | string (date-time) | yes |
| `person_id` | string (uuid) | no |
| `job_id` | string (uuid) | no |
| `mine` | boolean | no |
| `unassigned` | boolean | no |
| `include_cancelled` | boolean | no |

## Example

```http
GET https://doingwork.work/v1/schedule?shop_id=00000000-0000-4000-8000-000000000000&from=...&to=...
```

## Output

| Field | Type | Always there |
|---|---|---|
| `items` | array | yes |
| `items[].visit_id` | string (uuid) | yes |
| `items[].shop_id` | string (uuid) | yes |
| `items[].job_id` | string (uuid) | yes |
| `items[].job_summary` | ['string', 'null'] | yes |
| `items[].starts_at` | string (date-time) | yes |
| `items[].ends_at` | string (date-time) | yes |
| `items[].title` | ['string', 'null'] | yes |
| `items[].notes` | ['string', 'null'] | yes |
| `items[].status` | string | yes |
| `items[].cancel_reason` | ['string', 'null'] | yes |
| `items[].series_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].started_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].done_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].done_note` | ['string', 'null'] | yes |
| `items[].arrival_window_minutes` | ['integer', 'null'] | yes |
| `items[].arrive_by` | string (date-time) | yes |
| `items[].on_the_way_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].eta_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].arrived_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].assignee_ids` | array | yes |
| `truncated` | boolean | yes |

```json
{
  "items": [
    {
      "visit_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "job_id": "00000000-0000-4000-8000-000000000000",
      "job_summary": "...",
      "starts_at": "...",
      "ends_at": "...",
      "title": "...",
      "notes": "...",
      "status": "...",
      "cancel_reason": "...",
      "series_id": "00000000-0000-4000-8000-000000000000",
      "started_at": "...",
      "done_at": "...",
      "done_note": "...",
      "arrival_window_minutes": "...",
      "arrive_by": "...",
      "on_the_way_at": "...",
      "eta_at": "...",
      "arrived_at": "...",
      "assignee_ids": [
        "00000000-0000-4000-8000-000000000000"
      ]
    }
  ],
  "truncated": true
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/availability/hours` |
| MCP tool | `availability_setHours` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `person_id` | string (uuid) | no |
| `hours` | array | yes |
| `hours[].weekday` | integer | yes |
| `hours[].start` | string | yes |
| `hours[].end` | string | yes |

## Example

```http
POST https://doingwork.work/v1/availability/hours
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "hours": [
    {
      "weekday": 1,
      "start": "...",
      "end": "..."
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# availability.addTimeOff

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/availability/time-off` |
| MCP tool | `availability_addTimeOff` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `person_id` | string (uuid) | no |
| `starts_at` | string (date-time) | yes |
| `ends_at` | string (date-time) | yes |
| `reason` | string | no |

## Example

```http
POST https://doingwork.work/v1/availability/time-off
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "starts_at": "...",
  "ends_at": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# availability.removeTimeOff

Remove a time-off entry.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/availability/time-off/remove` |
| MCP tool | `availability_removeTimeOff` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `time_off_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/availability/time-off/remove
content-type: application/json
Idempotency-Key: 3f1c...

{
  "time_off_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# availability.get

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/availability` |
| MCP tool | `availability_get` |
| Writes | no |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `person_id` | string (uuid) | no |
| `from` | string (date-time) | yes |
| `to` | string (date-time) | yes |

## Example

```http
GET https://doingwork.work/v1/availability?shop_id=00000000-0000-4000-8000-000000000000&from=...&to=...
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/schedule/settings` |
| MCP tool | `schedule_settings` |
| Writes | no |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/schedule/settings?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `time_zone` | string | yes |
| `time_zone_set` | boolean | no |
| `hours` | array | yes |
| `hours[].weekday` | integer | yes |
| `hours[].start` | string | yes |
| `hours[].end` | string | yes |
| `hours_source` | string (one of shop, default) | yes |
| `arrival_window_minutes` | integer | yes |
| `buffer_minutes` | integer | yes |
| `reminder_hours` | integer | yes |
| `on_my_way_notices` | boolean | yes |

```json
{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "time_zone": "...",
  "hours": [
    {
      "weekday": 1,
      "start": "...",
      "end": "..."
    }
  ],
  "hours_source": "shop",
  "arrival_window_minutes": 1,
  "buffer_minutes": 1,
  "reminder_hours": 1,
  "on_my_way_notices": true
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/schedule/hours` |
| MCP tool | `schedule_setShopHours` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `hours` | array | yes |
| `hours[].weekday` | integer | yes |
| `hours[].start` | string | yes |
| `hours[].end` | string | yes |

## Example

```http
POST https://doingwork.work/v1/schedule/hours
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "hours": [
    {
      "weekday": 1,
      "start": "...",
      "end": "..."
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/schedule/rules` |
| MCP tool | `schedule_setRules` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `arrival_window_minutes` | integer | no |
| `buffer_minutes` | integer | no |
| `reminder_hours` | integer | no |
| `on_my_way_notices` | boolean | no |
| `time_zone` | string | no |

## Example

```http
POST https://doingwork.work/v1/schedule/rules
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/schedule/find-time` |
| MCP tool | `schedule_findTime` |
| Writes | no |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `duration_minutes` | integer | yes |
| `from` | string (date-time) | no |
| `to` | string (date-time) | no |
| `person_ids` | array | no |
| `together` | boolean | no |
| `step_minutes` | integer | no |
| `limit` | integer | no |

## Example

```http
POST https://doingwork.work/v1/schedule/find-time
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "duration_minutes": 1
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `time_zone` | string | yes |
| `hours_source` | string (one of shop, default) | yes |
| `buffer_minutes` | integer | yes |
| `arrival_window_minutes` | integer | no |
| `slots` | array | yes |
| `slots[].starts_at` | string (date-time) | yes |
| `slots[].ends_at` | string (date-time) | yes |
| `slots[].arrive_by` | string (date-time) | yes |
| `slots[].person_ids` | array | yes |

```json
{
  "time_zone": "...",
  "hours_source": "shop",
  "buffer_minutes": 1,
  "slots": [
    {
      "starts_at": "...",
      "ends_at": "...",
      "arrive_by": "...",
      "person_ids": [
        "00000000-0000-4000-8000-000000000000"
      ]
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/visits/on-the-way` |
| MCP tool | `visits_onTheWay` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `visit_id` | string (uuid) | yes |
| `eta_at` | string (date-time) | no |
| `eta_minutes` | integer | no |

## Example

```http
POST https://doingwork.work/v1/visits/on-the-way
content-type: application/json
Idempotency-Key: 3f1c...

{
  "visit_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/visits/arrived` |
| MCP tool | `visits_arrived` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `visit_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/visits/arrived
content-type: application/json
Idempotency-Key: 3f1c...

{
  "visit_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/visits/notices` |
| MCP tool | `visits_notices` |
| Writes | no |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `visit_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/visits/notices?visit_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `visit_id` | string (uuid) | yes |
| `items` | array | yes |
| `items[].notice_id` | string (uuid) | no |
| `items[].kind` | string (one of appointment_reminder, on_my_way) | no |
| `items[].due_at` | string (date-time) | no |
| `items[].status` | string (one of pending, queued, cancelled, skipped) | no |
| `items[].outcome` | ['string', 'null'] | no |
| `items[].customer_email_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | no |
| `items[].finished_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | no |

```json
{
  "visit_id": "00000000-0000-4000-8000-000000000000",
  "items": [
    {
      "notice_id": "00000000-0000-4000-8000-000000000000",
      "kind": "appointment_reminder",
      "due_at": "...",
      "status": "pending",
      "outcome": "...",
      "customer_email_id": "00000000-0000-4000-8000-000000000000",
      "finished_at": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shops.setInvoiceReminders

Turn the automatic unpaid-invoice reminder on or off for the whole shop (owner only). When on, a customer gets one friendly email reminder a set number of days (default 1) after an invoice or deposit request is sent if it is still unpaid. Mail comes from a doingwork.work address on the shop's behalf, with replies going to the owner.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/shops/invoice-reminders` |
| MCP tool | `shops_setInvoiceReminders` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Turn the automatic unpaid-invoice reminder on or off for the whole shop (owner only). When on, a customer gets one friendly email reminder a set number of days (default 1) after an invoice or deposit request is sent if it is still unpaid.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `enabled` | boolean | yes |
| `delay_days` | integer | no |

## Example

```http
POST https://doingwork.work/v1/shops/invoice-reminders
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "enabled": true
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shops.setContact

Set the phone and email customers see when they open a canceled invoice or quote link (owner only). Leave a field out or send null to fall back to the owner's own email and phone.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/shops/contact` |
| MCP tool | `shops_setContact` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Set the phone and email customers see when they open a canceled invoice or quote link (owner only). Leave a field out or send null to fall back to the owner's own email and phone.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `phone` | ['string', 'null'] | no |
| `email` | ['string', 'null'] | no |

## Example

```http
POST https://doingwork.work/v1/shops/contact
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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). Off by default. A worker whose own see_costs is off still does not see costs.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/shops/field-costs` |
| MCP tool | `shops_setFieldCosts` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

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). Off by default.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `enabled` | boolean | yes |

## Example

```http
POST https://doingwork.work/v1/shops/field-costs
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "enabled": true
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shops.setHubLinkInEmails

Turn the customer hub link in quote and invoice emails on or off for the whole shop (owner only). On by default: each quote or invoice email the customer gets from Doing Work ends with a link to their hub, where they see their quotes, invoices and visits with you.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/shops/hub-link-in-emails` |
| MCP tool | `shops_setHubLinkInEmails` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Turn the customer hub link in quote and invoice emails on or off for the whole shop (owner only). On by default: each quote or invoice email the customer gets from Doing Work ends with a link to their hub, where they see their quotes, invoices and visits with you.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `enabled` | boolean | yes |

## Example

```http
POST https://doingwork.work/v1/shops/hub-link-in-emails
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "enabled": true
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# invoices.setReminder

Turn the automatic unpaid reminder on or off for one invoice (owner or office).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/invoices/reminder` |
| MCP tool | `invoices_setReminder` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Turn the automatic unpaid reminder on or off for one invoice (owner or office).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `invoice_id` | string (uuid) | yes |
| `enabled` | boolean | yes |

## Example

```http
POST https://doingwork.work/v1/invoices/reminder
content-type: application/json
Idempotency-Key: 3f1c...

{
  "invoice_id": "00000000-0000-4000-8000-000000000000",
  "enabled": true
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# pricebook.saveItem

Add a price book item, or change one when item_id is given (owner or office). Items are a pick list for quote lines that workers write; they are never filled into a quote automatically.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/pricebook/items` |
| MCP tool | `pricebook_saveItem` |
| Writes | yes, send an Idempotency-Key |
| Area | [Quotes (unlimited)](https://doingwork.work/docs/reference/#quotes-unlimited) |

## When to use it

Add a price book item, or change one when item_id is given (owner or office). Items are a pick list for quote lines that workers write; they are never filled into a quote automatically.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `item_id` | string (uuid) | no |
| `kind` | string (one of service, material) | no |
| `name` | string | no |
| `description` | string | no |
| `unit` | string | no |
| `cost_minor` | integer | no |
| `unit_price_minor` | integer | no |
| `markup_percent` | number | no |
| `taxable` | boolean | no |
| `category` | string | no |

## Example

```http
POST https://doingwork.work/v1/pricebook/items
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# pricebook.list

List the shop's price book items, by name. Filter by category or text; archived items are left out unless asked for. cost_minor is null for a worker the owner has not let see costs.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/pricebook/items` |
| MCP tool | `pricebook_list` |
| Writes | no |
| Area | [Quotes (unlimited)](https://doingwork.work/docs/reference/#quotes-unlimited) |

## When to use it

List the shop's price book items, by name. Filter by category or text; archived items are left out unless asked for.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `category` | string | no |
| `q` | string | no |
| `include_archived` | boolean | no |

## Example

```http
GET https://doingwork.work/v1/pricebook/items?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# pricebook.archiveItem

Archive a price book item, or bring it back with archived false (owner or office). Quotes that used it keep their lines.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/pricebook/items/archive` |
| MCP tool | `pricebook_archiveItem` |
| Writes | yes, send an Idempotency-Key |
| Area | [Quotes (unlimited)](https://doingwork.work/docs/reference/#quotes-unlimited) |

## When to use it

Archive a price book item, or bring it back with archived false (owner or office). Quotes that used it keep their lines.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `item_id` | string (uuid) | yes |
| `archived` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/pricebook/items/archive
content-type: application/json
Idempotency-Key: 3f1c...

{
  "item_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# templates.save

Save a quote template: worker-written lines (and good, better, best options) to start quotes from. Give template_id to replace one (owner or office).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/quote-templates` |
| MCP tool | `templates_save` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

Save a quote template: worker-written lines (and good, better, best options) to start quotes from. Give template_id to replace one (owner or office).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `template_id` | string (uuid) | no |
| `name` | string | yes |
| `lines` | array | yes |
| `lines[].description` | string | yes |
| `lines[].quantity` | number | yes |
| `lines[].unit_price_minor` | integer | yes |
| `lines[].option_key` | string | no |
| `lines[].optional` | boolean | no |
| `lines[].kind` | string (one of item, fee, discount) | no |
| `lines[].taxable` | boolean | no |
| `options` | array | no |
| `options[].key` | string | yes |
| `options[].label` | string | yes |

## Example

```http
POST https://doingwork.work/v1/quote-templates
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "name": "Ruiz Plumbing",
  "lines": [
    {
      "description": "40 gallon gas heater, installed",
      "quantity": "...",
      "unit_price_minor": 50000
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# templates.list

List the shop's saved quote templates with their lines. To use one, pass its lines and options to quotes.create, changed as the worker says.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/quote-templates` |
| MCP tool | `templates_list` |
| Writes | no |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

List the shop's saved quote templates with their lines. To use one, pass its lines and options to quotes.create, changed as the worker says.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `include_archived` | boolean | no |

## Example

```http
GET https://doingwork.work/v1/quote-templates?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# templates.archive

Archive a quote template, or bring it back with archived false (owner or office).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/quote-templates/archive` |
| MCP tool | `templates_archive` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

Archive a quote template, or bring it back with archived false (owner or office).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `template_id` | string (uuid) | yes |
| `archived` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/quote-templates/archive
content-type: application/json
Idempotency-Key: 3f1c...

{
  "template_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# quotes.respond

Customer declines a quote or asks for changes (hosted page). Asking for changes needs a note and puts the quote back to draft for the shop to revise.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/quotes/respond` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Customer declines a quote or asks for changes (hosted page). Asking for changes needs a note and puts the quote back to draft for the shop to revise.

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `t` | string | yes |
| `response` | string (one of declined, change_requested) | yes |
| `note` | string | no |

## Example

```http
POST https://doingwork.work/v1/quotes/respond
content-type: application/json
Idempotency-Key: 3f1c...

{
  "t": "...",
  "response": "declined"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/quotes/activity` |
| MCP tool | `quotes_activity` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `quote_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/quotes/activity?quote_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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). 0 turns it off. Default 3.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/shops/quote-followup` |
| MCP tool | `shops_setQuoteFollowup` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Set how many days after a quote is sent the customer gets one friendly follow-up email if they have not answered (owner only). 0 turns it off.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `days` | integer | yes |

## Example

```http
POST https://doingwork.work/v1/shops/quote-followup
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "days": 1
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# invoices.create

Create an invoice without a quote, from lines the worker wrote (never filled in by AI). Optional due date, memo and deposit. Send it with invoices.send.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/invoices/direct` |
| MCP tool | `invoices_create` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Create an invoice without a quote, from lines the worker wrote (never filled in by AI). Optional due date, memo and deposit.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `lines` | array | yes |
| `lines[].description` | string | yes |
| `lines[].quantity` | number | yes |
| `lines[].unit_price_minor` | integer | yes |
| `lines[].kind` | string (one of item, fee, discount) | no |
| `lines[].taxable` | boolean | no |
| `due_on` | string | no |
| `memo` | string | no |
| `deposit_minor` | integer | no |
| `tax_rate_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | no |

## Example

```http
POST https://doingwork.work/v1/invoices/direct
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "lines": [
    {
      "description": "40 gallon gas heater, installed",
      "quantity": "...",
      "unit_price_minor": 50000
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# invoices.createBatch

Create many invoices at once (up to 100), each from a job and the lines the worker wrote. All or nothing. With send=true each is sent and its pay link returned.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/invoices/batch` |
| MCP tool | `invoices_createBatch` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Create many invoices at once (up to 100), each from a job and the lines the worker wrote. All or nothing.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `send` | boolean | no |
| `items` | array | yes |
| `items[].job_id` | string (uuid) | yes |
| `items[].lines` | array | yes |
| `items[].lines[].description` | string | yes |
| `items[].lines[].quantity` | number | yes |
| `items[].lines[].unit_price_minor` | integer | yes |
| `items[].lines[].kind` | string (one of item, fee, discount) | no |
| `items[].lines[].taxable` | boolean | no |
| `items[].due_on` | string | no |
| `items[].memo` | string | no |
| `items[].tax_rate_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | no |

## Example

```http
POST https://doingwork.work/v1/invoices/batch
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "items": [
    {
      "job_id": "00000000-0000-4000-8000-000000000000",
      "lines": [
        {
          "description": "40 gallon gas heater, installed",
          "quantity": "...",
          "unit_price_minor": 50000
        }
      ]
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# invoices.get

One invoice with its lines, what has been paid, the balance, the due date, and whether a bank payment is still clearing. display_status is payment_pending while a bank payment clears (3 to 5 business days), otherwise the invoice status. last_bank_payment_failed_at is set when the latest bank payment failed and the customer can pay again.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/invoices/get` |
| MCP tool | `invoices_get` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

One invoice with its lines, what has been paid, the balance, the due date, and whether a bank payment is still clearing. display_status is payment_pending while a bank payment clears (3 to 5 business days), otherwise the invoice status.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `invoice_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/invoices/get?invoice_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `invoice_id` | string (uuid) | yes |
| `job_id` | string (uuid) | yes |
| `status` | string | yes |
| `currency` | string | yes |
| `total_minor` | integer | yes |
| `collected_minor` | integer | yes |
| `balance_minor` | integer | yes |
| `deposit_minor` | ['integer', 'null'] | yes |
| `issued_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `due_on` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', ')'] | yes |
| `memo` | ['string', 'null'] | yes |
| `bank_payment_clearing` | boolean | yes |
| `display_status` | string | yes |
| `last_bank_payment_failed_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `lines` | array | yes |
| `lines[].position` | integer | yes |
| `lines[].description` | string | yes |
| `lines[].quantity` | number | yes |
| `lines[].unit_price_minor` | integer | yes |
| `lines[].kind` | string | yes |
| `lines[].taxable` | boolean | yes |
| `lines[].amount_minor` | integer | yes |
| `tax` | object | no |
| `tax.name` | ['string', 'null'] | yes |
| `tax.jurisdiction` | ['string', 'null'] | yes |
| `tax.rate_bps` | integer | yes |
| `tax.taxable_minor` | integer | yes |
| `tax.tax_minor` | integer | yes |

```json
{
  "invoice_id": "00000000-0000-4000-8000-000000000000",
  "job_id": "00000000-0000-4000-8000-000000000000",
  "status": "...",
  "currency": "...",
  "total_minor": 50000,
  "collected_minor": 50000,
  "balance_minor": 50000,
  "deposit_minor": "...",
  "issued_at": "...",
  "due_on": "...",
  "memo": "...",
  "bank_payment_clearing": true,
  "display_status": "...",
  "last_bank_payment_failed_at": "...",
  "lines": [
    {
      "position": 1,
      "description": "40 gallon gas heater, installed",
      "quantity": "...",
      "unit_price_minor": 50000,
      "kind": "...",
      "taxable": true,
      "amount_minor": 50000
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# invoices.pdf

The invoice as a PDF (base64), with shop, customer, lines, paid so far and balance. Attach it to email or save it.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/invoices/pdf` |
| MCP tool | `invoices_pdf` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

The invoice as a PDF (base64), with shop, customer, lines, paid so far and balance. Attach it to email or save it.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `invoice_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/invoices/pdf?invoice_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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). An invoice with no due date is due when sent.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/invoices/aging` |
| MCP tool | `invoices_aging` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

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). An invoice with no due date is due when sent.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `as_of` | string | no |

## Example

```http
GET https://doingwork.work/v1/invoices/aging?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shops.setTips

Turn tips on or off on this shop's invoice pay page (owner only). Off by default. A tip is paid with the invoice, never counts toward the invoice total, and shows in tips.report.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/shops/tips` |
| MCP tool | `shops_setTips` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Turn tips on or off on this shop's invoice pay page (owner only). Off by default.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `enabled` | boolean | yes |

## Example

```http
POST https://doingwork.work/v1/shops/tips
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "enabled": true
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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). No link means no email. Default 1 day; 0 turns it off. Review emails show the shop name and mailing_address in the footer, so a link can only be set once the shop has a mailing address (send it here).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/shops/review-request` |
| MCP tool | `shops_setReviewRequest` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

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). No link means no email.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `review_url` | string | no |
| `mailing_address` | string | no |
| `days` | integer | no |

## Example

```http
POST https://doingwork.work/v1/shops/review-request
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# tips.report

Tips customers paid, by invoice and in total, between two dates (owner or office). Tips are kept apart from invoice totals. Refunds of a payment are not split out from its tip.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/tips/report` |
| MCP tool | `tips_report` |
| Writes | no |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

Tips customers paid, by invoice and in total, between two dates (owner or office). Tips are kept apart from invoice totals.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `from` | string | yes |
| `to` | string | yes |

## Example

```http
GET https://doingwork.work/v1/tips/report?shop_id=00000000-0000-4000-8000-000000000000&from=...&to=...
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# cards.setupLink

Get a secure page where the customer saves a card with the shop's payment processor (owner or office). Send the link to the customer; Doing Work never sees the card number. Saving a new card replaces the old one.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/cards/setup-link` |
| MCP tool | `cards_setupLink` |
| Writes | no |
| Area | [Stripe connection](https://doingwork.work/docs/reference/#stripe-connection) |

## When to use it

Get a secure page where the customer saves a card with the shop's payment processor (owner or office). Send the link to the customer; Doing Work never sees the card number.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/cards/setup-link
content-type: application/json
Idempotency-Key: 3f1c...

{
  "customer_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# cards.resave

Open the card saving page from a save-your-card-again email link (hosted page only). Returns the processor URL where the customer saves the card on the shop's current payments account.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/cards/resave` |
| MCP tool | Not an MCP tool |
| Writes | no |
| Area | [Stripe connection](https://doingwork.work/docs/reference/#stripe-connection) |

## When to use it

Open the card saving page from a save-your-card-again email link (hosted page only). Returns the processor URL where the customer saves the card on the shop's current payments account.

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `k` | string | yes |

## Example

```http
POST https://doingwork.work/v1/cards/resave
content-type: application/json
Idempotency-Key: 3f1c...

{
  "k": "..."
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `url` | string | yes |

```json
{
  "url": "https://example.com/hooks/doing-work"
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# cards.get

The card a customer has on file (brand, last 4, expiry) and whether auto-pay is on (owner or office).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/cards/get` |
| MCP tool | `cards_get` |
| Writes | no |
| Area | [Stripe connection](https://doingwork.work/docs/reference/#stripe-connection) |

## When to use it

The card a customer has on file (brand, last 4, expiry) and whether auto-pay is on (owner or office).

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/cards/get?customer_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# cards.remove

Remove a customer's saved card (owner or office). Auto-pay turns off with it.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/cards/remove` |
| MCP tool | `cards_remove` |
| Writes | yes, send an Idempotency-Key |
| Area | [Stripe connection](https://doingwork.work/docs/reference/#stripe-connection) |

## When to use it

Remove a customer's saved card (owner or office). Auto-pay turns off with it.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/cards/remove
content-type: application/json
Idempotency-Key: 3f1c...

{
  "customer_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# customers.setAutopay

Turn auto-pay on or off for a customer (owner or office). On needs a saved card. With auto-pay on, each invoice sent to them is charged to the card on its due date (or the day it is sent if it has none). A failed charge is not retried; you get an email and the pay link still works.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/customers/autopay` |
| MCP tool | `customers_setAutopay` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Turn auto-pay on or off for a customer (owner or office). On needs a saved card.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |
| `enabled` | boolean | yes |

## Example

```http
POST https://doingwork.work/v1/customers/autopay
content-type: application/json
Idempotency-Key: 3f1c...

{
  "customer_id": "00000000-0000-4000-8000-000000000000",
  "enabled": true
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# maintenance.createPlan

Start a maintenance plan for a job's customer (owner or office). You write the priced lines for one term (never filled in by AI). The first term's invoice is created now (and sent if send_first is true). When a term ends, the next term's invoice is created automatically and sent if auto_send is on.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/maintenance-plans` |
| MCP tool | `maintenance_createPlan` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

Start a maintenance plan for a job's customer (owner or office). You write the priced lines for one term (never filled in by AI).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `name` | string | yes |
| `term_months` | integer | yes |
| `starts_on` | string | no |
| `lines` | array | yes |
| `lines[].description` | string | yes |
| `lines[].quantity` | number | yes |
| `lines[].unit_price_minor` | integer | yes |
| `lines[].kind` | string (one of item, fee, discount) | no |
| `lines[].taxable` | boolean | no |
| `tax_rate_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | no |
| `auto_send` | boolean | no |
| `send_first` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/maintenance-plans
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "name": "Ruiz Plumbing",
  "term_months": 1,
  "lines": [
    {
      "description": "40 gallon gas heater, installed",
      "quantity": "...",
      "unit_price_minor": 50000
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# maintenance.getPlan

One maintenance plan: price per term, current term, renewal date, and each term's invoice.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/maintenance-plans/get` |
| MCP tool | `maintenance_getPlan` |
| Writes | no |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

One maintenance plan: price per term, current term, renewal date, and each term's invoice.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `plan_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/maintenance-plans/get?plan_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# maintenance.listPlans

A shop's maintenance plans, optionally for one customer or by status, soonest renewal first.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/maintenance-plans` |
| MCP tool | `maintenance_listPlans` |
| Writes | no |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

A shop's maintenance plans, optionally for one customer or by status, soonest renewal first.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `customer_id` | string (uuid) | no |
| `status` | string (one of active, cancelled) | no |

## Example

```http
GET https://doingwork.work/v1/maintenance-plans?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# maintenance.setPrice

Replace a plan's priced lines (owner or office; you write them, never AI), and optionally its tax rate. Applies from the next term's invoice; invoices already made keep their price.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/maintenance-plans/price` |
| MCP tool | `maintenance_setPrice` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

Replace a plan's priced lines (owner or office; you write them, never AI), and optionally its tax rate. Applies from the next term's invoice; invoices already made keep their price.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `plan_id` | string (uuid) | yes |
| `lines` | array | yes |
| `lines[].description` | string | yes |
| `lines[].quantity` | number | yes |
| `lines[].unit_price_minor` | integer | yes |
| `lines[].kind` | string (one of item, fee, discount) | no |
| `lines[].taxable` | boolean | no |
| `tax_rate_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | no |

## Example

```http
POST https://doingwork.work/v1/maintenance-plans/price
content-type: application/json
Idempotency-Key: 3f1c...

{
  "plan_id": "00000000-0000-4000-8000-000000000000",
  "lines": [
    {
      "description": "40 gallon gas heater, installed",
      "quantity": "...",
      "unit_price_minor": 50000
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# maintenance.cancelPlan

Cancel a maintenance plan (owner or office). No more renewal invoices. Invoices already sent stay as they are.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/maintenance-plans/cancel` |
| MCP tool | `maintenance_cancelPlan` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

Cancel a maintenance plan (owner or office). No more renewal invoices.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `plan_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/maintenance-plans/cancel
content-type: application/json
Idempotency-Key: 3f1c...

{
  "plan_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# taxes.saveRate

Add or change a sales tax rate the shop charges in one jurisdiction (owner or office). You enter the rate; Doing Work never looks it up. rate_bps is hundredths of a percent (600 = 6%). Changing a rate applies to new quotes and invoices only. is_default makes it the rate used when a quote or invoice does not name one.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/tax-rates` |
| MCP tool | `taxes_saveRate` |
| Writes | yes, send an Idempotency-Key |
| Area | [Invoices and payments (unlimited)](https://doingwork.work/docs/reference/#invoices-and-payments-unlimited) |

## When to use it

Add or change a sales tax rate the shop charges in one jurisdiction (owner or office). You enter the rate; Doing Work never looks it up.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `tax_rate_id` | string (uuid) | no |
| `name` | string | yes |
| `jurisdiction` | string | yes |
| `rate_bps` | integer | yes |
| `is_default` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/tax-rates
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "name": "Ruiz Plumbing",
  "jurisdiction": "US-ID",
  "rate_bps": 1
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# taxes.listRates

The shop's tax rates. Archived (replaced or removed) rates are left out unless include_archived is true.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/tax-rates` |
| MCP tool | `taxes_listRates` |
| Writes | no |
| Area | [Invoices and payments (unlimited)](https://doingwork.work/docs/reference/#invoices-and-payments-unlimited) |

## When to use it

The shop's tax rates. Archived (replaced or removed) rates are left out unless include_archived is true.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `include_archived` | boolean | no |

## Example

```http
GET https://doingwork.work/v1/tax-rates?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# taxes.archiveRate

Stop using a tax rate (owner or office). Quotes and invoices that already used it keep it.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/tax-rates/archive` |
| MCP tool | `taxes_archiveRate` |
| Writes | yes, send an Idempotency-Key |
| Area | [Invoices and payments (unlimited)](https://doingwork.work/docs/reference/#invoices-and-payments-unlimited) |

## When to use it

Stop using a tax rate (owner or office). Quotes and invoices that already used it keep it.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `tax_rate_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/tax-rates/archive
content-type: application/json
Idempotency-Key: 3f1c...

{
  "tax_rate_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shops.setTaxReportBasis

Choose how the sales tax report counts a period (owner only). accrual (default): invoices sent in the period. cash: money collected in the period, after refunds. Pick the one your state files on.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/shops/tax-report-basis` |
| MCP tool | `shops_setTaxReportBasis` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Choose how the sales tax report counts a period (owner only). accrual (default): invoices sent in the period.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `basis` | string (one of accrual, cash) | yes |

## Example

```http
POST https://doingwork.work/v1/shops/tax-report-basis
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "basis": "accrual"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# taxes.salesReport

Sales tax report for a period (owner or office), shop local dates, from and to inclusive. Uses the shop's basis (shops.setTaxReportBasis) unless basis is given. accrual: invoices sent in the period, with sales, taxable sales, tax billed, and tax collected so far. cash: money collected in the period through Doing Work (payments minus refunds, tips left out), with the tax share of it. Grouped by the tax name, jurisdiction and rate on each invoice. Untaxed sales are one row with no rate. Void invoices are left out. For filing, check the numbers against your own records.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/tax-rates/report` |
| MCP tool | `taxes_salesReport` |
| Writes | no |
| Area | [Invoices and payments (unlimited)](https://doingwork.work/docs/reference/#invoices-and-payments-unlimited) |

## When to use it

Sales tax report for a period (owner or office), shop local dates, from and to inclusive. Uses the shop's basis (shops.setTaxReportBasis) unless basis is given.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `from` | string (date) | yes |
| `to` | string (date) | yes |
| `basis` | string (one of accrual, cash) | no |

## Example

```http
GET https://doingwork.work/v1/tax-rates/report?shop_id=00000000-0000-4000-8000-000000000000&from=...&to=...
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# quotes.addAttachment

Show an uploaded photo or file of the same job with a quote, with an optional caption. Upload it first with the media ops. The customer sees it on the quote page.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/quotes/attachments` |
| MCP tool | `quotes_addAttachment` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Show an uploaded photo or file of the same job with a quote, with an optional caption. Upload it first with the media ops.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `quote_id` | string | yes |
| `media_id` | string | yes |
| `caption` | string | no |

## Example

```http
POST https://doingwork.work/v1/quotes/attachments
content-type: application/json
Idempotency-Key: 3f1c...

{
  "quote_id": "00000000-0000-4000-8000-000000000000",
  "media_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# quotes.removeAttachment

Take a photo or file off a quote. The media item itself stays on the job.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/quotes/attachments/remove` |
| MCP tool | `quotes_removeAttachment` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Take a photo or file off a quote. The media item itself stays on the job.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `attachment_id` | string | yes |

## Example

```http
POST https://doingwork.work/v1/quotes/attachments/remove
content-type: application/json
Idempotency-Key: 3f1c...

{
  "attachment_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# quotes.listAttachments

Photos and files shown with a quote, in order.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/quotes/attachments` |
| MCP tool | `quotes_listAttachments` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Photos and files shown with a quote, in order.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `quote_id` | string | yes |

## Example

```http
GET https://doingwork.work/v1/quotes/attachments?quote_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# media.tag

Add or remove tags on a photo (for example: leak, panel, permit). Tags are lower case; returns the photo's tags.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/media/tags` |
| MCP tool | `media_tag` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Add or remove tags on a photo (for example: leak, panel, permit). Tags are lower case; returns the photo's tags.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `media_id` | string (uuid) | yes |
| `add` | array | no |
| `remove` | array | no |

## Example

```http
POST https://doingwork.work/v1/media/tags
content-type: application/json
Idempotency-Key: 3f1c...

{
  "media_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# tags.list

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/tags` |
| MCP tool | `tags_list` |
| Writes | no |
| Area | [Imports, custom fields, tags and search](https://doingwork.work/docs/reference/#imports-custom-fields-tags-and-search) |

## When to use it

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

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/tags?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/media/search` |
| MCP tool | `media_search` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

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

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `tags` | array | no |
| `job_id` | string (uuid) | no |
| `stage` | string (one of before, during, after, other) | no |
| `from` | string | no |
| `to` | string | no |
| `limit` | integer | no |

## Example

```http
GET https://doingwork.work/v1/media/search?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# jobs.findByTag

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/jobs/by-tag` |
| MCP tool | `jobs_findByTag` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

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

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `tags` | array | yes |

## Example

```http
GET https://doingwork.work/v1/jobs/by-tag?shop_id=00000000-0000-4000-8000-000000000000&tags=['...']
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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). The photo is never changed; earlier versions are kept. An empty list clears the markup.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/media/markup` |
| MCP tool | `media_saveMarkup` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

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). The photo is never changed; earlier versions are kept.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `media_id` | string (uuid) | yes |
| `shapes` | array | yes |
| `shapes[].type` | string (one of arrow, line, rect, ellipse, text, freehand) | yes |
| `shapes[].points` | array | yes |
| `shapes[].color` | string | no |
| `shapes[].width` | integer | no |
| `shapes[].text` | string | no |

## Example

```http
POST https://doingwork.work/v1/media/markup
content-type: application/json
Idempotency-Key: 3f1c...

{
  "media_id": "00000000-0000-4000-8000-000000000000",
  "shapes": [
    {
      "type": "arrow",
      "points": [
        [
          "..."
        ]
      ]
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# media.getMarkup

A photo's markup: the latest version unless one is named, with an SVG overlay to lay over the original photo. Also lists all versions.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/media/markup` |
| MCP tool | `media_getMarkup` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

A photo's markup: the latest version unless one is named, with an SVG overlay to lay over the original photo. Also lists all versions.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `media_id` | string (uuid) | yes |
| `version` | integer | no |

## Example

```http
GET https://doingwork.work/v1/media/markup?media_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shares.create

Make a link to a gallery of a job's photos that anyone with the link can open without signing in. Name the photos, or leave media_ids out to share every photo on the job (including ones added later). Expires after days (1 to 90, default 14); revoke it any time. Markup shows over the photos unless include_markup is false. Set include_daily_log to also show the job's daily log entries (off by default; names of who wrote them are not shown). The url is shown once.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/shares` |
| MCP tool | `shares_create` |
| Writes | yes, send an Idempotency-Key |
| Area | [Job photos](https://doingwork.work/docs/reference/#job-photos) |

## When to use it

Make a link to a gallery of a job's photos that anyone with the link can open without signing in. Name the photos, or leave media_ids out to share every photo on the job (including ones added later).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `media_ids` | array | no |
| `days` | integer | no |
| `title` | string | no |
| `include_markup` | boolean | no |
| `include_daily_log` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/shares
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shares.list

Photo share links on a job, newest first, with whether each is still active and how often it was opened. The link itself is not shown again.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/shares` |
| MCP tool | `shares_list` |
| Writes | no |
| Area | [Job photos](https://doingwork.work/docs/reference/#job-photos) |

## When to use it

Photo share links on a job, newest first, with whether each is still active and how often it was opened. The link itself is not shown again.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/shares?job_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shares.revoke

Turn a photo share link off now. Anyone opening it after this sees that it is no longer available.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/shares/revoke` |
| MCP tool | `shares_revoke` |
| Writes | yes, send an Idempotency-Key |
| Area | [Job photos](https://doingwork.work/docs/reference/#job-photos) |

## When to use it

Turn a photo share link off now. Anyone opening it after this sees that it is no longer available.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `share_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/shares/revoke
content-type: application/json
Idempotency-Key: 3f1c...

{
  "share_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# logs.add

Write a daily log entry on a job: what happened on site that day, in the worker's own words. Any shop member can add one. log_date defaults to today in the shop's time zone. Attach photos already on the job with media_ids.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/daily-log` |
| MCP tool | `logs_add` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

Write a daily log entry on a job: what happened on site that day, in the worker's own words. Any shop member can add one.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `body` | string | yes |
| `log_date` | string | no |
| `media_ids` | array | no |

## Example

```http
POST https://doingwork.work/v1/daily-log
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "body": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# logs.list

Daily log entries on a job, oldest first, optionally between two dates (inclusive). Removed entries are left out.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/daily-log` |
| MCP tool | `logs_list` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

Daily log entries on a job, oldest first, optionally between two dates (inclusive). Removed entries are left out.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `from` | string | no |
| `to` | string | no |

## Example

```http
GET https://doingwork.work/v1/daily-log?job_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# logs.remove

Take a daily log entry out of the log. Only the worker who wrote it or the owner can. It stays in history and in any PDF already made.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/daily-log/remove` |
| MCP tool | `logs_remove` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

Take a daily log entry out of the log. Only the worker who wrote it or the owner can.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `entry_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/daily-log/remove
content-type: application/json
Idempotency-Key: 3f1c...

{
  "entry_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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. Name photos with media_ids or leave it out for every photo on the job. Core stores the PDF with its sha256 and returns it (base64).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/documents/photo-report` |
| MCP tool | `documents_photoReport` |
| Writes | yes, send an Idempotency-Key |
| Area | [Signed job records](https://doingwork.work/docs/reference/#signed-job-records) |

## When to use it

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. Name photos with media_ids or leave it out for every photo on the job.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `title` | string | no |
| `notes` | string | no |
| `media_ids` | array | no |

## Example

```http
POST https://doingwork.work/v1/documents/photo-report
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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. Optional from and to dates (inclusive). Core stores the PDF with its sha256 and returns it (base64).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/documents/daily-log` |
| MCP tool | `documents_dailyLog` |
| Writes | yes, send an Idempotency-Key |
| Area | [Signed job records](https://doingwork.work/docs/reference/#signed-job-records) |

## When to use it

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. Optional from and to dates (inclusive).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `from` | string | no |
| `to` | string | no |
| `include_photos` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/documents/daily-log
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# documents.list

PDF documents core made for a job (photo reports, daily logs and submitted forms), newest first, with each file's sha256.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/documents` |
| MCP tool | `documents_list` |
| Writes | no |
| Area | [Signed job records](https://doingwork.work/docs/reference/#signed-job-records) |

## When to use it

PDF documents core made for a job (photo reports, daily logs and submitted forms), newest first, with each file's sha256.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/documents?job_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# documents.get

One stored job PDF (base64) with its sha256, byte for byte as it was made.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/documents/get` |
| MCP tool | `documents_get` |
| Writes | no |
| Area | [Signed job records](https://doingwork.work/docs/reference/#signed-job-records) |

## When to use it

One stored job PDF (base64) with its sha256, byte for byte as it was made.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `document_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/documents/get?document_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# expenses.add

Record money a worker spent on a job, with the receipt photo. The worker types the amount and tax from the receipt; core never reads or fills in amounts. spent_on defaults to today in the shop's time zone.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/expenses` |
| MCP tool | `expenses_add` |
| Writes | yes, send an Idempotency-Key |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

Record money a worker spent on a job, with the receipt photo. The worker types the amount and tax from the receipt; core never reads or fills in amounts.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `spent_on` | string | no |
| `vendor` | string | no |
| `category` | string (one of materials, equipment, subcontractor, permit, fuel, disposal, other) | no |
| `amount_minor` | integer | yes |
| `tax_minor` | integer | no |
| `receipt_media_id` | string (uuid) | no |
| `note` | string | no |

## Example

```http
POST https://doingwork.work/v1/expenses
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "amount_minor": 50000
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# expenses.update

Change an expense: date, vendor, category, amount, tax, receipt photo or note. Amounts are still typed by a worker. The worker who entered it, the owner or office can change it.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/expenses/update` |
| MCP tool | `expenses_update` |
| Writes | yes, send an Idempotency-Key |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

Change an expense: date, vendor, category, amount, tax, receipt photo or note. Amounts are still typed by a worker.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `expense_id` | string (uuid) | yes |
| `spent_on` | string | no |
| `vendor` | string | no |
| `category` | string (one of materials, equipment, subcontractor, permit, fuel, disposal, other) | no |
| `amount_minor` | integer | no |
| `tax_minor` | integer | no |
| `receipt_media_id` | string (uuid) | no |
| `note` | string | no |

## Example

```http
POST https://doingwork.work/v1/expenses/update
content-type: application/json
Idempotency-Key: 3f1c...

{
  "expense_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# expenses.remove

Take an expense off the books (kept in history). The worker who entered it, the owner or office can.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/expenses/remove` |
| MCP tool | `expenses_remove` |
| Writes | yes, send an Idempotency-Key |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

Take an expense off the books (kept in history). The worker who entered it, the owner or office can.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `expense_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/expenses/remove
content-type: application/json
Idempotency-Key: 3f1c...

{
  "expense_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# expenses.list

Expenses on a job, or across the shop between dates, with totals by category. Owner and office see every expense; a field worker sees the ones they entered.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/expenses` |
| MCP tool | `expenses_list` |
| Writes | no |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

Expenses on a job, or across the shop between dates, with totals by category. Owner and office see every expense; a field worker sees the ones they entered.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | no |
| `shop_id` | string (uuid) | no |
| `from` | string | no |
| `to` | string | no |
| `category` | string (one of materials, equipment, subcontractor, permit, fuel, disposal, other) | no |

## Example

```http
GET https://doingwork.work/v1/expenses?job_id=00000000-0000-4000-8000-000000000000&shop_id=00000000-0000-4000-8000-000000000000&from=...&to=...&category=materials
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/search` |
| MCP tool | `search_global` |
| Writes | no |
| Area | [Imports, custom fields, tags and search](https://doingwork.work/docs/reference/#imports-custom-fields-tags-and-search) |

## When to use it

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

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `q` | string | yes |
| `limit` | integer | no |

## Example

```http
GET https://doingwork.work/v1/search?shop_id=00000000-0000-4000-8000-000000000000&q=...
```

## Output

| Field | Type | Always there |
|---|---|---|
| `customers` | array | yes |
| `customers[].customer_id` | string (uuid) | yes |
| `customers[].name` | string | yes |
| `customers[].email` | ['string', 'null'] | yes |
| `customers[].phone` | ['string', 'null'] | yes |
| `properties` | array | yes |
| `properties[].property_id` | string (uuid) | yes |
| `properties[].customer_id` | string (uuid) | yes |
| `properties[].label` | ['string', 'null'] | yes |
| `properties[].address_line1` | string | yes |
| `jobs` | array | yes |
| `jobs[].job_id` | string (uuid) | yes |
| `jobs[].customer_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `jobs[].status` | string | yes |
| `jobs[].summary` | ['string', 'null'] | yes |
| `requests` | array | yes |
| `requests[].request_id` | string (uuid) | yes |
| `requests[].customer_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `requests[].title` | string | yes |

```json
{
  "customers": [
    {
      "customer_id": "00000000-0000-4000-8000-000000000000",
      "name": "Ruiz Plumbing",
      "email": "owner@example.com",
      "phone": "..."
    }
  ],
  "properties": [
    {
      "property_id": "00000000-0000-4000-8000-000000000000",
      "customer_id": "00000000-0000-4000-8000-000000000000",
      "label": "...",
      "address_line1": "..."
    }
  ],
  "jobs": [
    {
      "job_id": "00000000-0000-4000-8000-000000000000",
      "customer_id": "00000000-0000-4000-8000-000000000000",
      "status": "...",
      "summary": "..."
    }
  ],
  "requests": [
    {
      "request_id": "00000000-0000-4000-8000-000000000000",
      "customer_id": "00000000-0000-4000-8000-000000000000",
      "title": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/events/catalog` |
| MCP tool | `events_catalog` |
| Writes | no |
| Area | [Developers and platforms](https://doingwork.work/docs/reference/#developers-and-platforms) |

## When to use it

Add an https endpoint and Doing Work posts signed events to it as things happen. The signing secret is shown once.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| (none) |  |  |

## Example

```http
GET https://doingwork.work/v1/events/catalog?
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# webhooks.deliveries

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/webhooks/deliveries` |
| MCP tool | `webhooks_deliveries` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Add an https endpoint and Doing Work posts signed events to it as things happen. The signing secret is shown once.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `endpoint_id` | string (uuid) | yes |
| `status` | string (one of pending, succeeded, failed, dead) | no |
| `limit` | integer | no |
| `before_id` | integer | no |

## Example

```http
GET https://doingwork.work/v1/webhooks/deliveries?endpoint_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# webhooks.retry

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/webhooks/retry` |
| MCP tool | `webhooks_retry` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Add an https endpoint and Doing Work posts signed events to it as things happen. The signing secret is shown once.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `delivery_id` | integer | yes |

## Example

```http
POST https://doingwork.work/v1/webhooks/retry
content-type: application/json
Idempotency-Key: 3f1c...

{
  "delivery_id": 1
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# imports.fields

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/imports/fields` |
| MCP tool | `imports_fields` |
| Writes | no |
| Area | [Imports, custom fields, tags and search](https://doingwork.work/docs/reference/#imports-custom-fields-tags-and-search) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| (none) |  |  |

## Example

```http
GET https://doingwork.work/v1/imports/fields?
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/imports/preview` |
| MCP tool | `imports_preview` |
| Writes | no |
| Area | [Imports, custom fields, tags and search](https://doingwork.work/docs/reference/#imports-custom-fields-tags-and-search) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `csv` | string | yes |
| `preset` | string (one of jobber, housecall_pro, companycam) | no |
| `mapping` | array | no |
| `mapping[].column` | string | yes |
| `mapping[].field` | string (one of name, first_name, last_name, is_company, company_name, email, phone, lead_source, tags, notes, external_id, property.line1, property.line2, property.locality, property.region, property.postal_code, property.full_address, billing.line1, billing.line2, billing.locality, billing.region, billing.postal_code, billing.country, skip) | yes |

## Example

```http
POST https://doingwork.work/v1/imports/preview
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "csv": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/imports/run` |
| MCP tool | `imports_run` |
| Writes | yes, send an Idempotency-Key |
| Area | [Imports, custom fields, tags and search](https://doingwork.work/docs/reference/#imports-custom-fields-tags-and-search) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `csv` | string | yes |
| `mapping` | array | yes |
| `mapping[].column` | string | yes |
| `mapping[].field` | string (one of name, first_name, last_name, is_company, company_name, email, phone, lead_source, tags, notes, external_id, property.line1, property.line2, property.locality, property.region, property.postal_code, property.full_address, billing.line1, billing.line2, billing.locality, billing.region, billing.postal_code, billing.country, skip) | yes |
| `mode` | string (one of all_or_nothing, per_row) | no |
| `dry_run` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/imports/run
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "csv": "...",
  "mapping": [
    {
      "column": "...",
      "field": "name"
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/shops/export` |
| MCP tool | `shops_export` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `include_files` | boolean | no |
| `delivery` | string (one of link, inline) | no |
| `link_minutes` | integer | no |

## Example

```http
POST https://doingwork.work/v1/shops/export
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `filename` | string | yes |
| `content_type` | string | yes |
| `bytes` | integer | yes |
| `sha256` | string | yes |
| `delivery` | string | yes |
| `download_url` | string | no |
| `expires_at` | string (date-time) | no |
| `max_downloads` | integer | no |
| `export_id` | string (uuid) | no |
| `zip_base64` | string | no |
| `tables` | object | yes |
| `skipped_tables` | array | yes |
| `files` | integer | yes |

```json
{
  "filename": "...",
  "content_type": "...",
  "bytes": 1,
  "sha256": "...",
  "delivery": "...",
  "tables": {},
  "skipped_tables": [
    "..."
  ],
  "files": 1
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | A person's own sign-in only (agent tokens are refused) |
| REST | `POST /v1/keys` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

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.

## What to tell the owner

Ask the person to do this with their own sign-in. Your agent token is refused here.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string | yes |
| `label` | string | yes |
| `scopes` | array | no |
| `audience` | string (one of any, mcp) | no |
| `accept_terms_version` | string | yes |

## Example

```http
POST https://doingwork.work/v1/keys
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "label": "...",
  "accept_terms_version": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/keys` |
| MCP tool | `keys_list` |
| Writes | no |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string | yes |
| `audience` | string (one of any, mcp) | no |
| `include_revoked` | boolean | no |

## Example

```http
GET https://doingwork.work/v1/keys?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/keys/revoke` |
| MCP tool | `keys_revoke` |
| Writes | yes, send an Idempotency-Key |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `key_id` | string | yes |
| `reason` | string | no |

## Example

```http
POST https://doingwork.work/v1/keys/revoke
content-type: application/json
Idempotency-Key: 3f1c...

{
  "key_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# agents.register

auth.md agent registration. type anonymous: get a claim_token, then call the claim endpoint with the email of the shop owner who should claim you. type service_auth: pass login_hint (that email) and get the claim code in one step. Before the claim you only reach public calls (pre_claim_scopes is empty). Optional scope narrows the key you will get (read_only, no_money); client_name is shown to the owner. identity_assertion is not accepted (issuer_not_enabled).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/agent/identity` |
| MCP tool | `agents_register` |
| Writes | yes, send an Idempotency-Key |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

auth.md agent registration. type anonymous: get a claim_token, then call the claim endpoint with the email of the shop owner who should claim you.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `type` | string (one of anonymous, service_auth, identity_assertion) | yes |
| `login_hint` | string | no |
| `assertion_type` | string | no |
| `assertion` | string | no |
| `client_name` | string | no |
| `scope` | string | no |

## Example

```http
POST https://doingwork.work/v1/agent/identity
content-type: application/json
Idempotency-Key: 3f1c...

{
  "type": "anonymous"
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `registration_id` | string | yes |
| `registration_type` | string (one of anonymous, service_auth) | yes |
| `pre_claim_scopes` | array | yes |
| `claim_url` | string | yes |
| `claim_token` | string | yes |
| `claim_token_expires` | string (date-time) | yes |
| `post_claim_scopes` | array | yes |
| `claim` | object | no |
| `claim.user_code` | string | yes |
| `claim.expires_in` | integer | yes |
| `claim.verification_uri` | string | yes |
| `claim.interval` | integer | yes |

```json
{
  "registration_id": "00000000-0000-4000-8000-000000000000",
  "registration_type": "anonymous",
  "pre_claim_scopes": [
    "..."
  ],
  "claim_url": "...",
  "claim_token": "...",
  "claim_token_expires": "...",
  "post_claim_scopes": [
    "..."
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# agents.claim

auth.md claim. Pass your claim_token and the email of the shop owner who should claim you. You get a 6 digit user_code and a verification_uri: tell the person to open the link, sign in with that email and type the code. Then poll the token endpoint with grant_type urn:workos:agent-auth:grant-type:claim. Calling again with the same claim_token starts a fresh code and cancels the old one.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/agent/identity/claim` |
| MCP tool | `agents_claim` |
| Writes | yes, send an Idempotency-Key |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

auth.md claim. Pass your claim_token and the email of the shop owner who should claim you.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `claim_token` | string | yes |
| `email` | string | yes |

## Example

```http
POST https://doingwork.work/v1/agent/identity/claim
content-type: application/json
Idempotency-Key: 3f1c...

{
  "claim_token": "...",
  "email": "owner@example.com"
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `registration_id` | string | yes |
| `claim_attempt_id` | string | yes |
| `status` | string (one of initiated) | yes |
| `expires_at` | string (date-time) | yes |
| `claim_attempt` | object | yes |
| `claim_attempt.user_code` | string | yes |
| `claim_attempt.expires_in` | integer | yes |
| `claim_attempt.verification_uri` | string | yes |
| `claim_attempt.interval` | integer | yes |

```json
{
  "registration_id": "00000000-0000-4000-8000-000000000000",
  "claim_attempt_id": "00000000-0000-4000-8000-000000000000",
  "status": "initiated",
  "expires_at": "...",
  "claim_attempt": {
    "user_code": "...",
    "expires_in": 1,
    "verification_uri": "...",
    "interval": 1
  }
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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. Makes a named agent key for that shop. Optional scopes narrow it further than the agent asked.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/agent/identity/approve` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

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. Makes a named agent key for that shop.

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `claim_attempt_token` | string | yes |
| `user_code` | string | yes |
| `shop_id` | string | yes |
| `scopes` | array | no |
| `accept_terms_version` | string | yes |

## Example

```http
POST https://doingwork.work/v1/agent/identity/approve
content-type: application/json
Idempotency-Key: 3f1c...

{
  "claim_attempt_token": "...",
  "user_code": "...",
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "accept_terms_version": "..."
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `claimed` | boolean | yes |
| `key_id` | string | yes |
| `label` | string | yes |
| `scopes` | array | yes |

```json
{
  "claimed": true,
  "key_id": "00000000-0000-4000-8000-000000000000",
  "label": "...",
  "scopes": [
    "..."
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# agents.deny

Hosted claim page only: the person turns the agent down. The registration closes and the agent's poll gets access_denied.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/agent/identity/deny` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

Hosted claim page only: the person turns the agent down. The registration closes and the agent's poll gets access_denied.

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `claim_attempt_token` | string | yes |

## Example

```http
POST https://doingwork.work/v1/agent/identity/deny
content-type: application/json
Idempotency-Key: 3f1c...

{
  "claim_attempt_token": "..."
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `denied` | boolean | yes |

```json
{
  "denied": true
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/reports/revenue` |
| MCP tool | `reports_revenue` |
| Writes | no |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `from` | string | yes |
| `to` | string | yes |

## Example

```http
GET https://doingwork.work/v1/reports/revenue?shop_id=00000000-0000-4000-8000-000000000000&from=...&to=...
```

## Output

| Field | Type | Always there |
|---|---|---|
| `months` | array | yes |
| `total` | array | yes |

```json
{
  "months": [
    {}
  ],
  "total": [
    {}
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/reports/owed` |
| MCP tool | `reports_owed` |
| Writes | no |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/reports/owed?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `buckets` | array | yes |
| `invoices` | integer | yes |

```json
{
  "buckets": [
    {}
  ],
  "invoices": 1
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# reports.quoteWinRate

Of the quotes made between two dates (inclusive, UTC) and sent, how many were signed, declined, or are still waiting. Win rate is signed over signed plus declined; null when none are decided yet.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/reports/quote-win-rate` |
| MCP tool | `reports_quoteWinRate` |
| Writes | no |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

Of the quotes made between two dates (inclusive, UTC) and sent, how many were signed, declined, or are still waiting. Win rate is signed over signed plus declined; null when none are decided yet.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `from` | string | yes |
| `to` | string | yes |

## Example

```http
GET https://doingwork.work/v1/reports/quote-win-rate?shop_id=00000000-0000-4000-8000-000000000000&from=...&to=...
```

## Output

| Field | Type | Always there |
|---|---|---|
| `sent` | integer | yes |
| `signed` | integer | yes |
| `declined` | integer | yes |
| `waiting` | integer | yes |
| `win_rate` | ['number', 'null'] | yes |

```json
{
  "sent": 1,
  "signed": 1,
  "declined": 1,
  "waiting": 1,
  "win_rate": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# members.permissions

What each current worker in a shop may do beyond their role: see_prices, see_costs, take_money, delete and manage_schedule. The owner sees everyone; anyone else sees only themselves. The owner always has every permission.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/members/permissions` |
| MCP tool | `members_permissions` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

What each current worker in a shop may do beyond their role: see_prices, see_costs, take_money, delete and manage_schedule. The owner sees everyone; anyone else sees only themselves.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `person_id` | string (uuid) | no |

## Example

```http
GET https://doingwork.work/v1/members/permissions?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `members` | array | yes |
| `members[].membership_id` | string (uuid) | yes |
| `members[].person_id` | string (uuid) | yes |
| `members[].display_name` | string | no |
| `members[].role` | string (one of owner, office, field) | yes |
| `members[].permissions` | object | yes |
| `members[].permissions.see_prices` | boolean | yes |
| `members[].permissions.see_costs` | boolean | yes |
| `members[].permissions.take_money` | boolean | yes |
| `members[].permissions.delete` | boolean | yes |
| `members[].permissions.manage_schedule` | boolean | yes |
| `members[].customized` | boolean | yes |

```json
{
  "members": [
    {
      "membership_id": "00000000-0000-4000-8000-000000000000",
      "person_id": "00000000-0000-4000-8000-000000000000",
      "role": "owner",
      "permissions": {
        "see_prices": true,
        "see_costs": true,
        "take_money": true,
        "delete": true,
        "manage_schedule": true
      },
      "customized": true
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# members.setPermissions

Owner only, not an agent: turn a worker's permissions on or off. Give only the flags to change (see_prices, see_costs, take_money, delete, manage_schedule). They narrow what the worker's role allows and apply to the worker's own agents too; they never give more than the role. A role change starts again from the defaults (all on).

|  |  |
|---|---|
| Who | A person's own sign-in only (agent tokens are refused) |
| REST | `POST /v1/members/permissions` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Owner only, not an agent: turn a worker's permissions on or off. Give only the flags to change (see_prices, see_costs, take_money, delete, manage_schedule).

## What to tell the owner

Ask the person to do this with their own sign-in. Your agent token is refused here.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `person_id` | string (uuid) | yes |
| `see_prices` | boolean | no |
| `see_costs` | boolean | no |
| `take_money` | boolean | no |
| `delete` | boolean | no |
| `manage_schedule` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/members/permissions
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "person_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# legal.documents

The legal documents Doing Work asks people to accept, with the current version and a link to the text. A document marked placeholder has no text yet and cannot be accepted.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/legal/documents` |
| MCP tool | `legal_documents` |
| Writes | no |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

The legal documents Doing Work asks people to accept, with the current version and a link to the text. A document marked placeholder has no text yet and cannot be accepted.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| (none) |  |  |

## Example

```http
GET https://doingwork.work/v1/legal/documents?
```

## Output

| Field | Type | Always there |
|---|---|---|
| `documents` | array | yes |
| `documents[].document` | string | yes |
| `documents[].title` | string | yes |
| `documents[].version` | string | yes |
| `documents[].placeholder` | boolean | yes |
| `documents[].accepted_by` | string | yes |
| `documents[].url` | string | yes |

```json
{
  "documents": [
    {
      "document": "...",
      "title": "...",
      "version": "...",
      "placeholder": true,
      "accepted_by": "...",
      "url": "https://example.com/hooks/doing-work"
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/legal/status` |
| MCP tool | `legal_status` |
| Writes | no |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| (none) |  |  |

## Example

```http
GET https://doingwork.work/v1/legal/status?
```

## Output

| Field | Type | Always there |
|---|---|---|
| `documents` | array | yes |
| `documents[].document` | string | yes |
| `documents[].current_version` | string | yes |
| `documents[].placeholder` | boolean | yes |
| `documents[].accepted` | array | yes |
| `documents[].accepted[].version` | string | yes |
| `documents[].accepted[].accepted_at` | string | yes |
| `documents[].needs_acceptance` | boolean | yes |

```json
{
  "documents": [
    {
      "document": "...",
      "current_version": "...",
      "placeholder": true,
      "accepted": [
        {
          "version": "...",
          "accepted_at": "..."
        }
      ],
      "needs_acceptance": true
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# legal.accept

Record that the signed-in person accepts the current version of a legal document (clickwrap). The person does this themselves; an agent cannot accept for them. Refused while the document is a placeholder or when the version is not the current one.

|  |  |
|---|---|
| Who | A person's own sign-in only (agent tokens are refused) |
| REST | `POST /v1/legal/accept` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Sign-in and agent access](https://doingwork.work/docs/reference/#sign-in-and-agent-access) |

## When to use it

Record that the signed-in person accepts the current version of a legal document (clickwrap). The person does this themselves; an agent cannot accept for them.

## What to tell the owner

Ask the person to do this with their own sign-in. Your agent token is refused here.

## Input

| Field | Type | Required |
|---|---|---|
| `document` | string (one of terms, privacy) | yes |
| `version` | string | yes |

## Example

```http
POST https://doingwork.work/v1/legal/accept
content-type: application/json
Idempotency-Key: 3f1c...

{
  "document": "terms",
  "version": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/signatures` |
| MCP tool | `signatures_request` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `kind` | string (one of contract, job_document) | yes |
| `job_id` | string (uuid) | no |
| `customer_id` | string (uuid) | no |
| `title` | string | yes |
| `text` | string | no |
| `document_id` | string (uuid) | no |
| `signer_role` | string (one of customer, member) | no |
| `signer_email` | string | no |
| `link_days` | integer | no |

## Example

```http
POST https://doingwork.work/v1/signatures
content-type: application/json
Idempotency-Key: 3f1c...

{
  "kind": "contract",
  "title": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# signatures.newLink

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/signatures/link` |
| MCP tool | `signatures_newLink` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `signature_request_id` | string (uuid) | yes |
| `link_days` | integer | no |

## Example

```http
POST https://doingwork.work/v1/signatures/link
content-type: application/json
Idempotency-Key: 3f1c...

{
  "signature_request_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# signatures.cancel

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/signatures/cancel` |
| MCP tool | `signatures_cancel` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `signature_request_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/signatures/cancel
content-type: application/json
Idempotency-Key: 3f1c...

{
  "signature_request_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/signatures/get` |
| MCP tool | `signatures_get` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `signature_request_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/signatures/get?signature_request_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `signature_request_id` | string (uuid) | yes |
| `shop_id` | string (uuid) | yes |
| `job_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `customer_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `kind` | string | yes |
| `subject_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `title` | string | yes |
| `job_document_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `document_sha256` | string | yes |
| `signer_role` | string | yes |
| `signer_email` | ['string', 'null'] | yes |
| `status` | string | yes |
| `created_by` | string (uuid) | yes |
| `created_at` | string (date-time) | yes |
| `cancelled_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `text` | ['string', 'null'] | yes |
| `signature` |  | yes |

```json
{
  "signature_request_id": "00000000-0000-4000-8000-000000000000",
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "job_id": "00000000-0000-4000-8000-000000000000",
  "customer_id": "00000000-0000-4000-8000-000000000000",
  "kind": "...",
  "subject_id": "00000000-0000-4000-8000-000000000000",
  "title": "...",
  "job_document_id": "00000000-0000-4000-8000-000000000000",
  "document_sha256": "...",
  "signer_role": "...",
  "signer_email": "...",
  "status": "...",
  "created_by": "00000000-0000-4000-8000-000000000000",
  "created_at": "...",
  "cancelled_at": "...",
  "text": "...",
  "signature": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/signatures` |
| MCP tool | `signatures_list` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | no |
| `job_id` | string (uuid) | no |
| `customer_id` | string (uuid) | no |
| `kind` | string | no |
| `subject_id` | string (uuid) | no |
| `status` | string (one of open, signed, cancelled) | no |
| `limit` | integer | no |

## Example

```http
GET https://doingwork.work/v1/signatures?shop_id=00000000-0000-4000-8000-000000000000&job_id=00000000-0000-4000-8000-000000000000&customer_id=00000000-0000-4000-8000-000000000000&kind=...&subject_id=00000000-0000-4000-8000-000000000000&status=open&limit=1
```

## Output

| Field | Type | Always there |
|---|---|---|
| `items` | array | yes |
| `items[].signature_request_id` | string (uuid) | yes |
| `items[].shop_id` | string (uuid) | yes |
| `items[].job_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].customer_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].kind` | string | yes |
| `items[].subject_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].title` | string | yes |
| `items[].job_document_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].document_sha256` | string | yes |
| `items[].signer_role` | string | yes |
| `items[].signer_email` | ['string', 'null'] | yes |
| `items[].status` | string | yes |
| `items[].created_by` | string (uuid) | yes |
| `items[].created_at` | string (date-time) | yes |
| `items[].cancelled_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].signature_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].signer_name` | ['string', 'null'] | yes |
| `items[].signed_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |

```json
{
  "items": [
    {
      "signature_request_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "job_id": "00000000-0000-4000-8000-000000000000",
      "customer_id": "00000000-0000-4000-8000-000000000000",
      "kind": "...",
      "subject_id": "00000000-0000-4000-8000-000000000000",
      "title": "...",
      "job_document_id": "00000000-0000-4000-8000-000000000000",
      "document_sha256": "...",
      "signer_role": "...",
      "signer_email": "...",
      "status": "...",
      "created_by": "00000000-0000-4000-8000-000000000000",
      "created_at": "...",
      "cancelled_at": "...",
      "signature_id": "00000000-0000-4000-8000-000000000000",
      "signer_name": "...",
      "signed_at": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# signatures.sign

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

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/signatures/sign` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `t` | string | yes |
| `document_sha256` | string | yes |
| `signer_name` | string | yes |
| `esign_consent` | string (one of yes) | yes |

## Example

```http
POST https://doingwork.work/v1/signatures/sign
content-type: application/json
Idempotency-Key: 3f1c...

{
  "t": "...",
  "document_sha256": "...",
  "signer_name": "...",
  "esign_consent": "yes"
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `signature_id` | string (uuid) | yes |
| `signature_request_id` | string (uuid) | yes |
| `document_sha256` | string | yes |
| `consent_sha256` | string | yes |
| `signed_at` | string (date-time) | yes |

```json
{
  "signature_id": "00000000-0000-4000-8000-000000000000",
  "signature_request_id": "00000000-0000-4000-8000-000000000000",
  "document_sha256": "...",
  "consent_sha256": "...",
  "signed_at": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/checklist-templates` |
| MCP tool | `checklists_saveTemplate` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `checklist_template_id` | string (uuid) | no |
| `name` | string | yes |
| `description` | string | no |
| `signoff_required` | boolean | no |
| `items` | array | yes |
| `items[].label` | string | yes |
| `items[].required` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/checklist-templates
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "name": "Ruiz Plumbing",
  "items": [
    {
      "label": "..."
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# checklists.listTemplates

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/checklist-templates` |
| MCP tool | `checklists_listTemplates` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `include_archived` | boolean | no |

## Example

```http
GET https://doingwork.work/v1/checklist-templates?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `data` | array | yes |
| `data[].checklist_template_id` | string (uuid) | yes |
| `data[].shop_id` | string (uuid) | yes |
| `data[].name` | string | yes |
| `data[].description` | ['string', 'null'] | yes |
| `data[].signoff_required` | boolean | yes |
| `data[].archived_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `data[].updated_at` | string (date-time) | yes |
| `data[].items` | array | yes |
| `data[].items[].position` | integer | yes |
| `data[].items[].label` | string | yes |
| `data[].items[].required` | boolean | yes |

```json
{
  "data": [
    {
      "checklist_template_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "name": "Ruiz Plumbing",
      "description": "40 gallon gas heater, installed",
      "signoff_required": true,
      "archived_at": "...",
      "updated_at": "...",
      "items": [
        {
          "position": 1,
          "label": "...",
          "required": true
        }
      ]
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# checklists.archiveTemplate

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/checklist-templates/archive` |
| MCP tool | `checklists_archiveTemplate` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `checklist_template_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/checklist-templates/archive
content-type: application/json
Idempotency-Key: 3f1c...

{
  "checklist_template_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/checklists` |
| MCP tool | `checklists_create` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `visit_id` | string (uuid) | no |
| `checklist_template_id` | string (uuid) | no |
| `name` | string | no |
| `signoff_required` | boolean | no |
| `items` | array | no |
| `items[].label` | string | yes |
| `items[].required` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/checklists
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/checklists` |
| MCP tool | `checklists_list` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | no |
| `visit_id` | string (uuid) | no |

## Example

```http
GET https://doingwork.work/v1/checklists?job_id=00000000-0000-4000-8000-000000000000&visit_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `data` | array | yes |
| `data[].checklist_id` | string (uuid) | yes |
| `data[].shop_id` | string (uuid) | yes |
| `data[].job_id` | string (uuid) | yes |
| `data[].visit_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `data[].checklist_template_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `data[].name` | string | yes |
| `data[].signoff_required` | boolean | yes |
| `data[].open_required` | integer | yes |
| `data[].done_items` | integer | yes |
| `data[].total_items` | integer | yes |
| `data[].signature_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `data[].signoff_request_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `data[].locked` | ['string', 'null'] | yes |
| `data[].created_at` | string (date-time) | yes |

```json
{
  "data": [
    {
      "checklist_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "job_id": "00000000-0000-4000-8000-000000000000",
      "visit_id": "00000000-0000-4000-8000-000000000000",
      "checklist_template_id": "00000000-0000-4000-8000-000000000000",
      "name": "Ruiz Plumbing",
      "signoff_required": true,
      "open_required": 1,
      "done_items": 1,
      "total_items": 1,
      "signature_id": "00000000-0000-4000-8000-000000000000",
      "signoff_request_id": "00000000-0000-4000-8000-000000000000",
      "locked": "...",
      "created_at": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# checklists.get

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/checklists/get` |
| MCP tool | `checklists_get` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `checklist_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/checklists/get?checklist_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `checklist_id` | string (uuid) | yes |
| `shop_id` | string (uuid) | yes |
| `job_id` | string (uuid) | yes |
| `visit_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `checklist_template_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `name` | string | yes |
| `signoff_required` | boolean | yes |
| `open_required` | integer | yes |
| `done_items` | integer | yes |
| `total_items` | integer | yes |
| `signature_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `signoff_request_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `locked` | ['string', 'null'] | yes |
| `created_at` | string (date-time) | yes |
| `items` | array | yes |
| `items[].checklist_item_id` | string (uuid) | yes |
| `items[].position` | integer | yes |
| `items[].label` | string | yes |
| `items[].required` | boolean | yes |
| `items[].done` | boolean | yes |
| `items[].done_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].done_by` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].note` | ['string', 'null'] | yes |
| `items[].removed` | boolean | yes |

```json
{
  "checklist_id": "00000000-0000-4000-8000-000000000000",
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "job_id": "00000000-0000-4000-8000-000000000000",
  "visit_id": "00000000-0000-4000-8000-000000000000",
  "checklist_template_id": "00000000-0000-4000-8000-000000000000",
  "name": "Ruiz Plumbing",
  "signoff_required": true,
  "open_required": 1,
  "done_items": 1,
  "total_items": 1,
  "signature_id": "00000000-0000-4000-8000-000000000000",
  "signoff_request_id": "00000000-0000-4000-8000-000000000000",
  "locked": "...",
  "created_at": "...",
  "items": [
    {
      "checklist_item_id": "00000000-0000-4000-8000-000000000000",
      "position": 1,
      "label": "...",
      "required": true,
      "done": true,
      "done_at": "...",
      "done_by": "00000000-0000-4000-8000-000000000000",
      "note": "...",
      "removed": true
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/checklists/check` |
| MCP tool | `checklists_checkItem` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `checklist_item_id` | string (uuid) | yes |
| `done` | boolean | yes |
| `note` | string | no |

## Example

```http
POST https://doingwork.work/v1/checklists/check
content-type: application/json
Idempotency-Key: 3f1c...

{
  "checklist_item_id": "00000000-0000-4000-8000-000000000000",
  "done": true
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# checklists.addItems

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/checklists/items` |
| MCP tool | `checklists_addItems` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `checklist_id` | string (uuid) | yes |
| `items` | array | yes |
| `items[].label` | string | yes |
| `items[].required` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/checklists/items
content-type: application/json
Idempotency-Key: 3f1c...

{
  "checklist_id": "00000000-0000-4000-8000-000000000000",
  "items": [
    {
      "label": "..."
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/checklists/items/remove` |
| MCP tool | `checklists_removeItem` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `checklist_item_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/checklists/items/remove
content-type: application/json
Idempotency-Key: 3f1c...

{
  "checklist_item_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/checklists/sign-off` |
| MCP tool | `checklists_requestSignOff` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `checklist_id` | string (uuid) | yes |
| `signer_role` | string (one of customer, member) | no |
| `signer_email` | string | no |
| `link_days` | integer | no |

## Example

```http
POST https://doingwork.work/v1/checklists/sign-off
content-type: application/json
Idempotency-Key: 3f1c...

{
  "checklist_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/jobs/complete` |
| MCP tool | `jobs_complete` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `override_reason` | string | no |

## Example

```http
POST https://doingwork.work/v1/jobs/complete
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/forms/templates` |
| MCP tool | `forms_saveTemplate` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | no |
| `form_template_id` | string (uuid) | no |
| `name` | string | yes |
| `description` | string | no |
| `fields` | array | yes |
| `fields[].key` | string | yes |
| `fields[].label` | string | yes |
| `fields[].type` | string (one of heading, text, long_text, number, yes_no, choice, multi_choice, date, photos) | yes |
| `fields[].required` | boolean | no |
| `fields[].help` | string | no |
| `fields[].options` | array | no |
| `signoff_required` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/forms/templates
content-type: application/json
Idempotency-Key: 3f1c...

{
  "name": "Ruiz Plumbing",
  "fields": [
    {
      "key": "...",
      "label": "...",
      "type": "heading"
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# forms.listTemplates

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/forms/templates` |
| MCP tool | `forms_listTemplates` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `include_archived` | boolean | no |

## Example

```http
GET https://doingwork.work/v1/forms/templates?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `items` | array | yes |
| `items[].form_template_id` | string (uuid) | yes |
| `items[].shop_id` | string (uuid) | yes |
| `items[].name` | string | yes |
| `items[].description` | ['string', 'null'] | yes |
| `items[].signoff_required` | boolean | yes |
| `items[].fields` | array | yes |
| `items[].fields[].key` | string | yes |
| `items[].fields[].label` | string | yes |
| `items[].fields[].type` | string | yes |
| `items[].fields[].required` | boolean | yes |
| `items[].fields[].help` | string | no |
| `items[].fields[].options` | array | no |
| `items[].archived_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].updated_at` | string (date-time) | yes |

```json
{
  "items": [
    {
      "form_template_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "name": "Ruiz Plumbing",
      "description": "40 gallon gas heater, installed",
      "signoff_required": true,
      "fields": [
        {
          "key": "...",
          "label": "...",
          "type": "...",
          "required": true
        }
      ],
      "archived_at": "...",
      "updated_at": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# forms.archiveTemplate

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/forms/templates/archive` |
| MCP tool | `forms_archiveTemplate` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `form_template_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/forms/templates/archive
content-type: application/json
Idempotency-Key: 3f1c...

{
  "form_template_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/forms` |
| MCP tool | `forms_start` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `visit_id` | string (uuid) | no |
| `form_template_id` | string (uuid) | no |
| `copy_from_form_id` | string (uuid) | no |
| `name` | string | no |
| `fields` | array | no |
| `fields[].key` | string | yes |
| `fields[].label` | string | yes |
| `fields[].type` | string (one of heading, text, long_text, number, yes_no, choice, multi_choice, date, photos) | yes |
| `fields[].required` | boolean | no |
| `fields[].help` | string | no |
| `fields[].options` | array | no |
| `signoff_required` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/forms
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/forms/fill` |
| MCP tool | `forms_fill` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `form_id` | string (uuid) | yes |
| `answers` | object | yes |

## Example

```http
POST https://doingwork.work/v1/forms/fill
content-type: application/json
Idempotency-Key: 3f1c...

{
  "form_id": "00000000-0000-4000-8000-000000000000",
  "answers": {}
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/forms/submit` |
| MCP tool | `forms_submit` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `form_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/forms/submit
content-type: application/json
Idempotency-Key: 3f1c...

{
  "form_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/forms/cancel` |
| MCP tool | `forms_cancel` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `form_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/forms/cancel
content-type: application/json
Idempotency-Key: 3f1c...

{
  "form_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/forms/get` |
| MCP tool | `forms_get` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `form_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/forms/get?form_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `form_id` | string (uuid) | yes |
| `shop_id` | string (uuid) | yes |
| `job_id` | string (uuid) | yes |
| `visit_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `form_template_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `copied_from_form_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `name` | string | yes |
| `status` | string | yes |
| `signoff_required` | boolean | yes |
| `missing` | array | yes |
| `created_by` | string (uuid) | yes |
| `created_at` | string (date-time) | yes |
| `updated_at` | string (date-time) | yes |
| `submitted_by` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `submitted_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `document_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `document_sha256` | ['string', 'null'] | yes |
| `cancelled_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `signature_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `signoff_request_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `fields` | array | yes |
| `fields[].key` | string | yes |
| `fields[].label` | string | yes |
| `fields[].type` | string | yes |
| `fields[].required` | boolean | yes |
| `fields[].help` | string | no |
| `fields[].options` | array | no |
| `answers` | object | yes |

```json
{
  "form_id": "00000000-0000-4000-8000-000000000000",
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "job_id": "00000000-0000-4000-8000-000000000000",
  "visit_id": "00000000-0000-4000-8000-000000000000",
  "form_template_id": "00000000-0000-4000-8000-000000000000",
  "copied_from_form_id": "00000000-0000-4000-8000-000000000000",
  "name": "Ruiz Plumbing",
  "status": "...",
  "signoff_required": true,
  "missing": [
    "..."
  ],
  "created_by": "00000000-0000-4000-8000-000000000000",
  "created_at": "...",
  "updated_at": "...",
  "submitted_by": "00000000-0000-4000-8000-000000000000",
  "submitted_at": "...",
  "document_id": "00000000-0000-4000-8000-000000000000",
  "document_sha256": "...",
  "cancelled_at": "...",
  "signature_id": "00000000-0000-4000-8000-000000000000",
  "signoff_request_id": "00000000-0000-4000-8000-000000000000",
  "fields": [
    {
      "key": "...",
      "label": "...",
      "type": "...",
      "required": true
    }
  ],
  "answers": {}
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# forms.list

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/forms` |
| MCP tool | `forms_list` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | no |
| `visit_id` | string (uuid) | no |
| `status` | string (one of draft, submitted, cancelled) | no |

## Example

```http
GET https://doingwork.work/v1/forms?job_id=00000000-0000-4000-8000-000000000000&visit_id=00000000-0000-4000-8000-000000000000&status=draft
```

## Output

| Field | Type | Always there |
|---|---|---|
| `items` | array | yes |
| `items[].form_id` | string (uuid) | yes |
| `items[].shop_id` | string (uuid) | yes |
| `items[].job_id` | string (uuid) | yes |
| `items[].visit_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].form_template_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].copied_from_form_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].name` | string | yes |
| `items[].status` | string | yes |
| `items[].signoff_required` | boolean | yes |
| `items[].missing` | array | yes |
| `items[].created_by` | string (uuid) | yes |
| `items[].created_at` | string (date-time) | yes |
| `items[].updated_at` | string (date-time) | yes |
| `items[].submitted_by` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].submitted_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].document_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].document_sha256` | ['string', 'null'] | yes |
| `items[].cancelled_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].signature_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].signoff_request_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |

```json
{
  "items": [
    {
      "form_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "job_id": "00000000-0000-4000-8000-000000000000",
      "visit_id": "00000000-0000-4000-8000-000000000000",
      "form_template_id": "00000000-0000-4000-8000-000000000000",
      "copied_from_form_id": "00000000-0000-4000-8000-000000000000",
      "name": "Ruiz Plumbing",
      "status": "...",
      "signoff_required": true,
      "missing": [
        "..."
      ],
      "created_by": "00000000-0000-4000-8000-000000000000",
      "created_at": "...",
      "updated_at": "...",
      "submitted_by": "00000000-0000-4000-8000-000000000000",
      "submitted_at": "...",
      "document_id": "00000000-0000-4000-8000-000000000000",
      "document_sha256": "...",
      "cancelled_at": "...",
      "signature_id": "00000000-0000-4000-8000-000000000000",
      "signoff_request_id": "00000000-0000-4000-8000-000000000000"
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/forms/sign-off` |
| MCP tool | `forms_requestSignOff` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `form_id` | string (uuid) | yes |
| `signer_role` | string (one of customer, member) | no |
| `signer_email` | string | no |
| `link_days` | integer | no |

## Example

```http
POST https://doingwork.work/v1/forms/sign-off
content-type: application/json
Idempotency-Key: 3f1c...

{
  "form_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/notes` |
| MCP tool | `notes_add` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | no |
| `visit_id` | string (uuid) | no |
| `body` | string | yes |
| `audience` | string (one of team, office) | no |
| `pinned` | boolean | no |
| `media_ids` | array | no |

## Example

```http
POST https://doingwork.work/v1/notes
content-type: application/json
Idempotency-Key: 3f1c...

{
  "body": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/notes/voice-memo` |
| MCP tool | `notes_addVoiceMemo` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | no |
| `visit_id` | string (uuid) | no |
| `transcript` | string | yes |
| `duration_seconds` | integer | yes |
| `recorded_at` | string (date-time) | no |
| `transcribed_with` | string | no |
| `language` | string | no |
| `audience` | string (one of team, office) | no |
| `pinned` | boolean | no |
| `media_ids` | array | no |

## Example

```http
POST https://doingwork.work/v1/notes/voice-memo
content-type: application/json
Idempotency-Key: 3f1c...

{
  "transcript": "...",
  "duration_seconds": 1
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/notes/update` |
| MCP tool | `notes_update` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `note_id` | string (uuid) | yes |
| `body` | string | no |
| `media_ids` | array | no |
| `pinned` | boolean | no |
| `audience` | string (one of team, office) | no |

## Example

```http
POST https://doingwork.work/v1/notes/update
content-type: application/json
Idempotency-Key: 3f1c...

{
  "note_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# notes.remove

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/notes/remove` |
| MCP tool | `notes_remove` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `note_id` | string (uuid) | yes |
| `reason` | string | no |

## Example

```http
POST https://doingwork.work/v1/notes/remove
content-type: application/json
Idempotency-Key: 3f1c...

{
  "note_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# notes.get

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/notes/get` |
| MCP tool | `notes_get` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `note_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/notes/get?note_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `note_id` | string (uuid) | yes |
| `shop_id` | string (uuid) | yes |
| `job_id` | string (uuid) | yes |
| `visit_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `source` | string | yes |
| `body` | string | yes |
| `audience` | string | yes |
| `pinned` | boolean | yes |
| `media_ids` | array | yes |
| `recorded_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `duration_seconds` | ['integer', 'null'] | yes |
| `transcribed_with` | ['string', 'null'] | yes |
| `language` | ['string', 'null'] | yes |
| `revisions` | integer | yes |
| `written_by` | string (uuid) | yes |
| `written_by_name` | ['string', 'null'] | yes |
| `created_at` | string (date-time) | yes |
| `edited_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `removed_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `removed_by` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `remove_reason` | ['string', 'null'] | yes |
| `history` | array | yes |
| `history[].body` | string | yes |
| `history[].until` | string (date-time) | yes |

```json
{
  "note_id": "00000000-0000-4000-8000-000000000000",
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "job_id": "00000000-0000-4000-8000-000000000000",
  "visit_id": "00000000-0000-4000-8000-000000000000",
  "source": "...",
  "body": "...",
  "audience": "...",
  "pinned": true,
  "media_ids": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "recorded_at": "...",
  "duration_seconds": "...",
  "transcribed_with": "...",
  "language": "...",
  "revisions": 1,
  "written_by": "00000000-0000-4000-8000-000000000000",
  "written_by_name": "...",
  "created_at": "...",
  "edited_at": "...",
  "removed_at": "...",
  "removed_by": "00000000-0000-4000-8000-000000000000",
  "remove_reason": "...",
  "history": [
    {
      "body": "...",
      "until": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/notes` |
| MCP tool | `notes_list` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | no |
| `visit_id` | string (uuid) | no |
| `source` | string (one of typed, voice_memo) | no |
| `include_removed` | string (one of true, false) | no |

## Example

```http
GET https://doingwork.work/v1/notes?job_id=00000000-0000-4000-8000-000000000000&visit_id=00000000-0000-4000-8000-000000000000&source=typed&include_removed=true
```

## Output

| Field | Type | Always there |
|---|---|---|
| `items` | array | yes |
| `items[].note_id` | string (uuid) | yes |
| `items[].shop_id` | string (uuid) | yes |
| `items[].job_id` | string (uuid) | yes |
| `items[].visit_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].source` | string | yes |
| `items[].body` | string | yes |
| `items[].audience` | string | yes |
| `items[].pinned` | boolean | yes |
| `items[].media_ids` | array | yes |
| `items[].recorded_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].duration_seconds` | ['integer', 'null'] | yes |
| `items[].transcribed_with` | ['string', 'null'] | yes |
| `items[].language` | ['string', 'null'] | yes |
| `items[].revisions` | integer | yes |
| `items[].written_by` | string (uuid) | yes |
| `items[].written_by_name` | ['string', 'null'] | yes |
| `items[].created_at` | string (date-time) | yes |
| `items[].edited_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].removed_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].removed_by` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].remove_reason` | ['string', 'null'] | yes |

```json
{
  "items": [
    {
      "note_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "job_id": "00000000-0000-4000-8000-000000000000",
      "visit_id": "00000000-0000-4000-8000-000000000000",
      "source": "...",
      "body": "...",
      "audience": "...",
      "pinned": true,
      "media_ids": [
        "00000000-0000-4000-8000-000000000000"
      ],
      "recorded_at": "...",
      "duration_seconds": "...",
      "transcribed_with": "...",
      "language": "...",
      "revisions": 1,
      "written_by": "00000000-0000-4000-8000-000000000000",
      "written_by_name": "...",
      "created_at": "...",
      "edited_at": "...",
      "removed_at": "...",
      "removed_by": "00000000-0000-4000-8000-000000000000",
      "remove_reason": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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. Owner or office; needs seeing costs and prices.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/costs/job` |
| MCP tool | `costs_job` |
| Writes | no |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

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. Owner or office; needs seeing costs and prices.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/costs/job?job_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `kind` | string | yes |
| `status` | string | yes |
| `summary` | ['string', 'null'] | yes |
| `customer_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `occurred_at` | string (date-time) | yes |
| `currency` | string | yes |
| `contract_minor` | integer | yes |
| `invoiced_minor` | integer | yes |
| `tax_minor` | integer | yes |
| `draft_invoices_minor` | integer | yes |
| `refunded_minor` | integer | yes |
| `collected_minor` | integer | yes |
| `revenue_minor` | integer | yes |
| `labor_minutes` | integer | yes |
| `labor_pending_minutes` | integer | yes |
| `labor_cost_minor` | integer | yes |
| `labor_pending_cost_minor` | integer | yes |
| `uncosted_minutes` | integer | yes |
| `running_clocks` | integer | yes |
| `materials_cost_minor` | integer | yes |
| `materials_price_minor` | integer | yes |
| `uncosted_materials` | integer | yes |
| `expenses_minor` | integer | yes |
| `expenses_by_category` | object | yes |
| `fees_minor` | integer | yes |
| `cost_minor` | integer | yes |
| `profit_minor` | integer | yes |
| `margin` | ['number', 'null'] | yes |
| `unbilled_minor` | integer | yes |

```json
{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "kind": "...",
  "status": "...",
  "summary": "...",
  "customer_id": "00000000-0000-4000-8000-000000000000",
  "occurred_at": "...",
  "currency": "...",
  "contract_minor": 50000,
  "invoiced_minor": 50000,
  "tax_minor": 50000,
  "draft_invoices_minor": 50000,
  "refunded_minor": 50000,
  "collected_minor": 50000,
  "revenue_minor": 50000,
  "labor_minutes": 1,
  "labor_pending_minutes": 1,
  "labor_cost_minor": 50000,
  "labor_pending_cost_minor": 50000,
  "uncosted_minutes": 1,
  "running_clocks": 1,
  "materials_cost_minor": 50000,
  "materials_price_minor": 50000,
  "uncosted_materials": 1,
  "expenses_minor": 50000,
  "expenses_by_category": {},
  "fees_minor": 50000,
  "cost_minor": 50000,
  "profit_minor": 50000,
  "margin": "...",
  "unbilled_minor": 50000
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# costs.jobs

Job costing across jobs dated between two days (inclusive, in the shop's time zone), with totals. Sort by profit (lowest first, to find the losers), margin, or date. Up to 500 jobs; truncated says when more matched.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/costs/jobs` |
| MCP tool | `costs_jobs` |
| Writes | no |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

Job costing across jobs dated between two days (inclusive, in the shop's time zone), with totals. Sort by profit (lowest first, to find the losers), margin, or date.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `from` | string | yes |
| `to` | string | yes |
| `status` | array | no |
| `sort` | string (one of profit, margin, date) | no |
| `limit` | integer | no |

## Example

```http
GET https://doingwork.work/v1/costs/jobs?shop_id=00000000-0000-4000-8000-000000000000&from=...&to=...
```

## Output

| Field | Type | Always there |
|---|---|---|
| `jobs` | array | yes |
| `jobs[].job_id` | string (uuid) | yes |
| `jobs[].kind` | string | yes |
| `jobs[].status` | string | yes |
| `jobs[].summary` | ['string', 'null'] | yes |
| `jobs[].customer_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `jobs[].occurred_at` | string (date-time) | yes |
| `jobs[].currency` | string | yes |
| `jobs[].contract_minor` | integer | yes |
| `jobs[].invoiced_minor` | integer | yes |
| `jobs[].tax_minor` | integer | yes |
| `jobs[].draft_invoices_minor` | integer | yes |
| `jobs[].refunded_minor` | integer | yes |
| `jobs[].collected_minor` | integer | yes |
| `jobs[].revenue_minor` | integer | yes |
| `jobs[].labor_minutes` | integer | yes |
| `jobs[].labor_pending_minutes` | integer | yes |
| `jobs[].labor_cost_minor` | integer | yes |
| `jobs[].labor_pending_cost_minor` | integer | yes |
| `jobs[].uncosted_minutes` | integer | yes |
| `jobs[].running_clocks` | integer | yes |
| `jobs[].materials_cost_minor` | integer | yes |
| `jobs[].materials_price_minor` | integer | yes |
| `jobs[].uncosted_materials` | integer | yes |
| `jobs[].expenses_minor` | integer | yes |
| `jobs[].expenses_by_category` | object | yes |
| `jobs[].fees_minor` | integer | yes |
| `jobs[].cost_minor` | integer | yes |
| `jobs[].profit_minor` | integer | yes |
| `jobs[].margin` | ['number', 'null'] | yes |
| `jobs[].unbilled_minor` | integer | yes |
| `totals` | object | yes |
| `totals.jobs` | integer | yes |
| `totals.currency` | ['string', 'null'] | yes |
| `totals.margin` | ['number', 'null'] | yes |
| `totals.contract_minor` | integer | yes |
| `totals.invoiced_minor` | integer | yes |
| `totals.tax_minor` | integer | yes |
| `totals.draft_invoices_minor` | integer | yes |
| `totals.refunded_minor` | integer | yes |
| `totals.collected_minor` | integer | yes |
| `totals.revenue_minor` | integer | yes |
| `totals.labor_minutes` | integer | yes |
| `totals.labor_pending_minutes` | integer | yes |
| `totals.labor_cost_minor` | integer | yes |
| `totals.labor_pending_cost_minor` | integer | yes |
| `totals.uncosted_minutes` | integer | yes |
| `totals.materials_cost_minor` | integer | yes |
| `totals.materials_price_minor` | integer | yes |
| `totals.uncosted_materials` | integer | yes |
| `totals.expenses_minor` | integer | yes |
| `totals.fees_minor` | integer | yes |
| `totals.cost_minor` | integer | yes |
| `totals.profit_minor` | integer | yes |
| `totals.unbilled_minor` | integer | yes |
| `truncated` | boolean | yes |

```json
{
  "jobs": [
    {
      "job_id": "00000000-0000-4000-8000-000000000000",
      "kind": "...",
      "status": "...",
      "summary": "...",
      "customer_id": "00000000-0000-4000-8000-000000000000",
      "occurred_at": "...",
      "currency": "...",
      "contract_minor": 50000,
      "invoiced_minor": 50000,
      "tax_minor": 50000,
      "draft_invoices_minor": 50000,
      "refunded_minor": 50000,
      "collected_minor": 50000,
      "revenue_minor": 50000,
      "labor_minutes": 1,
      "labor_pending_minutes": 1,
      "labor_cost_minor": 50000,
      "labor_pending_cost_minor": 50000,
      "uncosted_minutes": 1,
      "running_clocks": 1,
      "materials_cost_minor": 50000,
      "materials_price_minor": 50000,
      "uncosted_materials": 1,
      "expenses_minor": 50000,
      "expenses_by_category": {},
      "fees_minor": 50000,
      "cost_minor": 50000,
      "profit_minor": 50000,
      "margin": "...",
      "unbilled_minor": 50000
    }
  ],
  "totals": {
    "jobs": 1,
    "currency": "...",
    "margin": "...",
    "contract_minor": 50000,
    "invoiced_minor": 50000,
    "tax_minor": 50000,
    "draft_invoices_minor": 50000,
    "refunded_minor": 50000,
    "collected_minor": 50000,
    "revenue_minor": 50000,
    "labor_minutes": 1,
    "labor_pending_minutes": 1,
    "labor_cost_minor": 50000,
    "labor_pending_cost_minor": 50000,
    "uncosted_minutes": 1,
    "materials_cost_minor": 50000,
    "materials_price_minor": 50000,
    "uncosted_materials": 1,
    "expenses_minor": 50000,
    "fees_minor": 50000,
    "cost_minor": 50000,
    "profit_minor": 50000,
    "unbilled_minor": 50000
  },
  "truncated": true
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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. Each list shows the first 25 with the full count.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/reports/today` |
| MCP tool | `reports_today` |
| Writes | no |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

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. Each list shows the first 25 with the full count.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `date` | string | no |

## Example

```http
GET https://doingwork.work/v1/reports/today?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `date` | string | yes |
| `time_zone` | string | yes |
| `currency` | string | yes |
| `visits_today` | object | yes |
| `visits_today.count` | integer | yes |
| `visits_today.items` | array | yes |
| `visits_today.items[].visit_id` | string (uuid) | yes |
| `visits_today.items[].job_id` | string (uuid) | yes |
| `visits_today.items[].job_summary` | ['string', 'null'] | yes |
| `visits_today.items[].customer_name` | ['string', 'null'] | yes |
| `visits_today.items[].title` | ['string', 'null'] | yes |
| `visits_today.items[].starts_at` | string (date-time) | yes |
| `visits_today.items[].ends_at` | string (date-time) | yes |
| `visits_today.items[].status` | string | yes |
| `visits_today.items[].assignees` | array | yes |
| `unassigned_visits` | object | yes |
| `unassigned_visits.count` | integer | yes |
| `unassigned_visits.items` | array | yes |
| `unassigned_visits.items[].visit_id` | string (uuid) | yes |
| `unassigned_visits.items[].job_id` | string (uuid) | yes |
| `unassigned_visits.items[].job_summary` | ['string', 'null'] | yes |
| `unassigned_visits.items[].customer_name` | ['string', 'null'] | yes |
| `unassigned_visits.items[].title` | ['string', 'null'] | yes |
| `unassigned_visits.items[].starts_at` | string (date-time) | yes |
| `unassigned_visits.items[].ends_at` | string (date-time) | yes |
| `unassigned_visits.items[].status` | string | yes |
| `unassigned_visits.items[].assignees` | array | yes |
| `new_requests` | object | yes |
| `new_requests.count` | integer | yes |
| `new_requests.items` | array | yes |
| `new_requests.items[].request_id` | string (uuid) | yes |
| `new_requests.items[].title` | string | yes |
| `new_requests.items[].source` | string | yes |
| `new_requests.items[].customer_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `new_requests.items[].created_at` | string (date-time) | yes |
| `quotes_waiting` | object | yes |
| `quotes_waiting.count` | integer | yes |
| `quotes_waiting.items` | array | yes |
| `quotes_waiting.items[].quote_id` | string (uuid) | yes |
| `quotes_waiting.items[].job_id` | string (uuid) | yes |
| `quotes_waiting.items[].customer_name` | ['string', 'null'] | yes |
| `quotes_waiting.items[].total_minor` | integer | yes |
| `quotes_waiting.items[].sent_at` | string (date-time) | yes |
| `quotes_waiting.items[].days_waiting` | integer | yes |
| `change_orders_waiting` | object | yes |
| `change_orders_waiting.count` | integer | yes |
| `change_orders_waiting.items` | array | yes |
| `change_orders_waiting.items[].change_order_id` | string (uuid) | yes |
| `change_orders_waiting.items[].job_id` | string (uuid) | yes |
| `change_orders_waiting.items[].number` | integer | yes |
| `change_orders_waiting.items[].total_minor` | integer | yes |
| `change_orders_waiting.items[].sent_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `invoices_overdue` | object | yes |
| `invoices_overdue.count` | integer | yes |
| `invoices_overdue.items` | array | yes |
| `invoices_overdue.items[].invoice_id` | string (uuid) | yes |
| `invoices_overdue.items[].job_id` | string (uuid) | yes |
| `invoices_overdue.items[].customer_name` | ['string', 'null'] | yes |
| `invoices_overdue.items[].owed_minor` | integer | yes |
| `invoices_overdue.items[].due_on` | string | yes |
| `invoices_overdue.items[].days_overdue` | integer | yes |
| `draft_invoices` | object | yes |
| `draft_invoices.count` | integer | yes |
| `draft_invoices.items` | array | yes |
| `draft_invoices.items[].invoice_id` | string (uuid) | yes |
| `draft_invoices.items[].job_id` | string (uuid) | yes |
| `draft_invoices.items[].customer_name` | ['string', 'null'] | yes |
| `draft_invoices.items[].total_minor` | integer | yes |
| `draft_invoices.items[].created_at` | string (date-time) | yes |
| `jobs_to_invoice` | object | yes |
| `jobs_to_invoice.count` | integer | yes |
| `jobs_to_invoice.items` | array | yes |
| `jobs_to_invoice.items[].job_id` | string (uuid) | yes |
| `jobs_to_invoice.items[].summary` | ['string', 'null'] | yes |
| `jobs_to_invoice.items[].customer_name` | ['string', 'null'] | yes |
| `jobs_to_invoice.items[].occurred_at` | string (date-time) | yes |
| `time_to_approve` | object | yes |
| `time_to_approve.entries` | integer | yes |
| `time_to_approve.minutes` | integer | yes |
| `time_to_approve.people` | integer | yes |
| `time_to_approve.oldest_started_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `approvals_pending` | object | yes |
| `approvals_pending.count` | integer | yes |
| `approvals_pending.items` | array | yes |
| `approvals_pending.items[].approval_id` | string (uuid) | yes |
| `approvals_pending.items[].op_id` | string | yes |
| `approvals_pending.items[].amount_minor` | ['integer', 'null'] | yes |
| `approvals_pending.items[].requested_by` | string (uuid) | yes |
| `approvals_pending.items[].expires_at` | string (date-time) | yes |

```json
{
  "date": "...",
  "time_zone": "...",
  "currency": "...",
  "visits_today": {
    "count": 1,
    "items": [
      {
        "visit_id": "00000000-0000-4000-8000-000000000000",
        "job_id": "00000000-0000-4000-8000-000000000000",
        "job_summary": "...",
        "customer_name": "...",
        "title": "...",
        "starts_at": "...",
        "ends_at": "...",
        "status": "...",
        "assignees": [
          "00000000-0000-4000-8000-000000000000"
        ]
      }
    ]
  },
  "unassigned_visits": {
    "count": 1,
    "items": [
      {
        "visit_id": "00000000-0000-4000-8000-000000000000",
        "job_id": "00000000-0000-4000-8000-000000000000",
        "job_summary": "...",
        "customer_name": "...",
        "title": "...",
        "starts_at": "...",
        "ends_at": "...",
        "status": "...",
        "assignees": [
          "00000000-0000-4000-8000-000000000000"
        ]
      }
    ]
  },
  "new_requests": {
    "count": 1,
    "items": [
      {
        "request_id": "00000000-0000-4000-8000-000000000000",
        "title": "...",
        "source": "...",
        "customer_id": "00000000-0000-4000-8000-000000000000",
        "created_at": "..."
      }
    ]
  },
  "quotes_waiting": {
    "count": 1,
    "items": [
      {
        "quote_id": "00000000-0000-4000-8000-000000000000",
        "job_id": "00000000-0000-4000-8000-000000000000",
        "customer_name": "...",
        "total_minor": 50000,
        "sent_at": "...",
        "days_waiting": 1
      }
    ]
  },
  "change_orders_waiting": {
    "count": 1,
    "items": [
      {
        "change_order_id": "00000000-0000-4000-8000-000000000000",
        "job_id": "00000000-0000-4000-8000-000000000000",
        "number": 1,
        "total_minor": 50000,
        "sent_at": "..."
      }
    ]
  },
  "invoices_overdue": {
    "count": 1,
    "items": [
      {
        "invoice_id": "00000000-0000-4000-8000-000000000000",
        "job_id": "00000000-0000-4000-8000-000000000000",
        "customer_name": "...",
        "owed_minor": 50000,
        "due_on": "...",
        "days_overdue": 1
      }
    ]
  },
  "draft_invoices": {
    "count": 1,
    "items": [
      {
        "invoice_id": "00000000-0000-4000-8000-000000000000",
        "job_id": "00000000-0000-4000-8000-000000000000",
        "customer_name": "...",
        "total_minor": 50000,
        "created_at": "..."
      }
    ]
  },
  "jobs_to_invoice": {
    "count": 1,
    "items": [
      {
        "job_id": "00000000-0000-4000-8000-000000000000",
        "summary": "...",
        "customer_name": "...",
        "occurred_at": "..."
      }
    ]
  },
  "time_to_approve": {
    "entries": 1,
    "minutes": 1,
    "people": 1,
    "oldest_started_at": "..."
  },
  "approvals_pending": {
    "count": 1,
    "items": [
      {
        "approval_id": "00000000-0000-4000-8000-000000000000",
        "op_id": "00000000-0000-4000-8000-000000000000",
        "amount_minor": "...",
        "requested_by": "00000000-0000-4000-8000-000000000000",
        "expires_at": "..."
      }
    ]
  }
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/reports/week` |
| MCP tool | `reports_week` |
| Writes | no |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `week_of` | string | no |

## Example

```http
GET https://doingwork.work/v1/reports/week?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `from` | string | yes |
| `to` | string | yes |
| `time_zone` | string | yes |
| `currency` | string | yes |
| `this_week` | object | yes |
| `this_week.collected_minor` | integer | yes |
| `this_week.refunded_minor` | integer | yes |
| `this_week.outside_minor` | integer | yes |
| `this_week.invoiced_minor` | integer | yes |
| `this_week.invoices_issued` | integer | yes |
| `this_week.quotes_sent` | integer | yes |
| `this_week.quotes_signed` | integer | yes |
| `this_week.quotes_signed_minor` | integer | yes |
| `this_week.jobs_created` | integer | yes |
| `this_week.jobs_done` | integer | yes |
| `this_week.visits_scheduled` | integer | yes |
| `this_week.visits_done` | integer | yes |
| `this_week.new_customers` | integer | yes |
| `this_week.new_requests` | integer | yes |
| `this_week.minutes_logged` | integer | yes |
| `this_week.expenses_minor` | integer | yes |
| `last_week` | object | yes |
| `last_week.collected_minor` | integer | yes |
| `last_week.refunded_minor` | integer | yes |
| `last_week.outside_minor` | integer | yes |
| `last_week.invoiced_minor` | integer | yes |
| `last_week.invoices_issued` | integer | yes |
| `last_week.quotes_sent` | integer | yes |
| `last_week.quotes_signed` | integer | yes |
| `last_week.quotes_signed_minor` | integer | yes |
| `last_week.jobs_created` | integer | yes |
| `last_week.jobs_done` | integer | yes |
| `last_week.visits_scheduled` | integer | yes |
| `last_week.visits_done` | integer | yes |
| `last_week.new_customers` | integer | yes |
| `last_week.new_requests` | integer | yes |
| `last_week.minutes_logged` | integer | yes |
| `last_week.expenses_minor` | integer | yes |
| `change` | object | yes |
| `change.collected_minor` | integer | yes |
| `change.refunded_minor` | integer | yes |
| `change.outside_minor` | integer | yes |
| `change.invoiced_minor` | integer | yes |
| `change.invoices_issued` | integer | yes |
| `change.quotes_sent` | integer | yes |
| `change.quotes_signed` | integer | yes |
| `change.quotes_signed_minor` | integer | yes |
| `change.jobs_created` | integer | yes |
| `change.jobs_done` | integer | yes |
| `change.visits_scheduled` | integer | yes |
| `change.visits_done` | integer | yes |
| `change.new_customers` | integer | yes |
| `change.new_requests` | integer | yes |
| `change.minutes_logged` | integer | yes |
| `change.expenses_minor` | integer | yes |

```json
{
  "from": "...",
  "to": "...",
  "time_zone": "...",
  "currency": "...",
  "this_week": {
    "collected_minor": 50000,
    "refunded_minor": 50000,
    "outside_minor": 50000,
    "invoiced_minor": 50000,
    "invoices_issued": 1,
    "quotes_sent": 1,
    "quotes_signed": 1,
    "quotes_signed_minor": 50000,
    "jobs_created": 1,
    "jobs_done": 1,
    "visits_scheduled": 1,
    "visits_done": 1,
    "new_customers": 1,
    "new_requests": 1,
    "minutes_logged": 1,
    "expenses_minor": 50000
  },
  "last_week": {
    "collected_minor": 50000,
    "refunded_minor": 50000,
    "outside_minor": 50000,
    "invoiced_minor": 50000,
    "invoices_issued": 1,
    "quotes_sent": 1,
    "quotes_signed": 1,
    "quotes_signed_minor": 50000,
    "jobs_created": 1,
    "jobs_done": 1,
    "visits_scheduled": 1,
    "visits_done": 1,
    "new_customers": 1,
    "new_requests": 1,
    "minutes_logged": 1,
    "expenses_minor": 50000
  },
  "change": {
    "collected_minor": 50000,
    "refunded_minor": 50000,
    "outside_minor": 50000,
    "invoiced_minor": 50000,
    "invoices_issued": 1,
    "quotes_sent": 1,
    "quotes_signed": 1,
    "quotes_signed_minor": 50000,
    "jobs_created": 1,
    "jobs_done": 1,
    "visits_scheduled": 1,
    "visits_done": 1,
    "new_customers": 1,
    "new_requests": 1,
    "minutes_logged": 1,
    "expenses_minor": 50000
  }
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/customers/audience` |
| MCP tool | `customers_audience` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `tags_any` | array | no |
| `tags_all` | array | no |
| `tags_none` | array | no |
| `lead_sources` | array | no |
| `kind` | string (one of person, company) | no |
| `autopay` | boolean | no |
| `created_from` | string | no |
| `created_to` | string | no |
| `has_jobs` | boolean | no |
| `last_job_from` | string | no |
| `last_job_to` | string | no |
| `job_kinds` | array | no |
| `job_tags_any` | array | no |
| `job_summary_contains` | string | no |
| `jobs_from` | string | no |
| `jobs_to` | string | no |
| `has_open_quote` | boolean | no |
| `plan_status` | string (one of active, cancelled, none, any) | no |
| `plan_term_ends_from` | string | no |
| `plan_term_ends_to` | string | no |
| `cities` | array | no |
| `postal_codes` | array | no |
| `has_email` | boolean | no |
| `has_phone` | boolean | no |
| `owes_money` | boolean | no |
| `overdue` | boolean | no |
| `min_owed_minor` | integer | no |
| `min_spent_minor` | integer | no |
| `max_spent_minor` | integer | no |
| `include_archived` | boolean | no |
| `sort` | string (one of name, last_job_oldest, last_job_newest, created_newest) | no |
| `limit` | integer | no |
| `cursor` | string | no |

## Example

```http
POST https://doingwork.work/v1/customers/audience
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `time_zone` | string | yes |
| `currency` | string | yes |
| `sort` | string | yes |
| `counts` | object | yes |
| `counts.total` | integer | yes |
| `counts.with_email` | integer | yes |
| `counts.with_phone` | integer | yes |
| `counts.owing` | ['integer', 'null'] | yes |
| `counts.owed_minor` | ['integer', 'null'] | yes |
| `prices_hidden` | boolean | yes |
| `data` | array | yes |
| `data[].customer_id` | string (uuid) | yes |
| `data[].name` | string | yes |
| `data[].company_name` | ['string', 'null'] | yes |
| `data[].kind` | string | yes |
| `data[].email` | ['string', 'null'] | yes |
| `data[].phone` | ['string', 'null'] | yes |
| `data[].tags` | array | yes |
| `data[].lead_source` | ['string', 'null'] | yes |
| `data[].city` | ['string', 'null'] | yes |
| `data[].postal_code` | ['string', 'null'] | yes |
| `data[].jobs` | integer | yes |
| `data[].last_job_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `data[].open_quotes` | integer | yes |
| `data[].plan_status` | string | yes |
| `data[].owed_minor` | ['integer', 'null'] | yes |
| `data[].spent_minor` | ['integer', 'null'] | yes |
| `data[].archived` | boolean | yes |
| `data[].created_at` | string (date-time) | yes |
| `next_cursor` | ['string', 'null'] | yes |

```json
{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "time_zone": "...",
  "currency": "...",
  "sort": "...",
  "counts": {
    "total": 1,
    "with_email": 1,
    "with_phone": 1,
    "owing": "...",
    "owed_minor": "..."
  },
  "prices_hidden": true,
  "data": [
    {
      "customer_id": "00000000-0000-4000-8000-000000000000",
      "name": "Ruiz Plumbing",
      "company_name": "...",
      "kind": "...",
      "email": "owner@example.com",
      "phone": "...",
      "tags": [
        "..."
      ],
      "lead_source": "...",
      "city": "...",
      "postal_code": "83814",
      "jobs": 1,
      "last_job_at": "...",
      "open_quotes": 1,
      "plan_status": "...",
      "owed_minor": "...",
      "spent_minor": "...",
      "archived": true,
      "created_at": "..."
    }
  ],
  "next_cursor": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/messages/templates` |
| MCP tool | `messages_templates` |
| Writes | no |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/messages/templates?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `templates` | array | yes |
| `templates[].kind` | string | yes |
| `templates[].custom` | boolean | yes |
| `templates[].subject` | string | yes |
| `templates[].body` | string | yes |
| `templates[].link_placeholder` | string | yes |
| `templates[].placeholders` | array | yes |
| `templates[].default_subject` | string | yes |
| `templates[].default_body` | string | yes |
| `templates[].updated_by` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `templates[].updated_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |

```json
{
  "templates": [
    {
      "kind": "...",
      "custom": true,
      "subject": "...",
      "body": "...",
      "link_placeholder": "...",
      "placeholders": [
        "..."
      ],
      "default_subject": "...",
      "default_body": "...",
      "updated_by": "00000000-0000-4000-8000-000000000000",
      "updated_at": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/messages/templates` |
| MCP tool | `messages_saveTemplate` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `kind` | string (one of invoice_reminder, quote_followup, review_request) | yes |
| `subject` | string | yes |
| `body` | string | yes |

## Example

```http
POST https://doingwork.work/v1/messages/templates
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "kind": "invoice_reminder",
  "subject": "...",
  "body": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# messages.resetTemplate

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/messages/templates/reset` |
| MCP tool | `messages_resetTemplate` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `kind` | string (one of invoice_reminder, quote_followup, review_request) | yes |

## Example

```http
POST https://doingwork.work/v1/messages/templates/reset
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "kind": "invoice_reminder"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/messages/templates/preview` |
| MCP tool | `messages_previewTemplate` |
| Writes | no |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `kind` | string (one of invoice_reminder, quote_followup, review_request) | yes |
| `subject` | string | no |
| `body` | string | no |

## Example

```http
POST https://doingwork.work/v1/messages/templates/preview
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "kind": "invoice_reminder"
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `subject` | string | yes |
| `body` | string | yes |
| `from_name` | string | yes |
| `note` | string | yes |

```json
{
  "subject": "...",
  "body": "...",
  "from_name": "...",
  "note": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/change-orders` |
| MCP tool | `changes_create` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `title` | string | yes |
| `description` | string | no |
| `lines` | array | yes |
| `lines[].description` | string | yes |
| `lines[].quantity` | number | yes |
| `lines[].unit_price_minor` | integer | yes |
| `lines[].kind` | string (one of item, fee, discount) | no |
| `lines[].taxable` | boolean | no |
| `tax_rate_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | no |

## Example

```http
POST https://doingwork.work/v1/change-orders
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "title": "...",
  "lines": [
    {
      "description": "40 gallon gas heater, installed",
      "quantity": "...",
      "unit_price_minor": 50000
    }
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/change-orders` |
| MCP tool | `changes_list` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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).

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/change-orders?job_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `currency` | string | yes |
| `contract_minor` | integer | yes |
| `approved_changes_minor` | integer | yes |
| `revised_total_minor` | integer | yes |
| `pending_changes_minor` | integer | yes |
| `invoiced_minor` | integer | yes |
| `unbilled_minor` | integer | yes |
| `unbilled_changes_minor` | integer | yes |
| `unbilled_change_orders` | integer | yes |
| `items` | array | yes |
| `items[].change_order_id` | string (uuid) | yes |
| `items[].shop_id` | string (uuid) | yes |
| `items[].job_id` | string (uuid) | yes |
| `items[].number` | integer | yes |
| `items[].title` | string | yes |
| `items[].description` | ['string', 'null'] | yes |
| `items[].currency` | string | yes |
| `items[].subtotal_minor` | integer | yes |
| `items[].taxable_minor` | integer | yes |
| `items[].tax_rate_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].tax_name` | ['string', 'null'] | yes |
| `items[].tax_rate_bps` | ['integer', 'null'] | yes |
| `items[].tax_minor` | integer | yes |
| `items[].total_minor` | integer | yes |
| `items[].status` | string | yes |
| `items[].signature_request_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].signature_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].sent_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].approved_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].cancelled_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].cancel_reason` | ['string', 'null'] | yes |
| `items[].invoice_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].invoiced` | boolean | yes |
| `items[].created_by` | string (uuid) | yes |
| `items[].created_at` | string (date-time) | yes |

```json
{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "currency": "...",
  "contract_minor": 50000,
  "approved_changes_minor": 50000,
  "revised_total_minor": 50000,
  "pending_changes_minor": 50000,
  "invoiced_minor": 50000,
  "unbilled_minor": 50000,
  "unbilled_changes_minor": 50000,
  "unbilled_change_orders": 1,
  "items": [
    {
      "change_order_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "job_id": "00000000-0000-4000-8000-000000000000",
      "number": 1,
      "title": "...",
      "description": "40 gallon gas heater, installed",
      "currency": "...",
      "subtotal_minor": 50000,
      "taxable_minor": 50000,
      "tax_rate_id": "00000000-0000-4000-8000-000000000000",
      "tax_name": "...",
      "tax_rate_bps": "...",
      "tax_minor": 50000,
      "total_minor": 50000,
      "status": "...",
      "signature_request_id": "00000000-0000-4000-8000-000000000000",
      "signature_id": "00000000-0000-4000-8000-000000000000",
      "sent_at": "...",
      "approved_at": "...",
      "cancelled_at": "...",
      "cancel_reason": "...",
      "invoice_id": "00000000-0000-4000-8000-000000000000",
      "invoiced": true,
      "created_by": "00000000-0000-4000-8000-000000000000",
      "created_at": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# changes.get

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/change-orders/get` |
| MCP tool | `changes_get` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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).

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `change_order_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/change-orders/get?change_order_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `change_order_id` | string (uuid) | yes |
| `shop_id` | string (uuid) | yes |
| `job_id` | string (uuid) | yes |
| `number` | integer | yes |
| `title` | string | yes |
| `description` | ['string', 'null'] | yes |
| `currency` | string | yes |
| `subtotal_minor` | integer | yes |
| `taxable_minor` | integer | yes |
| `tax_rate_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `tax_name` | ['string', 'null'] | yes |
| `tax_rate_bps` | ['integer', 'null'] | yes |
| `tax_minor` | integer | yes |
| `total_minor` | integer | yes |
| `status` | string | yes |
| `signature_request_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `signature_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `sent_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `approved_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `cancelled_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `cancel_reason` | ['string', 'null'] | yes |
| `invoice_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `invoiced` | boolean | yes |
| `created_by` | string (uuid) | yes |
| `created_at` | string (date-time) | yes |
| `lines` | array | yes |
| `lines[].position` | integer | yes |
| `lines[].description` | string | yes |
| `lines[].quantity` | number | yes |
| `lines[].unit_price_minor` | integer | yes |
| `lines[].kind` | string | yes |
| `lines[].taxable` | boolean | yes |
| `lines[].amount_minor` | integer | yes |

```json
{
  "change_order_id": "00000000-0000-4000-8000-000000000000",
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "job_id": "00000000-0000-4000-8000-000000000000",
  "number": 1,
  "title": "...",
  "description": "40 gallon gas heater, installed",
  "currency": "...",
  "subtotal_minor": 50000,
  "taxable_minor": 50000,
  "tax_rate_id": "00000000-0000-4000-8000-000000000000",
  "tax_name": "...",
  "tax_rate_bps": "...",
  "tax_minor": 50000,
  "total_minor": 50000,
  "status": "...",
  "signature_request_id": "00000000-0000-4000-8000-000000000000",
  "signature_id": "00000000-0000-4000-8000-000000000000",
  "sent_at": "...",
  "approved_at": "...",
  "cancelled_at": "...",
  "cancel_reason": "...",
  "invoice_id": "00000000-0000-4000-8000-000000000000",
  "invoiced": true,
  "created_by": "00000000-0000-4000-8000-000000000000",
  "created_at": "...",
  "lines": [
    {
      "position": 1,
      "description": "40 gallon gas heater, installed",
      "quantity": "...",
      "unit_price_minor": 50000,
      "kind": "...",
      "taxable": true,
      "amount_minor": 50000
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/change-orders/send` |
| MCP tool | `changes_send` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `change_order_id` | string (uuid) | yes |
| `signer_email` | string | no |
| `link_days` | integer | no |

## Example

```http
POST https://doingwork.work/v1/change-orders/send
content-type: application/json
Idempotency-Key: 3f1c...

{
  "change_order_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/change-orders/cancel` |
| MCP tool | `changes_cancel` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `change_order_id` | string (uuid) | yes |
| `reason` | string | no |

## Example

```http
POST https://doingwork.work/v1/change-orders/cancel
content-type: application/json
Idempotency-Key: 3f1c...

{
  "change_order_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/change-orders/invoice` |
| MCP tool | `changes_invoice` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `change_order_ids` | array | no |
| `due_on` | string | no |
| `memo` | string | no |

## Example

```http
POST https://doingwork.work/v1/change-orders/invoice
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/timesheets/clock-in` |
| MCP tool | `timesheets_clockIn` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | no |
| `job_id` | string (uuid) | no |
| `visit_id` | string (uuid) | no |
| `person_id` | string (uuid) | no |
| `category` | string (one of work, drive, shop, break) | no |
| `notes` | string | no |
| `stop_running` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/timesheets/clock-in
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "job_id": "00000000-0000-4000-8000-000000000000",
  "visit_id": "00000000-0000-4000-8000-000000000000",
  "person_id": "00000000-0000-4000-8000-000000000000",
  "category": "work",
  "notes": "...",
  "stop_running": true
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/timesheets/clock-out` |
| MCP tool | `timesheets_clockOut` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `person_id` | string (uuid) | no |
| `ended_at` | string (date-time) | no |
| `notes` | string | no |

## Example

```http
POST https://doingwork.work/v1/timesheets/clock-out
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/timesheets/current` |
| MCP tool | `timesheets_current` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `person_id` | string (uuid) | no |

## Example

```http
GET https://doingwork.work/v1/timesheets/current?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `person_id` | string (uuid) | yes |
| `running` |  | yes |
| `running_minutes` | ['integer', 'null'] | yes |

```json
{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "person_id": "00000000-0000-4000-8000-000000000000",
  "running": "...",
  "running_minutes": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/timesheets` |
| MCP tool | `timesheets_add` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | no |
| `job_id` | string (uuid) | no |
| `visit_id` | string (uuid) | no |
| `person_id` | string (uuid) | no |
| `category` | string (one of work, drive, shop, break) | no |
| `started_at` | string (date-time) | yes |
| `ended_at` | string (date-time) | yes |
| `notes` | string | no |

## Example

```http
POST https://doingwork.work/v1/timesheets
content-type: application/json
Idempotency-Key: 3f1c...

{
  "started_at": "...",
  "ended_at": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/timesheets/update` |
| MCP tool | `timesheets_update` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `time_entry_id` | string (uuid) | yes |
| `started_at` | string (date-time) | no |
| `ended_at` | string (date-time) | no |
| `category` | string (one of work, drive, shop, break) | no |
| `job_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | no |
| `visit_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | no |
| `notes` | string | no |

## Example

```http
POST https://doingwork.work/v1/timesheets/update
content-type: application/json
Idempotency-Key: 3f1c...

{
  "time_entry_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/timesheets/void` |
| MCP tool | `timesheets_void` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `time_entry_id` | string (uuid) | yes |
| `reason` | string | no |

## Example

```http
POST https://doingwork.work/v1/timesheets/void
content-type: application/json
Idempotency-Key: 3f1c...

{
  "time_entry_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/timesheets/decide` |
| MCP tool | `timesheets_decide` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `time_entry_ids` | array | yes |
| `decision` | string (one of approve, reject) | yes |
| `note` | string | no |

## Example

```http
POST https://doingwork.work/v1/timesheets/decide
content-type: application/json
Idempotency-Key: 3f1c...

{
  "time_entry_ids": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "decision": "approve"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/timesheets/reopen` |
| MCP tool | `timesheets_reopen` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `time_entry_id` | string (uuid) | yes |
| `reason` | string | yes |

## Example

```http
POST https://doingwork.work/v1/timesheets/reopen
content-type: application/json
Idempotency-Key: 3f1c...

{
  "time_entry_id": "00000000-0000-4000-8000-000000000000",
  "reason": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/timesheets` |
| MCP tool | `timesheets_list` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | no |
| `job_id` | string (uuid) | no |
| `person_id` | string (uuid) | no |
| `from` | string | no |
| `to` | string | no |
| `status` | string (one of running, pending, approved, rejected, void) | no |
| `category` | string (one of work, drive, shop, break) | no |

## Example

```http
GET https://doingwork.work/v1/timesheets?shop_id=00000000-0000-4000-8000-000000000000&job_id=00000000-0000-4000-8000-000000000000&person_id=00000000-0000-4000-8000-000000000000&from=...&to=...&status=running&category=work
```

## Output

| Field | Type | Always there |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `time_zone` | string | yes |
| `items` | array | yes |
| `items[].time_entry_id` | string (uuid) | yes |
| `items[].shop_id` | string (uuid) | yes |
| `items[].person_id` | string (uuid) | yes |
| `items[].person_name` | ['string', 'null'] | yes |
| `items[].job_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].visit_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].category` | string | yes |
| `items[].started_at` | string (date-time) | yes |
| `items[].ended_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].minutes` | ['integer', 'null'] | yes |
| `items[].source` | string | yes |
| `items[].notes` | ['string', 'null'] | yes |
| `items[].status` | string | yes |
| `items[].cost_rate_minor` | ['integer', 'null'] | yes |
| `items[].labor_cost_minor` | ['integer', 'null'] | yes |
| `items[].decided_by` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].decided_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].decision_note` | ['string', 'null'] | yes |
| `items[].entered_by` | string (uuid) | yes |
| `items[].created_at` | string (date-time) | yes |
| `items[].updated_at` | string (date-time) | yes |
| `minutes` | integer | yes |
| `approved_minutes` | integer | yes |
| `pending_minutes` | integer | yes |
| `by_category_minutes` | object | yes |
| `by_person` | array | yes |
| `by_person[].person_id` | string (uuid) | yes |
| `by_person[].person_name` | ['string', 'null'] | yes |
| `by_person[].minutes` | integer | yes |
| `by_person[].approved_minutes` | integer | yes |
| `labor_cost_minor` | ['integer', 'null'] | yes |

```json
{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "time_zone": "...",
  "items": [
    {
      "time_entry_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "person_id": "00000000-0000-4000-8000-000000000000",
      "person_name": "...",
      "job_id": "00000000-0000-4000-8000-000000000000",
      "visit_id": "00000000-0000-4000-8000-000000000000",
      "category": "...",
      "started_at": "...",
      "ended_at": "...",
      "minutes": "...",
      "source": "...",
      "notes": "...",
      "status": "...",
      "cost_rate_minor": "...",
      "labor_cost_minor": "...",
      "decided_by": "00000000-0000-4000-8000-000000000000",
      "decided_at": "...",
      "decision_note": "...",
      "entered_by": "00000000-0000-4000-8000-000000000000",
      "created_at": "...",
      "updated_at": "..."
    }
  ],
  "minutes": 1,
  "approved_minutes": 1,
  "pending_minutes": 1,
  "by_category_minutes": {},
  "by_person": [
    {
      "person_id": "00000000-0000-4000-8000-000000000000",
      "person_name": "...",
      "minutes": 1,
      "approved_minutes": 1
    }
  ],
  "labor_cost_minor": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/timesheets/rates` |
| MCP tool | `timesheets_setRate` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `person_id` | string (uuid) | no |
| `cost_per_hour_minor` | ['integer', 'null'] | yes |
| `effective_from` | string (date-time) | no |

## Example

```http
POST https://doingwork.work/v1/timesheets/rates
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "cost_per_hour_minor": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/timesheets/rates` |
| MCP tool | `timesheets_listRates` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/timesheets/rates?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `current` | array | yes |
| `current[].labor_rate_id` | string (uuid) | yes |
| `current[].shop_id` | string (uuid) | yes |
| `current[].person_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `current[].person_name` | ['string', 'null'] | yes |
| `current[].cost_per_hour_minor` | ['integer', 'null'] | yes |
| `current[].effective_from` | string (date-time) | yes |
| `current[].set_by` | string (uuid) | yes |
| `current[].created_at` | string (date-time) | yes |
| `upcoming` | array | yes |
| `upcoming[].labor_rate_id` | string (uuid) | yes |
| `upcoming[].shop_id` | string (uuid) | yes |
| `upcoming[].person_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `upcoming[].person_name` | ['string', 'null'] | yes |
| `upcoming[].cost_per_hour_minor` | ['integer', 'null'] | yes |
| `upcoming[].effective_from` | string (date-time) | yes |
| `upcoming[].set_by` | string (uuid) | yes |
| `upcoming[].created_at` | string (date-time) | yes |

```json
{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "current": [
    {
      "labor_rate_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "person_id": "00000000-0000-4000-8000-000000000000",
      "person_name": "...",
      "cost_per_hour_minor": "...",
      "effective_from": "...",
      "set_by": "00000000-0000-4000-8000-000000000000",
      "created_at": "..."
    }
  ],
  "upcoming": [
    {
      "labor_rate_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "person_id": "00000000-0000-4000-8000-000000000000",
      "person_name": "...",
      "cost_per_hour_minor": "...",
      "effective_from": "...",
      "set_by": "00000000-0000-4000-8000-000000000000",
      "created_at": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/materials` |
| MCP tool | `materials_add` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `visit_id` | string (uuid) | no |
| `pricebook_item_id` | string (uuid) | no |
| `name` | string | no |
| `unit` | string | no |
| `quantity` | number | yes |
| `unit_price_minor` | integer | no |
| `unit_cost_minor` | integer | no |
| `taxable` | boolean | no |
| `notes` | string | no |

## Example

```http
POST https://doingwork.work/v1/materials
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "quantity": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/materials/update` |
| MCP tool | `materials_update` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `material_id` | string (uuid) | yes |
| `quantity` | number | no |
| `visit_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | no |
| `notes` | ['string', 'null'] | no |

## Example

```http
POST https://doingwork.work/v1/materials/update
content-type: application/json
Idempotency-Key: 3f1c...

{
  "material_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/materials/remove` |
| MCP tool | `materials_remove` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `material_id` | string (uuid) | yes |
| `reason` | string | no |

## Example

```http
POST https://doingwork.work/v1/materials/remove
content-type: application/json
Idempotency-Key: 3f1c...

{
  "material_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/materials` |
| MCP tool | `materials_list` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `visit_id` | string (uuid) | no |
| `include_removed` | boolean | no |

## Example

```http
GET https://doingwork.work/v1/materials?job_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `items` | array | yes |
| `items[].material_id` | string (uuid) | yes |
| `items[].shop_id` | string (uuid) | yes |
| `items[].job_id` | string (uuid) | yes |
| `items[].visit_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].pricebook_item_id` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].name` | string | yes |
| `items[].unit` | string | yes |
| `items[].quantity` | number | yes |
| `items[].unit_price_minor` | ['integer', 'null'] | yes |
| `items[].unit_cost_minor` | ['integer', 'null'] | yes |
| `items[].price_minor` | ['integer', 'null'] | yes |
| `items[].cost_minor` | ['integer', 'null'] | yes |
| `items[].taxable` | boolean | yes |
| `items[].notes` | ['string', 'null'] | yes |
| `items[].entered_by` | string (uuid) | yes |
| `items[].entered_by_name` | ['string', 'null'] | yes |
| `items[].created_at` | string (date-time) | yes |
| `items[].updated_at` | string (date-time) | yes |
| `items[].removed_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `items[].removed_by` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `items[].remove_reason` | ['string', 'null'] | yes |
| `price_total_minor` | ['integer', 'null'] | yes |
| `cost_total_minor` | ['integer', 'null'] | yes |
| `unpriced_count` | integer | yes |
| `uncosted_count` | integer | yes |

```json
{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "items": [
    {
      "material_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "job_id": "00000000-0000-4000-8000-000000000000",
      "visit_id": "00000000-0000-4000-8000-000000000000",
      "pricebook_item_id": "00000000-0000-4000-8000-000000000000",
      "name": "Ruiz Plumbing",
      "unit": "...",
      "quantity": "...",
      "unit_price_minor": "...",
      "unit_cost_minor": "...",
      "price_minor": "...",
      "cost_minor": "...",
      "taxable": true,
      "notes": "...",
      "entered_by": "00000000-0000-4000-8000-000000000000",
      "entered_by_name": "...",
      "created_at": "...",
      "updated_at": "...",
      "removed_at": "...",
      "removed_by": "00000000-0000-4000-8000-000000000000",
      "remove_reason": "..."
    }
  ],
  "price_total_minor": "...",
  "cost_total_minor": "...",
  "unpriced_count": 1,
  "uncosted_count": 1
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shops.transferStart

Start handing the shop to a current worker (owner, on a hosted page). They accept on their own hosted page within 14 days, and ownership moves at once. If the shop has a payments account, it stops taking new payments and only settles what is in flight; the new owner connects their own.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/shops/transfers` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Start handing the shop to a current worker (owner, on a hosted page). They accept on their own hosted page within 14 days, and ownership moves at once.

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `to_membership_id` | string (uuid) | yes |
| `old_owner_stays_as_office` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/shops/transfers
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "to_membership_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `transfer_id` | string (uuid) | yes |
| `shop_id` | string (uuid) | yes |
| `shop` | string | yes |
| `from_person_id` | string (uuid) | yes |
| `from_name` | ['string', 'null'] | yes |
| `to_person_id` | string (uuid) | yes |
| `to_name` | ['string', 'null'] | yes |
| `status` | string | yes |
| `old_owner_stays_as_office` | boolean | yes |
| `started_at` | string (date-time) | yes |
| `expires_at` | string (date-time) | yes |
| `completed_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `cancelled_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `processor_reverify_status` | string | yes |
| `you_are` | string | yes |

```json
{
  "transfer_id": "00000000-0000-4000-8000-000000000000",
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "shop": "...",
  "from_person_id": "00000000-0000-4000-8000-000000000000",
  "from_name": "...",
  "to_person_id": "00000000-0000-4000-8000-000000000000",
  "to_name": "...",
  "status": "...",
  "old_owner_stays_as_office": true,
  "started_at": "...",
  "expires_at": "...",
  "completed_at": "...",
  "cancelled_at": "...",
  "processor_reverify_status": "...",
  "you_are": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shops.transferAccept

Accept a hand-off and become the shop's owner (the person asked, on a hosted page). The person agrees to the Terms of Service by ticking a box (accept_terms_version); the record keeps the version, time, IP address and browser. Comes back with status expired when the 14 days ran out. Every sent invoice nobody has paid yet is voided and reissued as a draft with the same lines (invoices_reissued), so it pays into your payments account once you finish payment setup and send it. Customers are not emailed. Partly paid invoices, ones with a payment going through and ones with a card checkout open stay as they are and collect on your account once it is set up (invoices_kept).

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/shops/transfers/accept` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Accept a hand-off and become the shop's owner (the person asked, on a hosted page). The person agrees to the Terms of Service by ticking a box (accept_terms_version); the record keeps the version, time, IP address and browser.

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `transfer_id` | string (uuid) | yes |
| `accept_terms_version` | string | yes |

## Example

```http
POST https://doingwork.work/v1/shops/transfers/accept
content-type: application/json
Idempotency-Key: 3f1c...

{
  "transfer_id": "00000000-0000-4000-8000-000000000000",
  "accept_terms_version": "..."
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `transfer_id` | string (uuid) | yes |
| `shop_id` | string (uuid) | yes |
| `shop` | string | yes |
| `from_person_id` | string (uuid) | yes |
| `from_name` | ['string', 'null'] | yes |
| `to_person_id` | string (uuid) | yes |
| `to_name` | ['string', 'null'] | yes |
| `status` | string | yes |
| `old_owner_stays_as_office` | boolean | yes |
| `started_at` | string (date-time) | yes |
| `expires_at` | string (date-time) | yes |
| `completed_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `cancelled_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `processor_reverify_status` | string | yes |
| `you_are` | string | yes |
| `invoices_reissued` | array | yes |
| `invoices_reissued[].voided_invoice_id` | string (uuid) | yes |
| `invoices_reissued[].new_invoice_id` | string (uuid) | yes |
| `invoices_reissued[].total_minor` | integer | yes |
| `invoices_kept` | array | yes |
| `invoices_kept[].invoice_id` | string (uuid) | yes |
| `invoices_kept[].reason` | string | yes |

```json
{
  "transfer_id": "00000000-0000-4000-8000-000000000000",
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "shop": "...",
  "from_person_id": "00000000-0000-4000-8000-000000000000",
  "from_name": "...",
  "to_person_id": "00000000-0000-4000-8000-000000000000",
  "to_name": "...",
  "status": "...",
  "old_owner_stays_as_office": true,
  "started_at": "...",
  "expires_at": "...",
  "completed_at": "...",
  "cancelled_at": "...",
  "processor_reverify_status": "...",
  "you_are": "...",
  "invoices_reissued": [
    {
      "voided_invoice_id": "00000000-0000-4000-8000-000000000000",
      "new_invoice_id": "00000000-0000-4000-8000-000000000000",
      "total_minor": 50000
    }
  ],
  "invoices_kept": [
    {
      "invoice_id": "00000000-0000-4000-8000-000000000000",
      "reason": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shops.transferCancel

Stop an open hand-off: the owner cancels it, or the person asked declines it.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/shops/transfers/cancel` |
| MCP tool | `shops_transferCancel` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Stop an open hand-off: the owner cancels it, or the person asked declines it.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `transfer_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/shops/transfers/cancel
content-type: application/json
Idempotency-Key: 3f1c...

{
  "transfer_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shops.transfers

Hand-offs of a shop, newest first. The owner sees all of them; a worker sees only the ones naming them. Without shop_id: every hand-off naming you.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/shops/transfers` |
| MCP tool | `shops_transfers` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Hand-offs of a shop, newest first. The owner sees all of them; a worker sees only the ones naming them.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | no |

## Example

```http
GET https://doingwork.work/v1/shops/transfers?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `transfers` | array | yes |
| `transfers[].transfer_id` | string (uuid) | yes |
| `transfers[].shop_id` | string (uuid) | yes |
| `transfers[].shop` | string | yes |
| `transfers[].from_person_id` | string (uuid) | yes |
| `transfers[].from_name` | ['string', 'null'] | yes |
| `transfers[].to_person_id` | string (uuid) | yes |
| `transfers[].to_name` | ['string', 'null'] | yes |
| `transfers[].status` | string | yes |
| `transfers[].old_owner_stays_as_office` | boolean | yes |
| `transfers[].started_at` | string (date-time) | yes |
| `transfers[].expires_at` | string (date-time) | yes |
| `transfers[].completed_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `transfers[].cancelled_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `transfers[].processor_reverify_status` | string | yes |
| `transfers[].you_are` | string | yes |

```json
{
  "transfers": [
    {
      "transfer_id": "00000000-0000-4000-8000-000000000000",
      "shop_id": "00000000-0000-4000-8000-000000000000",
      "shop": "...",
      "from_person_id": "00000000-0000-4000-8000-000000000000",
      "from_name": "...",
      "to_person_id": "00000000-0000-4000-8000-000000000000",
      "to_name": "...",
      "status": "...",
      "old_owner_stays_as_office": true,
      "started_at": "...",
      "expires_at": "...",
      "completed_at": "...",
      "cancelled_at": "...",
      "processor_reverify_status": "...",
      "you_are": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shops.closeCheck

Whether the shop can close now (owner only). Blockers must be cleared first: an open hand-off, card or bank payments still going through, open disputes, active maintenance plans, visits booked ahead. Warnings say what closing does.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/shops/close` |
| MCP tool | `shops_closeCheck` |
| Writes | no |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Whether the shop can close now (owner only). Blockers must be cleared first: an open hand-off, card or bank payments still going through, open disputes, active maintenance plans, visits booked ahead.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/shops/close?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `can_close` | boolean | yes |
| `blockers` | array | yes |
| `blockers[].code` | string | yes |
| `blockers[].count` | integer | yes |
| `blockers[].message` | string | yes |
| `blockers[].total_minor` | integer | no |
| `warnings` | array | yes |
| `warnings[].code` | string | yes |
| `warnings[].count` | integer | yes |
| `warnings[].message` | string | yes |
| `warnings[].total_minor` | integer | no |

```json
{
  "can_close": true,
  "blockers": [
    {
      "code": "...",
      "count": 1,
      "message": "..."
    }
  ],
  "warnings": [
    {
      "code": "...",
      "count": 1,
      "message": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shops.close

Close the shop for good (owner, on a hosted page, typing the shop name). Everyone loses access, agent connections and webhooks stop, every customer link stops working, and the payments account only settles what is in flight. Records are kept. The owner can reopen it with shops.reopen for 90 days; after that photos, notes, templates, the price book and saved cards are deleted, and money, tax, signature and job records stay. Offer shops.export before closing. Refused with the blockers from shops.closeCheck while any remain.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/shops/close` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Close the shop for good (owner, on a hosted page, typing the shop name). Everyone loses access, agent connections and webhooks stop, every customer link stops working, and the payments account only settles what is in flight.

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `confirm_name` | string | yes |

## Example

```http
POST https://doingwork.work/v1/shops/close
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "confirm_name": "..."
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `status` | string | yes |
| `memberships_ended` | integer | yes |
| `links_ended` | integer | yes |
| `warnings` | array | yes |
| `warnings[].code` | string | yes |
| `warnings[].count` | integer | yes |
| `warnings[].message` | string | yes |
| `warnings[].total_minor` | integer | no |
| `reopen_until` | string (date-time) | yes |

```json
{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "status": "...",
  "memberships_ended": 1,
  "links_ended": 1,
  "warnings": [
    {
      "code": "...",
      "count": 1,
      "message": "..."
    }
  ],
  "reopen_until": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# shops.reopen

Reopen a closed shop (only the owner who closed it, on a hosted page, within 90 days of closing). You come back as owner. Workers are not added back: invite them again. Customer links that ended at close stay ended; send new ones. Connect your payments account again to take payments. After 90 days the shop cannot be reopened.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/shops/reopen` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Reopen a closed shop (only the owner who closed it, on a hosted page, within 90 days of closing). You come back as owner.

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/shops/reopen
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `status` | string | yes |
| `membership_id` | string (uuid) | yes |
| `closed_at` | string (date-time) | yes |

```json
{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "status": "...",
  "membership_id": "00000000-0000-4000-8000-000000000000",
  "closed_at": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# subs.add

Add a subcontractor to a job: a company and the one email they will sign in with. Returns a link once; share it with them yourself (Doing Work does not send it). They see the job summary, the work address, your scope note and upcoming visit times, and can leave you notes. They never see prices, the customer's name or contact details, or other subcontractors. Lasts expires_in_days (default 90, up to 365) until removed. Owner or office only.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/subs` |
| MCP tool | `subs_add` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

Add a subcontractor to a job: a company and the one email they will sign in with. Returns a link once; share it with them yourself (Doing Work does not send it).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `company_name` | string | yes |
| `email` | string (email) | yes |
| `contact_name` | string | no |
| `trade` | string | no |
| `scope_note` | string | no |
| `expires_in_days` | integer | no |

## Example

```http
POST https://doingwork.work/v1/subs
content-type: application/json
Idempotency-Key: 3f1c...

{
  "job_id": "00000000-0000-4000-8000-000000000000",
  "company_name": "...",
  "email": "owner@example.com"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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. The link is never shown again. Owner or office only.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/subs` |
| MCP tool | `subs_list` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

Subcontractors on a job, or on every job in the shop: status invited, joined, expired or removed, when they last opened their page. The link is never shown again.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | no |
| `shop_id` | string (uuid) | no |
| `include_removed` | boolean | no |

## Example

```http
GET https://doingwork.work/v1/subs?job_id=00000000-0000-4000-8000-000000000000&shop_id=00000000-0000-4000-8000-000000000000&include_removed=True
```

## Output

| Field | Type | Always there |
|---|---|---|
| `subcontractors` | array | yes |
| `subcontractors[].subcontractor_id` | string (uuid) | yes |
| `subcontractors[].job_id` | string (uuid) | yes |
| `subcontractors[].company_name` | string | yes |
| `subcontractors[].contact_name` | ['string', 'null'] | yes |
| `subcontractors[].email` | string | yes |
| `subcontractors[].trade` | ['string', 'null'] | yes |
| `subcontractors[].scope_note` | ['string', 'null'] | yes |
| `subcontractors[].status` | string | yes |
| `subcontractors[].created_by` | string (uuid) | yes |
| `subcontractors[].created_at` | string (date-time) | yes |
| `subcontractors[].expires_at` | string (date-time) | yes |
| `subcontractors[].joined_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `subcontractors[].removed_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `subcontractors[].last_opened_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `subcontractors[].opens` | integer | yes |

```json
{
  "subcontractors": [
    {
      "subcontractor_id": "00000000-0000-4000-8000-000000000000",
      "job_id": "00000000-0000-4000-8000-000000000000",
      "company_name": "...",
      "contact_name": "...",
      "email": "owner@example.com",
      "trade": "...",
      "scope_note": "...",
      "status": "...",
      "created_by": "00000000-0000-4000-8000-000000000000",
      "created_at": "...",
      "expires_at": "...",
      "joined_at": "...",
      "removed_at": "...",
      "last_opened_at": "...",
      "opens": 1
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# subs.update

Change a subcontractor's contact name, trade or scope note (what they see on their page). The email and link never change; remove and add again for a new person. Owner or office only.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/subs/update` |
| MCP tool | `subs_update` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

Change a subcontractor's contact name, trade or scope note (what they see on their page). The email and link never change; remove and add again for a new person.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `subcontractor_id` | string (uuid) | yes |
| `contact_name` | ['string', 'null'] | no |
| `trade` | ['string', 'null'] | no |
| `scope_note` | ['string', 'null'] | no |

## Example

```http
POST https://doingwork.work/v1/subs/update
content-type: application/json
Idempotency-Key: 3f1c...

{
  "subcontractor_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# subs.remove

Take a subcontractor off a job, for good: their link stops working at once. Their notes are kept. Owner or office only.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/subs/remove` |
| MCP tool | `subs_remove` |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

Take a subcontractor off a job, for good: their link stops working at once. Their notes are kept.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `subcontractor_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/subs/remove
content-type: application/json
Idempotency-Key: 3f1c...

{
  "subcontractor_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# subs.notes

Notes subcontractors left on a job, newest first. Owner or office only.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/subs/notes` |
| MCP tool | `subs_notes` |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

Notes subcontractors left on a job, newest first. Owner or office only.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `job_id` | string (uuid) | yes |
| `limit` | integer | no |

## Example

```http
GET https://doingwork.work/v1/subs/notes?job_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `notes` | array | yes |
| `notes[].note_id` | string (uuid) | yes |
| `notes[].subcontractor_id` | string (uuid) | yes |
| `notes[].company_name` | string | yes |
| `notes[].body` | string | yes |
| `notes[].created_at` | string (date-time) | yes |

```json
{
  "notes": [
    {
      "note_id": "00000000-0000-4000-8000-000000000000",
      "subcontractor_id": "00000000-0000-4000-8000-000000000000",
      "company_name": "...",
      "body": "...",
      "created_at": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# subs.view

The subcontractor's page for one job: the shop, job summary, work address, scope note, upcoming visit times and their notes. state is ok, gone (removed, expired, job cancelled or shop closed) or wrong_person (signed in with a different email, or someone else opened it first). Opening it the first time binds it to you. Hosted page only.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `GET /v1/subs/view` |
| MCP tool | Not an MCP tool |
| Writes | no |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

The subcontractor's page for one job: the shop, job summary, work address, scope note, upcoming visit times and their notes. state is ok, gone (removed, expired, job cancelled or shop closed) or wrong_person (signed in with a different email, or someone else opened it first).

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `k` | string | yes |

## Example

```http
GET https://doingwork.work/v1/subs/view?k=...
```

## Output

| Field | Type | Always there |
|---|---|---|
| `state` | string | yes |
| `subcontractor_id` | string (uuid) | no |
| `job_id` | string (uuid) | no |
| `shop` | string | no |
| `company_name` | string | no |
| `trade` | ['string', 'null'] | no |
| `scope_note` | ['string', 'null'] | no |
| `summary` | ['string', 'null'] | no |
| `job_status` | string | no |
| `address` | string | no |
| `time_zone` | string | no |
| `expires_at` | string (date-time) | no |
| `visits` | array | no |
| `visits[].visit_id` | string (uuid) | yes |
| `visits[].title` | ['string', 'null'] | yes |
| `visits[].starts_at` | string (date-time) | yes |
| `visits[].ends_at` | string (date-time) | yes |
| `notes` | array | no |
| `notes[].note_id` | string (uuid) | yes |
| `notes[].body` | string | yes |
| `notes[].created_at` | string (date-time) | yes |

```json
{
  "state": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# subs.addNote

Leave a note for the shop on this job, as the subcontractor (up to 20 a day). Hosted page only.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/subs/addNote` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Field work (checklists, forms, time, notes, change orders, subcontractors)](https://doingwork.work/docs/reference/#field-work-checklists-forms-time-notes-change-orders-subcontractors) |

## When to use it

Leave a note for the shop on this job, as the subcontractor (up to 20 a day). Hosted page only.

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `k` | string | yes |
| `body` | string | yes |

## Example

```http
POST https://doingwork.work/v1/subs/addNote
content-type: application/json
Idempotency-Key: 3f1c...

{
  "k": "...",
  "body": "..."
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `note_id` | string (uuid) | yes |

```json
{
  "note_id": "00000000-0000-4000-8000-000000000000"
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# invoices.void

Void an invoice nobody has paid yet (owner or office), with a reason. Pay links stop working and an open card checkout is expired. Refused when any money was collected, a card charge is pending or a bank payment is clearing: refund instead. A void invoice cannot be reopened; create a new one if needed. Change orders and maintenance terms on it can be billed again.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/invoices/void` |
| MCP tool | `invoices_void` |
| Writes | yes, send an Idempotency-Key |
| Area | [Shop](https://doingwork.work/docs/reference/#shop) |

## When to use it

Void an invoice nobody has paid yet (owner or office), with a reason. Pay links stop working and an open card checkout is expired.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `invoice_id` | string (uuid) | yes |
| `reason` | string | yes |

## Example

```http
POST https://doingwork.work/v1/invoices/void
content-type: application/json
Idempotency-Key: 3f1c...

{
  "invoice_id": "00000000-0000-4000-8000-000000000000",
  "reason": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/bulk` |
| MCP tool | `bulk_run` |
| Writes | yes, send an Idempotency-Key |
| Area | [Imports, custom fields, tags and search](https://doingwork.work/docs/reference/#imports-custom-fields-tags-and-search) |

## When to use it

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.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `action` | string (one of customers.addTags, customers.removeTags, customers.archive, customers.unarchive, jobs.addTags, jobs.removeTags, visits.assign) | yes |
| `ids` | array | yes |
| `tags` | array | no |
| `person_ids` | array | no |

## Example

```http
POST https://doingwork.work/v1/bulk
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "action": "customers.addTags",
  "ids": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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. Send report_id to change a saved one. Owner or office only.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/reports/saved` |
| MCP tool | `reports_save` |
| Writes | yes, send an Idempotency-Key |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

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. Send report_id to change a saved one.

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `report_id` | string (uuid) | no |
| `name` | string | yes |
| `op_id` | string (one of reports.revenue, reports.owed, reports.quoteWinRate, reports.today, reports.week, costs.jobs) | yes |
| `params` | object | no |
| `date_range` | ['string', 'null', ' ', '(', 'o', 'n', 'e', ' ', 'o', 'f', ' ', 't', 'o', 'd', 'a', 'y', ',', ' ', 't', 'h', 'i', 's', '_', 'w', 'e', 'e', 'k', ',', ' ', 'l', 'a', 's', 't', '_', 'w', 'e', 'e', 'k', ',', ' ', 't', 'h', 'i', 's', '_', 'm', 'o', 'n', 't', 'h', ',', ' ', 'l', 'a', 's', 't', '_', 'm', 'o', 'n', 't', 'h', ',', ' ', 'l', 'a', 's', 't', '_', '7', '_', 'd', 'a', 'y', 's', ',', ' ', 'l', 'a', 's', 't', '_', '3', '0', '_', 'd', 'a', 'y', 's', ',', ' ', 'l', 'a', 's', 't', '_', '9', '0', '_', 'd', 'a', 'y', 's', ',', ' ', 't', 'h', 'i', 's', '_', 'q', 'u', 'a', 'r', 't', 'e', 'r', ',', ' ', 'l', 'a', 's', 't', '_', 'q', 'u', 'a', 'r', 't', 'e', 'r', ',', ' ', 't', 'h', 'i', 's', '_', 'y', 'e', 'a', 'r', ',', ' ', 'l', 'a', 's', 't', '_', 'y', 'e', 'a', 'r', ',', ' ', 'N', 'o', 'n', 'e', ')'] | no |

## Example

```http
POST https://doingwork.work/v1/reports/saved
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "name": "Ruiz Plumbing",
  "op_id": "reports.revenue"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# reports.listSaved

The shop's saved reports, by name. Owner or office only.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/reports/saved` |
| MCP tool | `reports_listSaved` |
| Writes | no |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

The shop's saved reports, by name. Owner or office only.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/reports/saved?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `reports` | array | yes |
| `reports[].report_id` | string (uuid) | yes |
| `reports[].name` | string | yes |
| `reports[].op_id` | string | yes |
| `reports[].params` | object | yes |
| `reports[].date_range` | ['string', 'null'] | yes |
| `reports[].created_by` | string (uuid) | yes |
| `reports[].created_at` | string (date-time) | yes |
| `reports[].updated_at` | string (date-time) | yes |

```json
{
  "reports": [
    {
      "report_id": "00000000-0000-4000-8000-000000000000",
      "name": "Ruiz Plumbing",
      "op_id": "00000000-0000-4000-8000-000000000000",
      "params": {},
      "date_range": "...",
      "created_by": "00000000-0000-4000-8000-000000000000",
      "created_at": "...",
      "updated_at": "..."
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# reports.removeSaved

Remove a saved report (kept for the record; its name is free to use again).

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/reports/saved/remove` |
| MCP tool | `reports_removeSaved` |
| Writes | yes, send an Idempotency-Key |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

Remove a saved report (kept for the record; its name is free to use again).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `report_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/reports/saved/remove
content-type: application/json
Idempotency-Key: 3f1c...

{
  "report_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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. Returns the exact input used and the operation's result.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/reports/saved/run` |
| MCP tool | `reports_run` |
| Writes | no |
| Area | [Costs, expenses and reports](https://doingwork.work/docs/reference/#costs-expenses-and-reports) |

## When to use it

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. Returns the exact input used and the operation's result.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `report_id` | string (uuid) | yes |
| `as_of` | string | no |

## Example

```http
POST https://doingwork.work/v1/reports/saved/run
content-type: application/json
Idempotency-Key: 3f1c...

{
  "report_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `report` | object | yes |
| `report.report_id` | string (uuid) | yes |
| `report.name` | string | yes |
| `report.op_id` | string | yes |
| `report.params` | object | yes |
| `report.date_range` | ['string', 'null'] | yes |
| `report.created_by` | string (uuid) | yes |
| `report.created_at` | string (date-time) | yes |
| `report.updated_at` | string (date-time) | yes |
| `input` | object | yes |
| `result` | object | yes |

```json
{
  "report": {
    "report_id": "00000000-0000-4000-8000-000000000000",
    "name": "Ruiz Plumbing",
    "op_id": "00000000-0000-4000-8000-000000000000",
    "params": {},
    "date_range": "...",
    "created_by": "00000000-0000-4000-8000-000000000000",
    "created_at": "...",
    "updated_at": "..."
  },
  "input": {},
  "result": {}
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/hub/createLink` |
| MCP tool | `hub_createLink` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

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).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |
| `expires_in_days` | integer | no |

## Example

```http
POST https://doingwork.work/v1/hub/createLink
content-type: application/json
Idempotency-Key: 3f1c...

{
  "customer_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/hub/links` |
| MCP tool | `hub_links` |
| Writes | no |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

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).

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `customer_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/hub/links?customer_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `links` | array | yes |
| `links[].link_id` | string (uuid) | yes |
| `links[].customer_id` | string (uuid) | yes |
| `links[].created_by` | string (uuid) | yes |
| `links[].created_at` | string (date-time) | yes |
| `links[].expires_at` | string (date-time) | yes |
| `links[].revoked_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `links[].last_opened_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `links[].opens` | integer | yes |
| `links[].live` | boolean | yes |

```json
{
  "links": [
    {
      "link_id": "00000000-0000-4000-8000-000000000000",
      "customer_id": "00000000-0000-4000-8000-000000000000",
      "created_by": "00000000-0000-4000-8000-000000000000",
      "created_at": "...",
      "expires_at": "...",
      "revoked_at": "...",
      "last_opened_at": "...",
      "opens": 1,
      "live": true
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/hub/revokeLink` |
| MCP tool | `hub_revokeLink` |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

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).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `link_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/hub/revokeLink
content-type: application/json
Idempotency-Key: 3f1c...

{
  "link_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/visits/changeRequests` |
| MCP tool | `visits_changeRequests` |
| Writes | no |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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).

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `status` | string (one of open, accepted, declined, withdrawn, all) | no |
| `visit_id` | string (uuid) | no |
| `limit` | integer | no |

## Example

```http
GET https://doingwork.work/v1/visits/changeRequests?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

| Field | Type | Always there |
|---|---|---|
| `change_requests` | array | yes |
| `change_requests[].change_request_id` | string (uuid) | yes |
| `change_requests[].visit_id` | string (uuid) | yes |
| `change_requests[].job_id` | string (uuid) | yes |
| `change_requests[].customer_id` | string (uuid) | yes |
| `change_requests[].customer_name` | string | yes |
| `change_requests[].kind` | string | yes |
| `change_requests[].preferred_times` | ['string', 'null'] | yes |
| `change_requests[].note` | ['string', 'null'] | yes |
| `change_requests[].visit_starts_at` | string (date-time) | yes |
| `change_requests[].visit_now_starts_at` | string (date-time) | yes |
| `change_requests[].visit_status` | string | yes |
| `change_requests[].status` | string | yes |
| `change_requests[].requested_by` | string (uuid) | yes |
| `change_requests[].created_at` | string (date-time) | yes |
| `change_requests[].answered_by` | ['string', 'null', ' ', '(', 'u', 'u', 'i', 'd', ')'] | yes |
| `change_requests[].answered_at` | ['string', 'null', ' ', '(', 'd', 'a', 't', 'e', '-', 't', 'i', 'm', 'e', ')'] | yes |
| `change_requests[].answer_note` | ['string', 'null'] | yes |
| `change_requests[].applied` | boolean | yes |

```json
{
  "change_requests": [
    {
      "change_request_id": "00000000-0000-4000-8000-000000000000",
      "visit_id": "00000000-0000-4000-8000-000000000000",
      "job_id": "00000000-0000-4000-8000-000000000000",
      "customer_id": "00000000-0000-4000-8000-000000000000",
      "customer_name": "...",
      "kind": "...",
      "preferred_times": "...",
      "note": "...",
      "visit_starts_at": "...",
      "visit_now_starts_at": "...",
      "visit_status": "...",
      "status": "...",
      "requested_by": "00000000-0000-4000-8000-000000000000",
      "created_at": "...",
      "answered_by": "00000000-0000-4000-8000-000000000000",
      "answered_at": "...",
      "answer_note": "...",
      "applied": true
    }
  ]
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `POST /v1/visits/answerChange` |
| MCP tool | `visits_answerChange` |
| Writes | yes, send an Idempotency-Key |
| Area | [Scheduling and visits](https://doingwork.work/docs/reference/#scheduling-and-visits) |

## When to use it

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).

## What to tell the owner

Say what you did in plain words and what happens next.

## Input

| Field | Type | Required |
|---|---|---|
| `change_request_id` | string (uuid) | yes |
| `decision` | string (one of accept, decline) | yes |
| `note` | string | no |
| `starts_at` | string (date-time) | no |
| `ends_at` | string (date-time) | no |
| `apply` | boolean | no |

## Example

```http
POST https://doingwork.work/v1/visits/answerChange
content-type: application/json
Idempotency-Key: 3f1c...

{
  "change_request_id": "00000000-0000-4000-8000-000000000000",
  "decision": "accept"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `GET /v1/hub/view` |
| MCP tool | Not an MCP tool |
| Writes | no |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

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).

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `k` | string | yes |

## Example

```http
GET https://doingwork.work/v1/hub/view?k=...
```

## Output

| Field | Type | Always there |
|---|---|---|
| `state` | string | yes |
| `link_id` | string (uuid) | no |
| `shop_id` | string (uuid) | no |
| `customer_id` | string (uuid) | no |
| `shop` | string | no |
| `customer` | string | no |
| `time_zone` | string | no |
| `expires_at` | string (date-time) | no |
| `properties` | array | no |
| `properties[].property_id` | string (uuid) | yes |
| `properties[].label` | ['string', 'null'] | yes |
| `properties[].address` | string | yes |
| `jobs` | array | no |
| `jobs[].job_id` | string (uuid) | yes |
| `jobs[].summary` | ['string', 'null'] | yes |
| `jobs[].status` | string | yes |
| `jobs[].created_at` | string (date-time) | yes |
| `quotes` | array | no |
| `quotes[].quote_id` | string (uuid) | yes |
| `quotes[].job_id` | string (uuid) | yes |
| `quotes[].summary` | ['string', 'null'] | yes |
| `quotes[].status` | string | yes |
| `quotes[].version` | integer | yes |
| `quotes[].total_minor` | integer | yes |
| `quotes[].currency` | string | yes |
| `quotes[].can_open` | boolean | yes |
| `invoices` | array | no |
| `invoices[].invoice_id` | string (uuid) | yes |
| `invoices[].job_id` | string (uuid) | yes |
| `invoices[].summary` | ['string', 'null'] | yes |
| `invoices[].status` | string | yes |
| `invoices[].total_minor` | integer | yes |
| `invoices[].balance_minor` | integer | yes |
| `invoices[].currency` | string | yes |
| `invoices[].can_pay` | boolean | yes |
| `visits` | array | no |
| `visits[].visit_id` | string (uuid) | yes |
| `visits[].job_id` | string (uuid) | yes |
| `visits[].summary` | ['string', 'null'] | yes |
| `visits[].title` | ['string', 'null'] | yes |
| `visits[].starts_at` | string (date-time) | yes |
| `visits[].ends_at` | string (date-time) | yes |
| `visits[].status` | string | yes |
| `visits[].can_ask_change` | boolean | yes |
| `visits[].change` | ['object', 'null'] | yes |
| `requests` | array | no |
| `requests[].request_id` | string (uuid) | yes |
| `requests[].title` | string | yes |
| `requests[].stage` | string | yes |
| `requests[].created_at` | string (date-time) | yes |

```json
{
  "state": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/hub/open` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

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).

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `k` | string | yes |
| `kind` | string (one of quote, invoice) | yes |
| `id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/hub/open
content-type: application/json
Idempotency-Key: 3f1c...

{
  "k": "...",
  "kind": "quote",
  "id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `url` | string | yes |

```json
{
  "url": "https://example.com/hooks/doing-work"
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/hub/requestVisitChange` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

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).

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `k` | string | yes |
| `visit_id` | string (uuid) | yes |
| `kind` | string (one of reschedule, cancel) | yes |
| `preferred_times` | string | no |
| `note` | string | no |

## Example

```http
POST https://doingwork.work/v1/hub/requestVisitChange
content-type: application/json
Idempotency-Key: 3f1c...

{
  "k": "...",
  "visit_id": "00000000-0000-4000-8000-000000000000",
  "kind": "reschedule"
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `change_request_id` | string (uuid) | yes |
| `status` | string | yes |

```json
{
  "change_request_id": "00000000-0000-4000-8000-000000000000",
  "status": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# hub.withdrawVisitChange

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

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/hub/withdrawVisitChange` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

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).

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `k` | string | yes |
| `change_request_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/hub/withdrawVisitChange
content-type: application/json
Idempotency-Key: 3f1c...

{
  "k": "...",
  "change_request_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `change_request_id` | string (uuid) | yes |
| `status` | string | yes |

```json
{
  "change_request_id": "00000000-0000-4000-8000-000000000000",
  "status": "..."
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Hosted page only (a person does it in the browser) |
| REST | `POST /v1/hub/requestWork` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Customer hub, requests and customer emails](https://doingwork.work/docs/reference/#customer-hub-requests-and-customer-emails) |

## When to use it

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).

## What to tell the owner

Send the person the link. They finish this step themselves on the page; you never do it for them.

## Input

| Field | Type | Required |
|---|---|---|
| `k` | string | yes |
| `title` | string | yes |
| `details` | string | no |
| `preferred_times` | string | no |
| `property_id` | string (uuid) | no |

## Example

```http
POST https://doingwork.work/v1/hub/requestWork
content-type: application/json
Idempotency-Key: 3f1c...

{
  "k": "...",
  "title": "..."
}
```

## Output

| Field | Type | Always there |
|---|---|---|
| `request_id` | string (uuid) | yes |

```json
{
  "request_id": "00000000-0000-4000-8000-000000000000"
}
```

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | A person's own sign-in only (agent tokens are refused) |
| REST | `POST /v1/approvals/rules` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Team (unlimited workers)](https://doingwork.work/docs/reference/#team-unlimited-workers) |

## When to use it

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.

## What to tell the owner

Ask the person to do this with their own sign-in. Your agent token is refused here.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `op` | string | yes |
| `min_amount_minor` | integer | no |
| `note` | string | no |

## Example

```http
POST https://doingwork.work/v1/approvals/rules
content-type: application/json
Idempotency-Key: 3f1c...

{
  "shop_id": "00000000-0000-4000-8000-000000000000",
  "op": "..."
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# approvals.removeRule

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

|  |  |
|---|---|
| Who | A person's own sign-in only (agent tokens are refused) |
| REST | `POST /v1/approvals/rules/remove` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Team (unlimited workers)](https://doingwork.work/docs/reference/#team-unlimited-workers) |

## When to use it

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.

## What to tell the owner

Ask the person to do this with their own sign-in. Your agent token is refused here.

## Input

| Field | Type | Required |
|---|---|---|
| `rule_id` | string (uuid) | yes |

## Example

```http
POST https://doingwork.work/v1/approvals/rules/remove
content-type: application/json
Idempotency-Key: 3f1c...

{
  "rule_id": "00000000-0000-4000-8000-000000000000"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# approvals.listRules

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

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/approvals/rules` |
| MCP tool | `approvals_listRules` |
| Writes | no |
| Area | [Team (unlimited workers)](https://doingwork.work/docs/reference/#team-unlimited-workers) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/approvals/rules?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/approvals` |
| MCP tool | `approvals_list` |
| Writes | no |
| Area | [Team (unlimited workers)](https://doingwork.work/docs/reference/#team-unlimited-workers) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `shop_id` | string (uuid) | yes |
| `status` | string (one of pending, approved, denied, failed, expired) | no |

## Example

```http
GET https://doingwork.work/v1/approvals?shop_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | Agents can call this |
| REST | `GET /v1/approvals/get` |
| MCP tool | `approvals_get` |
| Writes | no |
| Area | [Team (unlimited workers)](https://doingwork.work/docs/reference/#team-unlimited-workers) |

## When to use it

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.

## What to tell the owner

Tell them what you found in plain words. Don't read back raw fields.

## Input

| Field | Type | Required |
|---|---|---|
| `approval_id` | string (uuid) | yes |

## Example

```http
GET https://doingwork.work/v1/approvals/get?approval_id=00000000-0000-4000-8000-000000000000
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |


---

# 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.

|  |  |
|---|---|
| Who | A person's own sign-in only (agent tokens are refused) |
| REST | `POST /v1/approvals/decide` |
| MCP tool | Not an MCP tool |
| Writes | yes, send an Idempotency-Key |
| Area | [Team (unlimited workers)](https://doingwork.work/docs/reference/#team-unlimited-workers) |

## When to use it

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.

## What to tell the owner

Ask the person to do this with their own sign-in. Your agent token is refused here.

## Input

| Field | Type | Required |
|---|---|---|
| `approval_id` | string (uuid) | yes |
| `decision` | string (one of approve, deny) | yes |
| `note` | string | no |

## Example

```http
POST https://doingwork.work/v1/approvals/decide
content-type: application/json
Idempotency-Key: 3f1c...

{
  "approval_id": "00000000-0000-4000-8000-000000000000",
  "decision": "approve"
}
```

## Output

JSON. The response shape isn't published yet; read the fields you need and ignore the rest. Errors are `{code, message, status}`.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_input`, `invalid_token`, `rate_limited` |  | Can happen on any call. See [errors](https://doingwork.work/docs/errors/). |
