Looking to hire Laravel developers? Try LaraJobs

laravel-previo maintained by beziworld

Description
Laravel client for the whole Previo hotel PMS API (XML, REST, POS, EQC): hotels, rates, availability, reservations, guests, rooms, billing and code lists.
Last update
2026/11/04 17:48 (dev-master)
License
Downloads
1

Comments
comments powered by Disqus

Laravel Previo

tests Latest Version License

Previo hotel PMS client for Laravel covering the whole public Previo API: the XML API (/x1/), the REST API (/rest/), the POS API (/pos/) and the EQC channel manager interface (/eqc1/), each function with a dedicated, typed method:

  • several Previo accounts (named connections, runtime credentials per tenant) and many hotels per account
  • hotel profile, room kinds, rooms with housekeeping status, photos, amenities, translations, news and guest reviews
  • pricelist, availability (daily and hourly), local taxes, discounts, services, stay packages, meal plans, price quotes
  • reservations: search, detail, create, modify, change status, cancel, check-in with signature, door lock PIN codes
  • reservation synchronisation of many hotels: streamed searches with flat memory use, incremental cursors, horizon imports, per-hotel error isolation, Laravel events and a scheduler-ready previo:sync-reservations command
  • guests: search, profile, create, update, delete, tags and categories; the hotel owning a guest card is resolved
  • accounts, account items, cash desk payments, invoices and receipts (PDF), accounting exports
  • code lists (countries, currencies, languages, segments, ZIP codes...), change polling (System.getChanges2)
  • point of sale posting and EQC availability/rate updates and booking retrieval
  • error documents turned into typed exceptions; safe retries for read-only calls, writes never retried
  • generic call(), rest() and posRequest() for anything Previo adds later

No ext-soap or XML library is needed: requests go through the Laravel HTTP client, so everything works with Http::fake().

Polska wersja dokumentacji

Installation

composer require beziworld/laravel-previo
php artisan vendor:publish --tag=previo-config   # optional
PREVIO_API_KEY=your-api-key   # from your Previo sales representative or info@previo.cz
PREVIO_HOTEL_ID=123456        # hotId of the property
PREVIO_LANGUAGE_ID=           # optional: 1 Czech, 2 English, 3 German

API access is a paid Previo add-on; Previo can issue sandbox (test hotel) credentials on request. The API key is sent as Authorization: ApiKey <key>. Accounts that still use the older login and password can set PREVIO_LOGIN and PREVIO_PASSWORD instead; they are then sent inside each XML request (the REST API needs an API key).

