Docs

The BroomDesk API

A small REST API over the records that matter: customers, jobs, visits and invoices. JSON in, JSON out, one key per integration. It is part of the Business plan, and you mint keys in Settings, API.

Authentication and the base URL

Every request carries an organization API key as a bearer token. A key is shown once, when it is created, and stored only as a SHA-256 hash, so a lost key is replaced rather than recovered. Send it on every request, over https, and never from a browser: a key is a password for your whole organization.

Base URL   https://broomdesk.com/api
Header     Authorization: Bearer bd_live_your_key_here

Scopes

A key carries read, or read and write. A write key always carries read as well, because an integration that creates a record needs to read it back. Calling an endpoint your key is not scoped for returns 403 with error.code = forbidden; the scope each endpoint needs is listed with it below.

Rate limit

120 requests per minute per organization, counted across every key you hold. Over the limit the answer is 429 with error.code = rate_limited. Minting a second key does not buy a second allowance; it buys a second name in your activity log.

The response envelope

Every body, success or failure, is a JSON object with a boolean ok. On success it carries data. On failure it carries error.code, from a closed vocabulary you can branch on, and error.message, which is written for the person reading the log. Branch on ok before touching anything else.

Success

{
  "ok": true,
  "data": {
    "id": "6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c",
    "first_name": "Dana"
  }
}

Failure

{
  "ok": false,
  "error": {
    "code": "not_found",
    "message": "No such resource."
  }
}

Pagination

List endpoints are paginated by cursor, not by page number. A page holds 50 records by default and 100 at most. Send the next_cursor you were given back as ?cursor= to get the next page; when it comes back null, you have everything.

The cursor is opaque: send it back exactly as received. Because it carries the sort position rather than an offset, a record created while you are walking cannot shift a page boundary and make you skip one. A cursor we did not issue is rejected with 400 rather than quietly serving page one, which would look to your code like the walk had finished.

{
  "ok": true,
  "data": {
    "items": [
      {
        "id": "6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c"
      }
    ],
    "next_cursor": "MjAyNi0wOC0xMVQxODowMjowMC4wMDBafDZiM2YwYTFj"
  }
}

Endpoints

Every endpoint is scoped to the organization the key belongs to. There is no organization id in any path: the key is the tenant.

Customers

People and companies you clean for.

GET/v1/customersread scope

Customers in the organization the API key belongs to, newest first.

Query parameters

limit
Page size. Defaults to 50, capped at 100.
cursor
Opaque cursor from a previous response's next_cursor. Results are ordered by created_at descending.
kind
Filter by residential or commercial.
updated_since
Only customers updated at or after this ISO 8601 timestamp.

Example request

curl "https://broomdesk.com/api/v1/customers?limit=25" \
  -H "Authorization: Bearer bd_live_your_key_here"

Example response (200)

{
  "ok": true,
  "data": {
    "items": [
      {
        "id": "6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c",
        "kind": "residential",
        "first_name": "Dana",
        "last_name": "Whitfield",
        "company_name": "Whitfield Offices",
        "email": "dana@example.com",
        "phone": "+15125550142",
        "source": "widget",
        "tags": [
          "referral"
        ],
        "notes": "Gate code is on file. Dog is friendly.",
        "balance_cents": 19485,
        "created_at": "2026-08-11T18:02:00.000Z",
        "updated_at": "2026-08-11T18:02:00.000Z"
      }
    ],
    "next_cursor": null
  }
}
POST/v1/customerswrite scope

Creates a customer with source `import`. Requires a key with the write scope.

Request body

Send at least one of first_name, last_name or company_name. Requires a key with the write scope.

{
  "kind": "residential",
  "first_name": "Dana",
  "last_name": "Whitfield",
  "company_name": "Whitfield Offices",
  "email": "dana@example.com",
  "phone": "+15125550142",
  "tags": [
    "referral"
  ],
  "notes": "Gate code is on file. Dog is friendly."
}

Example request

curl "https://broomdesk.com/api/v1/customers" \
  -X POST \
  -H "Authorization: Bearer bd_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"first_name":"Dana","last_name":"Whitfield","email":"dana@example.com","phone":"+15125550142"}'

Example response (201)

{
  "ok": true,
  "data": {
    "id": "6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c",
    "kind": "residential",
    "first_name": "Dana",
    "last_name": "Whitfield",
    "company_name": "Whitfield Offices",
    "email": "dana@example.com",
    "phone": "+15125550142",
    "source": "widget",
    "tags": [
      "referral"
    ],
    "notes": "Gate code is on file. Dog is friendly.",
    "balance_cents": 19485,
    "created_at": "2026-08-11T18:02:00.000Z",
    "updated_at": "2026-08-11T18:02:00.000Z"
  }
}
GET/v1/customers/{id}read scope

