Grout

REST API

Connect websites, automations, and internal tools to Grout.

Authentication

Create keys under Admin → API. Send the key with every request. Prefer the bearer header:

Authorization: Bearer grout_live_...
# or
x-api-key: grout_live_...

Keys are stored as SHA-256 hashes and can be revoked or rotated. Each key has scopes. A request without its required scope returns insufficient_scope and identifies the missing scope. Grant only the scopes an integration needs and use a separate key for each integration.

Errors

Every failure uses one shape, so you only write one error handler:

{
  "error": {
    "code": "invalid_request",
    "message": "Some fields are missing or invalid.",
    "fieldErrors": { "address.zip": "Must be a 5-digit US zip code" },
    "requestId": "0f4c…"
  }
}

code is stable and machine-readable; message is safe to show a person; fieldErrors maps a field to what is wrong with it. The requestId is also returned as an x-request-id header — quote it if you need help.

Pagination

List endpoints use cursor pagination. Pass the nextCursor from the previous response.

GET /api/v1/bookings?limit=50
{ "data": [  ], "hasMore": true, "nextCursor": "eyJ2IjoiMjAy…" }

GET /api/v1/bookings?limit=50&cursor=eyJ2IjoiMjAy…

Most list endpoints accept updatedSince for incremental polling.

Idempotency

Writes that change money or the calendar require an Idempotency-Key header. This includes creating a booking or quote, acting on a quote, cancelling or rescheduling a booking, and recording or reversing a payment. Retrying the same body with the same key returns the original response. Reusing the key with a different body returns 409.

curl -X POST /api/v1/bookings \
  -H "Authorization: Bearer grout_live_..." \
  -H "Idempotency-Key: 7f3c9a1e-..." \
  -H "Content-Type: application/json" \
  -d '{ ... }'

Rate limits

Every response includes x-ratelimit-limit, x-ratelimit-remaining, and x-ratelimit-reset. Exceeding the limit returns 429 with retry-after.

CORS

The six unauthenticated /api/public/* endpoints (catalog, quote, availability/slots, book, chat, waitlist) — the ones the React SDK calls — answer cross-origin browser requests from any origin. Every response, success or error, carries Access-Control-Allow-Origin: *, and each route answers an OPTIONS preflight. No credentials (Access-Control-Allow-Credentials) are ever sent or accepted, since these endpoints read no cookie or session.

/api/v1 has no CORS on purpose. It authenticates with a secret key (Authorization: Bearer grout_live_...) that must stay server-side — call it from your backend, not a browser.

Conventions

Money values use integer cents and rates use basis points. Times use ISO 8601. Bookings include a trackingUrl for the customer's public tracking page.

Endpoints

MethodPathDescriptionScope
GET/api/v1/servicesBookable catalog: industries → services → parameters/extras, frequencies, and active fee definitionscatalog:read
POST/api/v1/quotePrice a selection (server-authoritative; validates coupons and applies date, ZIP, and service fee rules)catalog:read
GET/api/v1/availabilityOpen start times for a service/date/zip, drive-time rankedcatalog:read
GET/api/v1/bookingsList bookings — status, customer, provider, date window, updatedSincebookings:read
POST/api/v1/bookingsCreate a booking, optionally recurring, multi-day, or several dates at oncebookings:write
GET/api/v1/bookings/:idFetch by id or human code (BB-xxxxx)bookings:read
POST/api/v1/bookings/:id/cancelCancel one visit, this-and-future, or end a plan after a datebookings:write
POST/api/v1/bookings/:id/rescheduleMove to a new slot, validated against real availability. Office-grade: not subject to the customer self-service reschedule cutoff, though an inside-cutoff move is noted in the activity logbookings:write
GET/api/v1/customersList/search customers with churn risk & lifetime statscustomers:read
POST/api/v1/customersCreate a customer, or return the existing one for that emailcustomers:write
GET/api/v1/leadsList enquiries by stage, owner, or recent changeleads:read
POST/api/v1/leadsCapture an enquiry — folds into an open lead with matching contactleads:write
GET/api/v1/leads/:idOne lead with its full timelineleads:read
PATCH/api/v1/leads/:idMove stage, reassign, or set a follow-up dateleads:write
GET/api/v1/quotesList quotes by status, lead, or customerquotes:read
POST/api/v1/quotesCreate a quote, optionally sending it immediatelyquotes:write
GET/api/v1/quotes/:idOne quote and its scheduled follow-upsquotes:read
POST/api/v1/quotes/:idsend · accept · reject · convert · pause · resumequotes:write
GET/api/v1/paymentsThe ledger, filtered by customer, booking, or datepayments:read
POST/api/v1/paymentsRecord money received outside Groutpayments:write
PATCH/api/v1/paymentsReverse an entry (entries are never edited)payments:write
GET/api/v1/providersActive roster with skills and service areasproviders:read
GET/api/v1/webhooksYour webhook endpointswebhooks:manage
POST/api/v1/webhooksRegister an endpoint — the secret is returned oncewebhooks:manage
PATCH/api/v1/webhooks/:idChange the URL, subscription, or active statewebhooks:manage
DELETE/api/v1/webhooks/:idStop delivering to an endpointwebhooks:manage

An extra in the catalog carries frequencySlugs — the plans it is offered on, an empty list meaning all of them — alongside discountExempt and firstVisitOnly. Read it before you build a selection: quoting or booking an extra on a frequency outside its list is refused with unprocessable, naming the extra, rather than priced with the line quietly dropped.

Example: create a booking

curl -X POST "https://booking.example.com/api/v1/bookings" \
  -H "Authorization: Bearer grout_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "serviceId": "service_id_from_catalog",
    "frequencySlug": "one_time",
    "startIso": "2026-08-10T09:00:00-05:00",
    "customer": {"name": "Jane Doe", "email": "jane@example.com", "phone": "555-0100"},
    "address": {"line1": "1 Main St", "city": "New York", "state": "NY", "zip": "10001"}
  }'

The machine-readable OpenAPI 3.1 specification lives at /api/openapi.json. You can also use the API playground.

On this page