Versione italiana →

WebReservations Partner API

Technical contract · version 1.0 · https://api.webreservations.it

WebReservations is the direct booking engine for the properties listed in your credentials. This API lets you download direct reservations — those taken on the property's own website and those recorded by its back office — following the same pattern you use for other channels: ask what is new, download it, confirm you have taken it.

Each credential sees only the properties whose owner has requested the connection: the list is returned by /v1/ping.

An Italian version of this document is also available, identical in content.

No credit card data, ever.
WebReservations neither collects nor stores card numbers, expiry dates or CVV codes: payments go through a gateway (Stripe, PayPal, Nexi) and the card never reaches us. In this API card fields do not exist: they are not optional, not empty, they are simply absent. For the financial side you get the method, the gateway, the amount collected, the outstanding balance and the transaction reference — the only verifiable handle, and the one to use in place of the card.

1. How it works

Three operations, in the pattern you already know from other channels:

OperationPurpose
GET /v1/reservationslist of what is new or changed
GET /v1/reservations/{id}the detail of a single reservation
POST /v1/reservations/{id}/confirmacknowledgement: stop sending it to me

The recommended cycle is one pass every few minutes:

  1. call GET /v1/reservations: it returns the reservations you have not acknowledged yet, or that have changed since you did;
  2. for each one you have stored, call POST /v1/reservations/{id}/confirm with your own identifier;
  3. from then on that reservation no longer appears in the list. If something changes later (amount, dates, cancellation) it comes back on its own: we compare a fingerprint of the data delivered, so you do not need to poll for changes yourself.

A cancelled reservation does not disappear: it comes back in the list with "status": "cancelled". That is your signal to put the room back on sale.

2. Authentication

We issue you two values:

keypublic, identifies your credentials. Sent in clear.
secretconfidential. With the recommended method it never travels.

All requests must use HTTPS. Plain HTTP is rejected.

2.1 Recommended method: signed request

X-WR-Keyyour key
X-WR-TimestampUnix time in seconds. Allowed skew: 5 minutes
X-WR-Noncerandom, 16 to 64 characters A-Z a-z 0-9 _ -, different on every request
X-WR-SignatureHMAC-SHA256, lowercase hexadecimal

The signature is computed over six elements separated by a single newline (\n). Missing parts stay as empty lines — they are not skipped:

METHOD      e.g. GET
PATH        e.g. /v1/reservations         (no host, no query string)
QUERY       e.g. pending=1&limit=50       (empty when absent)
TIMESTAMP   same value as X-WR-Timestamp
NONCE       same value as X-WR-Nonce
SHA256      hex sha256 of the body; for a GET, sha256 of the empty string

Complete example in PHP:

$key     = 'wr0123456789abcdef012345';
$secret = 'the-secret-we-issued-to-you';

$method = 'GET';
$path   = '/v1/reservations';
$query  = 'pending=1';
$body   = '';

$ts    = (string)time();
$nonce = bin2hex(random_bytes(12));

$toSign = $method . "\n" . $path . "\n" . $query . "\n"
        . $ts . "\n" . $nonce . "\n" . hash('sha256', $body);

$signature = hash_hmac('sha256', $toSign, $secret);

$headers = array(
    'X-WR-Key: '       . $key,
    'X-WR-Timestamp: ' . $ts,
    'X-WR-Nonce: '     . $nonce,
    'X-WR-Signature: ' . $signature,
);

Each nonce is valid once: an intercepted and replayed request gets request_replayed.

2.2 Fallback method: Basic

If your connector cannot sign requests, we accept Authorization: Basic base64(key:secret) — but only if we have configured the IP addresses you call from on your credentials. Without that list, Basic is rejected: the method that exposes the secret on every request is accepted only together with a network control. Send us the addresses (a prefix such as 80.88.87. covers a whole class) and we will enable them.

3. Endpoints

GET /v1/ping

Checks that the credentials work and shows which properties they can see.

{
  "ok": true,
  "data": {
    "client": "RoomCloud",
    "properties": [194, 207],
    "serverTime": "2026-09-02T10:15:00+02:00"
  }
}

GET /v1/reservations

ParameterMeaning
pending1 (default) only those not yet acknowledged or changed since. 0 all of them within the window.
from, tofilter on arrival date, format YYYY-MM-DD
afterid to continue from: pass back the next value
limitpage size, maximum 200 (default 100)
{
  "ok": true,
  "count": 2,
  "next": 18344,          // null when there is nothing more
  "data": [ { ... }, { ... } ]
}

We return reservations departing from 60 days ago onwards. If you need a longer history, tell us and we will widen the window.

GET /v1/reservations/{id}

A single reservation. 404 if it does not exist or does not belong to a property in your credentials: the two cases are deliberately indistinguishable. The id of any room line of a multi-room reservation returns the whole reservation, with its own id (see below); the same applies to confirm, whose response carries the id actually acknowledged.

POST /v1/reservations/{id}/confirm

Acknowledgement. JSON body with your own identifier:

{ "reference": "CRS-91138035" }

