# Quarters API > The public API of Quarters, property management software for furnished mid-term rentals. OAuth 2.0 client credentials, published listings, live availability, reservations, tenancy agreements and inquiries, with signed webhooks. Base URL https://go.quarters.live/api/v1. Machine-readable description: https://developers.quarters.live/openapi.json. Listing content is served from the snapshot the Publish button froze; only availability is live. Money is integer euro-cents. Dates are YYYY-MM-DD strings local to the property, check-out exclusive. Credentials are per organisation and come from Settings › Integrations in the dashboard. ## Guide: Quick start https://developers.quarters.live/guides/quick-start The API is how a system that is not Quarters reads and writes what Quarters holds for one organisation. It is deliberately small: a website reads listings and the calendar and posts inquiries; a contract, accounting or reporting system reads reservations and their agreements. All of them subscribe to webhooks instead of polling. Three steps get you from nothing to a response. The base URL for everything is `https://go.quarters.live/api/v1`. ### 1. Create credentials In the dashboard, open Settings › Integrations and create an API credential. You get a client ID (`qc_…`) and a client secret (`qs_…`). The secret is shown once; only its hash is stored, so it cannot be shown again. Lose it and you create a new set. Credentials belong to the organisation, not to a person. What they can read is exactly that organisation’s data, and a record that is not yours answers 404, never 403. ### 2. Get a token Exchange the credentials for an access token at the token endpoint. The token lasts 24 hours. Cache it and re-use it; mint a new one when a call answers 401. Request ``` curl -X POST "https://go.quarters.live/api/v1/oauth2/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=qc_YOUR_CLIENT_ID" \ -d "client_secret=qs_YOUR_CLIENT_SECRET" ``` Response · 200 ``` { "token_type": "Bearer", "access_token": "qat_…", "expires_in": 86400 } ``` ### 3. Make your first call Send the token as `Authorization: Bearer` on every call. Every published listing of your organisation, sorted by nickname: Request ``` curl "https://go.quarters.live/api/v1/listings" \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` An empty `listings` array means nothing has been published yet: the API serves what the Publish button froze, never what is being edited. Publish a property in the dashboard and call again. ### Where to go next - Conventions: money, dates, ids and caching, on every endpoint. - Webhooks: so your side never has to poll. - Build a booking website: listings, calendar and the form. - Sync reservations: for contracts, accounting or a guest portal. - The reference: every endpoint and every object. ## Guide: Authentication https://developers.quarters.live/guides/authentication OAuth 2.0 client credentials, the same grant Guesty’s Open API uses, so an integration written against one needs a different token URL and nothing else. ### The flow - Create credentials. Settings › Integrations issues a client ID and a client secret. The secret is shown once; only its hash is stored. - Exchange them for a token. POST the form to `/oauth2/token`. The answer is a bearer token that lasts 24 hours. - Send the bearer. `Authorization: Bearer ` on every call. Cache the token and re-use it until a 401 tells you it expired; then mint another. ### The token endpoint Send the form as `application/x-www-form-urlencoded`. HTTP Basic with `client_id:client_secret` is accepted too, and so is a JSON body with the same three fields. The full contract is on the reference page. Request ``` curl -X POST "https://go.quarters.live/api/v1/oauth2/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=qc_YOUR_CLIENT_ID" \ -d "client_secret=qs_YOUR_CLIENT_SECRET" ``` Response · 200 ``` { "token_type": "Bearer", "access_token": "qat_…", "expires_in": 86400 } ``` The token endpoint fails in RFC 6749’s own shape, `error` plus `error_description`, because that is what an OAuth client library reads. Every other endpoint fails as described under Errors. Status | error | When 400 | invalid_request | client_id or client_secret missing, or a body that is neither form-encoded nor JSON. 400 | unsupported_grant_type | Anything but grant_type=client_credentials. 401 | invalid_client | Unknown client id, wrong secret, or credentials that were revoked. ### Caching and expiry A token is good for 24 hours from the moment it is issued, and `expires_in` says so in seconds. Keep one per credential set in your own process or cache and re-use it for every call; minting a token per request is a wasted round trip and a wasted row. The simplest correct strategy: hold the token with the time you got it, mint a new one when it is older than 23 hours, and also whenever a call answers 401 `unauthorized`, which covers a clock that drifted and a credential that was revoked. The token response carries `Cache-Control: no-store`. That is for HTTP caches in between, not for you: cache the token in your application, never in a shared HTTP cache. ### Without a token A tool that cannot keep a token between runs, such as Zapier, Make or n8n, sends the credentials themselves on every call: HTTP Basic, with the client ID as the username and the secret as the password. It reaches the same data under the same limits, and Zapier, Make and n8n walks through each tool. In your own code, prefer the token. The secret then travels once a day instead of on every request, and a token that leaks from a log is dead within 24 hours. ### Revocation Revoking a credential set under Settings › Integrations invalidates every token issued from it at once; the next call answers 401. Credential sets are independent: a second set for a second system gets its own tokens, and revoking one leaves the other working. Create one set per integration so a leak costs one rotation, not all of them. ### Server to server only There are no CORS headers on purpose. Credentials and tokens belong on your server, never in a browser; a booking page calls your backend, and your backend calls Quarters. ## Guide: Conventions https://developers.quarters.live/guides/conventions These hold on every endpoint. Each one has cost an integration a day when it was missed. ### JSON, over HTTPS, server to server Every request and response body is JSON except the PDF endpoints and the token form. There are no CORS headers on purpose: credentials belong on your server, never in a browser. ### Money is integer euro-cents €2,400.00 is `240000`. There are no floats anywhere in this API, and your side should not introduce one. A `null` amount means the operator has not set one, which is different from zero. ### Dates are days, not instants A tenancy date is a `YYYY-MM-DD` string, a calendar day local to the property, not an instant. Parsing one into a UTC datetime is how a tenancy moves by a day for half the year. Instants (`createdAt`, `sentAt`, …) are ISO 8601 in UTC and carry the `Z`. ### Check-out is exclusive A tenancy from the 1st to the 30th occupies the 1st through the 29th. The 30th is available; the handover is at noon. The calendar says the same: a day is `booked` up to and not including check-out, and `nights` on a reservation is check-out minus check-in. ### Ids are opaque strings Store them as they come. Nothing is encoded in them and their shape may change. A listing id is the same id a reservation’s `propertyId` carries, so the two join. ### The listings response is the complete set Not a delta and not paginated. Rows that vanish have been unpublished, and your mirror should delete them. Reservations, by contrast, page and filter; use `updatedSince` for an incremental sync. ### Nothing is cacheable Every response carries `Cache-Control: no-store`. Cache in your own database, on your own terms, and let webhooks tell you when to refresh. ### Versioned in the path This is `/api/v1`. Fields are added without notice, and your parser should ignore fields it does not know. Nothing documented here is removed or renamed without a new prefix. ## Guide: Errors https://developers.quarters.live/guides/errors ### The shape A failure is a status that means what it says and a body with two fields: `error`, a stable slug you can switch on, and `message`, a sentence for a person, naming the field where one is at fault. A 401 also carries `WWW-Authenticate: Bearer`. Unknown paths under the base URL answer the same shape with 404 rather than an HTML page. Response · 400 ``` { "error": "invalid_request", "message": "Invalid body at \"endDate\": endDate must be after startDate." } ``` The token endpoint is the one exception: it fails as `error` plus `error_description`, the RFC 6749 shape an OAuth client library expects. See Authentication. ### The codes Status | error | When 400 | invalid_request | A field is missing or malformed. The message names it. 401 | unauthorized | No bearer, an expired one, or one whose credentials were revoked. Fetch a new token. 404 | not_found | Unknown id, an unpublished listing, or a path that does not exist. 409 | conflict | The request is well-formed but the record is not in a state that allows it. 413 | payload_too_large | A document over 25 MB. 415 | unsupported_media_type | A document that is not a PDF. 429 | rate_limited | The organisation’s daily enquiry allowance is spent. 500 | internal_error | Ours. Retry later; if it persists, write to us. Each endpoint’s reference page lists the failures it can actually produce, with the condition behind each. ### Retrying - Reads are safe to retry. Every GET can be repeated at will; a 500 or a timeout on one is a reason to try again after a moment. - Reporting an agreement is idempotent. A `PUT` with the same `externalId` updates the same envelope, so a lost response is fixed by sending the report again. - Creating an inquiry is not. A `POST` that timed out may have landed. Rather than retrying blindly, look for it: the inquiry carries your `metadata`, so put a request id there. - A 4xx is yours. Fix the request; the same request will fail the same way. A 401 is the one exception: mint a new token and send it again. ## Guide: Webhooks https://developers.quarters.live/guides/webhooks Register an https endpoint and Quarters POSTs to it when something changes. Each delivery is signed with a secret only the two of you hold, so you can prove the call came from us before acting on it. ### Register an endpoint Under Settings › Integrations, add the URL. The secret (`whsec_…`) is shown next to it. The test button sends a `ping` through the same path, and the last deliveries with their outcome are listed under the endpoint, so a receiver can be debugged from the dashboard. The URL must be https and resolve to a public address; that is checked at connection time, not at save time, so a hostname that later points inward is refused then. ### What arrives The body is an envelope: the delivery `id`, the `event`, when it was `createdAt`, and a small `data` object that points at the record. It is a pointer, not the record: read the current state from the API, which is what makes a late or repeated delivery harmless. Delivery body ``` { "id": "6f1c…", "event": "reservation.updated", "createdAt": "2026-09-13T10:00:00.000Z", "data": { "reservationId": "7c02…", "propertyId": "0d6e1c2a-…", "status": "confirmed", "previousStatus": "reserved" } } ``` Three headers travel with it: Header | Value x-housalot-event | The event name, the same as `event` in the body. x-housalot-delivery | The delivery id, the same as `id` in the body. Dedupe on it. x-housalot-signature | `sha256=` followed by the hex HMAC-SHA256 of the raw body under your endpoint’s secret. The header names carry the product’s former name and will be renamed only with notice and a transition period. ### Verify the signature Compute HMAC-SHA256 over the raw body, the bytes exactly as received and before any JSON parsing, under the endpoint’s secret, and compare it in constant time with the header. Reject anything that does not match before you read a byte of the body. Node ``` import { createHmac, timingSafeEqual } from "node:crypto"; // rawBody is the request body exactly as received, before any JSON parsing. export function verify(secret, rawBody, signatureHeader) { const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`; const actual = signatureHeader ?? ""; return actual.length === expected.length && timingSafeEqual(Buffer.from(actual), Buffer.from(expected)); } ``` Python ``` import hashlib import hmac # raw_body is the request body exactly as received, before any JSON parsing. def verify(secret: str, raw_body: bytes, signature_header: str | None) -> bool: expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(signature_header or "", expected) ``` Most web frameworks parse JSON before your handler runs and hand you an object. You need the bytes: read the raw body, or keep a copy of it, and verify that. ### Handle it - Answer fast. Any 2xx counts as received. Queue the work and return; a handler that takes longer than ten seconds is a failed delivery. - Dedupe on the delivery id. A delivery is sent once, but your side may see it twice through a retrying proxy. The id is in the body and in the header. - Read, don’t trust. The envelope says what changed; the API says what it is now. Fetch the record and act on that. - Plan for a miss. Delivery is one attempt with no retry queue. A full read of the resource is always the recovery, and it is cheap: listings are one call, reservations page with `updatedSince`. ### The events 7 events, listed in full with their payloads under Webhook events. - `listing.updated` A property was put on the website, or its changes were published. - `listing.deleted` A property was taken off the website or deleted. Drop it from your site. - `calendar.updated` Availability changed for a property: a booking, a cancellation, a block. - `reservation.created` A booking was created, in any status. - `reservation.updated` A booking changed: its status, dates, guest, money or notes. - `reservation.deleted` A booking was deleted outright, which is a data-entry mistake being undone. A cancellation is an update. - `ping` Somebody pressed the test button in settings. Confirms the endpoint and the signature. ## Guide: Build a booking website https://developers.quarters.live/guides/booking-website A website needs three things from Quarters: what the flats are, which days they are free, and somewhere to send the form. Each is one endpoint, and one webhook tells you when to look again. Everything here runs on your server; the browser talks to you, never to the API. ### Mirror the listings List every published listing and store the result. It is the complete set, not a delta: a listing that is no longer in the response has been unpublished, and your mirror should drop it. Render from the mirror, not from the API, so a page view never waits on us. Request ``` curl "https://go.quarters.live/api/v1/listings" \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` - `description` fields are the editor’s HTML; `title` is plain text. `translations` carries the same six fields per language, and you fall back to `description` for anything missing. - Photos come in four renditions with absolute URLs. Use `regular` in a grid, `large` on the detail page, `thumbnail` in a list. - `bedrooms`, `beds` and `bathrooms` are already counted so a card does not reduce `rooms`. Money is integer euro-cents; a null price is unset, not free. ### Draw the calendar Availability is the one thing you must not mirror. It is computed on request from bookings and blocks, so a tenancy taken thirty seconds ago is already `booked`. Ask for the window you are drawing, up to 1100 days, and ask again when the visitor moves. Request ``` curl "https://go.quarters.live/api/v1/listings/0d6e1c2a-…/calendar?start=2026-10-01&end=2026-10-04" \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` - `available` is bookable. `reserved` is held but not firm; show it as unavailable. `booked` and `unavailable` are taken. - Check-out is exclusive: a stay ending on the 30th leaves the 30th free, because the handover is at noon. Two stays can meet on one day. - `minNights` is the listing’s floor for a stay. Enforce it in the picker so the form never asks for a stay the operator will decline. ### Land the form as an inquiry Post the form to Create an inquiry. It lands as a row staff convert to a reservation in one click, resolved to a guest record, rather than as an email somebody re-types. Only `lastName` and `email` are required; everything else you collect that is not a named field travels in `metadata`. Request ``` curl -X POST "https://go.quarters.live/api/v1/inquiries" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "propertyId": "0d6e1c2a-…", "firstName": "Sofia", "lastName": "Rossi", "email": "sofia@example.com", "phone": "+39 333 000 0000", "startDate": "2026-10-01", "endDate": "2027-04-01", "adults": 2, "children": 0, "infants": 0, "pets": false, "message": "Two of us, relocating for a project.", "locale": "en", "form": "booking", "metadata": { "page": "https://www.example.com/house/…" } }' ``` An inquiry never blocks a day. Until staff convert it, the calendar still says `available`, and two visitors can ask for the same dates. That is by design: the operator decides who gets the flat. The organisation has a daily allowance of inquiries (500 by default), so a form that a bot can submit needs a captcha or a rate limit on your side first. ### Stay current Subscribe to three webhooks: - `listing.updated`: re-fetch that listing, or the whole set, and re-render. - `listing.deleted`: drop it from the mirror. - `calendar.updated`: if you cache calendar windows for a few minutes, evict that listing’s. And still re-fetch the full set on a schedule, once an hour say. Delivery is one attempt; the full read is the recovery, and for listings it is one call. ## Guide: Sync reservations https://developers.quarters.live/guides/reservation-sync A contract system, an accounting package or a guest portal wants every booking once and then only what changed. List reservations does both, and a webhook says when to ask. ### The initial load Page through everything, sorted by `updatedAt`, a hundred at a time. The order is stable (the sort key, then id) and `total` counts the same filters, so you know when you are done without probing past the end. Request ``` curl "https://go.quarters.live/api/v1/reservations?sort=updatedAt&limit=100" \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` Keep the `updatedAt` of the last row you stored. That is your cursor. ### Incremental sync From then on ask for bookings changed at or after the cursor. Because the bound is inclusive you will see the last row again; that is deliberate, and harmless if your write is an upsert on `id`. Request ``` curl "https://go.quarters.live/api/v1/reservations?updatedSince=2026-09-13T06:00:00.000Z&sort=updatedAt&limit=100" \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` Run it on a schedule, and run it when a `reservation.created`, `reservation.updated` or `reservation.deleted` webhook arrives. The webhook carries the id, so for a single change retrieving that one booking is enough; the scheduled sync is the safety net for a delivery that did not land. A deleted booking does not appear in the list. `reservation.deleted` is the only signal, and it is rare: cancellations are updates, and a deletion is a data-entry mistake being undone. ### What the statuses mean - `inquiry`: a request. Blocks nothing; may become a booking or be dropped. - `reserved`: held. The days are blocked; the contract is usually not signed yet. - `confirmed`: firm. When the booking `requiresContract`, this is the moment the agreement was completed. - `cancelled`: released. The days are free again from the moment of cancellation. The lifecycle stamps (`inquiredAt`, `reservedAt`, `confirmedAt`, `cancelledAt`) record when each status was entered and are never cleared, so a cancelled booking still says when it was confirmed. ### What the record carries - `pricing` is the money as agreed when the booking was made. A later rate change on the listing never rewrites it. `rateBasis` says whether `rentCents` is a month or a night; `monthlyCents` is rent, utilities and furniture added, present only for a monthly booking and null for a nightly one. - `property` is the flat’s identity as last published: name, slug, address and time zone, frozen by Publish like the listings. Only `ownerName` is read live, as the lessor the tenancy agreement names. It is null while the flat has never been published. - `agreement` is the current contract, or null. Its `status` and `document` say whether it is signed and archived. - `dashboardUrl` opens the same booking in the dashboard. Put it in the alert email or the back-office record so a person can get there in one click. ## Guide: Zapier, Make and n8n https://developers.quarters.live/guides/zapier-make-n8n Zapier, Make and n8n connect Quarters to thousands of other apps without code. A webhook starts the workflow, one HTTP step reads the record it names, and whatever comes next is up to you: a row in a spreadsheet, a message to the team, an invoice in the accounting package. In the other direction, a form anywhere can land as an inquiry. ### Before you start - Under Settings › Integrations, create an API credential for the tool: a client ID (`qc_…`) and a secret (`qs_…`). One per tool, so revoking one leaves the others working. - Every HTTP step signs in with HTTP Basic: the client ID as the username, the secret as the password. All three tools have it built in, so there is no token to fetch or keep. See Authentication. ### Every workflow, in four steps - Trigger. The tool gives you a webhook URL. Add it as an endpoint under Settings › Integrations and press the test button: a `ping` arrives, and the tool learns what a delivery looks like. - Filter. An endpoint receives every event. Continue only on the one you want, for example `event` is `reservation.updated` and `data.status` is `confirmed`. - Read. A delivery points at a record; it does not carry it. GET the record, for a booking `https://go.quarters.live/api/v1/reservations/` followed by `data.reservationId`, and map its fields into the next step. - Act. Any app the tool connects to. A delivery is sent once, with no retries. If the tool was down, the event is missed; a workflow that must not miss one also runs on a schedule and reads what changed with `updatedSince`, as in Sync reservations. ### Zapier - Trigger: Webhooks by Zapier › Catch Hook. Add the URL it shows as an endpoint and send the test. - Filter by Zapier on `event`, and on `data.status` where it matters. - Action: Webhooks by Zapier › GET to the record’s URL. In Basic Auth, enter the client ID and the secret joined by a vertical bar: `qc_…|qs_…`. ### Make - Trigger: Webhooks › Custom webhook. Add its address as an endpoint and send the test, so Make can determine the data structure. - A filter on the link after it, on `event` and `data.status`. - HTTP › Make a Basic Auth request to the record’s URL, with a Basic Auth keychain holding the client ID and the secret. Turn on Parse response so the next module can map the fields. ### n8n - A Webhook node, method POST. Add its production URL as the endpoint; the test URL only listens while the editor waits for it. - An If node on `{{ $json.body.event }}`. - An HTTP Request node to the record’s URL. Authentication: Generic Credential Type › Basic Auth, with the client ID and the secret. ### Send inquiries in A form that is not on your own website, a Typeform, a Webflow form, a lead ad, can land in Quarters as an inquiry, in the same inbox as one from your site. POST it to /inquiries as JSON, with the same HTTP Basic credentials: `lastName` and `email` are required, and `source` says where it came from, for example `typeform`. The guest gets a confirmation email when you switched on that form's reply under Website > Booking. If the form already sends one, send `sendConfirmation: false`. ### The signature Every delivery is signed, as described in Webhooks. Because it only names a record and the workflow reads that record with your own credentials, a forged call cannot put false data into the workflow: at most, it runs the workflow on a real record at the wrong moment. That matters when the workflow sends something, so verify there, over the raw body: - Zapier: trigger on Catch Raw Hook instead, and compare in a Code by Zapier step with Node’s `createHmac`. - Make: turn on JSON pass-through on the webhook and compare the header with `sha256=` followed by `sha256(body; hex; secret)`. - n8n: turn on Raw Body on the Webhook node and compute the HMAC with the Crypto node, SHA256, hex. ## Reference: Authentication OAuth 2.0 client credentials, the same grant Guesty’s Open API uses, so an integration written against one needs a different token URL and nothing else. A tool that cannot keep a token, such as Zapier, Make or n8n, sends the client ID and secret as HTTP Basic on every call instead. ### Exchange client credentials for an access token `POST /api/v1/oauth2/token` The OAuth 2.0 client-credentials grant, the one grant this API speaks. Send the form as application/x-www-form-urlencoded. HTTP Basic with client_id:client_secret is accepted too, and so is a JSON body with the same three fields. The answer is a bearer token that lasts 24 hours. Cache it and re-use it until a 401 says it expired, then mint another. Revoking the credentials under Settings › Integrations invalidates every token issued from them at once. Request body: TokenRequest (form-encoded). Response 200: Errors: - 400 `invalid_request`: client_id or client_secret missing, or a body that is neither form-encoded nor JSON. - 400 `unsupported_grant_type`: Anything but grant_type=client_credentials. - 401 `invalid_client`: Unknown client id, wrong secret, or credentials that were revoked. The response carries Cache-Control: no-store and Pragma: no-cache, as the RFC asks. Cache the token in your own process, not in an HTTP cache. Tokens are per credential set. A second set for a second system gets its own tokens, and revoking one leaves the other untouched. Example request: ```sh curl -X POST "https://go.quarters.live/api/v1/oauth2/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=qc_YOUR_CLIENT_ID" \ -d "client_secret=qs_YOUR_CLIENT_SECRET" ``` Example response: ```json { "token_type": "Bearer", "access_token": "qat_…", "expires_in": 86400 } ``` ## Reference: Listings Property content as the Publish button froze it. A property that has never been published does not exist here, and content being edited right now never leaks. Only the calendar is live. ### List every published listing `GET /api/v1/listings` The complete set of published properties, sorted by nickname. The full set of published properties for the credentials’ organisation, sorted by nickname. Not paginated and not a delta: if you mirror it, treat the response as the complete set and delete rows it no longer contains. That is how an unpublished listing disappears from your site. Response 200: - `listings` (Listing[], required) `description` fields are rich text, HTML from the editor; `title` is plain. `translations` carries the same six fields per site language, and a consumer falls back to `description` for anything missing. `bedrooms`, `beds` and `bathrooms` are derived from `rooms` and carried so a listing card does not have to reduce an array. `bathrooms` can be fractional. Image URLs are absolute. Four renditions per photo: `thumbnail`, `regular`, `large` and the `original`. Money is integer euro-cents; a null price means the operator has not set one. `rateBasis` says which of `monthlyRateCents` or `nightlyRateCents` this listing quotes. Example request: ```sh curl "https://go.quarters.live/api/v1/listings" \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` Example response: ```json { "listings": [ { "id": "0d6e1c2a-…", "slug": "jordaan-canal-loft", "nickname": "JOR-1A", "active": true, "isListed": true, "propertyType": "Apartment", "roomType": "Entire home/apt", "address": { "street": "Prinsengracht 123", "postalCode": "1015 DX", "city": "Amsterdam", "country": "NL", "area": "Jordaan", "latitude": 52.3731, "longitude": 4.8837 }, "accommodates": 2, "rooms": [ { "kind": "bedroom", "name": "", "beds": [ { "type": "queen", "count": 1 } ] }, { "kind": "full_bathroom", "name": "", "beds": [] } ], "bedrooms": 1, "beds": 1, "bathrooms": 1, "squareMeter": 58, "timezone": "Europe/Amsterdam", "pricing": { "currency": "EUR", "rateBasis": "monthly", "monthlyRateCents": 240000, "nightlyRateCents": null, "depositCents": 240000, "utilitiesCents": 15000, "furnitureCents": null }, "terms": { "minNights": 30, "maxNights": 365 }, "amenities": [ "wifi", "washer", "dishwasher" ], "description": { "title": "Canal loft in the Jordaan", "summary": "

