Book365 API · v1

Вашият сайт. Един календар.

Свържете сайта си с календарите и резервациите на Book365 чрез фирмени API ключове.

Покажете свободните си часове

Вградете календар в сайта си, без да разкривате данни за клиенти или тайни API ключове.

Приемайте резервации

Създавайте резервации от своя сайт с услуги с определена продължителност и защита от дублиране.

Синхронизирайте платформите

Четете промените и отмените на резервации чрез API.

On customer booking pages, selecting a date refreshes free times automatically using the existing browser GET route. It preserves customer fields and blocks stale times while loading. Selecting a service automatically reloads its options; Show availability remains available without JavaScript. These reads do not create reservations or change the API contract.

Browse by city

Public discovery accepts an optional city query value from the website city selector, for example /en/explore?city=bg-sofia. The choice applies to categories, service providers and search, and persists in the browser session. Use city=all to clear it. Company owners/managers maintain the city and country under business location details; address-only businesses remain in All cities until updated. This browser preference does not filter direct booking URLs, company calendars, API requests or embeds.

Native iOS and Android account screens use the first-party endpoints below.

Book365 mobile API v1

Base URL: https://book365.cloud/api/mobile/v1. This first-party API belongs to Book365's standalone website. Gateway does not proxy it. Never ship company API keys, Firebase service accounts, payment secrets or Google OAuth secrets in an app. Deploy the website revision before distributing the connected mobile build.

Send Accept: application/json. Writes accept JSON. All routes accept lang as a query parameter; inactive/unknown languages fall back to the saved website default. Responses include Content-Language and Cache-Control: private, no-store. The bootstrap language list and translations are resolved from current admin settings, including saved translation overrides; no config-cache clear is needed. city is an optional city key from bootstrap (for example bg-sofia); empty means all cities. A nonexistent city returns no providers. Prices are EUR decimal strings. Reservation times use UTC ISO 8601; the UI labels device-local display.

Public discovery

Method and path Parameters Response data
GET /bootstrap lang version:1, locale, languages map of code to name, translations map of text to translated text, cities:[{id,name}], pages:[{slug,title,url}], features:{maps,apple_login,google_login,messages,push}
GET /categories optional q (max 120), city (max 500) Array of {slug,name,group,description,icon,tone,services,providers}
GET /businesses optional configured category, q (max 120), city, page (1–10000) Array of public businesses; meta:{page,has_more}, 20 per page
GET /businesses/{business} business UUID Public business plus description,email,phone,payment_options,services
GET /pages/{page} configured page slug Published localized {title,summary,body}; 404 when unpublished
GET /businesses/{business}/services/{service}/availability integer service ID; required date (YYYY-MM-DD, today through one year ahead in business timezone) {date,timezone,business_timezone,calendar_id,service_id,duration_minutes,slots:[{starts_at,ends_at,label,available}]}
GET /businesses/{business}/services/{service}/quote Selection fields documented below The existing service-pricing quote: base/total price, selected extras and immutable selection breakdown

A public business is {id,name,category,address,city,timezone,location,url}. location contains the saved map coordinates when present. A service is {id,calendar_id,name,duration_minutes,price,currency,configuration,url}. configuration uses the website service-pricing contract: appointment/transport type, description, location requirement, route, passenger limits and add-ons. Quotes return {base_price,addons_total,total,currency,passengers,transport,addons}. transport is null or {country_code,origin,destination,included_passengers,max_passengers}; each quote add-on is {id,name,kind,quantity,unit_size,unit_price,total}. Native booking uses availability, quote and reservation endpoints with server-owned pricing. Detail payment_options is {requirement,deposit_type,deposit_value,methods, currency,method_modes}; methods contain only currently available providers and never credentials. Requirement is none|later|deposit|full; deposits are fixed EUR or percentage. Percentage advances round up to the next cent, capped at total. The url remains available for external website clients.

Discovery includes only active, publicly listed businesses with active calendars, active services, and a service translation in the selected language. Paid listing visibility uses the existing subscription rules. Categories without such providers are omitted. Calendar subscription coverage is additionally checked when reading availability and booking. Public responses never expose customer records, membership records, API keys or private calendar names.

Account authentication

Method and path Body Result
POST /login email (email, max 255), password (max 72) {data:{token,expires_at,user}}
POST /register name (1–120), email (unique, max 255), password (12–72, website AccountPassword rule), matching password_confirmation, role (user or company) Same, HTTP 201
POST /authorize PKCE S256 challenge (43 base64url characters), UUID state {data:{url}} to open in the system authentication browser
POST /authorize/exchange code (80 characters), PKCE verifier (43–128 unreserved characters) Same session response as login
GET /me Bearer session {data:{id,name,email,role}}
PUT /me required name (1–120 characters) Same profile; cannot change email, role or password
POST /logout Bearer session {data:{logged_out:true}}

Website passwords and account roles are reused. Email is normalized to lowercase. Registration cannot assign administrator privileges. Native tokens are 80 random characters; the server stores only their SHA-256 hashes. Tokens expire after 30 days, and password changes invalidate them. Store them in iOS Keychain / Android secure storage. No embedded app-wide login credential is needed.

