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
| Method | Path | Description | Scope |
|---|---|---|---|
| GET | /api/v1/services | Bookable catalog: industries → services → parameters/extras, frequencies, and active fee definitions | catalog:read |
| POST | /api/v1/quote | Price a selection (server-authoritative; validates coupons and applies date, ZIP, and service fee rules) | catalog:read |
| GET | /api/v1/availability | Open start times for a service/date/zip, drive-time ranked | catalog:read |
| GET | /api/v1/bookings | List bookings — status, customer, provider, date window, updatedSince | bookings:read |
| POST | /api/v1/bookings | Create a booking, optionally recurring, multi-day, or several dates at once | bookings:write |
| GET | /api/v1/bookings/:id | Fetch by id or human code (BB-xxxxx) | bookings:read |
| POST | /api/v1/bookings/:id/cancel | Cancel one visit, this-and-future, or end a plan after a date | bookings:write |
| POST | /api/v1/bookings/:id/reschedule | Move 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 log | bookings:write |
| GET | /api/v1/customers | List/search customers with churn risk & lifetime stats | customers:read |
| POST | /api/v1/customers | Create a customer, or return the existing one for that email | customers:write |
| GET | /api/v1/leads | List enquiries by stage, owner, or recent change | leads:read |
| POST | /api/v1/leads | Capture an enquiry — folds into an open lead with matching contact | leads:write |
| GET | /api/v1/leads/:id | One lead with its full timeline | leads:read |
| PATCH | /api/v1/leads/:id | Move stage, reassign, or set a follow-up date | leads:write |
| GET | /api/v1/quotes | List quotes by status, lead, or customer | quotes:read |
| POST | /api/v1/quotes | Create a quote, optionally sending it immediately | quotes:write |
| GET | /api/v1/quotes/:id | One quote and its scheduled follow-ups | quotes:read |
| POST | /api/v1/quotes/:id | send · accept · reject · convert · pause · resume | quotes:write |
| GET | /api/v1/payments | The ledger, filtered by customer, booking, or date | payments:read |
| POST | /api/v1/payments | Record money received outside Grout | payments:write |
| PATCH | /api/v1/payments | Reverse an entry (entries are never edited) | payments:write |
| GET | /api/v1/providers | Active roster with skills and service areas | providers:read |
| GET | /api/v1/webhooks | Your webhook endpoints | webhooks:manage |
| POST | /api/v1/webhooks | Register an endpoint — the secret is returned once | webhooks:manage |
| PATCH | /api/v1/webhooks/:id | Change the URL, subscription, or active state | webhooks:manage |
| DELETE | /api/v1/webhooks/:id | Stop delivering to an endpoint | webhooks: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.