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:

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.

FieldRaw valueMeans
subtotal25000$250.00
taxes[].rate87508.75% (basis points)
a discount-25000a $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.

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

ResponseWhat it actually means
HTML 404 pageThe 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.

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

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.

Structural surprises

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:

PatternResult
25 requests at ~4/secondNo 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:

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.