The Housecall Pro API, in practice
Housecall Pro has a REST API on its MAX plan. It is capable, and it behaves in several ways the documentation does not mention: money arrives in cents, some filters are accepted and then ignored, and a few writes return 200 without changing anything. This page is what we wish we had read first.
Everything below was checked against a live Housecall Pro MAX account rather than taken from the documentation. Where the two disagree, this page follows the account. Your account may still differ. Plans, permissions and data-entry habits all change what you see, so treat this as a strong prior, not a promise.
Do you have API access?
API access comes with Housecall Pro's MAX plan. In Housecall Pro open My Apps, click Go to App store, open API Key Management, and generate a key as either Full access or Read-only. You need an admin login to do it. The step by step version is on the MCP page.
That choice is the entire permission boundary. There is no per-endpoint scoping, no scopes list, and no way to grant one route and withhold another. Two consequences catch people out:
-
A read-only key returns
401on every write, with a message about permissions, which reads like a broken route if you only look at the status code. - Only an admin can generate a key, and admins have full account access, so the key reaches the whole book rather than the slice its creator happens to work in.
Not on MAX?
You are not shut out. Housecall Pro's dashboard exports customers, jobs, the price book, equipment and service agreements as CSV, and those files carry most of what you would want the API for. Start with the export guide. It needs no key and no plan change.
There is no self-serve developer dashboard and no app registration. OAuth credentials exist, but they are issued to official integration partners by email rather than through a signup flow, so for almost everyone the API key is the way in.
Connecting: base URL and auth
Two details that reject every request if you get them wrong:
Base URL https://api.housecallpro.com (no /v1, a version prefix 404s everything)
Auth Authorization: Token <your-key> (Token, not Bearer) Bearer is the near-universal convention and it is wrong here. If every route
returns 401 with a key you just generated, this is almost always why.
Money is integer cents, everywhere
Every money field on every entity, whether jobs, invoices, estimates or line items, is an integer number of cents.
| Field | Raw value | Means |
|---|---|---|
subtotal | 25000 | $250.00 |
taxes[].rate | 8750 | 8.75% (basis points) |
| a discount | -25000 | a $250 credit |
This runs both directions. Send unit_price: 1234 on a write and the job total
reads $12.34. So a program that passes a dollar figure straight through bills one hundredth
of the intended amount, and a report that reads cents as dollars overstates revenue a
hundredfold. Convert once, at the boundary, and state which side of it you are on.
Housecall Pro runs three path namespaces
This is the single most useful undocumented fact about the API, and it is why so many integrations ship phantom "not available" gaps. The same account and the same key reach three different namespaces, each with its own envelope and id style.
| Namespace | Example | Envelope | Ids |
|---|---|---|---|
| bare | /jobs, /customers | {page, page_size, total_pages, total_items, <entity>[]} | job_, cus_ |
/api/* | /api/price_book/services | {object:"list", data[], total_count} | olit_, pbmat_ |
/job_fields/* | /job_fields/job_types | bare-style | jbt_ |
A capability missing from one namespace often lives in another. /price_book/services returns 404 while /api/price_book/services
returns your whole price book. /job_types is a dead path while
/job_fields/job_types works. Job notes look write-only because
GET /jobs/{id}/notes 404s, but
GET /api/jobs/{id}/notes returns them.
One caveat worth internalising: /api/* is a per-route allowlist, not a
second front door. A handful of paths under that prefix answer an API key; most
return 401 to any key regardless of scope. Probe the specific route rather than assuming the
namespace is open.
Reading the error responses
Housecall Pro returns four shapes that all look like failure and mean very different things. Read the content type as well as the status code. This one table turns guesswork into a single request.
| Response | What it actually means |
|---|---|
| HTML 404 page | The route does not exist |
JSON 404 {"error":{"message":…}} | The route is real, but your id or a required parameter is wrong |
401 {"message":"Unauthorized"} | Outside the API surface entirely; no key reaches it |
401 …"does not have the necessary permissions" | A real route, but your key's scope is too narrow. Regenerate with full access |
There are also two error envelopes in circulation: some routes return
{"errors":{"field":"is missing"}} with a 422, others
{"error":{"message":"…"}} with a 400. Parse both.
Filters that are silently ignored
These parameters are accepted without complaint and then ignored. You get the full dataset back and no indication that filtering did not happen, which is worse than an error, because the result looks plausible.
updated_after, which returns everything-
Invoice date filters, including
service_date_min/max,invoice_date_min/max,start_date,end_dateall return the complete set ?status=on invoices, which returns empty or errors-
customer_idon equipment, plusper_pageandlimitgenerally, all silently unfiltered
Filters that do work: work_status[]= (array syntax) and
scheduled_start_min / scheduled_start_max on /jobs.
page_size=100 is honoured, which is worth setting. The default page size makes a
full sweep ten times longer than it needs to be.
The workaround for date windows
Invoices come back newest-first. Walk pages from page one and stop once rows fall before the start of your window, then filter client-side. It is more code than a query parameter, and it is the only way to get a correct date range.
The general rule: an ignored parameter does not error. Prove that a filter filters before you trust a number that depends on it.
The work_status vocabulary trap
The input vocabulary and the response vocabulary are different languages.
| You filter with | Responses actually say |
|---|---|
work_status[]=completed | complete rated, complete unrated, in progress,
user canceled, pro canceled |
The string completed never appears in a response. Code that
tests work_status == "completed" matches zero rows in a ten-thousand-job book and
raises no error. It just reports that nothing was completed. Bucket on the
complete prefix, and substring-match on cancel.
Delete semantics, and the revenue-corrupting one
Deletion behaves differently for every entity, and two of them will quietly corrupt totals.
- Deleted jobs still come back from
GET /jobs, carrying their full amount. The record gains adeleted_attimestamp and its status flips, but it stays in list responses. Anything counting jobs or summing revenue must filter ondeleted_ator deleted work inflates the total. -
pro canceleddoes not mean "canceled." Deleting a job and canceling a job produce the same status;deleted_atis the only discriminator. - Deleted customers vanish from lists but stay fetchable by id, returning 200 with the full record and no deleted flag of any kind. If you cache customer ids you cannot tell from the record that one is gone, so re-check membership with a list query.
- Leads delete cleanly. Tags cannot be deleted through the API at all.
Writes that return 200 and change nothing
The most dangerous behaviour on this API. Several writes return 200 OK and change
nothing at all. Never trust a 2xx on a write. Read it back, or it did not happen.
-
PATCH /jobs/{id}honours only the nestedscheduleobject. Flatscheduled_startkeys return 200 and are ignored. So doestags. -
A job's
descriptionis derived from its line items, so writing it returns 200 and does nothing, because there is no direct description write. -
PUT /customers/{id}does not replace, it merges, identical to PATCH. A PUT carrying onlyfirst_nameleaves every other field intact. Good news for safety, surprising if you expected REST semantics.
Structural surprises
-
/jobs/{id}/line_itemsis empty on completed jobs. The reliable record of what was billed is the invoice, which embeds itsitems[]inline. - Invoices have no top-level
customer_id. Resolve it through the invoice'sjob_id. - Invoices have no
total_amount.amountis post-discount pre-tax,subtotalis pre-discount pre-tax, and gross-with-tax isamountplus the sum oftaxes[].amount. - Estimate line items are readable, but only at the nested option path.
GET /estimates/{id}omits them, which makes them look unreachable. They are returned in full, including multi-line descriptions, at the option's own line items route, which is what makes read-back verification after a write possible. - Ids are layered. An estimate has a public id and its options have their own; several routes key on the option rather than the estimate.
Rate limits
Real, undocumented, and invisible until they fire. No X-RateLimit-* headers are
returned, only a request id. The important nuance is that the limit is
sustained-volume, not burst:
| Pattern | Result |
|---|---|
| 25 requests at ~4/second | No 429s at all |
| 507 requests at ~3.3/second | 429s on 12% of reads, and every one succeeded on retry |
So "we ran a quick test and saw no 429s" is not evidence of headroom. Anything longer than a few dozen calls needs retry-with-backoff, and the retries do succeed.
What is genuinely not on the public API
Not "we could not find it". These return an HTML 404, which is the tell that the route does not exist:
- No invoice creation. Invoices can be read, including a rendered document, but not created.
- No customer delete, and addresses are append-only, with no update and no delete.
- No tag delete, ever. Create and rename only. Deleting a tag is a dashboard action, and Housecall Pro warns that it erases the tag from historical records.
- No estimate update or delete, and no option update or delete.
- No top-level appointments collection. Appointments exist only nested under a job, where they have full CRUD.
- No price-book service creation. Materials, categories and price forms are fully writable; services are read-only, which is why an audit of your services can produce a worklist but cannot fix them for you.
- Service plans, recurring service plans, and equipment / Property Profile data are not on the public API in any namespace.
- Webhooks are partner-gated. There is no list route, and creating a subscription with an ordinary MAX key fails because it wants a registered partner app. See the webhooks page.
Full route-by-route detail, including what each write demands of you, is on the endpoint reference.
If what you need is on that list
These are limits of the Housecall Pro API itself, not of any particular client. No MCP server reaches equipment records, the real age of a unit, or service plan data, and neither does Zapier, a spreadsheet export or a script you write yourself. There is no route to them on any key.
The hosted backend covers that ground. Your own agent connects to it the same way it connects to the free MCP, and the only thing that changes is what it can see: equipment in the book and how old each unit really is, a nameplate photo read and written back as an equipment record, a replacement pipeline ranked in dollars, and the visits a service plan customer paid for and never used.
Common questions
Does Housecall Pro have an API?
Yes. Housecall Pro offers a REST API on its MAX plan. An admin generates a key from My Apps, then Go to App store, then API Key Management, choosing Full access or Read-only. There is no self-serve developer dashboard and no app registration. OAuth credentials are issued only to official integration partners.
What is the Housecall Pro API base URL?
https://api.housecallpro.com, with no /v1 prefix. Adding a version prefix returns 404 on every route.
Does the Housecall Pro API use Bearer tokens?
No. The header is "Authorization: Token <key>", not Bearer. Sending Bearer fails authentication on every route.
Are Housecall Pro API money values in dollars or cents?
Integer cents, on every money field of every entity. A subtotal of 25000 means $250.00. Tax rates are basis points, so 8750 means 8.75%, and discounts arrive as negative cents. Writes take cents too.
Does the Housecall Pro API have rate limits?
Yes, but they are undocumented and no X-RateLimit headers are returned. The limit is sustained-volume rather than burst: a 25-request burst at roughly 4 requests per second drew no 429s, while a 507-request sweep at roughly 3.3 requests per second drew 429s on 12% of reads, all of which succeeded on retry. Any sweep longer than a few dozen calls needs retry-with-backoff.
Why does my Housecall Pro job status filter never match anything?
Because the filter vocabulary and the response vocabulary are different. You filter with work_status[]=completed, but responses say "complete rated" or "complete unrated". Testing work_status == "completed" matches zero rows in a real book, silently. Match on the "complete" prefix instead.
Is HCP the same as HashiCorp Cloud Platform?
No. On this site HCP means Housecall Pro, the field-service platform for home-service businesses. HashiCorp Cloud Platform is an unrelated product that shares the abbreviation.
You do not have to write any of this yourself
Our free open-source MCP handles every quirk on this page: cents in both directions, the status vocabulary, the date-window walk, deleted rows kept out of totals, and no blind retry on a failed write. It is free forever, and it is the fastest way to find out whether the API answers your question at all.
Email hello@hcpapi.com to report anything here that is wrong or has changed on your account.