The Slots API provides endpoints for managing time slots that professionals can make available for booking. Slots support conflict detection to prevent overlapping reservations for the same professional.
Two slots for the same professional conflict when their time ranges overlap.
The overlap check uses a half-open interval model: [startTime, endTime).
| Scenario | Conflict? |
|---|---|
| Identical range | ✅ Yes |
| New slot starts inside existing | ✅ Yes |
| New slot ends inside existing | ✅ Yes |
| New slot fully wraps existing | ✅ Yes |
| New slot fully inside existing | ✅ Yes |
New slot starts exactly when existing ends (end == start) |
❌ No (adjacent) |
| New slot ends exactly when existing starts | ❌ No (adjacent) |
| No time overlap at all | ❌ No |
| Same time range, different professional | ❌ No |
When a conflict is detected, the API returns HTTP 409 Conflict:
{
"success": false,
"code": "CONFLICT",
"message": "Slot overlaps with an existing reservation for this professional",
"error": "Slot overlaps with an existing reservation for this professional",
"timestamp": "2026-01-01T00:00:00.000Z"
}Conflict prevention is enforced at two layers:
-
Service layer (
SlotService.createSlot/updateSlot)
Checks for conflicts in-memory before writing. Returns a fast409without a DB round-trip on the happy path. -
Database layer (migration
003_add_slot_conflict_exclusion)
A PostgreSQLEXCLUDE USING gistconstraint on theslotstable prevents overlapping rows from being inserted even under concurrent requests that race past the service-layer check.ALTER TABLE slots ADD CONSTRAINT excl_slots_no_overlap EXCLUDE USING gist ( professional_id WITH =, tstzrange(start_time, end_time) WITH && );
The
btree_gistextension is required to mix an equality operator (=) with a range operator (&&) in a single exclusion constraint.
- The service layer check is not a substitute for the DB constraint. Under concurrent load, two requests can both pass the service check before either commits. The DB constraint is the authoritative last line of defence.
- The DB constraint fires at statement time (
DEFERRABLE INITIALLY IMMEDIATE), which is the safest default. It cannot be deferred by client code. - Callers that receive a
409should not retry automatically — the conflict is deterministic and will not resolve without a change to the existing slot.
Lists all available slots. Results are served from Redis cache when available (TTL controlled by REDIS_SLOT_TTL_SECONDS env var, default 60s). The X-Cache response header indicates whether the response was a cache HIT or MISS.
Authentication
- Optional:
x-chronopay-user-idandx-chronopay-roleheaders for authenticated access
Responses
| Status | Code | Message | Condition |
|---|---|---|---|
200 OK |
- | Slots retrieved successfully | Request succeeded |
401 Unauthorized |
UNAUTHORIZED |
Authentication required | Missing auth headers (if required) |
403 Forbidden |
FORBIDDEN |
Role is not authorized for this action | Invalid role |
Success response
{
"slots": [
{
"id": 1,
"professional": "alice",
"startTime": 1704067200000,
"endTime": 1704070800000,
"createdAt": "2024-01-01T00:00:00.000Z"
}
]
}Response headers
X-Cache:HITorMISS- indicates cache status
Creates a new slot. Requires API key authentication for service access.
Authentication
- Required:
x-api-keyheader
Rate limiting
- Protected by auth-aware rate limiter
Payload limit
- Maximum payload size:
32kb
Idempotency
- Protected by idempotency middleware to prevent duplicate creations
Request body
| Field | Type | Required | Description |
|---|---|---|---|
professional |
string | ✅ | Professional identifier |
startTime |
string or number | ✅ | Start time (ISO-8601 string or Unix timestamp in ms) |
endTime |
string or number | ✅ | End time (ISO-8601 string or Unix timestamp in ms, must be > startTime) |
Responses
| Status | Code | Message | Condition |
|---|---|---|---|
201 Created |
- | Slot created successfully | Slot created |
400 Bad Request |
BAD_REQUEST |
Missing required fields | Missing professional, startTime, or endTime |
400 Bad Request |
BAD_REQUEST |
endTime must be greater than startTime | Invalid time range |
400 Bad Request |
BAD_REQUEST |
Slot validation error | Service layer validation failed |
401 Unauthorized |
UNAUTHORIZED |
Missing API key | Missing x-api-key header |
403 Forbidden |
FORBIDDEN |
Invalid API key | Invalid x-api-key |
409 Conflict |
CONFLICT |
Slot overlaps with an existing reservation | Conflict detected |
413 Payload Too Large |
PAYLOAD_TOO_LARGE |
Request body exceeds the 32kb limit | Payload exceeds 32kb |
429 Too Many Requests |
TOO_MANY_REQUESTS |
Rate limit exceeded | Too many requests |
500 Internal Server Error |
INTERNAL_ERROR |
Slot creation failed | Unexpected error |
Success response
{
"success": true,
"slot": {
"id": 1,
"professional": "alice",
"startTime": 1704067200000,
"endTime": 1704070800000,
"createdAt": "2024-01-01T00:00:00.000Z"
},
"meta": {
"invalidatedKeys": ["slots:all", "slots:list:all"]
}
}Returns a single slot by ID. Attempts to read from cache first, then falls back to data store.
Authentication
- Optional:
x-chronopay-user-idandx-chronopay-roleheaders for authenticated access
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
integer | ✅ | Slot ID (path parameter) |
Responses
| Status | Code | Message | Condition |
|---|---|---|---|
200 OK |
- | Slot found | Slot retrieved successfully |
400 Bad Request |
BAD_REQUEST |
Invalid slot id | ID is not a positive integer |
401 Unauthorized |
UNAUTHORIZED |
Authentication required | Missing auth headers (if required) |
403 Forbidden |
FORBIDDEN |
Role is not authorized for this action | Invalid role |
404 Not Found |
NOT_FOUND |
Slot not found | Slot does not exist |
Success response
{
"slot": {
"id": 1,
"professional": "alice",
"startTime": 1704067200000,
"endTime": 1704070800000,
"createdAt": "2024-01-01T00:00:00.000Z"
}
}Response headers
X-Cache:HITorMISS- indicates cache status
Updates an existing slot. Requires admin token authentication.
Authentication
- Required:
x-chronopay-admin-tokenheader
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
integer | ✅ | Slot ID (path parameter) |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
professional |
string | ❌ | New professional identifier |
startTime |
string or number | ❌ | New start time |
endTime |
string or number | ❌ | New end time |
At least one field must be provided.
Responses
| Status | Code | Message | Condition |
|---|---|---|---|
200 OK |
- | Slot updated | Update successful |
400 Bad Request |
BAD_REQUEST |
slotId must be a positive integer | Invalid ID format |
400 Bad Request |
BAD_REQUEST |
Missing required header: x-chronopay-admin-token | Missing admin token |
400 Bad Request |
BAD_REQUEST |
update payload must include at least one field | No fields provided |
400 Bad Request |
BAD_REQUEST |
Slot validation error | Service layer validation failed |
401 Unauthorized |
UNAUTHORIZED |
Missing required header: x-chronopay-admin-token | Missing admin token |
403 Forbidden |
FORBIDDEN |
Invalid admin token | Invalid admin token |
404 Not Found |
NOT_FOUND |
Slot {id} was not found | Slot does not exist |
409 Conflict |
CONFLICT |
Slot overlaps with an existing reservation | Updated range conflicts |
503 Service Unavailable |
SERVICE_UNAVAILABLE |
Update slot authorization is not configured | Admin token not configured |
500 Internal Server Error |
INTERNAL_ERROR |
Slot update failed | Unexpected error |
Success response
{
"success": true,
"slot": {
"id": 1,
"professional": "alice",
"startTime": 1704067200000,
"endTime": 1704070800000,
"createdAt": "2024-01-01T00:00:00.000Z"
}
}Deletes an existing slot. Requires owner or admin authentication.
Authentication
- Required:
x-user-idandx-roleheaders - Allowed roles:
admin(can delete any slot) - Owners: Professionals can delete their own slots
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
integer | ✅ | Slot ID (path parameter) |
Responses
| Status | Code | Message | Condition |
|---|---|---|---|
200 OK |
- | Slot deleted | Deletion successful |
400 Bad Request |
BAD_REQUEST |
Invalid slot id | ID is not a positive integer |
400 Bad Request |
BAD_REQUEST |
Caller identity is required | Missing x-user-id and x-role |
401 Unauthorized |
UNAUTHORIZED |
Authentication required | Missing auth headers |
403 Forbidden |
FORBIDDEN |
Access denied | Not owner or admin |
404 Not Found |
NOT_FOUND |
Slot not found | Slot does not exist |
Success response
{
"success": true,
"deletedSlotId": 1
}- List endpoint (
GET /api/v1/slots) is cached in Redis - Individual slot retrieval (
GET /api/v1/slots/:id) attempts cache first - Cache is invalidated on POST, PATCH, and DELETE operations
- Cache TTL is controlled by
REDIS_SLOT_TTL_SECONDSenvironment variable (default: 60s)
- POST endpoint requires API key authentication
- PATCH endpoint requires admin token
- DELETE endpoint requires authentication and authorization (owner or admin)
- Payload size is limited to 32kb to reduce abuse surface
- Rate limiting is applied to prevent abuse
- Conflict detection prevents overlapping slots for the same professional
- Self-booking is prevented in booking intents (professionals cannot book their own slots)