Authenticated routes require Authorization: Bearer <token>. Website browser cookies or company integration keys cannot replace that token. Logout revokes that session, its unused form tickets, and its registered push devices. Other mobile sessions remain independent. Invalid/expired credentials return 401.

Browser login uses the existing localized website login/registration and Google OAuth flow. GET /{lang}/mobile/authorize/{nonce} requires website authentication, then returns to book365://login?code=...&state=.... The native client checks state and exchanges the one-use code using its private verifier. Authorization requests expire after ten minutes; completed codes after one minute. Intercepting the custom-scheme callback does not grant a token without the verifier. Google retains its existing website redirect URI /auth/google/callback. Apple login is not yet implemented on the website; enabling its Apple capability alone does not activate it.

Native company management

The Android/iOS app renders native account, business, calendar, service, reservation, booking, time-off, map and content pages. It does not embed a browser. Company owners/managers may mutate their own company records; viewers have read-only access. Every operation reuses the website's business rules, membership checks, creation limits, calendar coverage, collision checks and pricing. Foreign records return 404/403. A forged company query cannot grant edit permission on a business in another membership. Company accounts alone can access /management.

Method and path Input Response
GET /management optional linked company or business UUID data:{companies,selected_company,businesses,categories,countries,timezones}
POST /management/companies Company fields below 201 {data:{id}}, company UUID
PUT /management/companies/{company} Company fields 200 {data:{id}}
POST /management/businesses Business fields plus required company_guid UUID 201 {data:{id}}; new business active
GET /management/businesses/{business} Business UUID Private business plus can_edit,payment_options,calendar_capacity,calendars,catalogue
PUT /management/businesses/{business} Business fields plus required boolean active 200 {data:{id}}
GET /management/businesses/{business}/services optional owned calendar_id integer, page 1–10000 data:[service], meta:{page,has_more}; 20 per page
GET /management/businesses/{business}/services/{service}/quote Same passenger/add-on selection as public quote Same quote, with private business membership enforced
POST /management/businesses/{business}/calendars Calendar fields 201 {data:{id}}, integer ID
POST /management/businesses/{business}/calendars/{calendar} Calendar fields 200 {data:{id}}
POST /management/businesses/{business}/calendars/{calendar}/services Service fields 201 {data:{id}}, integer ID
POST /management/businesses/{business}/calendars/{calendar}/services/{service} Service fields 200 {data:{id}}
POST /management/businesses/{business}/reservations Booking fields; staff email nullable, optional UTC ends_at {data:reservation,replayed:boolean}, 201 or 200 for identical retry
PATCH /management/businesses/{business}/reservations/{reservation} Required version (64-character current hash), status, UTC starts_at, customer.name; optional ends_at, customer email/phone, notes, service location {data:reservation,version,status_label,status_actions}; stale version 409
POST /management/businesses/{business}/reservations/{reservation}/payment UUID receipt_id, amount 0.01–999999.99 (≤2 decimal places), optional reference max 200 {data:{recorded:true}}; idempotent receipt for bank/in-person payment only
GET /management/businesses/{business}/time-off Business UUID data:[{id,calendar_id,label,starts_at,ends_at,version}], next 100 nonexpired periods in start order
POST /management/businesses/{business}/time-off UUID id, owned calendar_id, optional label max 120, local from and until (YYYY-MM-DDTHH:mm); current version for edits {id,version}; stale update 409
DELETE /management/businesses/{business}/time-off/{period} Required current version (64 characters) {deleted:true}; stale delete 409

Company fields: required name (1–120), optional nullable email (email, max 255), phone (40), address (2000), optional profile object. Allowed profile keys: configured country_code, registration_number (80), HTTP(S) website (255), description (2000), paired numeric latitude (-90..90) / longitude (-180..180), IANA timezone, and hours (seven weekday entries, same opening rules as calendars). Sending a profile replaces that profile; retain unchanged fields when editing.

Business fields: required name (1–120), configured category slug, IANA timezone; optional nullable description (5000), address (2000), email (255), phone (40), city (120) and configured country code (both together), location object containing paired latitude/longitude within the ranges above. Business reads use {id,company_id,name,category,description,address,city,country_code,email, phone,timezone,location,active}. Management companies use {id,name,email,phone, address,profile,membership,can_edit}. Categories are {id,name} choices (all configured categories for editing); public discovery still excludes empty categories. Countries map code to name; timezones are supported IANA identifiers.

Calendars: required name (1–120), boolean active, hours with exactly keys 1…7 (Monday…Sunday), each {open,close}. Times use HH:mm, either both null for closed or closing after opening. Optional closures is a comma/whitespace-separated string (max 4000) of at most 200 distinct YYYY-MM-DD dates. Reads return closures as an array and closed days as null; native editors normalize closed days before saving. Calendar reads are {id,name,active,covered,hours,closures}; calendar_capacity is an integer or null for unlimited. Capacity rules are enforced on creation and booking. Catalogue choices are {id,name} with translated names.

