\ 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;
}