Housecall Pro API endpoint reference
The public surface, route by route: what reads, what writes and the field each write demands, plus the routes that are documented but absent and the working routes the documentation never mentions.
Housecall Pro's own documentation is wrong in both directions. It lists routes that return an HTML 404, and omits routes that work. An integration built from the documentation alone ships broken code paths that fail only in production. Where this page and the documentation disagree, this page follows the live account.
Read the practical guide first if you have not, because the namespace rule and the error-shape table below assume it.
The read surface
Everything here answers an API key. Note which namespace each one lives in: a route missing
from the bare namespace very often exists under /api/ or
/job_fields/.
| Entity | Route | What to know |
|---|---|---|
| Customers | /customers | List and fetch. A deleted customer still returns 200 by id with no deleted marker |
| Jobs | /jobs | Reachable in all three namespaces. Filter on deleted_at or deleted work inflates your totals |
| Job notes | /api/jobs/{id}/notes | Bare path 404s, so notes look write-only. Needs a write-scoped key to read |
| Job count | /api/jobs/job_count | A cheap total that saves paginating the whole collection just to count |
| Invoices | /invoices | List is bare. Detail and a rendered document live at /api/invoices/{id} |
| Estimates | /estimates | The estimate omits line items; the option path returns them in full |
| Estimate line items | /estimates/{id}/options/{option}/line_items | Multi-line descriptions intact, which is what makes read-back after a write possible |
| Price book | /api/price_book/services | Also materials, material_categories and price_forms. Materials require an exact category parameter |
| Job types | /job_fields/job_types | The bare /job_types path does not exist |
| Lead sources | /lead_sources | The validation list for lead_source on customer create |
| Appointments | /jobs/{id}/appointments | Nested under a job only. An archived job answers 400, which is validation, not absence |
| Checklists | /checklists | Requires the array filter syntax job_uuids[]=; the singular form returns 422 |
| Tags | /tags | The account tag registry |
| Employees | /employees | Roles and permissions. No public employee create |
| Events | /events | Carries recurrence rule, address and assigned employees |
| Schedule availability | /company/schedule_availability | Plus /booking_windows |
| Service zones | /service_zones | Zip list and trip charge |
| Company | /company | The id is a bare UUID, not a prefixed one |
The write surface
Each of these exists and works with a full-access key. The second column is the thing it rejects you for missing, the detail that turns a 422 into a five-minute job instead of an afternoon.
| Route | What it requires |
|---|---|
POST /customers | Any one of first name, last name, email or phone |
PATCH /customers/{id} | A merge, so omitted fields are left untouched. PUT behaves identically |
POST /customers/{id}/addresses | street, city, state, zip and country all required. Append-only: no update, no delete |
POST /jobs | customer_id alone is enough; the job arrives needing scheduling |
POST /jobs/{id}/line_items | Money in cents. Update wants name |
POST /jobs/{id}/notes | Body key is content |
POST /jobs/{id}/attachments | Multipart file; returns 202 |
PUT /jobs/{id}/schedule | The nested shape is the only one honoured. DELETE unschedules without deleting the job |
POST /jobs/{id}/appointments | start_time, end_time and at least one employee, since it refuses to exist without one |
POST /jobs/{id}/tags | Wants tag_id, never a tag name. Removal is per-tag in the path |
POST /jobs/lock | The bulk form wants starting_at, because it is a date-based lock, not a list of job ids |
POST /leads | Create and convert only; convert requires Type |
POST /estimates | customer_id plus options[], each holding name and line_items[] |
POST /estimates/{id}/options | Undocumented and real. Adds an option to an existing estimate |
POST /tags | Create and rename only. There is no API delete, so treat creation as one-way |
POST /job_fields/job_types | name required |
POST /api/price_book/materials | Also material_categories and price_forms, with PUT and DELETE. Services are read-only |
Two writes that reach the customer
PUT /jobs/{id}/dispatch is what triggers the "on my way"
notification, and POST /estimates/options/approve approves an estimate on the
customer's behalf, driving their workflow. Both are ordinary-looking routes with
customer-visible consequences.
Gate them behind an explicit human confirmation, the same way you would an outbound message.
And note the approve route takes option_ids, plural, an array, which is easy
to miss and awkward to undo.
Documented, or expected, but absent
Each of these returns an HTML 404, the tell that the route itself does not exist, as opposed to a JSON 404 which means your id was wrong.
| Route | Reality |
|---|---|
DELETE /customers/{id} | No public customer delete; dashboard only |
PUT | DELETE /customers/{id}/addresses/{id} | Addresses are append-only |
PUT | DELETE /estimates/{id} | No estimate update or delete, and none for options either |
PUT | DELETE /leads/{id} | Leads are create and convert only |
DELETE /tags/{id} | Tags can never be deleted through the API |
POST | PUT | DELETE /appointments | No top-level collection; nested under a job only |
POST /api/price_book/services | Materials and categories create fine; services do not |
POST /invoices | No public invoice creation |
GET /webhooks/subscription | No list route exists; creating one needs a registered partner app |
Absent here means absent from the platform's public surface, so no client reaches these: not this MCP, not another one, not Zapier, not a script. Equipment and Property Profile data, service plans and the visits used against one, and a nameplate decoded from a photo are covered by the hosted backend, which your own agent connects to the same way it connects to the free MCP.
Works, but undocumented
These answer an API key and appear in no public documentation. Several are capabilities that look absent on the obvious path but exist under a different namespace.
- The whole price book under
/api/price_book/: services, materials, material categories and price forms. - Estimate line items at the nested option path, which allow verify-after-write.
- Job notes reads at
/api/jobs/{id}/notes, and a cheapjob_countalongside it. - Invoice detail and a rendered invoice document at
/api/invoices/{id}and its preview path. - Job types at
/job_fields/job_types, plus/lead_sources,/events,/routes,/service_zonesand/company/schedule_availability. - Adding an option to an existing estimate, which returns 201 and appears in no documentation.
Two write behaviours worth knowing before you build
- Estimate create silently drops some fields. Estimate-level summary and message fields, and the option-level message, do not persist. No error is returned. Set them in the Housecall Pro interface afterwards.
-
lead_sourceis a lookup, not free text. A value that does not exactly match an existing lead source is rejected. Fetch the list first and validate against it.
All of this is already handled
Our free open-source MCP implements every route on this page that is worth exposing to an assistant, with the quirks absorbed, and it is the only Housecall Pro MCP with no dead routes. The tables are here for anyone building their own client.
Found something that behaves differently on your account? We would genuinely like to know: hello@hcpapi.com.