After the Booking
A book request answers with a booking_uid and a status. This page explains what each status means, how you learn about a change, and what to do in each case. For the steps before the booking see the Workflow.
If a book request gets no answer, or a 5xx, don't book again: the booking may exist. Follow Retrying safely and contact support with the confirmation_uid.
Booking statuses
| Status | Meaning | Final? |
|---|---|---|
BOOKED |
The hotel holds the booking. The hotel's reference is in reference.supplier. |
Yes |
PENDING |
The request reached the hotel's reservation system, but the outcome isn't confirmed yet. We resolve it; see below. | No |
FAILED |
No booking was made. You can book again. | Yes |
CANCELLED |
The booking is cancelled. | Yes |
A 200 OK with status: FAILED is a rejected booking, not a transport error. Read the status, not only the HTTP code.
A pending booking
Treat a pending booking as made until you learn otherwise:
- Store the
booking_uidand tell your customer the booking is being confirmed. - Don't book the same stay again. A second request for the same hotel, dates and lead guest is refused with
409 Conflictwhile the first is pending; the message names the pendingbooking_uid. - Learn the outcome by polling or by webhook (next section). The status becomes
BOOKEDorFAILED. - To cancel a pending booking, send the cancel request anyway. It is refused with
409 Conflictwhile the outcome is unknown, but your request is recorded. See 4. FAQ - Quote & Booking for what happens next.
Learning the outcome
Polling
GET /hotelxv/api/v2/bookings?booking_uids=AXB-…,AXB-… returns the current status of each booking. Put all your pending bookings in one request. A sensible schedule is every 5 minutes during the first hour after the booking, then every 30 minutes until the status is final. Use polling if you'd rather not run a webhook endpoint.
Webhooks
We can send an HTTP POST to a URL you register with us whenever something happens to one of your bookings. To set it up, send the URL to support@apixv.com; you receive a signing secret through a separate channel.
Every message has the same envelope: an id, a type, the time it was created, and data holding the complete object. For bookings, data is the same JSON that GET /bookings returns for that booking, so you can store it directly.
{
"id": "msg_01JA6ZQ3V8K2M4N5P6Q7R8S9TW",
"type": "booking.updated",
"created_at": "2027-02-14T10:15:30Z",
"data": {
"booking_uid": "AXB-01JA6ZQ3V8K2M4N5P6Q7R8S9TW",
"status": "BOOKED",
"reference": {"supplier": "1234567|T18", "client": "my-reference-123", "hotel": null, "platform": null},
"price": {"currency": "EUR", "value": 41250},
"check_in": "2027-03-01",
"check_out": "2027-03-03"
}
}
(data is abbreviated here; the real message contains the full booking.)
The only message type today is booking.updated: the booking in data changed, and its status tells you what happened (BOOKED, FAILED or CANCELLED). Other types will follow for other objects; ignore any type you don't handle.
You don't necessarily see every change: if a booking changes several times before a message reaches you, you get its latest state. Treat each message as the booking's complete current state and replace what you stored.
Security
Our webhooks follow the Standard Webhooks specification, so you can verify them with the library for your language instead of writing the check yourself.
- Your URL must be
https://with a certificate from a public authority. We don't follow redirects. - Every message is signed. Three headers come with each delivery:
webhook-id(the message id),webhook-timestamp(Unix seconds) andwebhook-signature(v1,<base64>; during a secret rotation, two signatures separated by a space). The signature is the HMAC-SHA256 of<webhook-id>.<webhook-timestamp>.<raw body>, keyed with your secret. - Your secret looks like
whsec_…and is sent to you separately from the URL confirmation. Keep it in your secret store, never in the URL. To rotate it, ask support: for 24 hours we sign with both secrets, then only with the new one, so switch within that time (if you switch later, your endpoint rejects messages until you do; we keep retrying them, nothing is lost). - Verify before you act, and answer
400if the check fails or throws any error. Reject a message whose timestamp is more than five minutes off your clock; this stops replays. The library does both. - The same message can arrive twice.
webhook-idtells you it's the same one. If you process messages asynchronously and two for the same booking cross, keep the one with the latercreated_at.
Verifying with the official library (secret is your whsec_… secret, raw_body the request body exactly as received, headers the request headers):
# pip install standardwebhooks
from standardwebhooks import Webhook, WebhookVerificationError
wh = Webhook(secret)
try:
message = wh.verify(raw_body, headers) # headers: dict of the request headers
except Exception: # WebhookVerificationError, or a parsing error on malformed input
... # answer 400
// npm install standardwebhooks
import { Webhook, WebhookVerificationError } from "standardwebhooks";
const wh = new Webhook(secret);
try {
const message = wh.verify(rawBody, headers); // rawBody: the raw body as string or Buffer (never JSON.stringify(req.body)); headers: object with webhook-id, webhook-timestamp, webhook-signature
} catch (e) {
// answer 400 (WebhookVerificationError, or any other error on malformed input)
}
// Maven Central: com.standardwebhooks:standardwebhooks (Kotlin uses the same artifact)
import com.standardwebhooks.Webhook;
import com.standardwebhooks.exceptions.WebhookVerificationException;
Webhook wh = new Webhook(secret); // throws the checked EmptyWebhookSecretException: create it once at startup
try {
wh.verify(rawBody, headers); // rawBody: the body as a String, exactly as received; headers: Map<String, List<String>>
} catch (WebhookVerificationException e) {
// answer 400
}
// dotnet add package StandardWebhooks.StandardWebhooks
using StandardWebhooks;
using StandardWebhooks.Exceptions;
using System.Net;
var headers = new WebHeaderCollection(); // Verify takes a WebHeaderCollection:
foreach (var kv in Request.Headers) headers.Add(kv.Key, kv.Value.ToString()); // copy ASP.NET Core's headers into it
var wh = new Webhook(secret);
try {
wh.Verify(rawBody, headers); // rawBody: the body as a string, read before model binding
} catch (WebhookVerificationException) {
// answer 400
}
// go get github.com/standard-webhooks/standard-webhooks/libraries/go
import standardwebhooks "github.com/standard-webhooks/standard-webhooks/libraries/go"
wh, err := standardwebhooks.NewWebhook(secret)
if err != nil { return err }
if err := wh.Verify(rawBody, r.Header); err != nil { // rawBody: io.ReadAll(r.Body); pass r.Header itself (a map literal with lowercase keys isn't found)
// answer 400
}
// composer require standard-webhooks/standard-webhooks
use StandardWebhooks\Webhook;
use StandardWebhooks\Exception\WebhookVerificationException;
$wh = new Webhook($secret);
try {
$message = $wh->verify($rawBody, $headers); // $rawBody: file_get_contents('php://input'); $headers: array with LOWERCASE keys, e.g. array_change_key_case(getallheaders(), CASE_LOWER)
} catch (WebhookVerificationException $e) {
http_response_code(400);
}
# gem install standardwebhooks
require "standardwebhooks"
wh = StandardWebhooks::Webhook.new(secret)
begin
message = wh.verify(raw_body, headers) # headers: hash with LOWERCASE keys "webhook-id", "webhook-timestamp", "webhook-signature" (e.g. headers.transform_keys(&:downcase))
rescue StandardWebhooks::WebhookVerificationError, JSON::ParserError
# answer 400
end
// cargo add standardwebhooks
use standardwebhooks::Webhook;
let wh = Webhook::new(secret)?;
wh.verify(raw_body, &headers)?; // headers: http::HeaderMap; an Err means answer 400
Respond with 2xx within 10 seconds: store the message, answer, then do your own processing. Any other response, or no response, and we try again with increasing intervals until your endpoint accepts the message, for as long as it takes. A retried message keeps its webhook-id and carries the booking's current state with a fresh timestamp and signature, so an outage on your side never loses a change.
Cancelling
POST /hotelxv/api/v2/bookings/{booking_uid}/actions/cancel tells you whether the cancellation went through:
CANCELLED: done. The cancellation fee is inprice, and the booking's status is nowCANCELLED.FAILED: not cancelled. The booking is still valid.PENDING: the hotel hasn't answered yet. You receive abooking.updatedmessage when it has; or check the booking later.
See 5. FAQ - Management.