From then on the reservation leaves the pending=1 lists until something changes in the data we sent you.

4. The reservation object

{
  "id": 18342,
  "propertyId": 194,
  "status": "confirmed",           // or "cancelled"
  "source": "website",             // "website" | "admin_manual" | "agency"
  "createdAt": "2026-09-02 11:30:00",
  "checkin":  "2026-09-22",
  "checkout": "2026-09-25",
  "nights": 3,
  "guest": {
    "firstName": "Mario",
    "lastName": "Rossi",
    "email": "m.rossi@example.com",
    "phone": "+39 081 1234567"
  },
  "rooms": [{
    "roomId": 77,
    "roomName": "Doppia Vista Mare",
    "partnerRoomId": 21829,        // the roomId you know it by
    "quantity": 1,
    "adults": 2,
    "children": 1,
    "rateName": "BB Rimborsabile",
    "partnerRateId": 14266,        // the rateId you know it by
    "nightlyAverage": 116.67,      // room average per night
    "dayPrices": [                 // what each night cost
      { "day": "2026-09-22", "price": 110.00, "rooms": 1, "total": 110.00 },
      { "day": "2026-09-23", "price": 130.00, "rooms": 1, "total": 130.00 },
      { "day": "2026-09-24", "price": 110.00, "rooms": 1, "total": 110.00 }
    ]
  }],
  "amounts": {
    "currency": "EUR",
    "total": 350.00,               // stay + supplements
    "balance": 0.00
  },
  "extras": "Culla (EUR 15,00); Parcheggio (EUR 10,00)",
  "notes": "Arriving after 10pm",
  "payment": {
    "method": "stripe",            // stripe | paypal | nexi | bonifico
    "status": "paid",              // pending | guaranteed | paid | released
    "paid": 350.00,
    "reference": "pi_3Qabc123"     // identifier at the gateway
  }
}

Cancelled reservations additionally carry cancelledAt and cancellationReason.

Sources

source tells where the reservation comes from: website (booked online by the guest on the property's booking engine, including quotes and packages accepted online), admin_manual (entered by the property's staff: phone, e-mail, walk-in) and agency (entered by the staff on behalf of a travel agency). All three hold rooms in the same way and arrive in the same list, with the same fields.

Several rooms in one reservation

A reservation with more than one room type (for example 1 Double + 1 Suite, online or from the back office) is one object with one entry per room type in rooms[], each with its own codes, guests and dayPrices. amounts.total is the total of all rooms, payment.paid what has been collected for the whole reservation, checkin/checkout span all rooms. The id stays the same for the life of the reservation.

If the property removes one room, the reservation comes back in the list with that room no longer in rooms[] (and the totals updated): treat it as a modification and release that room. When every room is cancelled, status becomes cancelled and rooms[] lists all of them.

Nights. A stay from the 22nd to the 25th occupies the nights of the 22nd, 23rd and 24th: on the 25th the room is sellable again. nights already reflects this count.

Prices

For every reservation we send three things: amounts.total, the stay total including supplements; rooms[].nightlyAverage, the room average per night as computed by the engine at booking time; and extras, the supplements as recorded (free text with name and amount).

rooms[].dayPrices carries the breakdown: price is what one room cost that night, rooms how many were booked, total the product of the two. The sum of total is the stay without supplements; amounts.total includes them.

The breakdown may be missing. It is recorded at booking time and applies to recent reservations onwards: on older ones dayPrices is an empty array. Empty means we do not know, not it cost nothing: we do not derive those numbers by dividing the total by the nights, that would be an invented value wearing the clothes of a fact. For the same reason a night with no price set does not appear.

partnerRoomId and partnerRateId are null when that room or rate is not mapped: the reservation is still valid, we simply do not know which of your codes it corresponds to.

5. Errors

{ "ok": false, "error": "auth_failed", "message": "Authentication failed." }

The error field is stable and safe to branch on in code; message may change.

HTTPerrorWhat to do
400invalid_date, missing_referencefix the request
401auth_failedkey, secret or signature do not match
401signature_missingsignature headers are missing
401timestamp_out_of_windowsynchronise your server clock
401invalid_noncenonce out of format (16–64 characters)
401request_replayednonce reused: use a new one on every request
403https_requiredcall over HTTPS
403ip_not_allowedaddress not enabled: let us know
403basic_not_alloweduse the signature, or ask us to enable your IPs
403no_propertiescredentials with no properties: let us know
404not_foundunknown id, outside your properties, or wrong route
405method_not_allowedwrong HTTP method for that route
429rate_limitedover 120 requests per minute: slow down
429too_many_attemptstoo many failed authentications: clears within 15 minutes
503service_unavailableour problem, retry in a few minutes

6. Limits

7. Quick test

With the credentials in hand, the first command to try:

curl -s https://api.webreservations.it/v1/ping \
  -H "X-WR-Key: YOUR-KEY" \
  -H "X-WR-Timestamp: $TS" \
  -H "X-WR-Nonce: $NONCE" \
  -H "X-WR-Signature: $SIGNATURE"

If it answers with the list of properties, the connection is in place.