API v1

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 payment

Rates 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.

Authenticated request
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

API key scopes and what they allow
ScopeAllows
readRead properties, rates, availability, quotes and bookings.
rates:writeUpdate base pricing and replace seasonal rate overrides.
availability:writeBlock or reopen nights owned by the API integration.
bookings:writeCreate 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 200 or 201 response is { "data": ... }.
Error envelope
Errors are { "error": { code, message, details? } }.
Caching
Every response carries cache-control: no-store.
Lists
/properties and /bookings return x-total-count with the number of rows returned.
Dates
Calendar dates use YYYY-MM-DD. Impossible dates such as 2026-13-99 are rejected.
Money
Amounts are decimal numbers in the property currency, an ISO-4217 code.
Property references
Every {idOrSlug} accepts either a prop_... id or a slug.
Base URL
https://stayolo.com

Health

Unauthenticated readiness probes for deploys and load balancers.

GET/api/v1/health

Public

Check 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
curl -s https://stayolo.com/api/v1/health -H "Authorization: Bearer $STAYOLO_API_KEY"

Response 200

JSON
{
  "data": {
    "status": "ok",
    "apiVersion": "v1",
    "checkedAt": "2026-11-06T16:25:48.962Z",
    "checks": {
      "database": "ok",
      "migrations": "ok"
    }
  }
}

Errors

Errors for GET /api/v1/health
StatusCodeWhen
429rate_limitedThe per-IP request limit was exceeded. Wait for the number of seconds in retry-after.
503not_readyThe 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 read

List 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

Parameters for GET /api/v1/properties
NameInTypeRequiredDescription
publishedquerybooleannoWhen true, only published properties are returned.

Example request

cURL
curl -s "https://stayolo.com/api/v1/properties?published=true" -H "Authorization: Bearer $STAYOLO_API_KEY"

Response 200

