\ developers
4Reserve API
A REST API over your own business's bookings, plus webhooks that call you when a booking changes. Requests and responses are JSON. All times are UTC in ISO 8601.
Authentication
Create a token in Dashboard → Settings → API & webhooks and send it as a bearer token. A token belongs to one business and sees only that business.
curl http://localhost:8000/api/v1/bookings \
-H "Authorization: Bearer 4r_your_token" \
-H "Accept: application/json"
Limit: 120 requests per minute. API access must be included in the business's plan, otherwise calls answer 403 plan_required.
Endpoints
| GET | /business |
The business the token belongs to: name, slug, timezone, booking URL. |
| GET | /services |
All services with duration and price. |
| GET | /staff |
All team members. |
| GET | /customers |
Customers, newest first. Query: search, page, per_page (max 100). |
| GET | /availability |
Free start times for one day. Query: service_id, date (YYYY-MM-DD), staff_id (optional). |
| GET | /bookings |
Bookings by start time. Query: from, to, status, page, per_page. |
| GET | /bookings/{id} |
One booking. |
| POST | /bookings |
Create a booking. Same availability and plan rules as the booking page. |
| POST | /bookings/{id}/confirm |
pending → confirmed. |
| POST | /bookings/{id}/cancel |
pending or confirmed → cancelled. |
| POST | /bookings/{id}/complete |
pending or confirmed → completed. |
| POST | /bookings/{id}/no-show |
pending or confirmed → no_show. |
Create a booking
A start_at without a UTC offset is read in the business's timezone. Give the customer a phone, an email, or both.
curl -X POST http://localhost:8000/api/v1/bookings \
-H "Authorization: Bearer 4r_your_token" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"service_id": "0198c0de-…",
"staff_id": null,
"start_at": "2026-10-05T15:00:00+04:00",
"notes": "First visit",
"customer": { "name": "Ani Petrosyan", "phone": "+37499000000" }
}'
Answers 201 with the booking, 422 for invalid input, or 409 booking_rejected when the time is not free or the plan's monthly limit is reached.
The booking object
{
"id": "0198c0de-…",
"reference": "4R-48213",
"status": "pending", // pending | confirmed | completed | cancelled | no_show
"source": "api", // direct | onereserve | widget | telegram | manual | api
"start_at": "2026-10-05T11:00:00+00:00",
"end_at": "2026-10-05T12:00:00+00:00",
"amount": null, // set when the visit is completed
"notes": "First visit",
"service": { "id": "…", "name": "Haircut" },
"staff": { "id": "…", "name": "Aram" },
"customer": { "id": "…", "name": "Ani Petrosyan", "phone": "+37499000000", "email": null },
"created_at": "2026-09-30T09:12:44+00:00"
}
Lists are wrapped as { "data": [...], "meta": { "current_page", "per_page", "total" } }; single objects as { "data": {...} }.
Errors
{ "error": { "code": "not_found", "message": "Booking not found." } }
Codes: unauthenticated (401), plan_required (403), not_found (404), booking_rejected, invalid_status, verification_pending (409). Validation failures answer 422 with Laravel's standard errors object.
Webhooks
Add an HTTPS endpoint in the same settings page and choose its events: booking.created, booking.confirmed, booking.rescheduled, booking.cancelled, booking.completed, booking.no_show. We POST this body:
{
"id": "0198c0e1-…", // delivery id, same as the X-4Reserve-Delivery header
"event": "booking.confirmed",
"created_at": "2026-09-30T09:15:02+00:00",
"business_id": "…",
"data": { "booking": { …the booking object… } }
}
Answer with any 2xx within 10 seconds. Anything else is retried after 10 s, 1 min, 5 min and 15 min, then marked failed. Deliveries can arrive more than once and out of order — use the delivery id to ignore repeats.
Verify the signature
Every request carries X-4Reserve-Signature: sha256=… — the HMAC-SHA256 of the raw request body, keyed with the endpoint's signing secret. Compare it before trusting the payload:
$expected = 'sha256=' . hash_hmac('sha256', file_get_contents('php://input'), $signingSecret);
if (! hash_equals($expected, $_SERVER['HTTP_X_4RESERVE_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}