Services: required duration 1–1440 minutes, price 0–999999.99 with ≤2 decimals, boolean active; optional catalogue_id (64 characters, matching business category), or required new name (1–120) when no catalogue is selected. The website creates new shared definitions in the requested language. Configuration supports type:appointment|transport, description (2000), boolean requires_location, and transport country/origin/destination (200 each), included/max passengers (1–100, max ≥ included). At most 20 add-ons: optional existing UUID id, name (1–120), kind optional|passengers, price (same limits), unit_size/max_quantity (1–100), boolean active. Only existing IDs owned by this service can be reused. At most one passenger add-on, transport only, with capacity coverage. Private service reads: {id,calendar_id,catalogue_id,name,translated_name,duration_minutes,price,active, configuration}. All prices use EUR. Omitted configuration retains it; supplied configuration replaces it with the validated website representation.

Reservations: staff may choose exact periods, including outside ordinary hours, within the website's one-year booking window, 1–1440 elapsed minutes; explicit closures, unavailable periods, overlaps and calendar coverage still apply. Only pending/confirmed reservations can move; cancelled/completed cannot be rescheduled. Status must be current or a transition returned in status_actions. Time edits preserve saved pricing. Manual receipts cannot exceed the remaining balance or charge cancelled bookings; altered reuse of receipt IDs returns 409. Missing ownership cannot be repaired by changing a request company/business value.

Time off: reads use UTC; writes use local wall times in tz (IANA query value) or the business timezone. Native clients convert device-local selections to UTC and send tz=UTC with minute-precision from/until. The website checks end after start, a maximum of 366 days, existing reservation conflicts and current version. IDs are stable across retries.

Example update: PUT /management/businesses/{uuid} with {"name":"Авто М","category":"vehicle-servicing","timezone":"Europe/Sofia","active":true,"city":"Продановци","country_code":"BG","location":{"latitude":42.3472978,"longitude":23.5233750}}. Use an actual category ID returned by management when integrating.

Advanced payment-provider setup, subscriptions and developer tools are explicit system-browser links, where website sign-in may be required. Provider checkout also opens externally. Native saved-coordinate maps use OpenStreetMap tiles with attribution/cache; bootstrap features.maps controls map visibility. Website Google Maps settings remain the website's own renderer configuration. No native Maps SDK credential or browser API key is sent to the app.

Compatibility with older app versions

GET /workspace still returns {data:{can_edit,companies:[{id,name,membership}],businesses:[{id,name,company_id,active}],links:[{title,url}]}}. Optional business/company UUIDs must belong to current memberships. New clients use /management; legacy browser handoffs below remain supported for older builds.

POST /session-ticket requires a bearer token and path (max 2048): a local language-prefixed account/company/business/businesses/book/admin/developers path. It returns {data:{ticket,url}}. POST the 80-character ticket as a form body to /mobile/session using the Book365Mobile/ user agent. This is the only CSRF-exempt browser handoff: it rejects cross-site browser requests, consumes a one-minute, one-use secret atomically, creates a new website session, then returns HTTP 303 to the stored path. The native token never appears in URLs or browser JavaScript. The resulting browser session is bound to the mobile token; expiry/revocation also blocks its protected pages. The user agent changes presentation only, not permissions.

Reservations and messages

Method and path Body/query Response
GET /reservations optional page (1–10000), business_id UUID, status (pending/confirmed/completed/cancelled), ISO dates from/until (until after from); intersect existing ownership scope and intervals {data:[reservation],meta:{page,has_more}}, 20 per page
GET /reservations/{reservation} Integer ID owned by customer or linked company {data:reservation} plus business_name,business_address,business_location,timezone,can_edit,version,status_actions,has_conversation,can_message
POST /reservations/{reservation}/payment Booking customer only; UUID payment_id, `portion:advance balance`
POST /businesses/{business}/reservations Booking fields below {data:reservation}, 201 initially, 200 for an identical retry
GET /reservations/{reservation}/messages optional integer after >= 0 {data:[{id,body,mine,created_at}],meta:{can_send,has_more,next_cursor}}, 50 per page; created_at is UTC
POST /reservations/{reservation}/messages body (nonblank, max 2000), UUID idempotency_key {data:{id}}, 201; identical retries return the original ID
PUT /device token (max 4096), platform (ios or android) {data:{registered:true}}; records request language
DELETE /device no body {data:{registered:false}}; unregisters this session's devices

Reservation records reuse the existing company API representation: id,business_id,calendar_id,service_id,service_name,duration_minutes,price,currency, starts_at,ends_at,status,customer,service_location,pricing,payment,external_reference, notes,updated_at. List records add has_conversation,can_message,url. customer:{name,email,phone?}, service_location:{address?,latitude?,longitude?}; pricing is the quote snapshot described above. payment contains refunded,status,mode,method,requirement,currency,total,paid,remaining,advance_required, advance_remaining,reference,bank, using decimal money strings; bank is the company's payment-instruction snapshot when applicable. Customers see only bookings created by their user ID, never email-based matches. Company users see bookings belonging to their currently linked businesses. Administrators do not gain global private conversations through the mobile API.

Booking fields: required calendar_id, service_id (positive integers), starts_at (UTC YYYY-MM-DDTHH:MM:SSZ), customer.name (max 120), customer.email (email, max 255), UUID idempotency_key. Optional customer phone (max 40), notes (max 2000), external_reference (max 120), service location (address max 500; latitude -90..90 and longitude -180..180 together), expected_total / expected_advance (0..999999.99, at most two decimal places), payment_method (bank_transfer, in_person, stripe, revolut). Selection fields and limits are the existing ServicePricing::selectionRules() contract shown in the main company API reference: passengers (optional integer 1–100), addons (at most 20 entries), each containing a distinct UUID id and integer quantity 0–100. The server derives ownership, recomputes the price, checks selected resource/service relationships, listing/coverage, working hours, time off and conflicts. Extra ownership fields cannot assign another customer. Reusing a key with changed content returns 409; failed network writes must reuse their key.