JSON
{
  "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

Errors for GET /api/v1/properties
StatusCodeWhen
400key_in_queryThe request was rejected before a resource could be read, for example because an API key was supplied in the query string.
401invalid_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
401missing_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
403insufficient_scopeThe key is valid but does not carry the scope this operation requires.
429rate_limitedThe per-key or per-IP request limit was exceeded. Wait for the number of seconds in retry-after.

POST/api/v1/properties

Public

Create 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
curl -s -X POST https://stayolo.com/api/v1/properties -H "Authorization: Bearer $STAYOLO_API_KEY"

Response 200

JSON
{
  "error": {
    "code": "method_not_allowed",
    "message": "Create properties in the Stayolo dashboard; this API manages the ones you already have."
  }
}

Errors

Errors for POST /api/v1/properties
StatusCodeWhen
405method_not_allowedThis collection does not accept POST.

GET/api/v1/properties/{idOrSlug}

Scope read

Get 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

Parameters for GET /api/v1/properties/{idOrSlug}
NameInTypeRequiredDescription
———no—

Example request

cURL
curl -s https://stayolo.com/api/v1/properties/casa-do-mar -H "Authorization: Bearer $STAYOLO_API_KEY"

Response 200

JSON
{
  "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

Errors for GET /api/v1/properties/{idOrSlug}
StatusCodeWhen
400key_in_queryThe request was rejected before a resource could be read, for example because an API key was supplied in the query string.
401invalid_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
401missing_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
403insufficient_scopeThe key is valid but does not carry the scope this operation requires.
404not_foundNo matching property in this account.
429rate_limitedThe 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:write

Update 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

Parameters for PATCH /api/v1/properties/{idOrSlug}
NameInTypeRequiredDescription
———no—

Request bodyrequired

Request body fields for PATCH /api/v1/properties/{idOrSlug}
FieldTypeRequiredDescription
pricePerNightnumberno—
cleaningFeenumberno—
taxRatenumberno—
minStayintegerno—
maxStayintegerno—
weeklyDiscountPctintegerno—
monthlyDiscountPctintegerno—
includedGuestsintegerno—
extraGuestFeePerNightnumberno—

Example request

cURL
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

JSON
{
  "pricePerNight": 195,
  "minStay": 4,
  "weeklyDiscountPct": 12
}

Response 200

JSON
{
  "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

Errors for PATCH /api/v1/properties/{idOrSlug}
StatusCodeWhen
400key_in_queryThe request was rejected before a resource could be read, for example because an API key was supplied in the query string.
401invalid_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
401missing_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
403insufficient_scopeThe key is valid but does not carry the scope this operation requires.
404not_foundNo matching property in this account.
422empty_bodyThe body is empty, has unknown or invalid fields, or produces an impossible stay range.
422invalid_bodyThe body is empty, has unknown or invalid fields, or produces an impossible stay range.
422invalid_stay_rangeThe body is empty, has unknown or invalid fields, or produces an impossible stay range.
429rate_limitedThe 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 read

Get property rates

Returns the base nightly rate together with every seasonal override stored for the property. Overrides are ordered by startDate.

Parameters

Parameters for GET /api/v1/properties/{idOrSlug}/rates
NameInTypeRequiredDescription
———no—

Example request

cURL
curl -s https://stayolo.com/api/v1/properties/casa-do-mar/rates -H "Authorization: Bearer $STAYOLO_API_KEY"

Response 200

JSON
{
  "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

Errors for GET /api/v1/properties/{idOrSlug}/rates
StatusCodeWhen
400key_in_queryThe request was rejected before a resource could be read, for example because an API key was supplied in the query string.
401invalid_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
401missing_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
403insufficient_scopeThe key is valid but does not carry the scope this operation requires.
404not_foundNo matching property in this account.
429rate_limitedThe 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:write

Replace 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

Parameters for PUT /api/v1/properties/{idOrSlug}/rates
NameInTypeRequiredDescription
———no—

Request bodyrequired

Request body fields for PUT /api/v1/properties/{idOrSlug}/rates
FieldTypeRequiredDescription
ratesobject[]yes—
rates[].labelstringno—
rates[].startDatestringyes—
rates[].endDatestringyes—
rates[].pricePerNightnumbernullno—
rates[].minStayintegernullno—
rates[].adjustPctintegernullno—

Example request

cURL
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

JSON
{
  "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

JSON
{
  "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

Errors for PUT /api/v1/properties/{idOrSlug}/rates
StatusCodeWhen
400key_in_queryThe request was rejected before a resource could be read, for example because an API key was supplied in the query string.
401invalid_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
401missing_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
403insufficient_scopeThe key is valid but does not carry the scope this operation requires.
404not_foundNo matching property in this account.
422invalid_bodyThe body is invalid or two ranges overlap.
422overlapping_rangesThe body is invalid or two ranges overlap.
429rate_limitedThe 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 read

Get 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

Parameters for GET /api/v1/properties/{idOrSlug}/availability
NameInTypeRequiredDescription
———no—
———no—
———no—

Example request

cURL
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

JSON
{
  "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

Errors for GET /api/v1/properties/{idOrSlug}/availability
StatusCodeWhen
400key_in_queryThe request was rejected before a resource could be read, for example because an API key was supplied in the query string.
401invalid_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
401missing_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
403insufficient_scopeThe key is valid but does not carry the scope this operation requires.
404not_foundNo matching property in this account.
422invalid_rangeThe requested window is empty or too long.
422range_too_longThe requested window is empty or too long.
429rate_limitedThe 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:write

Update 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

Parameters for PUT /api/v1/properties/{idOrSlug}/availability
NameInTypeRequiredDescription
———no—

Request bodyrequired

Request body fields for PUT /api/v1/properties/{idOrSlug}/availability
FieldTypeRequiredDescription
datesobject[]yes—
dates[].datestringyes—
dates[].availablebooleanyes—

Example request

cURL
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

JSON
{
  "dates": [
    {
      "date": "2026-12-05",
      "available": false
    },
    {
      "date": "2026-12-06",
      "available": false
    },
    {
      "date": "2026-12-07",
      "available": true
    }
  ]
}

Response 200

JSON
{
  "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

Errors for PUT /api/v1/properties/{idOrSlug}/availability
StatusCodeWhen
400key_in_queryThe request was rejected before a resource could be read, for example because an API key was supplied in the query string.
401invalid_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
401missing_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
403insufficient_scopeThe key is valid but does not carry the scope this operation requires.
404not_foundNo matching property in this account.
422invalid_bodyThe body is invalid or contains a date that is not a real calendar date.
422invalid_dateThe body is invalid or contains a date that is not a real calendar date.
429rate_limitedThe 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 read

Quote 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

Parameters for GET /api/v1/properties/{idOrSlug}/quote
NameInTypeRequiredDescription
———no—
checkInquerystringyesCheck-in date as YYYY-MM-DD.
checkOutquerystringyesCheck-out date as YYYY-MM-DD.
guestsqueryintegernoNumber of guests (integer of at least 1, default 1).
promoCodequerystringnoPromo code to apply to this quote.
offerTokenquerystringnoToken of a personalised offer to apply.

Example request

cURL
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

JSON
{
  "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

Errors for GET /api/v1/properties/{idOrSlug}/quote
StatusCodeWhen
400key_in_queryThe request was rejected before a resource could be read, for example because an API key was supplied in the query string.
401invalid_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
401missing_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
403insufficient_scopeThe key is valid but does not carry the scope this operation requires.
404not_foundNo matching property in this account.
409offer_expiredThe offer cannot be used.
409offer_unavailableThe offer cannot be used.
422invalid_datesThe dates, guest count, stay length or promo code are invalid.
422invalid_guestsThe dates, guest count, stay length or promo code are invalid.
422promo_expiredThe dates, guest count, stay length or promo code are invalid.
422promo_invalidThe dates, guest count, stay length or promo code are invalid.
422promo_min_stayThe dates, guest count, stay length or promo code are invalid.
422stay_lengthThe dates, guest count, stay length or promo code are invalid.
422too_many_guestsThe dates, guest count, stay length or promo code are invalid.
429rate_limitedThe 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 read

List 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

Parameters for GET /api/v1/bookings
NameInTypeRequiredDescription
———no—
———no—
statusquerystring (inquiry | pending | confirmed | paid | partially_paid | cancelled | completed)noOnly return bookings with this status.
———no—

Example request

cURL
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

JSON
{
  "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

Errors for GET /api/v1/bookings
StatusCodeWhen
400key_in_queryThe request was rejected before a resource could be read, for example because an API key was supplied in the query string.
401invalid_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
401missing_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
403insufficient_scopeThe key is valid but does not carry the scope this operation requires.
429rate_limitedThe per-key or per-IP request limit was exceeded. Wait for the number of seconds in retry-after.

POST/api/v1/bookings

Scope bookings:write

Create 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

Parameters for POST /api/v1/bookings
NameInTypeRequiredDescription
Idempotency-KeyheaderstringnoOptional 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

Request body fields for POST /api/v1/bookings
FieldTypeRequiredDescription
propertystringyesProperty id or slug.
checkInstringyes—
checkOutstringyes—
guestsintegeryes—
guestobjectyes—
guest.namestringyes—
guest.emailstringyes—
guest.phonestringno—
guest.countrystringno—
termsAcceptedbooleanyesMust be true. A false value is refused with the terms_required error code.
promoCodestringno—
offerTokenstringno—
agreementAcceptedbooleanno—
roomIdstringnoThe 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
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

JSON
{
  "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

JSON
{
  "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

Errors for POST /api/v1/bookings
StatusCodeWhen
400key_in_queryThe request was rejected before a resource could be read, for example because an API key was supplied in the query string.
401invalid_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
401missing_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
403insufficient_scopeThe key is valid but does not carry the scope this operation requires.
404not_foundNo matching property in this account.
409agreement_requiredThe rental agreement, inventory, or idempotency key refuses the booking.
409conflictThe rental agreement, inventory, or idempotency key refuses the booking.
409idempotency_in_progressThe rental agreement, inventory, or idempotency key refuses the booking.
409idempotency_key_reusedThe rental agreement, inventory, or idempotency key refuses the booking.
422invalid_bodyThe body is invalid, terms were not accepted, or the stay or promo cannot be sold.
422invalid_datesThe body is invalid, terms were not accepted, or the stay or promo cannot be sold.
422promo_expiredThe body is invalid, terms were not accepted, or the stay or promo cannot be sold.
422promo_invalidThe body is invalid, terms were not accepted, or the stay or promo cannot be sold.
422promo_min_stayThe body is invalid, terms were not accepted, or the stay or promo cannot be sold.
422stay_lengthThe body is invalid, terms were not accepted, or the stay or promo cannot be sold.
422terms_requiredThe body is invalid, terms were not accepted, or the stay or promo cannot be sold.
422too_many_guestsThe body is invalid, terms were not accepted, or the stay or promo cannot be sold.
429rate_limitedThe per-key or per-IP request limit was exceeded. Wait for the number of seconds in retry-after.
502payment_unavailableThe payment provider could not be reached.

GET/api/v1/bookings/{ref}

Scope read

Get 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

Parameters for GET /api/v1/bookings/{ref}
NameInTypeRequiredDescription
refpathstringyesBooking reference: the row id without the bk_ prefix, in uppercase (6 to 12 hexadecimal or dash characters).

Example request

cURL
curl -s https://stayolo.com/api/v1/bookings/1F2E3D4C -H "Authorization: Bearer $STAYOLO_API_KEY"

Response 200

JSON
{
  "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

Errors for GET /api/v1/bookings/{ref}
StatusCodeWhen
400key_in_queryThe request was rejected before a resource could be read, for example because an API key was supplied in the query string.
401invalid_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
401missing_keyThe Authorization header is missing, malformed, or the key is unknown or revoked.
403insufficient_scopeThe key is valid but does not carry the scope this operation requires.
404not_foundNo matching booking in this account.
422invalid_refThe reference has the wrong shape.
429rate_limitedThe 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.
Retry-safe booking
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.

API rate limits
Request typeLimit
Reads240 requests per minute per key
Writes60 requests per minute per key
GET /health120 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 response
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests."
  }
}
HTTP status codes used by the API
StatusMeaning
400Malformed request, including a key passed in the query string.
401Missing, invalid or revoked API key.
403The key does not have the scope required by the operation.
404Not found, or the resource belongs to another account.
405Method not supported, for example creating a property through the API.
409State conflict: availability, agreement, idempotency or offer.
422Validation failed. details.issues carries the field-level errors when available.
429Rate limit exceeded; read retry-after.
502The payment provider is unavailable.
503The 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.

GET /api/v1/health
{
  "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.

List published properties
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.

HTML
<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.

stayolo:booked event
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.