GET /appointments/availability
Query bookable appointment slots for a given rooftop, route, and date. Returns the times a HopDrive driver could be at the consumer's pickup address — the times you can offer the consumer for their appointment.
Use this endpoint to populate a time-picker UI in your SLT (Service Lane Tool) or to surface availability via your scheduling integration before creating an appointment with POST /appointments.
Example Request
Authorization: Bearer <jwt>
Query Params
For each leg of the route (pickup and delivery), provide either an address string or a HopDrive location ID — whichever you have. Pass an address string and HopDrive resolves (and, when necessary, creates) the underlying location for you, the same way the move-creation endpoints accept addresses. Pass a location ID — e.g. one you got from GET /locations — and that leg is used as-is with no address lookup. The two legs are independent: you can supply a location ID for one and an address for the other.
| Field | Type | Required? | Description |
|---|---|---|---|
| customer_id | Number | Required | HopDrive dealer account ID (the rooftop). Must match an authorized rooftop on your bearer token. |
| pickup_address | String | Per leg† | The consumer's pickup address as a single string (e.g. 210 S Mulberry St, Richmond, VA 23220). |
| pickup_location_id | Number | Per leg† | HopDrive location ID for the pickup. Used as-is; takes precedence over pickup_address when both are sent. |
| delivery_address | String | Per leg† | The dealership service address as a single string. |
| delivery_location_id | Number | Per leg† | HopDrive location ID for the dealership service address. Used as-is; takes precedence over delivery_address. |
| date | String | Required | UTC calendar date in YYYY-MM-DD format. Slots are computed for the local timezone of the rooftop. |
| consumer_action | String | Optional | The scenario variant: concierge-pu, concierge-pu-no-rideshare, concierge-ret, concierge-ret-no-rideshare, concierge-loaner-pu, or concierge-loaner-ret. |
† For each leg you must supply one of its two inputs: pickup_address or pickup_location_id for the pickup, and delivery_address or delivery_location_id for the delivery. If both are sent for a leg the location ID is used and the address is ignored. If neither is sent for a leg, the request is rejected with REQUEST_INVALID.
The scheduling strategy and customer tier are not caller-configurable — both are derived from the rooftop's record on each request.
Example Response
{
"rooftop_timezone": "America/Chicago",
"eligible": true,
"reason": null,
"enforcement_mode": "forced",
"offset_increment_minutes": 15,
"offset_max_minutes": 15,
"slots": [
{
"pickup_start": "2026-05-14T13:00:00.000Z",
"appointment_time": "2026-05-14T13:20:00.000Z",
"return_end": "2026-05-14T13:45:00.000Z",
"available_drivers": 2,
"earliest_appointment_time": "2026-05-14T13:05:00.000Z",
"latest_appointment_time": "2026-05-14T13:35:00.000Z"
},
{
"pickup_start": "2026-05-14T13:30:00.000Z",
"appointment_time": "2026-05-14T13:50:00.000Z",
"return_end": "2026-05-14T14:15:00.000Z",
"available_drivers": 1,
"earliest_appointment_time": "2026-05-14T13:45:00.000Z",
"latest_appointment_time": "2026-05-14T14:05:00.000Z"
}
]
}
Response Fields
| Field | Type | Description |
|---|---|---|
| rooftop_timezone | String | IANA timezone of the rooftop. Use this to render slot times in the dealership's local clock without a second round-trip. |
| eligible | Boolean | true when bookable slots are returned. false for ineligible responses (see error responses below). |
| reason | String | null on eligible responses. On ineligible responses, a stable machine-readable reason code (APPOINTMENT_MANAGEMENT_DISABLED, etc.). |
| slots | Array | Bookable slot windows, sorted by appointment_time ascending. Empty array when eligible: false. |
| enforcement_mode | String | forced when the rooftop requires booked times to land inside an offered slot window (writes are validated, see below). suggestion when offered times are advisory and any appointment_time is accepted. Mirrors the setting that locks the time field in the dealer portal. |
| offset_increment_minutes | Number | The step size the dealer portal's adjustment arrows use. Advisory for API callers: any time inside a slot's window is accepted, aligned to this step or not. |
| offset_max_minutes | Number | The rooftop's configured cap on shifting a slot from its suggested time. 0 means slots are take-it-or-leave-it. Per-slot windows below already apply this cap. |
| slots[].pickup_start | String | ISO 8601 UTC — when the driver leaves the dealer for the consumer's pickup address. |
| slots[].appointment_time | String | ISO 8601 UTC — when the driver arrives at the consumer's pickup address. The customer-facing appointment anchor. |
| slots[].return_end | String | ISO 8601 UTC — when the vehicle is back at the dealer. Anchors when service can begin. |
| slots[].available_drivers | Number | Count of distinct drivers (or hypothetical gig-labor rows) that could fulfill this slot. Always ≥ 1. Filter on this to require redundancy. |
| slots[].earliest_appointment_time | String | ISO 8601 UTC — the earliest time this slot may be booked at. Starts at appointment_time - offset_max_minutes and shrinks when a neighboring booking, business hours, or a driver's shift leaves less room. Equals appointment_time when offsets are configured off. |
| slots[].latest_appointment_time | String | ISO 8601 UTC — the latest time this slot may be booked at. Same rules as earliest_appointment_time, in the other direction. |
Booking against a slot
When enforcement_mode is forced, POST /appointments (and PUT/PATCH
carrying an appointment_time) re-checks availability at write time and
rejects with 400 request_invalid unless the time falls inside some slot's
[earliest_appointment_time, latest_appointment_time], bounds inclusive. The
re-check means a slot another caller booked after your GET stops validating,
so handle the 400 by re-querying availability. When enforcement_mode is
suggestion, writes accept any appointment_time, exactly like dealership
staff typing a custom time in the portal.
Error Responses
| HTTP | Code | Cause |
|---|---|---|
| 400 | REQUEST_INVALID | A required query parameter is missing, malformed, or has the wrong type, or a leg is missing both its _address and _location_id. |
| 400 | ADDRESS_INVALID | An _address could not be geocoded, or matched more than one saved location. The error object's reason says which leg and which of the two. |
| 401 | ACCESS_DENIED | Missing / invalid bearer token, OR the bearer token does not authorize the requested customer_id. |
| 404 | REQUEST_INVALID | customer_id does not match an existing rooftop. |
| 406 | OUTSIDE_REGION | The addresses are valid but fall outside the rooftop's service area. Body carries reason: "OUT_OF_SERVICE_AREA" and out_of_service_area. |
| 406 | REQUEST_INVALID | Rooftop is not bookable (appointment management disabled, rooftop closed for the date, no drivers configured). See reason in body. |
| 500 | UNKNOWN | Unexpected server error. |
| 502 | UNKNOWN | Drive-time computation against Google Routes failed or timed out. Retry shortly. |
Ineligible Response Reasons
When HTTP 406 is returned, the response body's reason field carries a stable code:
| Reason | Meaning |
|---|---|
| APPOINTMENT_MANAGEMENT_DISABLED | The rooftop has not enabled appointment management. Contact HopDrive support to enable. |
| CLOSED:<closure-name> | The rooftop is closed for date (e.g., a holiday, PTO, or recurring weekly closure). The closure name follows the colon. |
| NO_DRIVERS_CONFIGURED | The rooftop's driver count is 0. Contact HopDrive support to provision driver capacity. |
| OUT_OF_SERVICE_AREA | An address is real but too far from the rooftop. The out_of_service_area object names the leg, its straight-line distance, and the rooftop's radius. |
Telling a bad address from an unserved one
These are separate outcomes, and the reason field distinguishes them without
parsing prose.
An address HopDrive cannot place on a map returns 400 with code
address_invalid and a reason of PICKUP_ADDRESS_INVALID or
DELIVERY_ADDRESS_INVALID. Correct the address and retry. When an address
matches several saved locations for the rooftop the reason is
PICKUP_ADDRESS_AMBIGUOUS / DELIVERY_ADDRESS_AMBIGUOUS, and a more specific
address (or the _location_id) resolves it.
{
"errors": [
{
"type": "validation_error",
"code": "address_invalid",
"message": "pickup_address could not be geocoded — check the address is real and complete",
"reason": "PICKUP_ADDRESS_INVALID",
"doc_url": "https://api.hopdrive.com/docs/error-codes#address_invalid"
}
]
}
An address that geocodes fine but sits beyond the rooftop's serviceable radius
returns 406 with reason: "OUT_OF_SERVICE_AREA". The address is correct;
HopDrive just doesn't drive that far for this rooftop. Distance is
straight-line from the rooftop, the same measure the eligibility endpoint uses,
and the radius comes from the rooftop's serviceable_radius_miles setting.
Rooftops that leave that setting empty never return this reason.
{
"rooftop_timezone": "America/Los_Angeles",
"eligible": false,
"reason": "OUT_OF_SERVICE_AREA",
"enforcement_mode": "forced",
"offset_increment_minutes": 10,
"offset_max_minutes": 45,
"out_of_service_area": {
"leg": "pickup",
"distance_miles": 74.6,
"serviceable_radius_miles": 25
},
"slots": [],
"errors": [
{
"type": "api_error",
"code": "outside_region",
"message": "The pickup address is 74.6 miles from this rooftop, outside its 25-mile service area.",
"reason": "OUT_OF_SERVICE_AREA",
"doc_url": "https://api.hopdrive.com/docs/error-codes#outside_region"
}
]
}
Neither of these looks like a rooftop with no capacity left. A day with every
slot taken returns 200, eligible: true, and an empty slots array.
Worked Examples
Cold lane (first call for this consumer pickup ↔ dealer pair)
The very first availability query for a new (pickup, dealer) tuple triggers an address-resolution step (geocoding + location lookup/creation) plus a Google Routes call inline to compute drive times. Expect ~200–500 ms response time on cold calls.
GET /v1/appointments/availability?customer_id=1234&pickup_address=210 S Mulberry St, Richmond, VA 23220&delivery_address=11161 Research Plaza Way, Richmond, VA 23236&date=2026-05-14
Response is identical in shape to the warm case; the resolved locations and drive times are now cached for subsequent calls. Re-sending the same address strings resolves to the same locations without re-creating them.
Warm lane (subsequent calls for the same pair)
After the first call, drive times are cached in HopDrive's lane table. Subsequent availability queries against the same (pickup, dealer) pair are sub-100 ms — they read the cached drive time and run only the slot-packing algorithm.
Disabled rooftop
{
"rooftop_timezone": "America/Chicago",
"eligible": false,
"reason": "APPOINTMENT_MANAGEMENT_DISABLED",
"slots": [],
"errors": [
{
"code": "ADDRESS_INVALID",
"message": "APPOINTMENT_MANAGEMENT_DISABLED"
}
]
}
The rooftop hasn't enabled appointment management. Coordinate with HopDrive support.
Rooftop closed for the date
{
"rooftop_timezone": "America/Chicago",
"eligible": false,
"reason": "CLOSED:Memorial Day",
"slots": [],
"errors": [
{
"code": "ADDRESS_INVALID",
"message": "CLOSED:Memorial Day"
}
]
}
The rooftop is closed for date due to the named closure. Suggest a different date to the consumer.
Saturated day (all slots booked)
A fully-booked rooftop returns an empty slots array but eligible: true:
{
"rooftop_timezone": "America/Chicago",
"eligible": true,
"reason": null,
"slots": []
}
This signals "the rooftop is bookable in principle, but no slots are available on this date." Different from eligible: false, which means the rooftop can't be booked at all.
Notes
- All timestamps are UTC ISO 8601. Use
rooftop_timezoneto render in the dealership's local clock. - Slot rounding is fixed at 5-minute intervals.
available_driversis informational. The endpoint does not reserve drivers — that happens at POST /appointments. A slot that's available now may not be at booking time.- Drive times are computed against Google Routes in traffic-unaware mode for consistency across calls.
- Lane / route data is cached indefinitely once computed. Road-network changes are rare enough that this is acceptable; contact HopDrive support if you suspect stale routing.