Conversations require a booking with a recorded customer owner. The customer and currently linked company users may read it; company owners/managers may reply. Viewer membership is read-only. Cancelled bookings retain readable history but reject new messages. Removed memberships lose access immediately. Message content and device tokens are encrypted at rest. Website users can use /{lang}/account/messages and /{lang}/account/messages/{reservation} with the same permissions; POSTing the latter requires browser CSRF and the same message fields. Mobile conversation polling runs every 12 seconds while the screen is active; fetch subsequent pages until has_more is false.

Push is optional. Admin → Integrations → Mobile message notifications saves an encrypted Firebase service account; enabling it queues alerts for registered recipient devices on new messages. The existing website scheduler runs book365:mobile-notifications --limit=25 each minute. Delivery rechecks membership and token validity, leases work, retries at most five times with backoff, and removes invalid FCM tokens. Alerts contain a translated generic “New message”, never message text or customer data. Tapping opens Messages. Device permission, FCM/APNs configuration and compatible Google Play services remain prerequisites. Enabling push does not retrospectively notify old messages. OS delivery is not instant or guaranteed; persisted conversations remain the source of truth.

Errors and limits

Validation errors use HTTP 422 with message and, where applicable, field errors. 401 means login required; 403 insufficient permissions; 404 missing/not-visible or out-of-scope record; 409 conflict/reused key/cancelled conversation; 410 consumed or expired browser authorization; 429 rate limit. Do not automatically retry writes with a new idempotency key. No fixture data is substituted for network failures.

Mobile routes allow 120 requests/minute/IP overall. Login also uses the website's 20/minute/IP and 5/minute/email+IP limits; registration 10/hour/IP. Authorization begins allow 10/minute, exchanges 30/minute, browser completion 20/minute; form tickets/consumption 30/minute, bookings 10/minute, message writes 30/minute. Native company/business creation and payment checkout also allow 10/minute; other management reads/writes share the overall 120/minute limit. Browser message pages allow 60/minute and message writes additionally 30/minute.

Example discovery: GET /api/mobile/v1/categories?lang=bg&city=bg-sofia. Example message body: {"body":"Could you confirm the address?","idempotency_key":"8ee4df24-d6d4-430e-a40c-26bd71bf0479"}.

Quick start

  1. Register a company account and open My account → Company / Calendar / Services.
  2. Create your company and business. Choose its timezone, then add a calendar for each vehicle, worker, room or piece of equipment. Configure services, durations, EUR prices, working hours and closed dates. Use Reservations → Unavailable time for holidays or breaks.
  3. Open API & website calendar. Select your company and business, choose read-only or read/write access, and confirm your password. Only company owners can issue keys.
  4. Copy the API key immediately. If you entered a webhook URL, also copy its signing secret. Both are shown once. Keys expire after one year; create a replacement and revoke the old key to rotate.

Book365 stores all companies, calendars, reservations, API credentials and delivery history in its own database. It runs independently. Each key is restricted to one business. Revocation, expiry, loss of owner membership or disabling the business stops API access, embeds and pending webhooks.

Authentication

Base URL: https://book365.cloud/api/v1. Use HTTPS and Authorization: Bearer YOUR_SECRET_KEY. API keys belong on your backend, never in URLs or browser JavaScript. API requests do not use login cookies. Limits: 60 requests/minute/key and 120/minute/IP. HTTP 429 includes Retry-After.

curl 'https://book365.cloud/api/v1/calendars' \
  -H 'Authorization: Bearer YOUR_SECRET_KEY' \
  -H 'Accept: application/json'

API reference

Success contains data. Errors contain message; validation errors can also contain errors. IDs are integers, except business_id (UUID) and event/cursor IDs (decimal strings). Use only the fields listed below. All reservation timestamps are UTC, formatted YYYY-MM-DDTHH:mm:ssZ. Availability dates and labels use the selected timezone, defaulting to the business timezone, including daylight saving changes.

Method & pathInputResult
GET /calendarsOptional page 1–10000; 50/page.data: [{id, name}]; meta: {page, has_more}.
GET /calendars/{id}/servicesA calendar in this business.data: [{id, name, duration_minutes, price, currency, configuration}]. Active services only; price is the base amount in EUR. See configuration below.
GET /calendars/{id}/availabilityRequired service_id, date (YYYY-MM-DD; today through one year ahead in the selected timezone). Optional timezone: valid IANA identifier; defaults to the business timezone and ignores browser cookies.data: {date, timezone, business_timezone, calendar_id, service_id, duration_minutes, slots: [{starts_at, ends_at, label, available}]}.
GET /calendars/{calendar}/services/{service}/quotepassengers and addons selection; see transport and pricing below. Read-only keys allowed.data: pricing breakdown; does not reserve a calendar.
GET /reservationsOptional page 1–10000, limit 1–100 (default 50), updated_since (UTC timestamp).data: reservation array; meta: {page, has_more}. Includes cancelled reservations.
GET /reservations/{id}Reservation in this business.data: reservation.
GET /reservation-changesOptional after (nonnegative event cursor; default 0), limit 1–100 (default 50).data: event array; meta: {next_cursor, has_more}. Stable, increasing cursor; includes every committed booking transition.
POST /reservationsJSON below and required Idempotency-Key.201 on creation; 200 on replay. data: reservation. Requires read/write access.
PATCH /reservations/{id}Exactly {"status":"cancelled"}.200 with data: reservation. Requires read/write access. Completed bookings cannot be cancelled.