…

", "space": "

…

", "neighborhood": "

…

", "access": null, "notes": null }, "translations": { "nl": { "title": "…", "summary": "

…

", "space": null, "neighborhood": null, "access": null, "notes": null } }, "images": [ { "id": "4b7a…", "isPrimary": true, "caption": null, "sortOrder": 0, "width": 2400, "height": 1600, "bytes": 481221, "thumbnail": "https://…", "regular": "https://…", "large": "https://…", "original": "https://…" } ], "publishedAt": "2026-08-28T09:12:44.000Z" } ] } ``` ### Retrieve one listing `GET /api/v1/listings/{id}` The same object as the list, for a single property. The same object the list serves, for one property. Path parameters: - `id` (string, required): The listing id, from /listings. Response 200: - `listing` (Listing, required) Errors: - 404 `not_found`: Unknown to your organisation, or never published. The two are indistinguishable on purpose. Example request: ```sh curl "https://go.quarters.live/api/v1/listings/0d6e1c2a-…" \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` Example response: ```json { "listing": { "id": "0d6e1c2a-…", "slug": "jordaan-canal-loft", "nickname": "JOR-1A", "active": true, "isListed": true, "propertyType": "Apartment", "roomType": "Entire home/apt", "address": { "street": "Prinsengracht 123", "postalCode": "1015 DX", "city": "Amsterdam", "country": "NL", "area": "Jordaan", "latitude": 52.3731, "longitude": 4.8837 }, "accommodates": 2, "rooms": [ { "kind": "bedroom", "name": "", "beds": [ { "type": "queen", "count": 1 } ] }, { "kind": "full_bathroom", "name": "", "beds": [] } ], "bedrooms": 1, "beds": 1, "bathrooms": 1, "squareMeter": 58, "timezone": "Europe/Amsterdam", "pricing": { "currency": "EUR", "rateBasis": "monthly", "monthlyRateCents": 240000, "nightlyRateCents": null, "depositCents": 240000, "utilitiesCents": 15000, "furnitureCents": null }, "terms": { "minNights": 30, "maxNights": 365 }, "amenities": [ "wifi", "washer", "dishwasher" ], "description": { "title": "Canal loft in the Jordaan", "summary": "