One customer by id.

Path parameters

id
The record id.

Example request

curl "https://broomdesk.com/api/v1/customers/6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c" \
  -H "Authorization: Bearer bd_live_your_key_here"

Example response (200)

{
  "ok": true,
  "data": {
    "id": "6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c",
    "kind": "residential",
    "first_name": "Dana",
    "last_name": "Whitfield",
    "company_name": "Whitfield Offices",
    "email": "dana@example.com",
    "phone": "+15125550142",
    "source": "widget",
    "tags": [
      "referral"
    ],
    "notes": "Gate code is on file. Dog is friendly.",
    "balance_cents": 19485,
    "created_at": "2026-08-11T18:02:00.000Z",
    "updated_at": "2026-08-11T18:02:00.000Z"
  }
}

Jobs

Recurring and one-time service agreements.

GET/v1/jobsread scope

Jobs in the organization, newest first.

Query parameters

limit
Page size. Defaults to 50, capped at 100.
cursor
Opaque cursor from a previous response's next_cursor. Results are ordered by created_at descending.
customer_id
Only jobs for this customer.
status
Filter by job status.

Example request

curl "https://broomdesk.com/api/v1/jobs?limit=25" \
  -H "Authorization: Bearer bd_live_your_key_here"

Example response (200)

{
  "ok": true,
  "data": {
    "items": [
      {
        "id": "6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c",
        "customer_id": "0d41c8a6-2f77-4f5e-9b0a-5c1e7a9d3b40",
        "property_id": "f5a2c1d7-8b64-4e02-9a3c-7d1b6e4f8a25",
        "service_type_id": "9c7e1b40-3d52-4a86-b1f0-6e8d2c5a7f31",
        "crew_id": "3a8d5f21-7c94-4b60-a2e5-9f1c0d6b48e7",
        "rrule": "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU",
        "dtstart": "2026-08-11T15:00:00.000Z",
        "until": "2026-12-31",
        "duration_min": 150,
        "price_cents": 18000,
        "frequency_label": "Every 2 weeks",
        "status": "active",
        "is_commercial": false,
        "billing_mode": "per_visit",
        "po_number": "PO-2291",
        "created_at": "2026-08-11T18:02:00.000Z",
        "updated_at": "2026-08-11T18:02:00.000Z"
      }
    ],
    "next_cursor": null
  }
}
GET/v1/jobs/{id}read scope

One job by id.

Path parameters

id
The record id.

Example request

curl "https://broomdesk.com/api/v1/jobs/6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c" \
  -H "Authorization: Bearer bd_live_your_key_here"

Example response (200)

{
  "ok": true,
  "data": {
    "id": "6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c",
    "customer_id": "0d41c8a6-2f77-4f5e-9b0a-5c1e7a9d3b40",
    "property_id": "f5a2c1d7-8b64-4e02-9a3c-7d1b6e4f8a25",
    "service_type_id": "9c7e1b40-3d52-4a86-b1f0-6e8d2c5a7f31",
    "crew_id": "3a8d5f21-7c94-4b60-a2e5-9f1c0d6b48e7",
    "rrule": "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU",
    "dtstart": "2026-08-11T15:00:00.000Z",
    "until": "2026-12-31",
    "duration_min": 150,
    "price_cents": 18000,
    "frequency_label": "Every 2 weeks",
    "status": "active",
    "is_commercial": false,
    "billing_mode": "per_visit",
    "po_number": "PO-2291",
    "created_at": "2026-08-11T18:02:00.000Z",
    "updated_at": "2026-08-11T18:02:00.000Z"
  }
}

Visits

Individual scheduled occurrences of a job.

GET/v1/visitsread scope

Visits in the organization, newest first.

Query parameters

limit
Page size. Defaults to 50, capped at 100.
cursor
Opaque cursor from a previous response's next_cursor. Results are ordered by created_at descending.
job_id
Only visits belonging to this job.
status
Filter by visit status.
start_after
Only visits scheduled to start at or after this ISO 8601 timestamp.
start_before
Only visits scheduled to start at or before this ISO 8601 timestamp.

Example request

curl "https://broomdesk.com/api/v1/visits?limit=25" \
  -H "Authorization: Bearer bd_live_your_key_here"

Example response (200)

{
  "ok": true,
  "data": {
    "items": [
      {
        "id": "6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c",
        "job_id": "b1c4d2e8-6a30-4f19-8d77-2e5f0a6c9b13",
        "sequence": 3,
        "scheduled_start": "2026-08-11T15:00:00.000Z",
        "scheduled_end": "2026-08-11T17:30:00.000Z",
        "status": "unconfirmed",
        "price_cents": 18000,
        "is_exception": false,
        "created_at": "2026-08-11T18:02:00.000Z",
        "updated_at": "2026-08-11T18:02:00.000Z"
      }
    ],
    "next_cursor": null
  }
}
GET/v1/visits/{id}read scope