Create a reservation

Required: calendar_id, service_id, starts_at (UTC timestamp with zero seconds), customer.name (1–120 characters), customer.email (valid email, max 255). Optional: customer.phone (max 40), external_reference (max 120), notes (max 2000), passengers, addons and expected_total (see below). Customer accepts only name/email/phone. Top-level fields not documented here are rejected.

curl 'https://book365.cloud/api/v1/reservations' \
  -X POST -H 'Authorization: Bearer YOUR_SECRET_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 7d61cd5d-f097-4c68-bfad-0a6b8b832977' \
  -d '{
    "calendar_id": 1,
    "service_id": 1,
    "starts_at": "2027-01-20T08:00:00Z",
    "customer": {"name": "Alex Example", "email": "[email protected]"},
    "external_reference": "my-site-1001"
  }'

For API, customer and embedded booking, use a starts_at value returned by availability. New bookings must start in the future, within one year, fit entirely inside working hours and start on a 15-minute grid measured from opening. Service duration can be 1–1440 minutes. Prices are nonnegative EUR amounts, configured by the business; request input cannot override them. Bookings start pending and immediately occupy their time. Staff can confirm, complete or cancel them in My businesses. Adjacent bookings are allowed; overlapping active bookings are rejected under a database transaction. Cancellation releases the time. Changing schedules never silently changes existing reservations.

Staff-selected reservation periods

Authenticated company owners/managers can choose exact start and end times in their panel, including outside opening hours. Dragging a range in Day/Week opens the modal with that period; both boundaries remain editable. Staff periods must start in the future within one year and last 1–1440 elapsed minutes. Explicit closed dates, vacation blocks, overlaps and paid calendar coverage remain enforced. Prices and payment terms stay unchanged when the duration changes. Cancelled/completed bookings cannot be rescheduled.

The private company reservation POST and PATCH accept from and until together as local YYYY-MM-DDTHH:mm in the resolved visitor timezone (tz). Alternatively, UTC starts_at and optional ends_at use YYYY-MM-DDTHH:mm:ssZ; omitting the end uses the service duration on creation or preserves the reservation duration on edit. Local fields take priority. Nonexistent local times return 422; new repeated local times use the earlier occurrence, while unchanged edited boundaries preserve their saved instant. The public API rejects staff-only fields and retains fixed service-duration booking rules.

Reservation reads, polling events and webhook snapshots expose the actual starts_at, ends_at and duration_minutes, which may differ from the service configuration. Integrations should use these saved values when displaying or synchronizing reservations.

Holidays and unavailable time

Companies can block a calendar for a date/time range in Reservations → Unavailable time. Availability slots overlapping a blocked period have available: false; website, staff, API and embedded booking all enforce the same restriction. Creating or rescheduling a booking into a blocked period returns 409. Vacation labels and private details are never included in public availability. Adjacent bookings are allowed; existing reservations must be moved or cancelled before their time can be blocked.

Company users edit reservation date/time, customer details, service location, notes and status in an autosaving modal. These changes preserve the agreed service, price, payment terms and client ownership, and emit the existing reservation.updated or reservation.cancelled event. The company-session PATCH response includes status_label and status_actions alongside data and version, so the modal can refresh its status badge and permitted actions after each save. Company-session editor routes are not available to API keys; the public PATCH route still accepts cancellation only. Time-off changes alone emit no reservation events, so always refresh availability before booking.

Readable website URLs

Public business and service links use saved Latin-character name slugs, for example /bg/businesses/salon-krasota/masazh. Older UUID/catalogue links redirect to the readable URL. These slugs are website addresses; external API business, calendar and service identifiers remain unchanged.

Website language URLs

Website pages and signed-in company forms use a language prefix, such as /bg/company/services or /en/developers. In browser-route examples, replace {lang} with a visible website language. The external /api/v1 API, embedded calendar and Google/payment callback addresses keep their existing URLs and contracts.

Business listing and calendar subscriptions

Book365 listing subscriptions are charged separately for each business/location; calendar subscriptions add capacity within that business. Disabled or unconfigured Stripe, or both applicable prices set to zero, means free access. Public discovery and GET/POST /{lang}/businesses/{business_slug}/{service_slug?} require an active business with free or paid listing coverage; unavailable listings return 404. Listing fees do not block private staff/API/embedded bookings. Those bookings still require calendar coverage when paid calendar pricing applies.

Billing uses authenticated, CSRF-protected company-owner routes, not API keys. POST /{lang}/company/subscription/checkout?business={uuid} accepts optional kind=calendar|listing (default calendar), quantity 1–500 for calendars or exactly 1 for listings, interval=monthly|annual and expected_amount in integer EUR cents per unit. Server-saved prices are authoritative. Changed prices or an existing nonterminal listing subscription return 409; free/unavailable billing returns 422. The response redirects to Stripe Checkout. Listing purchases grant no calendar capacity. Existing calendar subscriptions remain calendar subscriptions. Public API credentials cannot purchase, change or bypass subscription coverage.