…

", "space": "

…

", "neighborhood": "

…

", "access": null, "notes": null }, "translations": { "nl": { "title": "…", "summary": "

…

", "space": null, "neighborhood": null, "access": null, "notes": null } }, "images": [ { "id": "4b7a…", "isPrimary": true, "caption": null, "sortOrder": 0, "width": 2400, "height": 1600, "bytes": 481221, "thumbnail": "https://…", "regular": "https://…", "large": "https://…", "original": "https://…" } ], "publishedAt": "2026-08-28T09:12:44.000Z" } } ``` ### Retrieve live availability `GET /api/v1/listings/{id}/calendar` Per-day availability, computed on request. The one read that is not snapshot-bound. Per-day availability, computed on request from reservations and calendar blocks. This is the one read that is not snapshot-bound: a booking taken thirty seconds ago is reflected immediately, because a stale calendar double-books a nine-month tenancy. Path parameters: - `id` (string, required): The listing id. It must be published. Query parameters: - `start` (date): Defaults to today in the listing’s timezone. - `end` (date): Inclusive. Defaults to start + 730 days. The range may span at most 1100 days. Response 200: - `days` (CalendarDay[], required) Errors: - 400 `invalid_request`: A date that is not YYYY-MM-DD, end before start, or a span over 1100 days. - 404 `not_found`: Unknown or unpublished listing. `booked` is a confirmed tenancy; `reserved` is held but not yet firm; `unavailable` is a calendar block (renovation, owner use); `available` is bookable. Enquiries and options never block a day. Cancelling releases the days immediately. The end date is exclusive. A tenancy ending on the 30th leaves the 30th available, because the handover is at noon that day. `minNights` is the listing’s base minimum and `isBaseMinNights` is always true today; both are carried so a consumer written against per-day minimums keeps working if they ever vary. Example request: ```sh curl "https://go.quarters.live/api/v1/listings/0d6e1c2a-…/calendar?start=2026-10-01&end=2026-10-04" \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` Example response: ```json { "days": [ { "date": "2026-10-01", "status": "booked", "minNights": 30, "isBaseMinNights": true }, { "date": "2026-10-02", "status": "reserved", "minNights": 30, "isBaseMinNights": true }, { "date": "2026-10-03", "status": "unavailable", "minNights": 30, "isBaseMinNights": true }, { "date": "2026-10-04", "status": "available", "minNights": 30, "isBaseMinNights": true } ] } ``` ## Reference: Reservations Bookings, in every status. Read them to drive contracts, accounting or a guest portal. The embedded property block contains its last published identity. It is null for a property that has never been published; propertyId remains available. Unpublished edits never reach this response. Website visibility does not remove a booking or its published property identity. ### List reservations `GET /api/v1/reservations` Filtered, sorted and paged. `updatedSince` is the incremental-sync filter. Offset pagination on a stable order: the sort key, then id. `total` counts the same filters, so you can page to the end without probing. Query parameters: - `status` (string): Comma-separated: option, reserved, confirmed, cancelled. Default all. - `propertyId` (string): One property. - `startFrom` (date): Inclusive lower bound on the start date. - `startTo` (date): Inclusive upper bound on the start date. - `endFrom` (date): Inclusive lower bound on the end date. - `endTo` (date): Inclusive upper bound on the end date. - `updatedSince` (timestamp): Only bookings changed at or after this instant. The incremental-sync filter: keep the `updatedAt` of the last row you saw and ask from there. - `sort` (enum): Default startDate. - `order` (enum): Default asc. - `limit` (integer): 1–100, default 25. - `skip` (integer): Rows to skip, default 0. Response 200: - `reservations` (Reservation[], required) - `total` (integer, required): Rows matching the same filters, across every page. - `limit` (integer, required) - `skip` (integer, required) Errors: - 400 `invalid_request`: An unknown status, a malformed date or timestamp, a sort key that does not exist. Example request: ```sh curl "https://go.quarters.live/api/v1/reservations?status=reserved,confirmed&startFrom=2026-10-01&limit=50" \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` Example response: ```json { "reservations": [ { "id": "7c02…", "status": "reserved", "propertyId": "0d6e1c2a-…", "property": { "id": "0d6e1c2a-…", "nickname": "JOR-1A", "slug": "jordaan-canal-loft", "address": { "street": "Prinsengracht 123", "postalCode": "1015 DX", "city": "Amsterdam", "country": "NL" }, "ownerName": "A. de Vries", "timezone": "Europe/Amsterdam" }, "guest": { "id": "e31f…", "firstName": "Sofia", "lastName": "Rossi", "name": "Sofia Rossi", "email": "sofia@example.com", "phone": "+39 333 000 0000", "company": null }, "onContract": { "name": "Sofia Rossi", "email": "sofia@example.com", "phone": "+39 333 000 0000", "company": null }, "staying": { "name": "Sofia Rossi", "email": "sofia@example.com", "phone": "+39 333 000 0000", "company": null }, "startDate": "2026-10-01", "endDate": "2027-04-01", "nights": 182, "adults": 2, "children": 0, "infants": 0, "pets": false, "pricing": { "currency": "EUR", "rateBasis": "monthly", "rentCents": 240000, "utilitiesCents": 15000, "furnitureCents": 25000, "monthlyCents": 280000, "depositCents": 240000, "minStayMonths": 6 }, "requiresContract": true, "agreement": null, "dashboardUrl": "https://go.quarters.live/acme/reservations?reservation=7c02…", "optionAt": "2026-09-01T14:20:05.000Z", "reservedAt": "2026-09-08T09:58:40.000Z", "confirmedAt": null, "cancelledAt": null, "createdAt": "2026-09-01T14:20:05.000Z", "updatedAt": "2026-09-08T09:58:40.000Z" } ], "total": 132, "limit": 50, "skip": 0 } ``` ### Retrieve one reservation `GET /api/v1/reservations/{id}` The booking with its guest, property identity, agreed money and current agreement. The booking with its guest, the property’s identity, the money as agreed at booking time, and the current agreement if there is one. `dashboardUrl` opens the same record in the dashboard, for an alert email or a back-office link. Path parameters: - `id` (string, required): The reservation id, from the list or a webhook. Response 200: - `reservation` (Reservation, required) Errors: - 404 `not_found`: Unknown to your organisation. `pricing` is snapshotted when the booking is made, so a later rate change on the listing never rewrites an agreed booking. `rateBasis` says whether `rentCents` is a month or a night; `monthlyCents` is rent + utilities + furniture, present only for a monthly booking and null for a nightly one. `nights` is the end date minus the start date. `minStayMonths` is what the agreement states as the minimum term. `requiresContract` says whether this booking needs a signed agreement before it counts as confirmed; it is resolved at booking time, and where the organisation has booking rules for contracts it follows them until a contract is sent or signed. The lifecycle stamps (`optionAt`, `reservedAt`, `confirmedAt`, `cancelledAt`) record when a status was entered and are never cleared. Example request: ```sh curl "https://go.quarters.live/api/v1/reservations/7c02…" \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` Example response: ```json { "reservation": { "id": "7c02…", "status": "reserved", "propertyId": "0d6e1c2a-…", "property": { "id": "0d6e1c2a-…", "nickname": "JOR-1A", "slug": "jordaan-canal-loft", "address": { "street": "Prinsengracht 123", "postalCode": "1015 DX", "city": "Amsterdam", "country": "NL" }, "ownerName": "A. de Vries", "timezone": "Europe/Amsterdam" }, "guest": { "id": "e31f…", "firstName": "Sofia", "lastName": "Rossi", "name": "Sofia Rossi", "email": "sofia@example.com", "phone": "+39 333 000 0000", "company": null }, "onContract": { "name": "Sofia Rossi", "email": "sofia@example.com", "phone": "+39 333 000 0000", "company": null }, "staying": { "name": "Sofia Rossi", "email": "sofia@example.com", "phone": "+39 333 000 0000", "company": null }, "startDate": "2026-10-01", "endDate": "2027-04-01", "nights": 182, "adults": 2, "children": 0, "infants": 0, "pets": false, "pricing": { "currency": "EUR", "rateBasis": "monthly", "rentCents": 240000, "utilitiesCents": 15000, "furnitureCents": 25000, "monthlyCents": 280000, "depositCents": 240000, "minStayMonths": 6 }, "requiresContract": true, "agreement": null, "dashboardUrl": "https://go.quarters.live/acme/reservations?reservation=7c02…", "optionAt": "2026-09-01T14:20:05.000Z", "reservedAt": "2026-09-08T09:58:40.000Z", "confirmedAt": null, "cancelledAt": null, "createdAt": "2026-09-01T14:20:05.000Z", "updatedAt": "2026-09-08T09:58:40.000Z" } } ``` ## Reference: Agreements The tenancy agreement on a booking, as Quarters holds it. An organisation issues and signs its contracts in Quarters itself, or in the DocuSeal account it has connected under Settings › Contracts; either way the booking carries one current agreement, and this is where you read it. Writing is not offered: the moment an agreement completes, a `reserved` booking becomes `confirmed` and you receive a `reservation.updated` webhook. ### Retrieve the current agreement `GET /api/v1/reservations/{id}/agreement` The booking’s current agreement, or null when none has been issued. The booking’s current agreement, or null when none has been issued. Superseded versions are not listed; the current one carries its `version`. Path parameters: - `id` (string, required): The reservation id. Response 200: - `agreement` (Agreement | null, required) Errors: - 404 `not_found`: Unknown reservation. `provider` says which system issued this envelope and is kept for its lifetime, whatever the organisation chooses for the next one. A Quarters signer never exposes its signing link here; a DocuSeal signer carries the link DocuSeal gave it. `document` is set once the executed PDF has been archived in Quarters, and is the way to it. Example request: ```sh curl "https://go.quarters.live/api/v1/reservations/7c02…/agreement" \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` Example response: ```json { "agreement": { "id": "9a41…", "version": 1, "provider": "docuseal", "externalId": "1849213", "status": "sent", "templateName": null, "sentAt": "2026-09-08T10:02:11.000Z", "completedAt": null, "declinedAt": null, "voidedAt": null, "error": null, "documentUrl": null, "document": null, "signers": [ { "role": "lessor", "name": "Acme Stays B.V.", "email": "hello@example.com", "status": "completed", "signingUrl": null, "viewedAt": null, "completedAt": "2026-09-08T10:02:11.000Z", "declinedAt": null, "declineReason": null }, { "role": "lessee", "name": "Sofia Rossi", "email": "sofia@example.com", "status": "pending", "signingUrl": "https://docuseal.com/s/…", "viewedAt": null, "completedAt": null, "declinedAt": null, "declineReason": null } ], "createdAt": "2026-09-08T10:02:11.000Z", "updatedAt": "2026-09-08T10:02:11.000Z" } } ``` ### Retrieve the archived PDF `GET /api/v1/reservations/{id}/agreement/document` The bytes archived for the current agreement, as application/pdf. The bytes archived for the current agreement, as application/pdf. Path parameters: - `id` (string, required): The reservation id. Response 200: Errors: - 404 `not_found`: Unknown reservation, or no document has been archived for its current agreement. Example request: ```sh curl "https://go.quarters.live/api/v1/reservations/7c02…/agreement/document" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -o agreement.pdf ``` Example response: ```json Content-Type: application/pdf Content-Disposition: inline; filename="agreement-7c02….pdf" ``` ## Reference: Inquiries Your booking form lands here, as a row staff convert to a reservation in one click and resolve to a guest record, rather than as an email somebody re-types. ### Create an inquiry `POST /api/v1/inquiries` A booking request from your form, landing as a row staff act on. Anything your form collects beyond the fields below travels in `metadata` untyped, so the form can evolve without a contract change. Request body: InquiryInput (JSON). Response 201: - `id` (string, required): The inquiry’s id. Errors: - 400 `invalid_request`: A bad field, with a message naming its path. An unknown or unpublished propertyId is a 400 too. - 429 `rate_limited`: More than the organisation’s daily allowance (500 by default) in 24 hours. An inquiry never blocks a day. It becomes a booking when staff convert it. Example request: ```sh curl -X POST "https://go.quarters.live/api/v1/inquiries" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "propertyId": "0d6e1c2a-…", "firstName": "Sofia", "lastName": "Rossi", "email": "sofia@example.com", "phone": "+39 333 000 0000", "startDate": "2026-10-01", "endDate": "2027-04-01", "adults": 2, "children": 0, "infants": 0, "pets": false, "message": "Two of us, relocating for a project.", "locale": "en", "form": "booking", "metadata": { "page": "https://www.example.com/house/…" } }' ``` Example response: ```json { "id": "2c9d…" } ``` ## Reference: Webhook events - `listing.updated`: A property was put on the website, or its changes were published. Data: `{"propertyId":"0d6e1c2a-…"}` - `listing.deleted`: A property was taken off the website or deleted. Drop it from your site. Data: `{"propertyId":"0d6e1c2a-…"}` - `calendar.updated`: Availability changed for a property: a booking, a cancellation, a block. Data: `{"propertyId":"0d6e1c2a-…"}` - `reservation.created`: A booking was created, in any status. Data: `{"reservationId":"7c02…","propertyId":"0d6e1c2a-…","status":"reserved","previousStatus":null}` - `reservation.updated`: A booking changed: its status, dates, guest, money or notes. Data: `{"reservationId":"7c02…","propertyId":"0d6e1c2a-…","status":"confirmed","previousStatus":"reserved"}` - `reservation.deleted`: A booking was deleted outright, which is a data-entry mistake being undone. A cancellation is an update. Data: `{"reservationId":"7c02…","propertyId":"0d6e1c2a-…","status":"option","previousStatus":null}` - `ping`: Somebody pressed the test button in settings. Confirms the endpoint and the signature. Data: `{"orgSlug":"acme","sentAt":"2026-09-13T10:00:00.000Z"}` ## Reference: Objects ### Listing A property as the Publish button froze it. Content being edited right now never appears here; only the calendar is live. - `id` (string, required): Opaque. The same id a reservation’s `propertyId` carries. - `slug` (string, required) - `nickname` (string, required): The operator’s short name for the flat. - `active` (boolean, required): Always true: only listed properties are served, and a listed property is in operation. - `isListed` (boolean, required): Always true. A property taken off the website disappears from this list. - `propertyType` (string, required): Apartment, House, … as the operator classified it. - `roomType` (string, required): Entire home/apt, Private room, … - `address` (Address, required) - `accommodates` (integer, required): How many people the flat sleeps. - `rooms` (Room[], required) - `bedrooms` (integer, required): Derived from `rooms`, carried so a card does not have to reduce an array. - `beds` (integer, required): Derived from `rooms`. - `bathrooms` (number, required): Derived from `rooms`. Fractional for half bathrooms. - `squareMeter` (integer | null, required) - `timezone` (string, required): The IANA zone the listing’s dates are local to. - `pricing` (object, required) - `currency` ("EUR", required) - `rateBasis` (enum, required): Which of `monthlyRateCents` or `nightlyRateCents` this listing actually charges. The other still carries whatever figure the operator set; it is never converted from it. - `monthlyRateCents` (integer | null, required): Monthly rent. Integer euro-cents. - `nightlyRateCents` (integer | null, required): The nightly rate. Integer euro-cents. - `depositCents` (integer | null, required): Security deposit. Integer euro-cents. - `utilitiesCents` (integer | null, required): Monthly utilities. Integer euro-cents. - `furnitureCents` (integer | null, required): Monthly furniture fee. Integer euro-cents. - `terms` (object, required) - `minNights` (integer | null, required): The shortest stay the operator accepts. - `maxNights` (integer | null, required) - `amenities` (string[], required): Amenity keys, e.g. wifi, washer, dishwasher. - `description` (Description, required) - `translations` (map of Description, required): The same six fields per site language, keyed by language code. Fall back to `description` for anything missing. - `images` (Image[], required): Sorted by `sortOrder`; the primary photo first is not guaranteed. - `publishedAt` (timestamp, required): When the Publish button froze this content. ```json { "id": "0d6e1c2a-…", "slug": "jordaan-canal-loft", "nickname": "JOR-1A", "active": true, "isListed": true, "propertyType": "Apartment", "roomType": "Entire home/apt", "address": { "street": "Prinsengracht 123", "postalCode": "1015 DX", "city": "Amsterdam", "country": "NL", "area": "Jordaan", "latitude": 52.3731, "longitude": 4.8837 }, "accommodates": 2, "rooms": [ { "kind": "bedroom", "name": "", "beds": [ { "type": "queen", "count": 1 } ] }, { "kind": "full_bathroom", "name": "", "beds": [] } ], "bedrooms": 1, "beds": 1, "bathrooms": 1, "squareMeter": 58, "timezone": "Europe/Amsterdam", "pricing": { "currency": "EUR", "rateBasis": "monthly", "monthlyRateCents": 240000, "nightlyRateCents": null, "depositCents": 240000, "utilitiesCents": 15000, "furnitureCents": null }, "terms": { "minNights": 30, "maxNights": 365 }, "amenities": [ "wifi", "washer", "dishwasher" ], "description": { "title": "Canal loft in the Jordaan", "summary": "