One visit by id.

Path parameters

id
The record id.

Example request

curl "https://broomdesk.com/api/v1/visits/6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c" \
  -H "Authorization: Bearer bd_live_your_key_here"

Example response (200)

{
  "ok": true,
  "data": {
    "id": "6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c",
    "job_id": "b1c4d2e8-6a30-4f19-8d77-2e5f0a6c9b13",
    "sequence": 3,
    "scheduled_start": "2026-08-11T15:00:00.000Z",
    "scheduled_end": "2026-08-11T17:30:00.000Z",
    "status": "unconfirmed",
    "price_cents": 18000,
    "is_exception": false,
    "created_at": "2026-08-11T18:02:00.000Z",
    "updated_at": "2026-08-11T18:02:00.000Z"
  }
}

Invoices

Billing documents and their line items.

GET/v1/invoicesread scope

Invoices in the organization, newest first.

Query parameters

limit
Page size. Defaults to 50, capped at 100.
cursor
Opaque cursor from a previous response's next_cursor. Results are ordered by created_at descending.
customer_id
Only invoices for this customer.
status
Filter by invoice status.

Example request

curl "https://broomdesk.com/api/v1/invoices?limit=25" \
  -H "Authorization: Bearer bd_live_your_key_here"

Example response (200)

{
  "ok": true,
  "data": {
    "items": [
      {
        "id": "6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c",
        "customer_id": "0d41c8a6-2f77-4f5e-9b0a-5c1e7a9d3b40",
        "number": 1042,
        "status": "draft",
        "line_items": [
          {
            "label": "Deep clean, three bedrooms",
            "amount_cents": 18000
          }
        ],
        "subtotal_cents": 18000,
        "tax_cents": 1485,
        "tip_cents": 0,
        "total_cents": 19485,
        "amount_paid_cents": 0,
        "due_at": "2026-08-18T04:59:00.000Z",
        "sent_at": "2026-08-11T18:02:00.000Z",
        "visit_ids": [
          "c2e7a940-5b13-4d86-8f02-1a6d3c9e7b54"
        ],
        "created_at": "2026-08-11T18:02:00.000Z",
        "updated_at": "2026-08-11T18:02:00.000Z"
      }
    ],
    "next_cursor": null
  }
}
GET/v1/invoices/{id}read scope

One invoice by id, including its line items.

Path parameters

id
The record id.

Example request

curl "https://broomdesk.com/api/v1/invoices/6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c" \
  -H "Authorization: Bearer bd_live_your_key_here"

Example response (200)

{
  "ok": true,
  "data": {
    "id": "6b3f0a1c-9e5d-4a2b-8c7f-1d2e3f4a5b6c",
    "customer_id": "0d41c8a6-2f77-4f5e-9b0a-5c1e7a9d3b40",
    "number": 1042,
    "status": "draft",
    "line_items": [
      {
        "label": "Deep clean, three bedrooms",
        "amount_cents": 18000
      }
    ],
    "subtotal_cents": 18000,
    "tax_cents": 1485,
    "tip_cents": 0,
    "total_cents": 19485,
    "amount_paid_cents": 0,
    "due_at": "2026-08-18T04:59:00.000Z",
    "sent_at": "2026-08-11T18:02:00.000Z",
    "visit_ids": [
      "c2e7a940-5b13-4d86-8f02-1a6d3c9e7b54"
    ],
    "created_at": "2026-08-11T18:02:00.000Z",
    "updated_at": "2026-08-11T18:02:00.000Z"
  }
}

Error codes

Branch on error.code. The HTTP status and the code always agree, and the message may be reworded without warning, so never match on it.

StatusCodeWhen
400invalid_requestThe request was malformed: bad query parameter, cursor or body.
401unauthorizedMissing, malformed, unknown or revoked API key.
403forbiddenThe key is valid but lacks the scope this operation requires.
404not_foundNo such route, or no such record in this organization.
429rate_limitedRate limit exceeded: 120 requests per minute per organization.
500server_errorSomething failed on our side. Nothing was partially applied.

A missing key, a malformed key, an unknown key and a revoked key all return the same 401 and the same message. That is deliberate: a different answer for each would turn the endpoint into a way of testing whether a key exists.

OpenAPI document

The machine readable description of everything on this page is served unauthenticated, so you can generate a client before you have a key. It is built from the same route table the API dispatches on, which is why it cannot describe an endpoint that does not exist.

curl https://broomdesk.com/api/v1/openapi.json

Open the OpenAPI document

Something missing, or an endpoint you need that is not here? Tell us what you are building and we will tell you honestly whether it is on the way.