Skip to main content

Integration recipes

Each recipe lists the scopes to request, the calls in order, and what trips integrations up. External pilot partners run them against staging only; production access isn't offered yet. The rules behind each call are in the API guide; shared conventions are in the quickstart.

Website booking​

A booking page that prices a clean, saves the quote, creates the customer and books the first visit.

Scopes: pricing.read, quotes.write, quotes.read, customers.write, customers.read, homes.write, zones.read, availability.read, bookings.write. Add payments.write to collect a card.

  1. GET /quotes/config once, and cache it. Build the form from it, and use only its ids. Offer the frequencies with bookable: true, and offer customer_sources as the "How did you hear about us?" choice.
  2. POST /quotes/calculate as the customer chooses. Free, saves nothing.
  3. POST /quotes with the contact details, the chosen pricing and the chosen source as source_name. Keep the returned id and updated_at.
  4. PATCH /quotes/{id} for each change the customer makes, always with the latest updated_at.
  5. Create the customer with POST /customers. On 409 duplicate_customer, find the existing one with GET /customers/lookup?query=<email or phone> and use it.
  6. POST /customers/{customer_id}/homes with the service address. To check the postal code first, GET /zones?include_zips=true and match it: a postal code in no zone is refused. Keep the home's zone_id.
  7. GET /availability with zone_id from the home and duration_hours = the quote's pricing.first_visit_minutes ÷ 60, rounded up to two decimals (0.5 to 12).
  8. POST /quotes/{id}/book with expected_updated_at (the quote's updated_at), the customer_id and home_id from steps 5 and 6, and the chosen slot's date, a start inside its window as arrival_window_start, and team_id.
  9. Optional: POST /quotes/{id}/payment-links after booking, and send the url to the customer yourself.

Gotchas.

  • A frequency with bookable: false can be quoted but not booked (409 quote_not_bookable). Leave it off a self-booking form.
  • Availability isn't held. On 409 slot_unavailable, read availability again and let the customer choose again.
  • A saved price expires (expires_at). On 409 quote_expired, recalculate and PATCH the quote, then book with the new updated_at.
  • Mint the payment link after booking: a link minted earlier saves the card to the prospect, not the customer.
  • Use a new Idempotency-Key for each distinct request (every quote revision gets its own), and reuse a key only to retry that exact request.

Lead capture and CRM sync​

Send web-form and ad leads into Pulse, then keep a CRM in step.

Scopes: leads.write, leads.read, quotes.read, customers.read, webhooks.manage. Add customers.write to push contact changes back.

  1. POST /leads with contact details, postal code, medium, and first-touch attribution (UTM tags, gclid, fbclid). Attribution can only be set here.
  2. Subscribe to lead.created, lead.converted and customer.created, customer.status_changed.
  3. On lead.converted, read GET /customers/{data.customer_id} and link it to the CRM contact.
  4. PATCH /leads/{id} or PATCH /customers/{id} for contact changes, with the latest updated_at.

Gotchas.

  • Lead writes run no duplicate check. Call GET /customers/lookup before POST /leads if you don't want a lead for an existing customer.
  • lead.created also fires for leads from Pulse's own forms and widget, and for your own POST /leads; deduplicate on the lead id.
  • A converted lead leaves /leads and answers 404 there; read it from /quotes.
  • Email and phone can be replaced but not cleared.
  • Webhooks don't cover changes made in MaidCentral V2 (the classic app). Poll GET /leads and GET /customers with updated_since=<day of last run> to catch them, and reconcile with a periodic full read for what it can't report.

Nightly BI export​

Load Pulse data into a warehouse.

Scopes: customers.read, homes.read, services.read, jobs.read, billing.read, time.read, reports.read, usage.read.

  1. Customers, homes and service sets: page through each list with updated_since set to the day of your last run. The first run omits it.
  2. Jobs: GET /jobs over a rolling window (for example 30 days back to 60 ahead), with the required start_date and end_date, plus updated_since. Load history once in ranges of up to 366 days.
  3. Invoices and payments: GET /invoices and GET /payments with updated_since set to the day of your last run.
  4. Time: GET /time-summaries/daily and /jobs in ranges of up to 31 days.
  5. Reports: one call per report per run; they don't page.
  6. Check GET /usage after the run.

Gotchas.

  • Deleted records simply stop appearing, and updated_since doesn't report them or a few other changes (Incremental sync). Run a full read periodically, for example weekly, and compare.
  • Don't store cursors between runs; they expire after 24 hours.
  • /invoices, /payments and the reports share the report rate class: 10 calls a minute. Pace them.
  • Each page counts one operation, so use page_size=100.
  • Read data_available on every report before loading its numbers.

AI assistant lookup​

An assistant that answers questions about a customer.

Use the MCP server; it signs in with Pulse and needs no key, and its tools cover the steps below. Build your own with an API key only when you need a custom assistant.

Scopes: customers.read, homes.read, services.read, jobs.read. Add billing.read for balances, quotes.read for open quotes.

  1. GET /customers/lookup?query=… with an email, phone or name; up to 20 matches.
  2. GET /customers/{id}, /customers/{id}/homes, /customers/{id}/service-sets.
  3. GET /customers/{id}/jobs?start_date=…&end_date=… for recent and upcoming visits.
  4. GET /customers/{id}/billing for the balance.

Gotchas.

  • Phone lookup needs the digits exactly as stored, at least 7 of them. Try without a country code first.
  • Give the assistant a read-only key. Never put the key in a prompt.
  • Each call counts one operation; cache within a conversation.