…

", "space": "

…

", "neighborhood": "

…

", "access": null, "notes": null }, "translations": { "nl": { "title": "…", "summary": "

…

", "space": null, "neighborhood": null, "access": null, "notes": null } }, "images": [ { "id": "4b7a…", "isPrimary": true, "caption": null, "sortOrder": 0, "width": 2400, "height": 1600, "bytes": 481221, "thumbnail": "https://…", "regular": "https://…", "large": "https://…", "original": "https://…" } ], "publishedAt": "2026-08-28T09:12:44.000Z" } ``` ### Address Where the flat is. - `street` (string | null, required) - `postalCode` (string | null, required) - `city` (string | null, required) - `country` (string | null, required): ISO 3166-1 alpha-2. - `area` (string | null, required): The neighbourhood, as the operator names it. - `latitude` (number | null, required) - `longitude` (number | null, required) ```json { "street": "Prinsengracht 123", "postalCode": "1015 DX", "city": "Amsterdam", "country": "NL", "area": "Jordaan", "latitude": 52.3731, "longitude": 4.8837 } ``` ### Room A room of the flat. Bathrooms carry no beds; a living room may. - `kind` (enum, required) - `name` (string, required): Empty when the room has no name of its own. - `beds` (Bed[], required) ```json { "kind": "bedroom", "name": "", "beds": [ { "type": "queen", "count": 1 } ] } ``` ### Bed One kind of bed in a room, with how many of it there are. - `type` (enum, required) - `count` (integer, required) ```json { "type": "queen", "count": 1 } ``` ### Image A photo, in four renditions. URLs are absolute and stable until the listing is re-published. - `id` (string, required) - `isPrimary` (boolean, required): The cover photo. Exactly one image is primary. - `caption` (string | null, required) - `sortOrder` (integer, required): The operator’s order, ascending. - `width` (integer, required): Pixels, of the original. - `height` (integer, required): Pixels, of the original. - `bytes` (integer, required): Size of the original. - `thumbnail` (url, required): Absolute. - `regular` (url, required): Absolute. - `large` (url, required): Absolute. - `original` (url, required): Absolute. ```json { "id": "4b7a…", "isPrimary": true, "caption": null, "sortOrder": 0, "width": 2400, "height": 1600, "bytes": 481221, "thumbnail": "https://…", "regular": "https://…", "large": "https://…", "original": "https://…" } ``` ### Description The six prose fields of a listing. `title` is plain; the rest is the editor’s HTML. - `title` (string | null, required): Plain text. - `summary` (string | null, required): Rich text, as HTML from the editor. - `space` (string | null, required): Rich text. - `neighborhood` (string | null, required): Rich text. - `access` (string | null, required): Rich text. - `notes` (string | null, required): Rich text. ```json { "title": "Canal loft in the Jordaan", "summary": "

