Skip to main content

Services

The Services module covers paid workshop services a customer drops their own gear off for (e.g. an edge tune, a boot fit). A service booking is a booking with type: service whose line items are workshop jobs. Each job moves through a drop-off lifecycle:

Awaiting drop-off → In workshop → Ready for pickup → Picked up

A service can also be booked for a time with a person (booking_mode: time), such as a fitting or a lesson-style session. The customer picks a start time, and optionally who does it. These jobs carry start_at, end_at and staff instead of a drop-off day. Use Offered times to find free times, then send start_at on the booking line.

Key business rules (identical to the portal, POS and widget - the API calls the same code):

  • Capacity is per location. A service's daily_cap_quota is the cap per location per day, not a global total.
  • Not assumed dropped off. A new service booking starts awaiting drop-off - the SLA clock starts when you mark the job dropped_off.
  • Ready-by respects opening hours. ready_by = the service's sla_hours from the effective drop-off (the drop-off moment if within rental opening hours, otherwise the next opening), in the supplier's timezone.

Authentication & scopes​

ScopeGrants
services:readView the service catalog and service jobs
services:writeCreate service bookings, update job status and move appointments
settings:writeSet up services (create and update)

List services (catalog)​

GET /v1/services

Requires services:read.

Query parameters​

ParameterTypeDescription
location_idintegerOnly services offered at this location: ones assigned to it, plus ones with no locations set (offered at every location).
datedateInclude per-day, per-location remaining availability (availability object) for this drop-off date (YYYY-MM-DD).
activebooleanFilter by active status.
searchstringMatch the service name.
page, per_pageintegerPagination (max 100).
sortstringname, base_price, sort_order, created_at (prefix - for desc; default sort_order).

Response​

{
"data": [
{
"id": 12,
"name": "Edge tune & hot wax",
"description": "Sharpen edges, fill base, hot wax & buff.",
"base_price": 49.95,
"sla_hours": 24,
"requires_dropoff": true,
"daily_cap_quota": 8,
"booking_mode": "dropoff",
"duration_minutes": null,
"gap_after_minutes": null,
"max_at_once": null,
"min_notice_minutes": null,
"max_days_ahead": null,
"staff": [],
"is_active": true,
"sort_order": 0,
"location_ids": [2, 3],
"availability": { "daily_cap": 8, "available": 5, "is_full": false, "unlimited": false },
"created_at": "2026-06-01T09:00:00+00:00",
"updated_at": "2026-06-01T09:00:00+00:00"
}
],
"meta": { "current_page": 1, "per_page": 25, "total": 1, "total_pages": 1, "request_id": "...", "timestamp": "..." },
"links": { "first": "...", "prev": null, "next": null, "last": "..." }
}

booking_mode is dropoff (the customer leaves gear with you) or time (booked for a time with a person). The fields below only apply to time services:

FieldDescription
duration_minutesHow long one appointment takes. Times are offered in steps of this length.
gap_after_minutesTime kept free after each appointment before the same person starts the next.
max_at_onceMost appointments of this service that can run at the same time. null = no limit beyond who is free.
min_notice_minutesShortest notice before a start time.
max_days_aheadHow far ahead a time can be booked. null = no limit.
staffWho does it, as { "id", "name" }. Empty = anyone on the team.

location_ids lists the locations the service is offered at. Empty means every location. availability is only present when date is supplied.


Get a service​

GET /v1/services/{id}

Requires services:read. Returns a single catalog service in the same shape as the list data item.


Create or update a service​

POST  /v1/services
PATCH /v1/services/{id}

Requires settings:write. Uses the same checks as Settings > Services in the app. On PATCH, only the fields you send change.

