Build on the Stayolo API
Read and write your inventory, quote a stay and take bookings from your own software. The API uses the same pricing, availability, rental agreement and payment pipeline as the public booking site.
OpenAPI 3.1.0 · version 1.0.0 · 13 documented operations
Overview
The v1 API covers the inventory behind a Stayolo property: base pricing, seasonal rates, per-night availability, a server-side quote and the booking itself. Property creation is deliberately a dashboard flow, so there is no write endpoint for a new property.
Property
A listing with a stable id and slug, capacity, currency, base pricing, booking mode and payment mode.
Rates
Seasonal overrides on top of the base nightly price. Replacing the rate set is a full, idempotent replace.
Availability
One record per night. Each night says whether it is available and who holds it: direct, airbnb, api and so on.
Quote
A server-calculated price for a stay. It uses the same function and rules as the booking endpoint.
Booking
A reservation created through the public booking handler: same price, availability, agreement, Stripe checkout and emails.
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 1. Import rates │────▶│ 2. Block nights │────▶│ 3. Quote a stay │────▶│ 4. Create booking│
│ PUT /rates │ │ PUT /availability│ │ GET /quote │ │ POST /bookings │
└──────────────────┘ └──────────────────┘ └──────────────────┘ └──────────────────┘
seasonal prices api / OTA / direct price + payment same public site
and minimum stays holds per night breakdown + checks pipeline and paymentRates and availability can be written independently. A quote never creates a hold; only POST /bookings does.
Authentication
Send a key in the Authorization header as a bearer token. Keys start with sk_live_ or sk_test_. The secret is shown once when the key is created; Stayolo stores only its SHA-256 hash.
curl https://stayolo.com/api/v1/properties \
-H "Authorization: Bearer sk_live_..."Never put a key in the URL
An ?api_key= or ?key= parameter is rejected with 400 key_in_query. URLs end up in logs, browser history and referrers.
Missing, invalid or revoked keys
A missing bearer token returns 401 missing_key. An unknown prefix or a revoked key returns 401 invalid_key.
Scopes
| Scope | Allows |
|---|---|
| read | Read properties, rates, availability, quotes and bookings. |
| rates:write | Update base pricing and replace seasonal rate overrides. |
| availability:write | Block or reopen nights owned by the API integration. |
| bookings:write | Create bookings through the same pipeline as the public site. |
A key without the required scope receives 403 insufficient_scope with details: { required, granted[] }.
Conventions
- Success envelope
- Every
200or201response is{ "data": ... }. - Error envelope
- Errors are
{ "error": { code, message, details? } }. - Caching
- Every response carries
cache-control: no-store. - Lists
/propertiesand/bookingsreturnx-total-countwith the number of rows returned.- Dates
- Calendar dates use
YYYY-MM-DD. Impossible dates such as2026-13-99are rejected. - Money
- Amounts are decimal numbers in the property currency, an ISO-4217 code.
- Property references
- Every
{idOrSlug}accepts either aprop_...id or a slug. - Base URL
https://stayolo.com
Health
Unauthenticated readiness probes for deploys and load balancers.
GET/api/v1/health
PublicCheck API readiness
Unauthenticated deploy probe. Reports only whether the app is up, which API version it speaks, and whether the tables v1 needs exist. The probe is limited to 120 requests per minute per IP, has a two second internal timeout, and never exposes hostnames, driver versions or counters.
Example request
curl -s https://stayolo.com/api/v1/health -H "Authorization: Bearer $STAYOLO_API_KEY"Response 200
{
"data": {
"status": "ok",
"apiVersion": "v1",
"checkedAt": "2026-11-06T16:25:48.962Z",
"checks": {
"database": "ok",
"migrations": "ok"
}
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | The per-IP request limit was exceeded. Wait for the number of seconds in retry-after. |
| 503 | not_ready | The database is unreachable or the v1 migration has not run. |
Properties
Read the properties in the account and update their base pricing.
GET/api/v1/properties
Scope readList properties
Returns every property owned by the API key's account, optionally restricted to published listings. The payload omits the street address; use GET /properties/{idOrSlug} for the detail record. The x-total-count header repeats the number of rows returned.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| published | query | boolean | no | When true, only published properties are returned. |
Example request
curl -s "https://stayolo.com/api/v1/properties?published=true" -H "Authorization: Bearer $STAYOLO_API_KEY"Response 200
{
"data": [
{
"id": "prop_8f3c2a1b9d",
"slug": "casa-do-mar",
"name": "Casa do Mar",
"location": "Comporta, Portugal",
"propertyType": "villa",
"published": true,
"bookable": true,
"factsVerified": true,
"capacity": {
"guests": 6,
"bedrooms": 3,
"beds": 4,
"bathrooms": 2
},
"amenities": [
"Wi-Fi",
"Private pool",
"Sea view",
"Parking"
],
"currency": "EUR",
"pricing": {
"nightly": 180,
"cleaningFee": 60,
"taxRate": 0.06,
"minStay": 3,
"maxStay": 30,
"weeklyDiscountPct": 10,
"monthlyDiscountPct": 20,
"includedGuests": 4,
"extraGuestFeePerNight": 25,
"overrides": [
{
"label": "Summer",
"startDate": "2026-07-01",
"endDate": "2026-08-31",
"pricePerNight": 260,
"minStay": 5,
"adjustPct": null
},
{
"label": "Christmas",
"startDate": "2026-12-20",
"endDate": "2027-01-03",
"pricePerNight": null,
"minStay": 4,
"adjustPct": 35
}
]
},
"booking": {
"mode": "instant",
"paymentMode": "deposit",
"depositPercent": 30,
"balanceDueDays": 14,
"checkIn": "15:00",
"checkOut": "11:00"
},
"links": {
"site": "https://stayolo.com/site/casa-do-mar",
"embed": "https://stayolo.com/embed/casa-do-mar"
},
"createdAt": 1735689600000
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 400 | key_in_query | The request was rejected before a resource could be read, for example because an API key was supplied in the query string. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 401 | missing_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 403 | insufficient_scope | The key is valid but does not carry the scope this operation requires. |
| 429 | rate_limited | The per-key or per-IP request limit was exceeded. Wait for the number of seconds in retry-after. |
POST/api/v1/properties
PublicCreate a property (not allowed)
Creating a property is a dashboard flow with photos, verified facts and a publish gate, not a single API call. This method always answers 405 before authentication, so it is listed here only to make the absence explicit.
Example request
curl -s -X POST https://stayolo.com/api/v1/properties -H "Authorization: Bearer $STAYOLO_API_KEY"Response 200
{
"error": {
"code": "method_not_allowed",
"message": "Create properties in the Stayolo dashboard; this API manages the ones you already have."
}
}Errors
| Status | Code | When |
|---|---|---|
| 405 | method_not_allowed | This collection does not accept POST. |
GET/api/v1/properties/{idOrSlug}
Scope readGet a property
Returns one property by id or slug, including its street address and current pricing overrides. A property belonging to another account is reported as 404 so the API never confirms that an id exists.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| — | — | — | no | — |
Example request
curl -s https://stayolo.com/api/v1/properties/casa-do-mar -H "Authorization: Bearer $STAYOLO_API_KEY"Response 200
{
"data": {
"id": "prop_8f3c2a1b9d",
"slug": "casa-do-mar",
"name": "Casa do Mar",
"location": "Comporta, Portugal",
"address": "Rua da Praia 14, 7580-123 Comporta, Portugal",
"propertyType": "villa",
"published": true,
"bookable": true,
"factsVerified": true,
"capacity": {
"guests": 6,
"bedrooms": 3,
"beds": 4,
"bathrooms": 2
},
"amenities": [
"Wi-Fi",
"Private pool",
"Sea view",
"Parking"
],
"currency": "EUR",
"pricing": {
"nightly": 180,
"cleaningFee": 60,
"taxRate": 0.06,
"minStay": 3,
"maxStay": 30,
"weeklyDiscountPct": 10,
"monthlyDiscountPct": 20,
"includedGuests": 4,
"extraGuestFeePerNight": 25,
"overrides": [
{
"label": "Summer",
"startDate": "2026-07-01",
"endDate": "2026-08-31",
"pricePerNight": 260,
"minStay": 5,
"adjustPct": null
},
{
"label": "Christmas",
"startDate": "2026-12-20",
"endDate": "2027-01-03",
"pricePerNight": null,
"minStay": 4,
"adjustPct": 35
}
]
},
"booking": {
"mode": "instant",
"paymentMode": "deposit",
"depositPercent": 30,
"balanceDueDays": 14,
"checkIn": "15:00",
"checkOut": "11:00"
},
"links": {
"site": "https://stayolo.com/site/casa-do-mar",
"embed": "https://stayolo.com/embed/casa-do-mar"
},
"createdAt": 1735689600000
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | key_in_query | The request was rejected before a resource could be read, for example because an API key was supplied in the query string. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 401 | missing_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 403 | insufficient_scope | The key is valid but does not carry the scope this operation requires. |
| 404 | not_found | No matching property in this account. |
| 429 | rate_limited | The per-key or per-IP request limit was exceeded. Wait for the number of seconds in retry-after. |
PATCH/api/v1/properties/{idOrSlug}
Scope rates:writeUpdate property pricing
Updates base pricing and stay constraints on one property; omitted fields keep their current value. The body is strict and must contain at least one known field, and the resulting minStay may not exceed maxStay. Returns the updated property detail.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| — | — | — | no | — |
Request bodyrequired
| Field | Type | Required | Description |
|---|---|---|---|
| pricePerNight | number | no | — |
| cleaningFee | number | no | — |
| taxRate | number | no | — |
| minStay | integer | no | — |
| maxStay | integer | no | — |
| weeklyDiscountPct | integer | no | — |
| monthlyDiscountPct | integer | no | — |
| includedGuests | integer | no | — |
| extraGuestFeePerNight | number | no | — |
Example request
curl -s -X PATCH https://stayolo.com/api/v1/properties/casa-do-mar -H "Authorization: Bearer $STAYOLO_API_KEY" -H "Content-Type: application/json" -d '{"pricePerNight":195,"minStay":4,"weeklyDiscountPct":12}'Example body
{
"pricePerNight": 195,
"minStay": 4,
"weeklyDiscountPct": 12
}Response 200
{
"data": {
"id": "prop_8f3c2a1b9d",
"slug": "casa-do-mar",
"name": "Casa do Mar",
"location": "Comporta, Portugal",
"address": "Rua da Praia 14, 7580-123 Comporta, Portugal",
"propertyType": "villa",
"published": true,
"bookable": true,
"factsVerified": true,
"capacity": {
"guests": 6,
"bedrooms": 3,
"beds": 4,
"bathrooms": 2
},
"amenities": [
"Wi-Fi",
"Private pool",
"Sea view",
"Parking"
],
"currency": "EUR",
"pricing": {
"nightly": 195,
"cleaningFee": 60,
"taxRate": 0.06,
"minStay": 4,
"maxStay": 30,
"weeklyDiscountPct": 12,
"monthlyDiscountPct": 20,
"includedGuests": 4,
"extraGuestFeePerNight": 25,
"overrides": [
{
"label": "Summer",
"startDate": "2026-07-01",
"endDate": "2026-08-31",
"pricePerNight": 260,
"minStay": 5,
"adjustPct": null
},
{
"label": "Christmas",
"startDate": "2026-12-20",
"endDate": "2027-01-03",
"pricePerNight": null,
"minStay": 4,
"adjustPct": 35
}
]
},
"booking": {
"mode": "instant",
"paymentMode": "deposit",
"depositPercent": 30,
"balanceDueDays": 14,
"checkIn": "15:00",
"checkOut": "11:00"
},
"links": {
"site": "https://stayolo.com/site/casa-do-mar",
"embed": "https://stayolo.com/embed/casa-do-mar"
},
"createdAt": 1735689600000
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | key_in_query | The request was rejected before a resource could be read, for example because an API key was supplied in the query string. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 401 | missing_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 403 | insufficient_scope | The key is valid but does not carry the scope this operation requires. |
| 404 | not_found | No matching property in this account. |
| 422 | empty_body | The body is empty, has unknown or invalid fields, or produces an impossible stay range. |
| 422 | invalid_body | The body is empty, has unknown or invalid fields, or produces an impossible stay range. |
| 422 | invalid_stay_range | The body is empty, has unknown or invalid fields, or produces an impossible stay range. |
| 429 | rate_limited | The per-key or per-IP request limit was exceeded. Wait for the number of seconds in retry-after. |
Rates
Read and replace the seasonal pricing overrides of a property.
GET/api/v1/properties/{idOrSlug}/rates
Scope readGet property rates
Returns the base nightly rate together with every seasonal override stored for the property. Overrides are ordered by startDate.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| — | — | — | no | — |
Example request
curl -s https://stayolo.com/api/v1/properties/casa-do-mar/rates -H "Authorization: Bearer $STAYOLO_API_KEY"Response 200
{
"data": {
"propertyId": "prop_8f3c2a1b9d",
"slug": "casa-do-mar",
"currency": "EUR",
"nightly": 180,
"overrides": [
{
"label": "Summer",
"startDate": "2026-07-01",
"endDate": "2026-08-31",
"pricePerNight": 260,
"minStay": 5,
"adjustPct": null
},
{
"label": "Christmas",
"startDate": "2026-12-20",
"endDate": "2027-01-03",
"pricePerNight": null,
"minStay": 4,
"adjustPct": 35
}
]
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | key_in_query | The request was rejected before a resource could be read, for example because an API key was supplied in the query string. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 401 | missing_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 403 | insufficient_scope | The key is valid but does not carry the scope this operation requires. |
| 404 | not_found | No matching property in this account. |
| 429 | rate_limited | The per-key or per-IP request limit was exceeded. Wait for the number of seconds in retry-after. |
PUT/api/v1/properties/{idOrSlug}/rates
Scope rates:writeReplace property rates
Replaces the complete seasonal override table for one property: ranges absent from the body are deleted. Up to 400 ranges are accepted, each declaring at least one of pricePerNight, adjustPct or minStay, and overlapping ranges are refused. Repeated calls with the same body are idempotent.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| — | — | — | no | — |
Request bodyrequired
| Field | Type | Required | Description |
|---|---|---|---|
| rates | object[] | yes | — |
| rates[].label | string | no | — |
| rates[].startDate | string | yes | — |
| rates[].endDate | string | yes | — |
| rates[].pricePerNight | numbernull | no | — |
| rates[].minStay | integernull | no | — |
| rates[].adjustPct | integernull | no | — |
Example request
curl -s -X PUT https://stayolo.com/api/v1/properties/casa-do-mar/rates -H "Authorization: Bearer $STAYOLO_API_KEY" -H "Content-Type: application/json" -d '{"rates":[{"label":"Summer","startDate":"2026-07-01","endDate":"2026-08-31","pricePerNight":260,"minStay":5},{"label":"Christmas","startDate":"2026-12-20","endDate":"2027-01-03","adjustPct":35,"minStay":4}]}'Example body
{
"rates": [
{
"label": "Summer",
"startDate": "2026-07-01",
"endDate": "2026-08-31",
"pricePerNight": 260,
"minStay": 5
},
{
"label": "Christmas",
"startDate": "2026-12-20",
"endDate": "2027-01-03",
"adjustPct": 35,
"minStay": 4
}
]
}Response 200
{
"data": {
"propertyId": "prop_8f3c2a1b9d",
"slug": "casa-do-mar",
"currency": "EUR",
"nightly": 180,
"overrides": [
{
"label": "Summer",
"startDate": "2026-07-01",
"endDate": "2026-08-31",
"pricePerNight": 260,
"minStay": 5,
"adjustPct": null
},
{
"label": "Christmas",
"startDate": "2026-12-20",
"endDate": "2027-01-03",
"pricePerNight": null,
"minStay": 4,
"adjustPct": 35
}
]
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | key_in_query | The request was rejected before a resource could be read, for example because an API key was supplied in the query string. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 401 | missing_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 403 | insufficient_scope | The key is valid but does not carry the scope this operation requires. |
| 404 | not_found | No matching property in this account. |
| 422 | invalid_body | The body is invalid or two ranges overlap. |
| 422 | overlapping_ranges | The body is invalid or two ranges overlap. |
| 429 | rate_limited | The per-key or per-IP request limit was exceeded. Wait for the number of seconds in retry-after. |
Availability
Read the nightly calendar and additively block or release nights owned by the API.
GET/api/v1/properties/{idOrSlug}/availability
Scope readGet property availability
Returns one row per night in the requested window with the price the pricing engine would charge and who holds each unavailable night. The window defaults to today through today + 90 days, must be at most 366 nights, and to is exclusive.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| — | — | — | no | — |
| — | — | — | no | — |
| — | — | — | no | — |
Example request
curl -s "https://stayolo.com/api/v1/properties/casa-do-mar/availability?from=2026-12-01&to=2026-12-04" -H "Authorization: Bearer $STAYOLO_API_KEY"Response 200
{
"data": {
"propertyId": "prop_8f3c2a1b9d",
"slug": "casa-do-mar",
"from": "2026-12-01",
"to": "2026-12-04",
"currency": "EUR",
"days": [
{
"date": "2026-12-01",
"available": true,
"blockedBy": null,
"price": 180,
"minStay": 3,
"currency": "EUR"
},
{
"date": "2026-12-02",
"available": true,
"blockedBy": null,
"price": 180,
"minStay": 3,
"currency": "EUR"
},
{
"date": "2026-12-03",
"available": false,
"blockedBy": "airbnb",
"price": 180,
"minStay": 3,
"currency": "EUR"
}
]
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | key_in_query | The request was rejected before a resource could be read, for example because an API key was supplied in the query string. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 401 | missing_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 403 | insufficient_scope | The key is valid but does not carry the scope this operation requires. |
| 404 | not_found | No matching property in this account. |
| 422 | invalid_range | The requested window is empty or too long. |
| 422 | range_too_long | The requested window is empty or too long. |
| 429 | rate_limited | The per-key or per-IP request limit was exceeded. Wait for the number of seconds in retry-after. |
PUT/api/v1/properties/{idOrSlug}/availability
Scope availability:writeUpdate property availability
Applies an additive set of calendar changes for nights owned by the api source. available=false blocks a free night, while available=true releases only a night previously blocked by this API; nights sold by a booking or imported from a channel are skipped and reported. Existing non-API holds are never overwritten.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| — | — | — | no | — |
Request bodyrequired
| Field | Type | Required | Description |
|---|---|---|---|
| dates | object[] | yes | — |
| dates[].date | string | yes | — |
| dates[].available | boolean | yes | — |
Example request
curl -s -X PUT https://stayolo.com/api/v1/properties/casa-do-mar/availability -H "Authorization: Bearer $STAYOLO_API_KEY" -H "Content-Type: application/json" -d '{"dates":[{"date":"2026-12-05","available":false},{"date":"2026-12-06","available":false},{"date":"2026-12-07","available":true}]}'Example body
{
"dates": [
{
"date": "2026-12-05",
"available": false
},
{
"date": "2026-12-06",
"available": false
},
{
"date": "2026-12-07",
"available": true
}
]
}Response 200
{
"data": {
"propertyId": "prop_8f3c2a1b9d",
"slug": "casa-do-mar",
"applied": {
"blocked": [
"2026-12-05",
"2026-12-06"
],
"opened": [
"2026-12-07"
]
},
"skipped": [
{
"date": "2026-12-03",
"reason": "held by airbnb"
}
]
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | key_in_query | The request was rejected before a resource could be read, for example because an API key was supplied in the query string. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 401 | missing_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 403 | insufficient_scope | The key is valid but does not carry the scope this operation requires. |
| 404 | not_found | No matching property in this account. |
| 422 | invalid_body | The body is invalid or contains a date that is not a real calendar date. |
| 422 | invalid_date | The body is invalid or contains a date that is not a real calendar date. |
| 429 | rate_limited | The per-key or per-IP request limit was exceeded. Wait for the number of seconds in retry-after. |
Quotes
Price a stay with the same engine and rules used when a booking is created.
GET/api/v1/properties/{idOrSlug}/quote
Scope readQuote a stay
Prices a stay with the same function POST /bookings uses, so the quoted total is the amount that will be charged. It returns per-night rates, fees, tax, promo or offer discounting, the deposit plan and the availability of every night. Promo and offer discounts do not stack; the larger one wins.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| — | — | — | no | — |
| checkIn | query | string | yes | Check-in date as YYYY-MM-DD. |
| checkOut | query | string | yes | Check-out date as YYYY-MM-DD. |
| guests | query | integer | no | Number of guests (integer of at least 1, default 1). |
| promoCode | query | string | no | Promo code to apply to this quote. |
| offerToken | query | string | no | Token of a personalised offer to apply. |
Example request
curl -s "https://stayolo.com/api/v1/properties/casa-do-mar/quote?checkIn=2026-12-01&checkOut=2026-12-04&guests=4&promoCode=WINTER10" -H "Authorization: Bearer $STAYOLO_API_KEY"Response 200
{
"data": {
"propertyId": "prop_8f3c2a1b9d",
"slug": "casa-do-mar",
"currency": "EUR",
"checkIn": "2026-12-01",
"checkOut": "2026-12-04",
"nights": 3,
"guests": 4,
"breakdown": {
"nightlyRates": [
{
"date": "2026-12-01",
"rate": 180,
"reason": "Base rate"
},
{
"date": "2026-12-02",
"rate": 180,
"reason": "Base rate"
},
{
"date": "2026-12-03",
"rate": 180,
"reason": "Base rate"
}
],
"subtotal": 540,
"cleaningFee": 60,
"tax": 29.16,
"extraGuestFee": 0,
"discountPercent": 10,
"discountAmount": 54,
"total": 575.16
},
"promoCode": "WINTER10",
"offerApplied": false,
"minStay": 3,
"payment": {
"mode": "deposit",
"upfront": 172.55,
"balance": 402.61,
"balanceDueDays": 14
},
"availability": {
"available": true,
"nights": [
{
"date": "2026-12-01",
"available": true,
"blockedBy": null
},
{
"date": "2026-12-02",
"available": true,
"blockedBy": null
},
{
"date": "2026-12-03",
"available": true,
"blockedBy": null
}
]
}
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | key_in_query | The request was rejected before a resource could be read, for example because an API key was supplied in the query string. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 401 | missing_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 403 | insufficient_scope | The key is valid but does not carry the scope this operation requires. |
| 404 | not_found | No matching property in this account. |
| 409 | offer_expired | The offer cannot be used. |
| 409 | offer_unavailable | The offer cannot be used. |
| 422 | invalid_dates | The dates, guest count, stay length or promo code are invalid. |
| 422 | invalid_guests | The dates, guest count, stay length or promo code are invalid. |
| 422 | promo_expired | The dates, guest count, stay length or promo code are invalid. |
| 422 | promo_invalid | The dates, guest count, stay length or promo code are invalid. |
| 422 | promo_min_stay | The dates, guest count, stay length or promo code are invalid. |
| 422 | stay_length | The dates, guest count, stay length or promo code are invalid. |
| 422 | too_many_guests | The dates, guest count, stay length or promo code are invalid. |
| 429 | rate_limited | The per-key or per-IP request limit was exceeded. Wait for the number of seconds in retry-after. |
Bookings
Create reservations through the public booking pipeline and read the account's reservation feed.
GET/api/v1/bookings
Scope readList bookings
Returns the account's reservations ordered by createdAt descending, with propertySlug and propertyName included. from/to filter by overlap (checkOut >= from, checkIn <= to) and limit is capped at 200. The x-total-count header repeats the number of rows returned.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| — | — | — | no | — |
| — | — | — | no | — |
| status | query | string (inquiry | pending | confirmed | paid | partially_paid | cancelled | completed) | no | Only return bookings with this status. |
| — | — | — | no | — |
Example request
curl -s "https://stayolo.com/api/v1/bookings?from=2026-12-01&to=2026-12-31&status=pending&limit=50" -H "Authorization: Bearer $STAYOLO_API_KEY"Response 200
{
"data": [
{
"ref": "1F2E3D4C",
"status": "pending",
"source": "api",
"propertyId": "prop_8f3c2a1b9d",
"checkIn": "2026-12-01",
"checkOut": "2026-12-04",
"nights": 3,
"guests": 4,
"total": 575.16,
"currency": "EUR",
"paidAmount": 0,
"balanceDueAmount": 402.61,
"balanceDueDate": "2026-11-17",
"promoCode": "WINTER10",
"createdAt": 1762350000000,
"propertySlug": "casa-do-mar",
"propertyName": "Casa do Mar"
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 400 | key_in_query | The request was rejected before a resource could be read, for example because an API key was supplied in the query string. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 401 | missing_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 403 | insufficient_scope | The key is valid but does not carry the scope this operation requires. |
| 429 | rate_limited | The per-key or per-IP request limit was exceeded. Wait for the number of seconds in retry-after. |
POST/api/v1/bookings
Scope bookings:writeCreate a booking
Creates a reservation through the same handler used by the public booking page: identical pricing, availability, rental agreement, Stripe checkout and emails. An optional Idempotency-Key makes retries safe: the same payload replays the original response, a different payload is refused, and a concurrent attempt reports that the key is in progress. The status is pending when a payment is required and inquiry otherwise.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | no | Optional key of at most 190 characters. Reusing it with the same payload replays the original response; reusing it with a different payload returns idempotency_key_reused. |
Request bodyrequired
| Field | Type | Required | Description |
|---|---|---|---|
| property | string | yes | Property id or slug. |
| checkIn | string | yes | — |
| checkOut | string | yes | — |
| guests | integer | yes | — |
| guest | object | yes | — |
| guest.name | string | yes | — |
| guest.email | string | yes | — |
| guest.phone | string | no | — |
| guest.country | string | no | — |
| termsAccepted | boolean | yes | Must be true. A false value is refused with the terms_required error code. |
| promoCode | string | no | — |
| offerToken | string | no | — |
| agreementAccepted | boolean | no | — |
| roomId | string | no | The room to sell. Send it whenever the property reports more than one room; booking the property instead would close every room for one guest. |
Example request
curl -s -X POST https://stayolo.com/api/v1/bookings -H "Authorization: Bearer $STAYOLO_API_KEY" -H "Content-Type: application/json" -H "Idempotency-Key: casa-do-mar-dec-2026-0001" -d '{"property":"casa-do-mar","checkIn":"2026-12-01","checkOut":"2026-12-04","guests":4,"guest":{"name":"Ana Martins","email":"[email protected]","phone":"+351 912 345 678","country":"PT"},"termsAccepted":true,"promoCode":"WINTER10"}'Example body
{
"property": "casa-do-mar",
"checkIn": "2026-12-01",
"checkOut": "2026-12-04",
"guests": 4,
"guest": {
"name": "Ana Martins",
"email": "[email protected]",
"phone": "+351 912 345 678",
"country": "PT"
},
"termsAccepted": true,
"promoCode": "WINTER10"
}Response 201
{
"data": {
"ref": "1F2E3D4C",
"status": "pending",
"total": 575.16,
"currency": "EUR",
"chargeTotal": 172.55,
"paidAmount": 0,
"balanceDueAmount": 402.61,
"balanceDueDate": "2026-11-17",
"checkIn": "2026-12-01",
"checkOut": "2026-12-04",
"guests": 4,
"paymentUrl": "https://checkout.stripe.com/c/pay/cs_test_casa_do_mar",
"mode": "payment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | key_in_query | The request was rejected before a resource could be read, for example because an API key was supplied in the query string. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 401 | missing_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 403 | insufficient_scope | The key is valid but does not carry the scope this operation requires. |
| 404 | not_found | No matching property in this account. |
| 409 | agreement_required | The rental agreement, inventory, or idempotency key refuses the booking. |
| 409 | conflict | The rental agreement, inventory, or idempotency key refuses the booking. |
| 409 | idempotency_in_progress | The rental agreement, inventory, or idempotency key refuses the booking. |
| 409 | idempotency_key_reused | The rental agreement, inventory, or idempotency key refuses the booking. |
| 422 | invalid_body | The body is invalid, terms were not accepted, or the stay or promo cannot be sold. |
| 422 | invalid_dates | The body is invalid, terms were not accepted, or the stay or promo cannot be sold. |
| 422 | promo_expired | The body is invalid, terms were not accepted, or the stay or promo cannot be sold. |
| 422 | promo_invalid | The body is invalid, terms were not accepted, or the stay or promo cannot be sold. |
| 422 | promo_min_stay | The body is invalid, terms were not accepted, or the stay or promo cannot be sold. |
| 422 | stay_length | The body is invalid, terms were not accepted, or the stay or promo cannot be sold. |
| 422 | terms_required | The body is invalid, terms were not accepted, or the stay or promo cannot be sold. |
| 422 | too_many_guests | The body is invalid, terms were not accepted, or the stay or promo cannot be sold. |
| 429 | rate_limited | The per-key or per-IP request limit was exceeded. Wait for the number of seconds in retry-after. |
| 502 | payment_unavailable | The payment provider could not be reached. |
GET/api/v1/bookings/{ref}
Scope readGet a booking
Returns one booking by its reference, normalised to uppercase and expected in the form 1F2E3D4C. The property slug and name are included. A booking from another account is reported as 404.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| ref | path | string | yes | Booking reference: the row id without the bk_ prefix, in uppercase (6 to 12 hexadecimal or dash characters). |
Example request
curl -s https://stayolo.com/api/v1/bookings/1F2E3D4C -H "Authorization: Bearer $STAYOLO_API_KEY"Response 200
{
"data": {
"ref": "1F2E3D4C",
"status": "pending",
"source": "api",
"propertyId": "prop_8f3c2a1b9d",
"checkIn": "2026-12-01",
"checkOut": "2026-12-04",
"nights": 3,
"guests": 4,
"total": 575.16,
"currency": "EUR",
"paidAmount": 0,
"balanceDueAmount": 402.61,
"balanceDueDate": "2026-11-17",
"promoCode": "WINTER10",
"createdAt": 1762350000000,
"propertySlug": "casa-do-mar",
"propertyName": "Casa do Mar"
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | key_in_query | The request was rejected before a resource could be read, for example because an API key was supplied in the query string. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 401 | missing_key | The Authorization header is missing, malformed, or the key is unknown or revoked. |
| 403 | insufficient_scope | The key is valid but does not carry the scope this operation requires. |
| 404 | not_found | No matching booking in this account. |
| 422 | invalid_ref | The reference has the wrong shape. |
| 429 | rate_limited | The per-key or per-IP request limit was exceeded. Wait for the number of seconds in retry-after. |
Idempotency
Send an Idempotency-Key header (up to 190 characters) on POST /api/v1/bookings. Keys are scoped to the account. Repeating the same key with the same payload replays the original booking with idempotent-replayed: true.
- Same key with a different payload returns
409 idempotency_key_reused. - A retry while the original request is still in flight returns
409 idempotency_in_progress. - Generate a new key for a genuinely new booking, not for every retry of the same one.
curl -X POST https://stayolo.com/api/v1/bookings \
-H "Authorization: Bearer sk_live_..." \
-H "Idempotency-Key: 7f3c1e0a-8b2d-4c1e-9f4a-2d6b8c0e1a3f" \
-H "Content-Type: application/json" \
-d '{
"property": "my-property-slug",
"checkIn": "2026-12-20",
"checkOut": "2026-12-27",
"guests": 4,
"guest": { "name": "Ada Lovelace", "email": "[email protected]" },
"termsAccepted": true
}'Rate limits & errors
Limits apply per API key. The public health probe is limited by IP address instead and needs no key.
| Request type | Limit |
|---|---|
| Reads | 240 requests per minute per key |
| Writes | 60 requests per minute per key |
GET /health | 120 requests per minute per IP |
Exceeding a limit returns 429 rate_limited with a retry-after header in seconds. Every error uses the same JSON shape:
{
"error": {
"code": "rate_limited",
"message": "Too many requests."
}
}| Status | Meaning |
|---|---|
| 400 | Malformed request, including a key passed in the query string. |
| 401 | Missing, invalid or revoked API key. |
| 403 | The key does not have the scope required by the operation. |
| 404 | Not found, or the resource belongs to another account. |
| 405 | Method not supported, for example creating a property through the API. |
| 409 | State conflict: availability, agreement, idempotency or offer. |
| 422 | Validation failed. details.issues carries the field-level errors when available. |
| 429 | Rate limit exceeded; read retry-after. |
| 502 | The payment provider is unavailable. |
| 503 | The health probe is not ready; check details.check. |
The error table on each operation lists the exact codes that operation returns.
Versioning
The API is versioned in the path under /api/v1. The health probe reports the same version in data.apiVersion, so a deployment can be checked without an API key.
{
"data": {
"status": "ok",
"apiVersion": "v1",
"checkedAt": "2026-11-06T16:25:48.962Z",
"checks": {
"database": "ok",
"migrations": "ok"
}
}
}Within v1, changes are additive: new optional response fields and new operations may appear, while existing fields keep their meaning. Pin integrations to the documented shapes and ignore fields you do not use.
Multiple properties
List the properties visible to the key, then use each property id or slug in the nested endpoints. The same {idOrSlug} works everywhere a property is referenced.
curl "https://stayolo.com/api/v1/properties?published=true" \
-H "Authorization: Bearer sk_live_..."A key only sees its own account. Requesting another account's property returns 404 not_found, never 403, so the API does not confirm that an id exists. Use published=true to list only published properties.
Embed widget
The booking widget is an iframe served by Stayolo, so the host page needs no API key, CSS or JavaScript bundle. Place an empty container where the widget should appear and load the script with the property slug.
<div id="stayolo-booking"></div>
<script src="https://stayolo.com/embed.js"
data-stayolo-property="my-property-slug"></script>Optional attributes: data-stayolo-target chooses the mount selector instead of #stayolo-booking, and data-stayolo-on-book names a global callback.
document
.querySelector("#stayolo-booking")
?.addEventListener("stayolo:booked", (event) => {
const { property, booking } = event.detail;
const { ref, total, paymentUrl } = booking;
console.log({ property, ref, total, paymentUrl });
});The event bubbles from the container and its detail is { property, booking }; the booking carries { ref, total, paymentUrl }. The widget keeps its iframe height in sync with the content, so a host page does not need to size it.