…

", "space": "

…

", "neighborhood": "

…

", "access": null, "notes": null } ``` ### CalendarDay One day of a listing’s availability. - `date` (date, required) - `status` (enum, required): `booked` is a confirmed tenancy; `reserved` is held but not yet firm; `unavailable` is a calendar block such as a renovation; `available` is bookable. - `minNights` (integer, required): The listing’s base minimum. - `isBaseMinNights` (boolean, required): Always true today; carried so a consumer written against per-day minimums keeps working if they ever vary. ```json { "date": "2026-10-01", "status": "booked", "minNights": 30, "isBaseMinNights": true } ``` ### Reservation A booking with its guest, the property’s identity, the money as agreed at booking time and the current agreement if there is one. - `id` (string, required): Opaque. - `status` (ReservationStatus, required) - `propertyId` (string, required) - `property` (PropertyIdentity | null, required) - `guest` (Guest, required) - `onContract` (Party, required) - `staying` (Party, required) - `startDate` (date, required) - `endDate` (date, required): Exclusive: the handover is at noon that day. - `nights` (integer, required): The booking’s end minus its start. - `adults` (integer | null, required) - `children` (integer | null, required) - `infants` (integer | null, required) - `pets` (boolean, required) - `pricing` (object, required): Snapshotted when the booking was made. A later rate change on the listing never rewrites an agreed booking. - `currency` ("EUR", required) - `rateBasis` (enum, required): What `rentCents` is per: the property's when the booking was made, or the other where the organisation set the booking to it. - `rentCents` (integer | null, required): Rent as agreed, per `rateBasis`: a month or a night. Integer euro-cents. - `utilitiesCents` (integer | null, required): Monthly utilities as agreed. Integer euro-cents. - `furnitureCents` (integer | null, required): Monthly furniture fee as agreed. Integer euro-cents. - `monthlyCents` (integer | null, required): Rent + utilities + furniture, for a monthly booking. Null for a nightly one. Integer euro-cents. - `depositCents` (integer | null, required): Security deposit as agreed. Integer euro-cents. - `minStayMonths` (integer | null, required): What the agreement states as the minimum term. - `requiresContract` (boolean, required): Whether this booking needs a signed agreement before it counts as confirmed. Resolved at booking time. Where the organisation has booking rules for contracts it follows them until a contract is sent or signed, and `updatedAt` moves when it changes. - `agreement` (Agreement | null, required) - `dashboardUrl` (url, required): Opens the same record in the dashboard, for an alert email or a back-office link. - `optionAt` (timestamp | null, required): When the status was entered. Never cleared. - `reservedAt` (timestamp | null, required): When the status was entered. Never cleared. - `confirmedAt` (timestamp | null, required): When the status was entered. Never cleared. - `cancelledAt` (timestamp | null, required): When the status was entered. Never cleared. - `createdAt` (timestamp, required) - `updatedAt` (timestamp, required): The incremental-sync key: see `updatedSince` on the list. ```json { "id": "7c02…", "status": "reserved", "propertyId": "0d6e1c2a-…", "property": { "id": "0d6e1c2a-…", "nickname": "JOR-1A", "slug": "jordaan-canal-loft", "address": { "street": "Prinsengracht 123", "postalCode": "1015 DX", "city": "Amsterdam", "country": "NL" }, "ownerName": "A. de Vries", "timezone": "Europe/Amsterdam" }, "guest": { "id": "e31f…", "firstName": "Sofia", "lastName": "Rossi", "name": "Sofia Rossi", "email": "sofia@example.com", "phone": "+39 333 000 0000", "company": null }, "onContract": { "name": "Sofia Rossi", "email": "sofia@example.com", "phone": "+39 333 000 0000", "company": null }, "staying": { "name": "Sofia Rossi", "email": "sofia@example.com", "phone": "+39 333 000 0000", "company": null }, "startDate": "2026-10-01", "endDate": "2027-04-01", "nights": 182, "adults": 2, "children": 0, "infants": 0, "pets": false, "pricing": { "currency": "EUR", "rateBasis": "monthly", "rentCents": 240000, "utilitiesCents": 15000, "furnitureCents": 25000, "monthlyCents": 280000, "depositCents": 240000, "minStayMonths": 6 }, "requiresContract": true, "agreement": null, "dashboardUrl": "https://go.quarters.live/acme/reservations?reservation=7c02…", "optionAt": "2026-09-01T14:20:05.000Z", "reservedAt": "2026-09-08T09:58:40.000Z", "confirmedAt": null, "cancelledAt": null, "createdAt": "2026-09-01T14:20:05.000Z", "updatedAt": "2026-09-08T09:58:40.000Z" } ``` ### ReservationStatus `option` blocks nothing; `reserved` is held; `confirmed` is firm; `cancelled` releases its days. - `option` - `reserved` - `confirmed` - `cancelled` ```json reserved ``` ### PropertyIdentity The flat’s identity as last published: name, slug, address and time zone, frozen by Publish like the rest of /listings. `ownerName` alone is read live: the lessor the tenancy agreement names. Null while the flat has never been published. - `id` (string, required) - `nickname` (string, required) - `slug` (string, required) - `address` (object, required) - `street` (string | null, required) - `postalCode` (string | null, required) - `city` (string | null, required) - `country` (string | null, required): ISO 3166-1 alpha-2. - `ownerName` (string | null, required): The lessor, as a tenancy agreement must name them. - `timezone` (string, required) ```json { "id": "0d6e1c2a-…", "nickname": "JOR-1A", "slug": "jordaan-canal-loft", "address": { "street": "Prinsengracht 123", "postalCode": "1015 DX", "city": "Amsterdam", "country": "NL" }, "ownerName": "A. de Vries", "timezone": "Europe/Amsterdam" } ``` ### Guest The contact who made the booking. It may be an agency or an HR desk booking for someone else: `onContract` and `staying` say who. - `id` (string, required) - `firstName` (string | null, required) - `lastName` (string, required) - `name` (string, required): First and last name, joined. - `email` (string | null, required) - `phone` (string | null, required) - `company` (string | null, required) ```json { "id": "e31f…", "firstName": "Sofia", "lastName": "Rossi", "name": "Sofia Rossi", "email": "sofia@example.com", "phone": "+39 333 000 0000", "company": null } ``` ### Party Who a booking names for one role: `onContract` is who the tenancy agreement is made out to, `staying` who stays in the flat. The contact, unless the booking says otherwise. - `name` (string, required): First and last name, joined. Empty when the booking names the role but not the person. - `email` (string | null, required) - `phone` (string | null, required) - `company` (string | null, required) ```json { "name": "Sofia Rossi", "email": "sofia@example.com", "phone": "+39 333 000 0000", "company": null } ``` ### Agreement The tenancy agreement on a booking, in its current version. - `id` (string, required) - `version` (integer, required): Counts up on every re-issue. Superseded versions are not listed. - `provider` (enum, required): `quarters` for an envelope Quarters rendered and signed itself, `docuseal` for one created in the organisation’s DocuSeal account, `guesty` for a record imported from Guesty. Each envelope keeps the provider it was issued with. - `externalId` (string | null, required): The DocuSeal submission id, or the Guesty reservation id; null for a Quarters envelope. - `status` (enum, required) - `templateName` (string | null, required): The Quarters template used; null for a DocuSeal envelope. - `sentAt` (timestamp | null, required) - `completedAt` (timestamp | null, required) - `declinedAt` (timestamp | null, required) - `voidedAt` (timestamp | null, required) - `error` (string | null, required): Why it failed. Only kept while `status` is `failed`. - `documentUrl` (string | null, required): The provider’s own copy of the executed document, where it keeps a lasting link. Null for DocuSeal, whose links expire within the hour. - `document` (object | null, required): Set once the executed PDF has been archived in Quarters: sealed at signing for its own envelopes, fetched from DocuSeal for those. - `signers` (Signer[], required) - `createdAt` (timestamp, required) - `updatedAt` (timestamp, required) ```json { "id": "9a41…", "version": 1, "provider": "docuseal", "externalId": "1849213", "status": "sent", "templateName": null, "sentAt": "2026-09-08T10:02:11.000Z", "completedAt": null, "declinedAt": null, "voidedAt": null, "error": null, "documentUrl": null, "document": null, "signers": [ { "role": "lessor", "name": "Acme Stays B.V.", "email": "hello@example.com", "status": "completed", "signingUrl": null, "viewedAt": null, "completedAt": "2026-09-08T10:02:11.000Z", "declinedAt": null, "declineReason": null }, { "role": "lessee", "name": "Sofia Rossi", "email": "sofia@example.com", "status": "pending", "signingUrl": "https://docuseal.com/s/…", "viewedAt": null, "completedAt": null, "declinedAt": null, "declineReason": null } ], "createdAt": "2026-09-08T10:02:11.000Z", "updatedAt": "2026-09-08T10:02:11.000Z" } ``` ### Signer One party to the agreement. - `role` (string, required): lessee, lessor, guarantor, witness, or a role of the template’s own. - `name` (string, required) - `email` (string, required) - `status` (enum, required) - `signingUrl` (string | null, required): Only for a DocuSeal envelope: the party’s signing page there. A Quarters signing link is a secret and never leaves the app. - `viewedAt` (timestamp | null, required) - `completedAt` (timestamp | null, required) - `declinedAt` (timestamp | null, required) - `declineReason` (string | null, required) ```json { "role": "lessee", "name": "Sofia Rossi", "email": "sofia@example.com", "status": "pending", "signingUrl": "https://docuseal.com/s/…", "viewedAt": null, "completedAt": null, "declinedAt": null, "declineReason": null } ``` ### InquiryInput A booking request from your form. Text is trimmed. - `propertyId` (string): Must be one of your published listings, so a typo fails loudly rather than creating an orphan. - `firstName` (string) - `lastName` (string, required) - `email` (email, required) - `phone` (string) - `company` (string): The sender’s company, for an HR desk or an agency asking on someone’s behalf. - `staying` (object): Who will stay, when the sender books for someone else: an employee, a relative. The sender stays the contact staff deal with; the booking made from this starts with this person as the one staying. - `firstName` (string) - `lastName` (string, required) - `email` (email) - `phone` (string) - `company` (string) - `startDate` (date) - `endDate` (date): Must be after startDate. - `adults` (integer): The tenants picker, one field per age band. - `children` (integer) - `infants` (integer) - `pets` (boolean) - `message` (string) - `locale` (string): Which language they were reading. - `source` (string): Where the form lives. Defaults to website. - `form` (enum): Which kind of form sent it: booking, contact, brochure or waitlist. `source` follows the form, and the confirmation uses that form’s wording from Website > Booking. Without it, a request naming a property or dates is a booking, anything else a contact message. - `sendConfirmation` (boolean): Lets the guest have a confirmation that their request arrived. It goes only when the organisation switched on that form’s reply in Website > Booking, in its wording there, in `locale`, once per inquiry. `metadata.page` and `metadata.brochureUrl` fill its property link and brochure link. Send false when your own system already confirms. - `metadata` (object): Anything else your form collects. At most 10 KB serialised. Travels untyped, so the form can evolve without a contract change. ```json { "propertyId": "0d6e1c2a-…", "firstName": "Sofia", "lastName": "Rossi", "email": "sofia@example.com", "phone": "+39 333 000 0000", "startDate": "2026-10-01", "endDate": "2027-04-01", "adults": 2, "children": 0, "infants": 0, "pets": false, "message": "Two of us, relocating for a project.", "locale": "en", "form": "booking", "metadata": { "page": "https://www.example.com/house/…" } } ``` ### TokenRequest The form posted to the token endpoint. HTTP Basic with client_id:client_secret is accepted in place of the two fields, and so is the same form as a JSON object. - `grant_type` ("client_credentials", required): Always client_credentials. - `client_id` (string, required): The client ID from Settings › Integrations, e.g. qc_… - `client_secret` (string, required): The secret shown once when the credentials were created, e.g. qs_… ```json { "grant_type": "client_credentials", "client_id": "qc_YOUR_CLIENT_ID", "client_secret": "qs_YOUR_CLIENT_SECRET" } ``` ### TokenResponse A bearer token, good for 24 hours. - `token_type` ("Bearer", required) - `access_token` (string, required): The bearer, e.g. qat_… Send it as Authorization: Bearer on every call. - `expires_in` (integer, required): Seconds until the token expires. Always 86400. ```json { "token_type": "Bearer", "access_token": "qat_…", "expires_in": 86400 } ``` ### Error Every failure on a resource endpoint. A 401 also carries WWW-Authenticate: Bearer. - `error` (string, required): A stable slug for the failure class. Switch on it. - `message` (string, required): A sentence for a person. Names the field where one is at fault. ```json { "error": "invalid_request", "message": "Invalid body at \"endDate\": endDate must be after startDate." } ``` ### OAuthError The token endpoint fails in RFC 6749’s own shape rather than the API’s, because that is what an OAuth client library reads. - `error` (string, required): An RFC 6749 error code. - `error_description` (string, required): A sentence for a person. ```json { "error": "invalid_client", "error_description": "Unknown client id, wrong secret, or revoked." } ```