Client service location

POST /reservations accepts optional service_location (object or null), with only address (nullable string, max 500 characters), latitude (nullable number, -90 to 90) and longitude (nullable number, -180 to 180). Coordinates must be supplied together, even when an address is present. If configuration.requires_location is true, a nonblank address or both coordinates is required. Zero coordinates are valid. Invalid or missing required location returns 422. Changing location on an idempotent retry returns 409.

"service_location": {"address":"12 Example Street, Sofia","latitude":42.6977,"longitude":23.3219}

Reservation responses, polling events and signed webhook snapshots include service_location or null. The location and event payload are encrypted at rest. Public availability never reveals customer locations. API creation requires customer name and email; staff can book with only a name, so response customer.email can be null. Phone remains optional.

Admin → Integrations → Google Maps configures the browser key for Book365’s address/map controls. The company’s shareable /{lang}/businesses/{business_slug}/{service_slug?} page lets signed-in clients reserve active services and enter any required location, with typed address, map selection or device location requested on click. Keys remain on your backend for your own website’s API integration. The Google Maps browser key is a separate, referrer-restricted credential.

Reservation response

{
  "data": {
    "id": 123, "business_id": "11111111-1111-4111-8111-111111111111",
    "calendar_id": 1, "service_id": 1, "service_name": "Haircut",
    "duration_minutes": 60, "price": "30.00", "currency": "EUR",
    "pricing": {"base_price": "30.00", "addons_total": "0.00", "total": "30.00",
      "currency": "EUR", "passengers": null, "transport": null, "addons": []},
    "starts_at": "2027-01-20T08:00:00Z", "ends_at": "2027-01-20T09:00:00Z",
    "status": "pending",
    "customer": {"name": "Alex Example", "email": "[email protected]"},
    "service_location": null,
    "external_reference": "my-site-1001", "notes": null,
    "updated_at": "2026-09-15T10:00:00Z"
  }
}

Status: pending, confirmed, completed or cancelled. Allowed staff transitions: pending → confirmed/cancelled; confirmed → completed/cancelled. Completed and cancelled are terminal. Repeating the current status is a successful no-op. The API permits cancellation only. Bookings are retained for synchronization, with no hard-delete endpoint. The API does not collect payments; a service price does not enable Stripe checkout or subscription charging.

Transport, add-ons and price quotes

Company owners/managers create or edit company profiles under My businesses (name, email, phone, address, country, registration number, website, description). Create a business, then a calendar for each vehicle or worker. Under that calendar, Add service / Edit service opens the service editor. All services can have optional extras. Transport services additionally require an operating country, pickup location, destination, included passenger count and maximum passenger count. Country codes use the bundled 249-country ISO list. Routes are descriptive fixed routes; there is no distance calculation, dispatch or shared-seat inventory. Each reservation exclusively occupies its calendar for its actual booked duration (1–1440 minutes; staff may override the service duration); include driving and turnaround time in that duration.

The company sets every price, unit size and quantity limit. The service price is the base EUR amount. configuration has:

  • requires_location: boolean, defaults to false. When true, booking requires a client address or coordinate pair.
  • type: appointment or transport (legacy services default to appointment).
  • description: nullable Markdown text, up to 2000 characters including formatting. Strip raw HTML, unsafe links and images before rendering.
  • country_code: ISO alpha-2, origin and destination: up to 200 characters; these are required for transport and null for appointments.
  • included_passengers, max_passengers: integers 1–100 for transport, with maximum >= included; null for appointments.
  • addons: up to 20 definitions, including inactive definitions. Each has UUID id, name (1–120 characters), kind (optional or passengers), price (EUR decimal string, 0–999999.99), unit_size (1–100), max_quantity (1–100), and boolean active. IDs belong to their service and are retained when edited.

An optional add-on charges quantity × price; unit_size describes how many items that unit covers. For example, one unit can represent two bikes. At most one passenger rule is allowed per transport service: its automatically calculated quantity is ceil(max(0, passengers - included_passengers) / unit_size). The company can choose one person per unit, two people, or another group size. Each started group is charged in full. Capacity above the included count requires an active passenger rule with enough units to cover it; zero-priced units allow free extra passengers. Quantities never increase the service's declared capacity.

GET /api/v1/calendars/{calendar}/services/{service}/quote accepts the same passengers and addons selection fields as booking creation, as query parameters. Both read-only and read/write API keys can quote within their business. Unknown query fields are rejected. Example:

GET /api/v1/calendars/1/services/2/quote?passengers=5&addons[0][id]=11111111-1111-4111-8111-111111111111&addons[0][quantity]=2
Authorization: Bearer YOUR_SECRET_KEY
Accept: application/json