Other settings: PREVIO_BASE_URL (default https://api.previo.app), PREVIO_TIMEOUT (30 s), PREVIO_RETRIES (2), PREVIO_RETRY_DELAY (500 ms), PREVIO_TIMEZONE (Europe/Prague, used to read the local date-times Previo returns) and PREVIO_APP_ID (application id sent to the POS API).

Heavy calls (reservation searches, System.getChanges2, payments, the XML guest search, reports and document exports) use PREVIO_LONG_TIMEOUT (120 s) instead of PREVIO_TIMEOUT: the reservation search of a busy hotel returns several megabytes and regularly takes longer than 30 seconds. PREVIO_CONNECT_TIMEOUT (10 s) limits the connection phase of every call, so an unreachable API still fails fast.

Hotel, room kinds and rates

use Beziworld\Previo\Facades\Previo;

$hotel = Previo::hotel();              // Hotel: name, address, phones, emails, checkInTime, latitude, stars...

foreach (Previo::roomKinds() as $kind) {   // list<RoomKind>
    $kind->id;                             // obkId
    $kind->name;
    $kind->capacity();                     // beds + extra beds
    $kind->rooms;                          // list<Room>: id (objId), name
}

$rates = Previo::rates('2026-07-01', '2026-08-31', currencies: ['CZK', 'EUR'], ratePlanIds: [84855]);

foreach ($rates->ratePlans as $plan) {
    foreach ($plan->seasons as $season) {          // Season: from, to
        $price = $season->roomKind(94676)?->price('CZK', occupancy: 2);
        $price?->amount;                           // null when the hotel left the price empty
    }
}

Previo::ratePlans();   // list<RatePlan>: id, name, isBasePlan, nonRefundable, parentId

Without ratePlanIds Previo returns the base rate plan; without currencies the default currency of the hotel.

Availability

$availability = Previo::availability('2026-07-01', '2026-07-31', roomKindIds: [94676, 94681]);

$plan = $availability[0];                                   // one entry per rate plan
$plan->on('2026-07-10', 94676);                             // free rooms that day
$plan->minimumFor(94676, '2026-07-10', '2026-07-14');       // free rooms for the whole stay

Previo limits one availability request to 365 days.

Reservations

use Beziworld\Previo\Data\ContactPerson;
use Beziworld\Previo\Data\NewReservation;
use Beziworld\Previo\Data\Reservation;
use Beziworld\Previo\Data\RoomBooking;

// Search: termType is overlap (default), created, check-in, check-out or modified
$arrivals = Previo::reservations(today(), today(), termType: 'check-in', statuses: [Reservation::STATUS_CONFIRMED]);
$changed = Previo::reservations(today()->subDay(), today(), termType: 'modified');
$found = Previo::reservations('2026-01-01', '2026-12-31', fulltext: 'Novák', limit: 50);

foreach ($arrivals as $reservation) {   // one entry per booked room
    $reservation->reservationId;        // resId, shared by the rooms of a group booking
    $reservation->roomReservationId;    // comId
    $reservation->voucher;
    $reservation->from;                 // CarbonImmutable, hotel local time
    $reservation->nights();
    $reservation->roomKindName;
    ($reservation->guests[0] ?? null)?->fullName();
    $reservation->isCancelled();
}

$detail = Previo::reservation(54855023);   // REST detail as an array

// Create
$created = Previo::createReservation(new NewReservation(
    from: '2026-07-10 14:00:00',
    to: '2026-07-14 10:00:00',
    rooms: [
        new RoomBooking(roomKindId: 94676, guestCount: 2, guests: [
            ['firstName' => 'Jan', 'lastName' => 'Novák', 'email' => 'jan@example.com', 'phone' => '+420600000000'],
        ]),
    ],
    statusId: Reservation::STATUS_CONFIRMED,
    voucher: 'WEB-1001',
    note: 'Booked on the website',
    totalPrice: 8400,
    currencyId: 1,
    contactPerson: new ContactPerson('Jan Novák', 'jan@example.com'),
));

$resId = $created[0]->reservationId;

// Modify, change status, cancel
Previo::updateReservation($resId, ['term' => ['from' => '2026-07-10 14:00:00', 'to' => '2026-07-15 10:00:00']]);
Previo::changeReservationStatus($resId, Reservation::STATUS_CONFIRMED);
Previo::cancelRoomReservation($resId, roomReservationId: 71805339);   // one room of a group
Previo::cancelReservation($resId);                                    // every room

Reservation writes are never retried automatically: after a timeout, search by voucher before sending again. Payment card data is deliberately not supported by NewReservation.

Large searches: stream instead of loading

Hotel.searchReservations honours limit and offset only together with fulltext and does not report the number of matches: any other search returns the whole matching set in one response. For a busy hotel and a long period that is many megabytes, and turning it into arrays can exhaust a 128 MB worker. reservations() loads the whole set; reservationsCursor() takes the same arguments and returns a LazyCollection that saves the response to a temporary file while it downloads and converts one <reservation> at a time, so the memory use stays flat:

Previo::reservationsCursor(today()->subYear(), today()->addYear(), termType: 'overlap')
    ->each(function (Reservation $reservation): void {
        // store or queue one room reservation at a time
    });

// Any other read-only XML function with a large list can be streamed the same way
foreach (Previo::streamCall('Hotel.getGuests', ['hotId' => 123], 'guest') as $guest) { /* array */ }

The request is sent when the collection is iterated (once per iteration); transient failures are retried before the first reservation is produced and the temporary file is removed when the iteration ends.

Multi-room reservations

One reservation (resId) with several rooms or services comes back as several room reservations (comId). Previo attaches the guests to one component only; the others have an empty guest list, in the PMS as well. Reading guests from a single component (for example the one a check-in link points to) therefore finds nobody although the reservation has guests. Group the components and read the guests from the group:

use Beziworld\Previo\Data\ReservationGroup;

foreach (ReservationGroup::fromReservations(Previo::reservations('2026-07-01', '2026-07-31')) as $group) {
    $group->reservationId;
    $group->primary();       // the component with the guests, else the first one
    $group->secondary();     // the other components
    $group->guests();        // guests of every component, each once
    $group->totalPrice();    // sum of the component prices without cancelled ones
    $group->from(); $group->to(); $group->currency(); $group->component(146138711);
}

Statuses and change polling

Previo::reservationStatuses();   // list<ReservationStatus>: 1 option, 2 confirmed, 3 checked in, 7 cancelled...

// Previo has no webhooks. Poll System.getChanges2 every 5 to 15 minutes; each call consumes the changes.
foreach (Previo::changes(['getRates', 'getFreeCapacity']) as $change) {
    $change->hotelId;
    $change->type;   // name of the function whose data changed, e.g. "getRates"
}

// At most 100 notices come back per call; drainChanges() calls again until a partial batch arrives.
Previo::drainChanges(allHotels: true);

php artisan previo:poll-changes {connection?} {--type=*} drains the notices of every hotel of a connection and dispatches a Beziworld\Previo\Events\ChangeDetected event (connection, change) for each of them.

Multiple accounts and hotels

One API key may give access to many hotels, and one application may use many Previo accounts. Accounts are configured as named connections, in the same way as database connections; options outside connections are shared defaults:

// config/previo.php
'default' => env('PREVIO_CONNECTION', 'default'),

'connections' => [
    'default' => [
        'api_key' => env('PREVIO_API_KEY'),
        'hotel_id' => env('PREVIO_HOTEL_ID'),
        'language_id' => env('PREVIO_LANGUAGE_ID'),
    ],
    'krakow' => [
        'api_key' => env('PREVIO_KRAKOW_API_KEY'),
        'hotel_id' => env('PREVIO_KRAKOW_HOTEL_ID'),
        'language_id' => 2,
    ],
],
Previo::hotel();                                   // default connection
Previo::connection('krakow')->reservations(...);   // another account

// Credentials stored per tenant in the database, merged with the shared options
$client = Previo::using([
    'api_key' => $tenant->previo_api_key,
    'hotel_id' => $tenant->previo_hotel_id,
]);
$client->availability('2026-07-01', '2026-07-31');

// Hotels of one account and of every configured connection
Previo::availableHotels();      // list<AvailableHotel>: hotelId, name, connection
Previo::allAvailableHotels();   // every connection, each hotel tagged with its connection name

// Another hotel through the same credentials
Previo::forHotel(730099)->reservations('2026-07-01', '2026-07-31');
Previo::forHotel(730099, connection: 'krakow')->rooms();

foreach (Previo::connection('krakow')->hotelClients() as $hotelId => $hotelClient) {   // array<int, PrevioClient>
    $hotelClient->changes();
}

Language and time zone per hotel

Hotels of one account may need different languages (code lists, room kind names, REST X-Previo-Language-ID) or time zones. Overrides inside a connection are applied by forHotel() (and by the synchronizer below); hotels without an override use the options of the connection:

'connections' => [
    'default' => [
        'api_key' => env('PREVIO_API_KEY'),
        'language_id' => 1,
        'hotels' => [
            758015 => ['language_id' => 2, 'timezone' => 'Europe/Warsaw'],
        ],
    ],
],

A config file without connections (only top-level api_key, hotel_id...) keeps working as a single default connection. Every method below is available on any client: Previo::connection('x')->billing()->payments() works the same way as Previo::billing()->payments().

Synchronising many hotels

ReservationSynchronizer keeps a local copy of the reservations of every hotel of a connection. It walks the hotels listed in the connection option sync_hotels (array or PREVIO_SYNC_HOTELS=123,456), or every hotel returned by availableHotels(), streams each search (see above) and dispatches events:

  • ReservationFetched (connection, hotelId, typed Reservation, mode) for every room reservation;
  • HotelSyncCompleted (connection, hotelId, result) or HotelSyncFailed (connection, hotelId, exception, result) per hotel. A failing hotel is reported and skipped; the other hotels continue.

Modes:

Mode Term type Period Cursor
incremental modified stored cursor of the hotel minus overlap_days until now plus overlap_days moved to the start of the run, only when the whole hotel succeeded
horizon overlap today minus overlap_days until horizon_months ahead never moved
range any explicit from and to never moved

The incremental mode catches every new booking, edit and status change cheaply; the horizon mode imports reservations booked long in advance (a future period never matches modified). Without a stored cursor the incremental period starts at today minus the overlap, so run the horizon mode once for the initial import.

// app/Listeners/StorePrevioReservation.php
public function handle(ReservationFetched $event): void
{
    StorePrevioReservation::dispatch($event->hotelId, $event->reservation->raw);   // keep listeners light
}
// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('previo:sync-reservations')->everyTenMinutes()->withoutOverlapping();
Schedule::command('previo:sync-reservations --mode=horizon')->dailyAt('03:30');
Schedule::command('previo:sync-reservations krakow')->everyTenMinutes();   // another connection
Schedule::command('previo:poll-changes')->everyTenMinutes();

previo:sync-reservations {connection?} {--mode=incremental|horizon} {--from=} {--to=} {--term-type=} {--months=} {--hotel=*} prints a summary per hotel and exits with a failure code when any hotel failed. Runs of one connection never overlap: a run that finds another one in progress (cache lock) ends without doing anything. In code:

use Beziworld\Previo\Sync\ReservationSynchronizer;

$result = app(ReservationSynchronizer::class)->incremental('krakow');
app(ReservationSynchronizer::class)->horizon(months: 12, hotelIds: [758015]);
app(ReservationSynchronizer::class)->range('2026-01-01', '2026-03-31', termType: 'created');
app(ReservationSynchronizer::class)->incremental(Previo::using($tenantCredentials));   // runtime credentials

$result->successful(); $result->count(); $result->failed();   // list<HotelSyncResult>: hotelId, count, error, from, to

Options live under sync in config/previo.php: overlap_days (2), horizon_months (18), statuses (empty: every status), cache_store and lock_seconds. Cursors are kept per connection and hotel in the cache by CacheSyncStateStore; bind your own Beziworld\Previo\Sync\SyncStateStore (for example backed by a table) when the cursor must survive a cache flush:

$this->app->singleton(SyncStateStore::class, DatabaseSyncStateStore::class);

Guests

$page = Previo::guests(lastName: 'Novák', limit: 50);   // REST search; GuestPage: total, guests (max. 100 per page)
$page = Previo::searchGuests(fulltext: 'Novák');        // XML search (name, e-mail and address)

$guest = Previo::guest(237384154);   // Guest: names, contact, address, gender, identity, nationalityIds, tagIds...
$guest = Previo::createGuest(['firstName' => 'Jan', 'lastName' => 'Novák', 'email' => 'jan@example.com',
    'address' => ['city' => 'Praha', 'couId' => 1], 'identity' => ['idNumber' => 'AB123', 'guiId' => 2]]);
Previo::updateGuest($guest->id, ['phone' => '+420600000000', 'tags' => [1473418]]);
Previo::deleteGuest($guest->id);

Previo::guestTags();         // list<GuestTag>
Previo::guestCategories();   // GuestCategories: default(), categories (adult, child...), discount model

A guest card belongs to the hotel it was created in. GET and PUT rest/guests/{id} answer 404 when the card is addressed through another hotel of the same account, even when a reservation of that hotel lists the guest (a returning guest who first stayed at another property). Resolve the owner first:

Previo::forHotel($reservationHotelId)->guestOwnerHotel($guestId);           // ?int: current hotel first, then the others
Previo::guestOwnerHotel($guestId, candidateHotelIds: [758015, 730099]);    // without the availableHotels() call
Previo::guest($guestId, resolveOwner: true);                               // or guestAnyHotel($guestId)
Previo::updateGuest($guestId, $changes, resolveOwner: true);

Only a 404 moves on to the next hotel; any other failure (rejected key, outage, rate limit) is thrown, so it is never mistaken for a missing card. The owner is remembered by the connection client and its hotel clients (forgetGuestOwners() clears it); GuestNotFound is thrown when no hotel holds the card.

tags in createGuest() and updateGuest() is normalised: Previo accepts only a list of tag ids or a comma separated id string and rejects anything else ("Tags must be an array tag IDs or string with IDs separated by commas"). Ids, GuestTag objects, arrays with id or tagId, keyed arrays from forms and comma separated strings become a list of ids; a value without any valid id is left out of the request.

Rooms, housekeeping and check-in

use Beziworld\Previo\Data\HotelRoom;

foreach (Previo::rooms() as $room) {   // list<HotelRoom>
    $room->name;
    $room->cleanStatusId;              // HotelRoom::CLEAN_UNTIDY, CLEAN_TIDY, CLEAN_INSPECTION, CLEAN_CHECK_UP
}

Previo::setRoomCleanStatus(763548, HotelRoom::CLEAN_TIDY);
Previo::premiseKinds();               // list<PremiseKind>: capacity, number of rooms, facilities
Previo::objectKinds();                // list<RoomKind> including non-accommodation spaces
Previo::hourlyAvailability('2026-07-01', '2026-07-02', [470597]);   // hourly rented spaces, max. 72 hours

Previo::updateCheckInGuest($resId, ['gueId' => 36491910, 'hasCompleteInfo' => true, /* identity, address... */]);
Previo::uploadCheckInSignature($resId, guestId: 36491910, signature: $pngBase64);
Previo::checkIn(roomReservationId: 65542463);   // arrival today, a complete guest, tidy room
Previo::cardKeys(65542463);                     // list<CardKey>: door lock PIN codes

Accounts, payments and documents

use Beziworld\Previo\Data\BillingDocument;
use Beziworld\Previo\Data\NewAccountItem;
use Beziworld\Previo\Data\Payment;

$account = Previo::billing()->account($resId);   // Account: total, paid, items (list<AccountItem>)
$account->itemsTotal();

Previo::billing()->addAccountItems($comId, 'CZK', [
    new NewAccountItem('Parking', price: 199.50, vatRate: 21, count: 2, segmentId: 8),
]);

Previo::billing()->payments(limit: 50);   // Page<Payment>: total, items
Previo::billing()->addPayment(invoiceId: 31799225, price: 500, currencyId: 1, methodId: Payment::METHOD_CASH);
$issued = Previo::billing()->payReservation($resId, BillingDocument::TYPE_INVOICE, Payment::METHOD_BANK_TRANSFER, withPdf: true);

Previo::billing()->documents('2026-07-01', '2026-07-31', types: ['INV']);   // list<BillingDocument>
Previo::billing()->reservationDocuments($comId);
$pdf = Previo::billing()->printDocument($issued->id);                      // binary PDF content
$xml = Previo::billing()->exportDocuments('2026-07-01', '2026-07-31', 'pohoda');
Previo::billing()->gatewayPayment($comId)->isPaid();

Writes (account items, payments, guests) are never retried automatically.

Prices, services and content

Previo::pricing()->currencies();               // list<Currency> accepted by the hotel
Previo::pricing()->fees();                     // list<Fee>: local visitor tax, accommodation tax
Previo::pricing()->cancellationPolicy()->conditionFor(hoursBeforeArrival: 48);
Previo::pricing()->priceActions();             // list<PriceAction>: seasonal, first/last minute, long stay
Previo::pricing()->services('2026-07-01', '2026-07-05', ['CZK']);   // list<Service> (parking, breakfast...)
Previo::pricing()->stayPackages();             // list<StayPackage>
Previo::pricing()->mealPlans('2026-07-01', '2026-07-05')[0]->price('2026-07-02', mealTypeId: 2, roomKindId: 470625, guestCategoryId: 83014);
Previo::pricing()->paymentMethods('2026-07-01', '2026-07-05');
Previo::pricing()->suggest([['from' => '2026-07-01', 'to' => '2026-07-05', 'roomKindId' => 61,
    'guests' => [['guestCategoryId' => 167238, 'mealTypeId' => 2]]]]);   // PriceSuggestion

Previo::content()->messages(['en', 'de']);   // translated texts
Previo::content()->photogalleries();         // list<Photogallery>: cover(), photos
Previo::content()->amenities();              // list<Amenity>
Previo::content()->reviews(limit: 20);       // list<GuestReview>
Previo::content()->reviewSummary();
Previo::content()->news();

Previo::companies();          // partners: companies, travel agencies
Previo::overviewReport('2026-07-01', '2026-07-31');

Code lists

Previo::system()->countries();   // list<Country>
Previo::system()->currencies();  // list<Currency>
Previo::system()->languages();   // list<Language>
Previo::system()->segments();    // list<CodebookEntry>: account item segments
Previo::system()->meals();       // meal types
Previo::system()->zips(1);       // list<ZipCode> of a country

Code lists change rarely; cache them in your application.

Point of sale (POS)

For restaurant and bar systems that post consumption to hotel accounts (set PREVIO_APP_ID when Previo assigned an application id to your integration):

foreach (Previo::pos()->currentReservations() as $stay) {   // guests in the hotel right now
    $stay->roomName;
    $stay->guests;   // list<Guest>
}

Previo::pos()->addItems($stay->roomReservationId, currencyId: 1, items: [
    new NewAccountItem('Beer 0.5 l', price: 59, vatRate: 21, count: 2, segmentId: 1, ownerType: 'guest', ownerId: 37880011),
]);

EQC (channel managers)

use Beziworld\Previo\Data\EqcRatePlanUpdate;
use Beziworld\Previo\Data\EqcRoomTypeUpdate;

Previo::eqc()->updateAvailabilityAndRates('2026-07-01', '2026-07-31', [
    new EqcRoomTypeUpdate(roomTypeId: 61, availability: 3, ratePlans: [
        new EqcRatePlanUpdate(ratePlanId: 84855, currency: 'CZK', perOccupancyRates: [1 => 1500, 2 => 1900], minLengthOfStay: 2),
    ]),
]);

$pending = Previo::eqc()->bookings();               // pending bookings (not retried)
$recent = Previo::eqc()->bookings(daysInPast: 1);   // list<EqcBooking>

Any other function

Previo::call('Hotel.getMeals', ['hotId' => 123456], idempotent: true);   // any XML function, raw array
Previo::rest('GET', 'calendar/meals', ['filterFrom' => '2026-07-01', 'filterTo' => '2026-07-31']);   // any REST route
Previo::posRequest('GET', 'codebooks/segments', hotelScoped: false);       // any POS route

call() does not add hotId for you. Pass idempotent: true only for read-only functions; those are retried.

API coverage

Every function of the official Previo documentation (XML, REST, POS and EQC) has a dedicated method. Methods written as billing()->... are called on a client or on the facade, for example Previo::billing()->payments().

XML API (/x1/) Method
Hotel.addAccountItem billing()->addAccountItems()
Hotel.addPayment billing()->addPayment()
Hotel.get hotel()
Hotel.getAccountItems billing()->accountItems()
Hotel.getCompanies companies()
Hotel.getConditions pricing()->cancellationPolicy()
Hotel.getCurrencies pricing()->currencies()
Hotel.getDocuments billing()->reservationDocuments()
Hotel.getFees pricing()->fees()
Hotel.getAllFees pricing()->allFees()
Hotel.getGatewayPayInformation billing()->gatewayPayment()
Hotel.getGuestCategories (deprecated) hotelGuestCategories()
Hotel.getGuestReviews content()->reviews()
Hotel.getGuestReviewsSummary content()->reviewSummary()
Hotel.getGuests searchGuests()
Hotel.getMeals (deprecated) pricing()->meals()
Hotel.getMessages content()->messages()
Hotel.getNews content()->news()
Hotel.getObjectKindPhotogallery content()->roomKindPhotogallery()
Hotel.getObjectKinds objectKinds()
Hotel.getProperties content()->amenities()
Hotel.getRates rates()
Hotel.getRoomKinds roomKinds()
Hotel.getPaymentMethods pricing()->paymentMethods()
Hotel.getPayments billing()->payments()
Hotel.getPhotogalleries content()->photogalleries()
Hotel.getPriceActions pricing()->priceActions()
Hotel.getPackages pricing()->services()
Hotel.getStayPackages pricing()->stayPackages()
Hotel.reservation createReservation(), updateReservation(), changeReservationStatus(), cancelReservation(), cancelRoomReservation()
Hotel.searchReservations reservations(), reservationsCursor(), ReservationSynchronizer
Hotel.setCollaboration setCollaboration()
Hotels.availableHotels availableHotels(), hotelClients(), allAvailableHotels()
Hotels.search searchHotels()
Booking.getFreeCapacity (deprecated) freeCapacity()
System.getChanges2 changes(), system()->changes()
System.getCountries system()->countries()
System.getCreditCardTypes system()->creditCardTypes()
System.getCurrencies system()->currencies()
System.getHotelProperties system()->amenities()
System.getHotelTypes system()->hotelTypes()
System.getLanguages system()->languages()
System.getLocalities system()->localities()
System.getMeals system()->meals()
System.getNtakMainCategories system()->ntakMainCategories()
System.getNtakSubCategories system()->ntakSubCategories()
System.getObjectKindProperties system()->roomKindProperties()
System.getPropertyAttributes system()->propertyAttributes()
System.getReservationStatuses reservationStatuses(), system()->reservationStatuses()
System.getSegments system()->segments()
System.getStayProcedures system()->stayProcedures()
System.getStayPrograms system()->stayPrograms()
System.getZips system()->zips()
REST API (/rest/) Method
GET account billing()->account()
GET billing/documents billing()->documents()
GET billing/documents/export billing()->exportDocuments()
GET billing/documents/print billing()->printDocument()
GET calendar/availability availability()
GET calendar/hourly-availability hourlyAvailability()
GET calendar/meals pricing()->mealPlans()
GET events/search crmEvents()
GET guest-categories guestCategories()
GET guests guests()
GET guests/tags guestTags()
GET guests/{id} guest(), guestAnyHotel(), guestOwnerHotel()
POST guests createGuest()
PUT guests/{id} updateGuest()
DELETE guests/{id} deleteGuest()
POST invoice/addPayment billing()->payReservation()
GET packages/search pricing()->searchStayPackages()
GET premise/kind premiseKinds()
GET rate-plan ratePlans()
GET reports/overview overviewReport()
GET reservation reservation()
PUT reservation/checkIn updateCheckInGuest()
POST reservation/CheckInSignature uploadCheckInSignature()
POST reservations/accounts/suggester pricing()->suggest()
GET reservations/rooms/card-locking-keys cardKeys()
POST reservations/rooms/check-in checkIn()
GET rooms rooms()
GET rooms/{id} room()
PUT rooms/{id}/clean-statuses setRoomCleanStatus()
GET services/search pricing()->searchServices()
POS API (/pos/) and EQC (/eqc1/) Method
GET pos/hotels pos()->hotels()
GET pos/codebooks/currencies pos()->currencies()
GET pos/codebooks/segments pos()->segments()
GET pos/reservations/current pos()->currentReservations()
POST pos/accounts/items pos()->addItems()
POST eqc1/ar (AvailRateUpdate) eqc()->updateAvailabilityAndRates()
POST eqc1/br (BookingRetrieval) eqc()->bookings()

Errors

Exception When
InvalidConfiguration API key (or login and password) missing, hotel id missing for a hotel-scoped call, invalid timezone
ApiException XML <error> document, REST or POS error body, EQC <Error code>, HTTP error, unreadable response or connection failure

ApiException exposes apiCode() (Previo code such as 2004 invalid credentials, 1023 unauthorized hotel, 1030 simultaneous access, 6000 reservation locked; or the REST code), httpStatus(), operation() (for example Hotel.searchReservations, GET rest/rate-plan, GET pos/hotels or EQC.AvailRateUpdate), userMessage() (localised REST message) and the helpers isAuthenticationError(), isAuthorizationError(), isTransportError() and isRetryable(). All exceptions extend PrevioException.

Read-only calls are retried (PREVIO_RETRIES) on connection errors, HTTP 5xx, 1003 (service temporarily unavailable) and 1030, because Previo rejects a call while another one of the same account is still running.

Development

composer install
composer ci   # pint --test, phpstan (level 8), pest

Author

Created and maintained by Kamil Ćwiertnia, beziworld.eu.

Need a hotel / PMS integration? Get in touch through beziworld.eu.

License

MIT. See LICENSE.md. This package is not affiliated with or endorsed by Previo.