Developer quickstart
Pulse Connect is a JSON REST API for server-side integrations, CLI tools, MCP servers and approved AI applications.
| Setting | Value |
|---|---|
| Contract | connect-v1 (version 1.0.0-pilot, see the changelog) |
| Base URL | https://api.staging.maidcentral.ai/api/connect/v1 (staging). External pilot partners use staging only; production access isn't offered yet. |
| Authentication | X-API-Key header; keys start with pc_ |
| Methods | GET for reads; POST and PATCH for writes; DELETE only for webhook subscriptions |
| OpenAPI | Browse the API reference or download the OpenAPI document. Its servers list staging first (the default for generated clients), then production. |
Before you start
- Build and run on staging only. During the external pilot you connect to staging, a copy of production that MaidCentral refreshes periodically. Production access isn't offered yet.
- Get a staging key from MaidCentral. MaidCentral enables the API for your branch on staging and issues the key (key limits). Every staging refresh wipes keys and webhook subscriptions, so ask for a new key and re-create your subscriptions after one. The key is shown once: keep it in a secret manager, never in source control, URLs, browser code, logs, prompts or tickets.
- Treat staging data as real. It is a copy of the branch's own data: create a test customer for your write tests, and don't book, move, skip or cancel on real customers until the business owner approves it.
export PULSE_CONNECT_KEY='pc_REPLACE_WITH_ONE_TIME_SECRET'
export PULSE_CONNECT_BASE='https://api.staging.maidcentral.ai/api/connect/v1'
Verify the connection
curl --fail-with-body -H "X-API-Key: $PULSE_CONNECT_KEY" "$PULSE_CONNECT_BASE/me"
curl --fail-with-body -H "X-API-Key: $PULSE_CONNECT_KEY" "$PULSE_CONNECT_BASE/capabilities"
/me shows the branch, key, granted scopes and contract version. /capabilities shows which families are on for the branch. Both are free.
Read data
curl --fail-with-body -H "X-API-Key: $PULSE_CONNECT_KEY" \
--get --data-urlencode "[email protected]" "$PULSE_CONNECT_BASE/customers/lookup"
curl --fail-with-body -H "X-API-Key: $PULSE_CONNECT_KEY" \
"$PULSE_CONNECT_BASE/customers?page_size=100"
curl --fail-with-body -H "X-API-Key: $PULSE_CONNECT_KEY" \
"$PULSE_CONNECT_BASE/jobs?start_date=2026-01-01&end_date=2026-03-31&page_size=100"
The integration recipes show full call sequences for booking, lead capture, sync and assistants.
Scopes
Grant a key only the scopes its integration needs. The label is what the administrator ticks in Pulse.
| Scope | Label in Pulse | Allows |
|---|---|---|
usage.read | Usage | GET /usage |
customers.read | Customers (read) | Customer list, lookup and detail; also needed with the second scope on a customer's homes, service sets and billing |
tags.read | Tags (read) | GET /tags: the branch's customer tags |
homes.read | Homes (read) | Homes |
services.read | Services (read) | Service sets |
jobs.read | Jobs (read) | Jobs, including /customers/{id}/jobs |
teams.read | Teams (read) | GET /teams |
technicians.read | Technicians (read) | Technicians |
zones.read | Zones (read) | GET /zones |
availability.read | Availability (read) | GET /availability |
billing.read | Billing (read) | Invoices, payments, balances (with customers.read for per-customer routes) |
leads.read | Leads (read) | Leads |
quotes.read | Quotes (read) | Quotes |
pricing.read | Booking configuration and pricing (read) | GET /quotes/config, POST /quotes/calculate |
time.read | Time (read) | Time summaries |
reports.read | Reports (read) | The seven reports |
scorecards.read | Scorecards (read) | Customer ratings |
customers.write | Customers (write) | Create and update customers, and add or remove their tags |
homes.write | Homes (write) | Add and update homes |
leads.write | Leads (write) | Create and update leads |
quotes.write | Quotes (write) | Save and revise quotes |
bookings.write | Bookings (write) | Book a saved quote |
jobs.change | Jobs (change) | Move, skip or cancel a visit, and list the move and skip reasons |
services.cancel | Services (cancel) | Cancel a recurring service from a date, and list its Cancel Service reasons |
payments.write | Payments (write) | Mint a save-card link |
customer_notes.write | Customer notes (write) | Customer notes |
quote_notes.write | Quote notes (write) | Quote notes |
technician_notes.write | Technician notes (write) | Technician notes |
webhooks.manage | Webhooks (manage) | Every /webhooks and /webhook-events route |
No scope grants another: quotes.write doesn't grant quotes.read, and customers.write doesn't grant customer notes. Subscribing to a webhook event also needs that event's read scope.
Conventions
These rules apply to every operation. The API guide covers what is specific to each one.
Ids
Resource ids are strings of digits; send them back as strings. Webhook, event and delivery ids, and payment-link ids, are not numeric. A few query parameters take an integer or a list instead (zone_id on /availability, comma-separated team_ids); the API reference shows each. An id from another branch answers 404 resource_not_found, exactly like an unknown one.
Time zones
| Kind | Format | Examples | Zone |
|---|---|---|---|
| Instant | ISO 8601 with Z | created_at, updated_at, scheduled_start_at, booked_date, paid_date, expires_at, generated_at, webhook occurred_at | UTC |
| Date | YYYY-MM-DD | job_date, first_job_date, issued_date, due_date, work_date; date filters such as start_date, created_from, updated_since | Branch |
| Local time | HH:mm or HH:mm:ss | Availability windows, move scheduled_start_at (YYYY-MM-DDTHH:mm), booking arrival_window_start, report times, job.scheduled scheduled_start_time | Branch |
"Branch" is the time zone GET /branches reports. Availability, job actions, booking, time summaries, invoice dates and the operational reports use the branch's scheduling day, which is the same zone except for a branch whose zone is set to UTC: that branch schedules in US Eastern while GET /branches still reports Etc/UTC.
Pagination
- Collections return
data,has_moreandnext_cursor.page_sizeis 1–100, default 50. - While
has_moreis true, sendnext_cursorunchanged with the same route and filters. - Cursors are confidential, bound to the key's branch, the operation and the filters, and expire 24 hours after the traversal's first page. An expired or altered cursor is
400 invalid_cursor: restart from the first page. Never store a cursor as a sync checkpoint: store the branch-local date of each run and send it asupdated_sinceon the next (Incremental sync). - Order is stable within one traversal; no permanent sort order is promised.
- The per-customer billing lists page by number (
page), not cursor.
Idempotency
Every write needs an Idempotency-Key header: 1–128 letters, digits, ., _ or -, generated by you and kept with the change it belongs to. A write without one is 400 idempotency_key_required.
- The same key, method, route and body within 24 hours returns the first response unchanged, with
Pulse-Connect-Idempotent-Replay: true. Nothing runs again and nothing counts toward usage. - The same key on the same route with a different body is
409 idempotency_conflict. The same key on a different route is a new write. - A refused write stores no receipt, so retrying a refused request runs it again.
- An API key runs one write at a time, across every write route, webhook management included. A write that waits more than two seconds is
503 service_unavailable; retry it with the sameIdempotency-Key.
Concurrency
Every customer, home, lead and quote PATCH, and booking, sends expected_updated_at, the record's updated_at from your last read (job actions call it version). A record changed since is 409 version_conflict and nothing is written: read it again and reapply.
Limits
- A request body is at most 64 KB (
413 request_too_large). - A response over the size limit in
/capabilitiesis413 result_too_large: use a smaller page or a narrower range.
Rate limits
| Class | Operations | Per minute | Per hour |
|---|---|---|---|
| Context | /me, /branches, /capabilities, /usage | 60 | 1,000 |
| Read | Customers, homes, service sets, jobs, leads, quotes, per-customer billing, teams, technicians, zones, time summaries, scorecards, /quotes/config, /quotes/calculate, the reason lists | 120 | 2,000 |
| Report | The seven reports, /invoices, /payments (one shared bucket) | 10 | 100 |
| Availability | /availability | 30 | 600 |
| Write | Every create, update, booking, note, job action, service cancel and payment link | 60 | 600 |
| Webhook admin | /webhooks and /webhook-events routes | 30 | 300 |
Each class is counted per key and per branch, per minute and per hour; whichever limit is reached first applies. All keys on a branch share the branch limits. Rate limits are separate from the monthly allowance.
Each class is a separate bucket, so reaching the limit in one class, such as a nightly invoice export, doesn't use up another class's allowance, such as availability for a booking flow.
Every request with an API key, including a refused one, also counts toward a ceiling of 120 per minute for that key, across all classes. Give bulk jobs their own key so they can't reach the ceiling your booking flow's key depends on. A separate ceiling of 600 requests per minute applies to each client IP address; an IPv6 address shares it with every address in its /64.
Errors
Errors use application/problem+json with error_code and correlation_id. Every authenticated response carries Pulse-Connect-Operation-Id; send it and your X-Correlation-Id (optional, 1–128 letters, digits, ., _ or -) to support, never the key or response body.
| Status | error_code | Action |
|---|---|---|
| 400 | invalid_request | Fix the field named in detail. |
| 400 | invalid_cursor | Restart from the first page. |
| 400 | idempotency_key_required | Send a valid Idempotency-Key. |
| 401 | invalid_credentials | Check the key is complete, active and unexpired. |
| 403 | insufficient_scope, capability_not_enabled | Add the scope, or ask MaidCentral to enable the family. |
| 404 | resource_not_found | Unknown or another branch's id. |
| 409 | context_not_configured | The branch is missing a currency or time zone the operation needs. Contact MaidCentral; nothing was changed. |
| 409 | version_conflict, idempotency_conflict, duplicate_customer | Re-read; use a new key for a different request; look the customer up. |
| 409 | Other codes | Operation-specific; see the API guide. |
| 413 | result_too_large, request_too_large | Smaller page, narrower range, or smaller body. |
| 429 | rate_limited | Wait for Retry-After, then back off with jitter. |
| 503 | service_unavailable | Retry after Retry-After. When it is absent, back off exponentially. |
Rotate a key
Keys can't be edited. Ask MaidCentral for the replacement (the branch needs a free key slot; it can hold up to 10 active keys), verify /me with it, switch your client over, then ask MaidCentral to revoke the old one. Revoking pauses the webhook subscriptions the old key was checked against; resume them with the new key.