For POST /reservations, add these top-level fields when needed:

  • passengers: required for transport, integer 1–100 not exceeding capacity; omit or null for appointments.
  • addons: optional array of at most 20 {id, quantity} objects. IDs must be distinct active optional add-ons of the selected service. quantity is an integer 0–100 within the configured maximum; zero means not selected. Omitted extras cost nothing. Passenger-rule IDs must never be submitted here: the server applies the rule automatically. Extra nested fields such as price are rejected.
  • expected_total: optional EUR amount 0–999999.99 with at most two decimals. If it differs from the server-calculated total, booking returns 409 with no reservation created. Get a fresh quote and show the new amount before submitting revised booking details. A retry of a previously accepted identical payload still returns the original reservation even after prices change.

Quotes return data: {base_price, addons_total, total, currency, passengers, transport, addons}. Amounts are two-decimal EUR strings, calculated in integer cents; the total is limited to EUR 999,999.99. transport is null for appointments, otherwise {country_code, origin, destination, included_passengers, max_passengers}. Each selected add-on line has {id, name, kind, quantity, unit_size, unit_price, total}. Zero-quantity and inactive add-ons are omitted. A quote does not hold a vehicle or freeze the price. Always handle overlap and price conflicts on booking.

Example company configuration: base EUR 100 includes two passengers; extra groups of two cost EUR 30, one bike costs EUR 10.50, and one ski-equipment unit costs EUR 5.25. Five passengers, two bikes and one ski unit total EUR 186.25: 100 + 2×30 + 2×10.50 + 5.25. These are illustrative prices, never seeded plans.

New reservation responses expose the complete quote as pricing, and reservation price is the total, including extras. Both staff bookings and API bookings use this calculation. Stored snapshots retain route, passenger count and add-on names, units and prices after service edits/removal. Polling and signed webhooks carry the same immutable event-time breakdown. Existing reservations without a breakdown return pricing: null; older stored events can omit pricing entirely. Pricing failures return 422; missing/out-of-scope resources return 404; changed expected totals return 409. No new payment collection or tax calculation is added. The calendar iframe still displays free/busy slots, without a price-selection form. External sites use the API for options, quotes and booking submission.

Safe retries

Idempotency-Key must contain 16–128 letters, digits, dots, underscores, colons or hyphens. Use a UUID for each intended booking. After a timeout or 503, retry the same payload and key. The same business and key return the existing reservation, even after service settings change or the booking time passes. A different payload with that key returns 409. The Idempotency-Replayed response header is true/false. Keys and request hashes are retained with booking history. Never change the key just because a request timed out.

Polling changes

Start with GET /reservation-changes?after=0. Apply each returned event in id order, save next_cursor only after processing the page, and continue while has_more is true. Use your saved cursor on the next poll. Event IDs may have gaps. Reprocessing a page is safe when you deduplicate by event id. Unlike timestamp filtering, this cursor captures multiple changes in the same second. The reservation list is useful for a full reconciliation; updated_since is inclusive.

Signed webhooks

Enter your public HTTPS endpoint when generating the API key. Only port 443 is supported; credentials/fragments, private/reserved IPs and redirects are rejected. Book365 sends a POST for reservation.created, reservation.updated and reservation.cancelled. Each notification contains:

{
  "id": "456",
  "type": "reservation.created",
  "occurred_at": "2026-09-15T10:00:00Z",
  "data": { "id": 123, "status": "pending", "...": "full reservation as above" }
}

The Book365-Event-Id header matches id. Book365-Signature contains t=UNIX_SECONDS,v1=HEX_SIGNATURE. Compute HMAC-SHA256 using the signing secret over timestamp + "." + exact_raw_request_body. Compare signatures in constant time, reject timestamps outside a five-minute clock window, and only then decode/apply the event. Example verification in PHP:

$header = $_SERVER['HTTP_BOOK365_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
if (!preg_match('/^t=([0-9]+),v1=([a-f0-9]{64})$/D', $header, $m)
    || abs(time() - (int) $m[1]) > 300
    || !hash_equals(hash_hmac('sha256', $m[1].'.'.$body, $signingSecret), $m[2])) {
    http_response_code(401);
    exit;
}
$event = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
// Durably enqueue the verified event, deduplicating by $event['id'].
http_response_code(204);

Acknowledge with any 2xx status within 10 seconds after durably accepting the event. Delivery is at least once: duplicates and out-of-order arrivals are possible. Deduplicate by event id and avoid replacing newer reservation state with an older event. Use the polling cursor to recover missed events. API and manual staff changes use the same transactional event log.

The scheduler runs every minute and processes a bounded batch; delivery normally begins on the next run, and busy queues may take longer. Non-2xx responses/timeouts retry after 1, 5, 15, 60, 180, 720 and 1440 minutes (8 attempts total). Delivery status and the latest 30 delivery records are visible under API & website calendar. Owners can password-confirm a retry of failed deliveries. Revoked/expired keys or removed access cancel pending deliveries. New keys receive future events only; polling can retrieve older history.

Embedded calendar

Set the exact HTTPS origin of the company website, for example https://www.example.com, without a path or trailing slash. Copy the generated iframe from your account. It contains a public identifier, never an API secret. It shows calendar/service/date selection and free/busy slots without customer names or booking details. Booking submission stays on your server through the API. The embed uses no third-party cookies. A same-origin script detects the device timezone; without JavaScript it defaults to the business timezone. Optional tz selects an IANA timezone; tz_auto=1 restores device detection. CSP frame-ancestors limits where the page can be embedded; the availability page itself is public. Revoke its key to disable it.

Calendar subscription coverage

When Stripe billing and a nonzero calendar price apply, every calendar requires paid company subscription capacity, including calendars created during a free period. Coverage is assigned by ascending calendar ID and includes inactive calendars. Uncovered calendars return the normal availability shape with an empty slots array. New reservations return 422 with {"message":"Calendar subscription coverage is required."}. Calendar/service lists and quotes remain readable. Existing reservation reads, changes, cancellation and idempotent retries remain available. These rules also apply to website booking and embedded calendars. No coverage requirement applies when Stripe is disabled/unconfigured or both applicable prices are zero; administrator creation limits still apply.

Stripe subscription webhook (site administrators)

POST /api/stripe/webhook is a separate Stripe-only endpoint. It does not accept company API keys or reservation data and is never proxied to another platform. Configure its signing secret and enable paid listings and calendars in Admin → Integrations → Stripe; manage monthly/annual EUR listing prices, individual business overrides and category-based calendar prices in Admin → Subscription pricing.

Subscribe to checkout.session.completed, checkout.session.expired, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.paid and invoice.payment_failed. Send the original Stripe JSON body (maximum 256 KiB) and Stripe-Signature; the timestamp must be within 300 seconds and v1 must match HMAC-SHA256 of timestamp.raw_body. Test/live mode must match the saved keys. No session or CSRF token is used.

Success or an ignored signed event returns 200 {"received":true}. Invalid signature/mode returns 400; oversized bodies 413; rate limiting 429; unavailable configuration or provider refresh returns 503. Refresh failures return {"message":"Subscription refresh failed. Retry delivery."}. Retry failed deliveries. Current Stripe state, matched to the stored Checkout purchase, customer, mode, EUR amount, interval and quantity, determines coverage; an event payload or browser return never grants it. Duplicate or out-of-order events cannot duplicate capacity. Unpaid renewal never extends paid coverage. The website scheduler also reconciles subscriptions every five minutes. Stripe signature documentation.

Reservation service payments

Reservation API payment contract

  • GET /api/v1/payment-options: read/read-write key, business-scoped response: data: {requirement, deposit_type, deposit_value, methods, currency, method_modes}. deposit_value is a decimal string. method_modes maps online methods to test|live and offline methods to null. No bank details or credentials are returned.
  • POST /api/v1/reservations additionally accepts optional payment_method (bank_transfer|in_person|stripe|revolut) and expected_advance (EUR decimal, up to two decimals, 0–999999.99). A sole available method is selected automatically; multiple methods require a choice. Unavailable/missing required methods return 422. No-payment/zero-total bookings require no method. Stale expected advance returns 409. Changing payment fields under an existing booking idempotency key returns 409.
  • POST /api/v1/reservations/{id}/payments: read/write key only; accepts exactly {payment_id: UUID, portion: "advance"|"balance"}. Returns 200 {"data":{"checkout_url":"https://provider-hosted-checkout/..."}}. Company/customer scope mismatch returns 404; read-only key 403; invalid/offline/ zero or sub-minimum payment 422; cancelled/completed attempt or unresolved attempt 409; provider/configuration failure 503. Same request ID never creates a second charge. API keys cannot change receipt balances or company payment settings.
  • All reservation GET/POST/PATCH responses and new polling/webhook events include payment: {status, method, mode, requirement, currency, total, paid, refunded, remaining, advance_required, advance_remaining, reference, bank}. Monetary fields are fixed two-decimal strings; paid is net of refunds. bank is null or the saved {holder, iban, bic, name, instructions} for the booked bank-transfer method. Method/mode/reference can be null on legacy/no-payment records. Provider secrets, connection IDs, receipt actors and internal request payloads are never exposed. Historical event snapshots remain as originally recorded and can lack payment.

Company settings apply to every location, and bookings keep their original payment terms. Booking status and payment status are separate. A bank transfer starts awaiting payment; staff verify actual receipts. Partial receipts reduce the balance. No payment means no balance tracking. Advance deposits include service extras, and pending bookings hold their slot until cancelled. Book365 takes no service-payment commission.

Stripe Connect and Revolut payments go to each company’s own merchant account. Clients use provider-hosted checkout. Webhooks and the local scheduler verify captured amounts; browser returns and customer declarations never grant credit. Provider refunds update balances, while cancellation does not issue a refund. Stripe company payments use a separate connected-account webhook at /api/payments/stripe/webhook; Revolut uses /api/payments/revolut/{connection_uuid}/webhook. Neither endpoint accepts company API keys. Both require signed raw bodies, five-minute timestamp tolerance and at most 256 KiB; success is 200, invalid signatures 400, oversized payloads 413, throttling 429, provider/configuration failure 503. Uncertain Revolut creation is recovered by merchant reference without a second create request.

HTTP status codes

200/201 success; 401 invalid/expired/revoked key; 403 insufficient permission; 404 missing or out-of-scope resource; 409 occupied time, changed expected total, idempotency mismatch or invalid status transition; 422 invalid fields, closed hours, off-grid time or missing calendar subscription coverage; 429 rate limit; 503 temporary unavailability. Availability is a snapshot, so always handle a booking conflict. Scheduling and reservation storage are local; no external service connection is required.

Book365

Добре дошли в Book365

Продължете с Google