Getting started
Create an API key in the admin app under Settings → API (property-manager account required — keys always act for the whole organization, so team members cannot create them). The base URL for every endpoint is:
https://<your-domain>/api/v1All endpoints are read-only GET requests returning JSON. A first call to try:
curl "https://<your-domain>/api/v1/listings" \
-H "Authorization: Bearer ck_live_…"Authentication
Send your key as a bearer token on every request:
Authorization: Bearer ck_live_…- The full key is shown once, at creation. Store it in a secret manager; the settings page only ever shows the last 4 characters again.
- A missing, unknown or revoked key gets
401 invalid_api_key. - If a key leaks, revoke it in Settings → API — revocation is immediate — and create a new one.
Rate limits
Each key may make 60 requests per minute, with a short burst allowance. Every response carries the current state:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1753500000 # unix secondsPast the limit you get 429 rate_limited with a Retry-After header (seconds). Well-behaved clients should pace themselves on X-RateLimit-Remaining instead of retrying on 429.
Pagination
List endpoints accept limit and offset and return:
{
"data": [ … ],
"totalCount": 137,
"nextOffset": 25 // null on the last page
}To page through results, repeat the request with offset=nextOffset until nextOffset is null. The sort order is stable, so pages never skip or repeat rows.
Errors
Every error is JSON with a stable machine-readable code:
{ "error": { "code": "invalid_params", "message": "limit: Too big …" } }| Status | Code | Meaning |
|---|---|---|
| 401 | invalid_api_key | Missing, unknown, or revoked key. |
| 404 | not_found | No resource matches (e.g. unknown check-in code). |
| 422 | invalid_params | A query param failed validation — including unknown param names, so typos surface instead of being ignored. |
| 429 | rate_limited | Rate limit exceeded; see Retry-After. |
| 500 | internal_error | Unexpected server error; safe to retry with backoff. |
Conventions
- Dates are
YYYY-MM-DDcalendar days in the property's local calendar (Europe/Rome); timestamps are ISO 8601 UTC. - All monetary amounts are EUR numbers (e.g. 486.5).
- Hours (
checkinFromHour,guestDeclaredArrivalHour, …) are 0–23 in the listing's local time. - Related data is expanded only when asked for via
include=— responses stay lean by default. - The API is versioned in the path (
/api/v1); fields may be added without notice, but existing fields will not change meaning or disappear within v1.
Webhooks
Instead of polling, register an HTTPS endpoint under Settings → Webhooks and pick the events you care about. Each occurrence is delivered as a POST with a JSON body:
{
"id": "9b2e6a1c-…", // unique per event — dedupe on this
"type": "checkin.completed",
"createdAt": "2026-08-14T15:04:05.000Z",
"data": {
"reservation": { …same object as GET /api/v1/reservations/{checkinCode},
incl. pmsReservationId / pmsListingId (Hostaway ids)… },
"payment": { … } // extra keys on some event types, see below
}
}| Event | Sent when |
|---|---|
| reservation.created | A reservation was created (PMS sync or manual import). |
| reservation.updated | Reservation core data changed (dates, guests, price, status…). Not sent for no-op sync replays. |
| reservation.cancelled | A reservation transitioned to cancelled. |
| checkin.completed | The guest flow reached its terminal complete state (documents, contract and payments all done). |
| checkin.awaiting_confirmation | The guest finished their part but the check-in needs host confirmation (e.g. ID documents to review). |
| checkin.submitted | The guest finished their part of the check-in (documents, contract, extras and payment step all done). Sent once per reservation, whether the check-in then goes straight to complete or waits for host confirmation — the one to use for a "check-in done, thank you" message. Not sent when a host marks the check-in complete from the admin. |
| payment.succeeded | A check-in payment (fees and/or security deposit) was captured via Stripe. |
| upsell.purchased | The guest bought one or more optional extras: ticked on the Extras step and paid with the check-in payment, or added later from the finished screen / extras link. Mandatory extras are never reported. |
| security_hold.placed | A security hold is now in place: a pre-authorization was authorized (any lane — scheduled, immediate, re-auth, guest link) or a deposit was charged. |
| security_hold.failed | A pre-authorization attempt failed (e.g. card declined) and may need action before arrival. |
| security_hold.released | The hold was given back: pre-authorization cancelled (including from the Stripe dashboard or by expiry) or deposit refunded. |
| security_hold.captured | The pre-authorized amount (or part of it) was captured as a charge. |
| contract.signed | The guest signed the rental contract. |
| document.review_required | Guest ID documents were flagged for host review (e.g. second upload or scanner mismatch). |
| portale_alloggiati.sent | Guest data was accepted by the Portale Alloggiati police portal. |
| portale_alloggiati.failed | The Portale Alloggiati send failed and needs review; the payload carries the per-guest error details. |
payment.succeededadds apaymentobject (stripePaymentIntentId,amount,feesAmount,depositAmount,paidAt);upsell.purchasedadds anupsellPurchaseobject (stripePaymentIntentId,amount,paidAt, anditems, each withupsellId,name,amount);security_hold.*add asecurityHoldobject (source,amount,stripePaymentIntent,detail, and the failuremessageonsecurity_hold.failed);portale_alloggiati.*add aportaleAlloggiatiobject with the error details on failure.- Test events: Settings → Webhooks → Send test event delivers a signed sample of any event type with a fake guest (
Test Guest,[email protected], PMS ids"0") and"test": trueat the top level of the body — real events never carry it, so filter on it before messaging a guest. - Respond with any
2xxwithin 10 seconds. Redirects are not followed; anything else counts as a failure. - Delivery is at-least-once and ordering is not guaranteed — dedupe on the event
idand rely on the embedded resource state, not on event order. - Failed deliveries are retried with backoff (roughly 1 min → 24 h, 8 attempts over ~2 days). An endpoint that keeps failing is disabled automatically; re-enable it from Settings → Webhooks, where you can also inspect recent deliveries and redeliver one manually.
Verifying signatures
Every delivery is signed with the endpoint's secret (shown once at creation). The X-Checkin-Signature header contains a unix timestamp and an HMAC-SHA256 of `${timestamp}.${rawBody}`:
X-Checkin-Signature: t=1753600000,v1=5257a869e7…
// Node.js
const crypto = require("node:crypto");
function verify(header, rawBody, secret) {
const { t, v1 } = Object.fromEntries(
header.split(",").map((p) => p.split("="))
);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // replay guard
const mac = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(v1));
}Compute the HMAC over the raw request body exactly as received — don't re-serialize the parsed JSON. The X-Checkin-Event and X-Checkin-Delivery headers carry the event type and delivery id for logging.
For no-code platforms whose built-in HMAC check only signs the body (Activepieces, n8n, Make, …), every delivery also carries X-Checkin-Signature-256: a plain hex HMAC-SHA256 of the raw body with the same secret, no timestamp and no prefix. Configure the platform with header name X-Checkin-Signature-256, algorithm SHA-256, hex encoding and an empty prefix. Prefer X-Checkin-Signature when you write your own receiver — the timestamp is what protects you from replays.
List listings
GET/api/v1/listings
Your properties: id, guest-facing name, your internal name, city, status, capacity and the standard check-in-from / checkout-by hours (0-23, listing local time). Use the returned listingId to filter reservations. Pass availableFrom/availableTo to keep only listings with no reservation on those nights.
- Listings deleted on the PMS side are never returned; includeInactive=true re-adds manually deactivated ones.
- Availability is based on reservations only — manual calendar blocks made in the PMS are not visible here.
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
| includeInactive | string | — | Also return manually deactivated listings (PMS-deleted ones stay hidden). |
| availableFrom | date (YYYY-MM-DD) | — | With availableTo, only listings with no reservation on any night in the range. |
| availableTo | date (YYYY-MM-DD) | — | Last night of the availability window; defaults to availableFrom. |
| city | string | — | Case-insensitive substring match on the listing's city. |
Example response
{
"data": [
{
"listingId": "a3f1c2e4-5b6d-4f7e-8a9b-0c1d2e3f4a5b",
"name": "Uptown Apartment with Balcony",
"internalName": "Uptown 2B",
"city": "Milan",
"status": "active",
"maxGuests": 4,
"checkinFromHour": 15,
"standardCheckoutHour": 10
}
]
}Get a listing
GET/api/v1/listings/{listingId}
One listing by its listingId — the same object the list returns and every reservation embeds.
- Deactivated listings are returned too (a direct id lookup); listings deleted on the PMS side 404.
No query parameters.
Example response
{
"data": {
"listingId": "a3f1c2e4-5b6d-4f7e-8a9b-0c1d2e3f4a5b",
"name": "Uptown Apartment with Balcony",
"internalName": "Uptown 2B",
"city": "Milan",
"status": "active",
"maxGuests": 4,
"checkinFromHour": 15,
"standardCheckoutHour": 10
}
}List reservations
GET/api/v1/reservations
Paginated reservations, oldest arrival first. All filters are optional and combine with AND: independent arrival and departure date ranges, cancellation status, listing, and a free-text query (guest-name substring, guest check-in code, PMS reservation id, guest email, or guest phone). Expand related data with include=payments,guests.
- Every reservation embeds its full listing object — the same shape GET /api/v1/listings returns.
- guestEmail / phone are the PMS copies; guestDeclaredEmail / guestDeclaredPhone are what the guest entered at online check-in (null if not given).
- query matches emails and phones both as the PMS sent them and as the guest entered them in online check-in. A full email address matches that mailbox ignoring case and +tags ([email protected] finds [email protected]; Gmail also ignores dots and googlemail.com); a fragment (3+ characters) matches as a substring. Phones compare digits only, with or without the country code (+39, 0039), from 6 digits up, so a partial number works.
- guestFlowCompletion is 'complete' when the guest finished online check-in, 'awaiting_confirmation' when it needs host review (see idDocumentsNeedReview and securityHoldStatus), 'incomplete' otherwise.
- securityHoldStatus: 'none' (no deposit/pre-auth due or not reached), 'awaiting_scheduled_preauth' (pre-auth scheduled, placed ~2 days before arrival), 'held' (hold in place), 'failed' (attempt failed, needs retry — securityHoldFailureReason then carries the card/bank decline or error text, null in every other state), 'released' (hold returned in full after checkout), 'captured' (damage money was taken).
- registeredGuests (include=guests) carries identity basics from submitted ID documents only — never document numbers.
- Amounts are EUR. totalPrice is the PMS booking total; payment rows are what was actually collected or refunded.
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
| arrivalStartDate | date (YYYY-MM-DD) | — | Earliest arrival date (YYYY-MM-DD), inclusive. |
| arrivalEndDate | date (YYYY-MM-DD) | — | Latest arrival date (YYYY-MM-DD), inclusive. |
| departureStartDate | date (YYYY-MM-DD) | — | Earliest departure date (YYYY-MM-DD), inclusive. |
| departureEndDate | date (YYYY-MM-DD) | — | Latest departure date (YYYY-MM-DD), inclusive. |
| status | active | cancelled | all | all | Filter on cancellation state. |
| listingId | uuid | — | Only reservations for this listing (see GET /v1/listings). |
| query | string | — | Guest-name substring, guest check-in code, PMS reservation id, guest email (full address or fragment), or guest phone (6+ digits). |
| include | string | — | Comma-separated expansions: payments, guests. |
| limit | integer (1–100) | 25 | Rows per page (max 100). |
| offset | integer (0–10000) | 0 | Rows to skip; pass a previous response's nextOffset. |
Example response
{
"data": [
{
"checkinCode": "8GY96S3N",
"pmsProvider": "hostaway",
"pmsReservationId": "41287653",
"pmsListingId": "205117",
"guestName": "Maria Rossi",
"guestEmail": "[email protected]",
"phone": "+39 333 1234567",
"listing": {
"listingId": "a3f1c2e4-5b6d-4f7e-8a9b-0c1d2e3f4a5b",
"name": "Uptown Apartment with Balcony",
"internalName": "Uptown 2B",
"city": "Milan",
"status": "active",
"maxGuests": 4,
"checkinFromHour": 15,
"standardCheckoutHour": 10
},
"arrivalDate": "2026-08-14",
"departureDate": "2026-08-17",
"nights": 3,
"numberOfGuests": 2,
"channelName": "Booking.com",
"totalPrice": 486.5,
"cancelled": false,
"guestFlowCompletion": "complete",
"idDocumentsNeedReview": false,
"securityHoldStatus": "held",
"securityHoldFailureReason": null,
"guestDeclaredArrivalHour": 15,
"guestDeclaredBeds": 2,
"guestDeclaredEmail": "[email protected]",
"guestDeclaredPhone": "+39 333 7654321",
"registeredGuests": [
{
"name": "Maria Rossi",
"dateOfBirth": "1990-04-02",
"nationality": "Italy",
"mainGuest": true
},
{
"name": "Luca Rossi",
"dateOfBirth": "1988-11-19",
"nationality": "Italy",
"mainGuest": false
}
],
"payments": [
{
"kind": "checkin",
"purpose": "payment",
"amount": 86.5,
"paidAt": "2026-08-01T09:12:44.000Z",
"stripePaymentIntent": "pi_3PqXk2Abc123def456",
"stripeRefundId": null,
"lineItems": [
{
"description": "Cleaning fee",
"amount": 60,
"vatRate": "22",
"exemptionTag": null
},
{
"description": "City tax",
"amount": 26.5,
"vatRate": "0",
"exemptionTag": "city_tax_fuori_campo"
}
]
}
]
}
],
"totalCount": 137,
"nextOffset": 25
}Get a reservation
GET/api/v1/reservations/{checkinCode}
One reservation by its guest check-in code (the code guests receive in their check-in link). Registered guests and payments are always included.
No query parameters.
Example response
{
"data": {
"checkinCode": "8GY96S3N",
"pmsProvider": "hostaway",
"pmsReservationId": "41287653",
"pmsListingId": "205117",
"guestName": "Maria Rossi",
"guestEmail": "[email protected]",
"phone": "+39 333 1234567",
"listing": {
"listingId": "a3f1c2e4-5b6d-4f7e-8a9b-0c1d2e3f4a5b",
"name": "Uptown Apartment with Balcony",
"internalName": "Uptown 2B",
"city": "Milan",
"status": "active",
"maxGuests": 4,
"checkinFromHour": 15,
"standardCheckoutHour": 10
},
"arrivalDate": "2026-08-14",
"departureDate": "2026-08-17",
"nights": 3,
"numberOfGuests": 2,
"channelName": "Booking.com",
"totalPrice": 486.5,
"cancelled": false,
"guestFlowCompletion": "complete",
"idDocumentsNeedReview": false,
"securityHoldStatus": "held",
"securityHoldFailureReason": null,
"guestDeclaredArrivalHour": 15,
"guestDeclaredBeds": 2,
"guestDeclaredEmail": "[email protected]",
"guestDeclaredPhone": "+39 333 7654321",
"registeredGuests": [
{
"name": "Maria Rossi",
"dateOfBirth": "1990-04-02",
"nationality": "Italy",
"mainGuest": true
},
{
"name": "Luca Rossi",
"dateOfBirth": "1988-11-19",
"nationality": "Italy",
"mainGuest": false
}
],
"payments": [
{
"kind": "checkin",
"purpose": "payment",
"amount": 86.5,
"paidAt": "2026-08-01T09:12:44.000Z",
"stripePaymentIntent": "pi_3PqXk2Abc123def456",
"stripeRefundId": null,
"lineItems": [
{
"description": "Cleaning fee",
"amount": 60,
"vatRate": "22",
"exemptionTag": null
},
{
"description": "City tax",
"amount": 26.5,
"vatRate": "0",
"exemptionTag": "city_tax_fuori_campo"
}
]
}
]
}
}