FieldTypeDescription
namestringRequired on create.
descriptionstringOptional.
base_pricenumberRequired on create.
booking_modestringdropoff (default) or time.
sla_hoursintegerTurnaround for a drop-off service.
daily_cap_quotaintegerMost per day, per location.
duration_minutesintegerRequired when booking_mode is time. 5 to 720, in steps of 5.
gap_after_minutesinteger0 to 240, in steps of 5.
max_at_onceinteger1 to 100.
min_notice_minutesinteger0 to 43200.
max_days_aheadinteger0 to 730.
staff_idsinteger[]Who does it. Each id must be on your team. Send [] to let anyone do it.
location_idsinteger[]Where it is offered.
is_activebooleanDefault true on create.
curl -X POST "https://app.equipdash.com/api/v1/services" \
-H "Authorization: Bearer eqd_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Boot fitting",
"base_price": 45,
"booking_mode": "time",
"duration_minutes": 45,
"gap_after_minutes": 15,
"staff_ids": [14, 15]
}'

Returns 201 (create) or 200 (update) with the service. A rule that fails returns 422 with error.code: validation_error and the field in error.details. A service that is not yours returns 404.


Offered times​

GET /v1/services/{id}/times?date=YYYY-MM-DD

Requires services:read. Lists the start times a time service is offered on one day, in your shop's timezone, with the people free at each. This is the same list the booking page, POS and Dash show. A time is offered when someone who does the service is free (not on time off, another appointment or a departure they lead, and rostered or inside the hours they gave), the shop is open for the whole appointment, no closure covers it, and the notice, days-ahead, at-most-at-once and daily limits allow it.

ParameterTypeRequiredDescription
datedateyesThe day, in your shop's timezone (YYYY-MM-DD).
staff_idintegernoOnly times this person is free. They must do this service.
location_idintegernoApply that location's closures.
ignore_job_idintegernoWhen changing an appointment, its id: its current time counts as free. Must be an appointment for this service.

Response​

{
"data": {
"service_id": 31,
"date": "2026-06-15",
"timezone": "America/Denver",
"duration_minutes": 60,
"staff_id": null,
"times": [
{
"start_at": "2026-06-15T09:00:00-06:00",
"end_at": "2026-06-15T10:00:00-06:00",
"time": "09:00",
"label": "9:00 AM",
"staff": [
{ "id": 14, "name": "Jane Doe" },
{ "id": 15, "name": "Alex Smith" }
]
}
]
},
"meta": { "request_id": "...", "timestamp": "..." }
}

An empty times list means nothing is free that day (or the shop is closed). A drop-off service returns 422 with error.code: not_time_booked. A missing or badly formatted date, or a staff_id who does not do the service, returns 422. A service that is not yours returns 404.


Create a service booking​

POST /v1/services/bookings

Requires services:write. Creates a standalone service booking with one or more service lines. Each line becomes a workshop job (awaiting drop-off, unless a same-day in-person drop-off applies).

Body​

FieldTypeRequiredDescription
customer_idintegeryesThe customer.
servicesarrayyesOne or more service lines.
services[].service_type_idintegeryesThe service to book.
services[].dropoff_datedatedrop-off servicesDrop-off date (YYYY-MM-DD).
services[].start_atstringtime servicesThe start time, from Offered times. ISO 8601 with an offset (2026-06-15T09:00:00-06:00), or YYYY-MM-DD HH:MM, read in your shop's timezone.
services[].staff_idintegernoWho does it, for a time service. Must be on your team and do this service. Leave it out for anyone: the person the customer saw last if free, otherwise whoever has the least booked that day.
services[].pricenumbernoOverride price (defaults to the service base price).
pickup_location_idintegernoDrop-off/pickup location (multi-site suppliers). Used for per-location capacity.
statusstringnodraft or confirmed (default confirmed).
notesstringnoInternal note.

Request example​

curl -X POST "https://app.equipdash.com/api/v1/services/bookings" \
-H "Authorization: Bearer eqd_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"customer_id": 501,
"pickup_location_id": 2,
"services": [
{ "service_type_id": 12, "dropoff_date": "2026-06-15", "price": 49.95 }
]
}'

