Houseler API

A REST API for reading and creating customers, bookings, and invoices in your Houseler account. It is the same API that powers the Houseler apps and the Houseler integration for Zapier.

Every request is scoped to the single company its API key belongs to. There is no cross-company access, and no endpoint accepts a company id as a parameter.

Base URL

https://houseler.com

All paths below are relative to this host. Requests and responses are JSON; send Content-Type: application/json on any request with a body.

Authentication

Authenticate with a per-company API key, sent in the X-Api-Key header:

curl https://houseler.com/api/zapier/me \
  -H "X-Api-Key: YOUR_API_KEY"

To create a key, sign in to Houseler and open Settings → Integrations. The key is shown once, at creation — store it somewhere safe, because it cannot be retrieved again. You can issue as many keys as you need and revoke any of them at any time.

A key carries the full access of the account that created it. Treat it like a password: keep it server-side, never commit it, and never put it in a browser or mobile client. If a key leaks, revoke it in Settings → Integrations — revocation takes effect immediately, and revoking a key does not affect your ability to sign in.

A missing, unknown, or revoked key returns 401 Unauthorized on every endpoint.

Conventions

Money
Always integer cents, never a decimal. $150.00 is 15000. Endpoints that return money also expose a matching *Dollars string ("150.00") where noted.
Phone numbers
US numbers, normalised to E.164 on write. Both +15125550147 and 5125550147 are accepted; anything that is not a valid 10-digit US number is rejected with a 400. Phone is unique per company and is how Houseler recognises a returning customer.
Dates and times
Sent and returned as ISO 8601 strings. Timestamps are UTC; supply an offset on any time you write so it is unambiguous.
Deletions
Records are soft-deleted. Deleted rows never appear in any response documented here.
Pagination
Where supported, via page (zero-based) and pageSize. The polling feeds are intentionally not paginated — they return the newest 50 records.

Reading data

GET/api/zapier/me

The company the API key belongs to.

Returns { companyId, companyName, brand }. Use it to verify a key is valid and to label a connection. Returns 401 if the key is missing, unknown, or revoked.

GET/api/zapier/customers

Customers, newest first.

Ordered by createdAt descending, capped at 50, soft-deleted rows excluded. Each row includes the customer’s current address. Built for polling: the ordering key is immutable, so page 1 is stable and rows can be de-duplicated on id.

GET/api/zapier/bookings

Bookings, newest first.

Ordered by createdAt descending, capped at 50. Recurring series are returned once as stored, not expanded into individual occurrences. Includes the linked customer, service, and team member.

GET/api/zapier/invoices

Paid invoices, newest first.

Ordered by paidAt descending — the moment the invoice was paid, not when it was created — so a back-dated invoice paid today still appears first. Money fields are integer cents; each also has a *Dollars string mirror (total / totalDollars) so you can map straight into accounting tools without a conversion step.

GET/api/customer

List customers, or look one up by phone.

Without parameters, returns a page of customers (page, pageSize; pageSize defaults to 50). With ?phone=<number>, returns the single matching customer, or 404 if there is none — phone is unique per company, so this is an exact-match lookup rather than a search.

GET/api/product

The services this company offers.

Each row has an id and a name, plus its configured prices.

Picker options

Compact id-and-label lists, intended to populate a dropdown in an integration UI rather than to be consumed as data.

GET/api/zapier/options/customers

Customer picker options.

Returns [{ id, name }] where name is "First Last — phone", 100 per page via ?page=. Phone is included because it is the disambiguator when two customers share a first name.

GET/api/zapier/options/appointments

Booking picker options.

Returns [{ id, name, start }]. The label is rendered in the company’s own timezone, so it reads as the operator scheduled it rather than in UTC.

GET/api/zapier/options/employees

Team member picker options.

Returns [{ id, name, role }].

Creating records

POST/api/customer

Create a customer.

Required: firstName, phone. Optional: lastName, email, addressLine1, addressLine2, city, state, zip, timezone, repeatCustomer.

There is no single name field. Because phone is unique per company, posting a number that already exists returns 409 Conflict rather than creating a duplicate — look the customer up first if you want find-or-create behaviour.

curl -X POST https://houseler.com/api/customer \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"firstName":"Dana","lastName":"Whitfield","phone":"+13105550162"}'
POST/api/appointment

Create a booking.

Required: start and end (start must be before end), plus either customerId or both customerPhone and customerFirstName. Passing phone and first name lets Houseler match an existing customer by phone, or create one if there is no match.

Optional: title, notes, productId, employeeId, customerLastName, and a nested address object. Bookings cannot overlap an existing one, so a taken slot returns 409 Conflict.

curl -X POST https://houseler.com/api/appointment \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "start": "2026-09-10T16:00:00.000Z",
    "end":   "2026-09-10T17:30:00.000Z",
    "customerPhone": "+13105550162",
    "customerFirstName": "Dana",
    "title": "Full detail"
  }'
POST/api/invoice

Create a draft invoice.

Required: customerId, and lineItems with at least one entry. Each line item needs a name and a unitPrice, and may include quantity (defaults to 1) and description.

Optional: notes, taxRate, dueDate, appointmentId. Omit taxRate to use the company default; send 0 for no tax. New invoices are created with status Draft.

unitPrice is in cents. A $150.00 line item is 15000, not 150. Sending dollars will under-bill by a factor of 100.
curl -X POST https://houseler.com/api/invoice \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "CUSTOMER_ID",
    "lineItems": [
      { "name": "Full detail", "unitPrice": 15000, "quantity": 1 }
    ]
  }'

Errors

Errors return the appropriate status with a JSON body:

{ "error": "Customer not found", "code": "NOT_FOUND" }

Validation failures add a details object naming the fields at fault.

StatusMeaning
400The request body failed validation.
401The API key is missing, unknown, or revoked.
403The key is valid but not permitted to do this.
404No matching record in this company.
409The write conflicts with an existing record — a duplicate phone number, or an overlapping booking.
500Something failed on our side. Safe to retry.

Using Zapier instead

If you would rather not write code, the Houseler integration for Zapier is built on these endpoints and covers the common cases: triggers for new customers, new bookings, and paid invoices, and actions to create customers, bookings, and invoices. It authenticates with the same API key.

Triggers poll for changes every few minutes rather than firing instantly. More about the Zapier integration.

Questions

Email hello@houseler.com. Include the endpoint and the time of the request and we can look it up.