Response 201​

Returns the created booking (standard booking shape) with a services array of jobs:

{
"data": {
"id": 9001,
"type": "service",
"status": "confirmed",
"services": [
{
"id": 4502,
"booking_id": 9001,
"service_type_id": 12,
"service_name": "Edge tune & hot wax",
"lifecycle_state": "awaiting_dropoff",
"drop_off_date": "2026-06-15T00:00:00+00:00",
"ready_by": "2026-06-16T09:00:00+00:00",
"price": 49.95
}
]
},
"meta": { "request_id": "...", "timestamp": "..." }
}

A time line comes back with start_at, end_at and staff:

{
"id": 4510,
"service_type_id": 31,
"service_name": "Boot fitting",
"is_appointment": true,
"start_at": "2026-06-15T09:00:00-06:00",
"end_at": "2026-06-15T10:00:00-06:00",
"staff": { "id": 14, "name": "Jane Doe" }
}

The time is checked again when the booking is saved. If it was taken in the meantime, the request returns 422 and nothing is booked. A staff_id that is not on your team returns 422 with "Pick a person from your team."

If the chosen day is full at that location, the request returns 422 with error.code: service_booking_failed (or the underlying validation error).


List service jobs​

GET /v1/services/jobs

Requires services:read. Lists customer service jobs (the workshop jobs tied to service bookings).

ParameterTypeDescription
location_idintegerOnly jobs whose booking is at this location.
datedateOnly jobs with this drop-off date.
statestringawaiting_dropoff, in_workshop, ready, or picked_up.
page, per_pageintegerPagination.

Each item is a job object (same shape as the services[] entry above, plus customer_id, customer_name, dropped_off_at, ready_at, picked_up_at, location_id). Every job carries is_appointment; for a time service it also has start_at, end_at (ISO 8601 in your shop's timezone) and staff ({ "id", "name" }, or null when nobody is assigned). These are null on drop-off jobs.


Update a job's status (lifecycle)​

POST /v1/services/jobs/{id}/dropped-off
POST /v1/services/jobs/{id}/ready
POST /v1/services/jobs/{id}/picked-up

Requires services:write. Advances a single job:

  • dropped-off - the customer handed the gear over. Starts the SLA clock and recomputes ready_by respecting opening hours. Moves the job to in_workshop.
  • ready - work is finished; the job is ready for pickup. Triggers the customer "ready for pickup" email.
  • picked-up - the customer collected. When every job on the booking is picked up, the booking moves to Picked up.

Response​

{
"data": {
"id": 4502,
"lifecycle_state": "in_workshop",
"ready_by": "2026-06-16T09:00:00+00:00",
"dropped_off_at": "2026-06-15T15:00:00+00:00"
},
"meta": { "request_id": "...", "timestamp": "..." }
}

Move an appointment​

POST /v1/services/jobs/{id}/move

Requires services:write. A cancelled booking or a finished appointment is refused. Moves a time job to a new start time and, optionally, a new person. The same checks as booking it apply, and the job's own current time does not count against it, so its old time is free again once it moves.

FieldTypeRequiredDescription
start_atstringyesThe new start, as on a booking line.
staff_idintegernoThe new person. Leave it out to keep the current person if they are free, otherwise anyone free.
curl -X POST "https://app.equipdash.com/api/v1/services/jobs/4510/move" \
-H "Authorization: Bearer eqd_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "start_at": "2026-06-15T14:00:00-06:00" }'

Returns the job with its new start_at, end_at and staff. A time that is not free, or a drop-off job, returns 422 with error.code: appointment_not_moved and a message saying why. A job that is not yours returns 404.


MCP tools​

The same operations are available to AI agents over MCP:

ToolScopePurpose
list_servicesservices:readList the catalog + per-location availability
create_service_bookingservices:writeBook a service for a customer
update_service_job_statusservices:writeAdvance a job (dropped_off / ready / picked_up)