PHPackages                             tbi-software/laravel-roomdoo - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. tbi-software/laravel-roomdoo

ActiveLibrary[API Development](/categories/api)

tbi-software/laravel-roomdoo
============================

Laravel client for the Roomdoo PMS internal API (folios, reservations, services, charges, invoices, contacts). Published by TBI Consulting Group SL under PolyForm Noncommercial 1.0.0 — see COMMERCIAL.md for commercial use.

00PHPCI passing

Since May 22Pushed 2mo agoCompare

[ Source](https://github.com/Tu-buen-camino/laravel-roomdoo)[ Packagist](https://packagist.org/packages/tbi-software/laravel-roomdoo)[ RSS](/packages/tbi-software-laravel-roomdoo/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependenciesVersions (2)Used By (0)

tbi-software/laravel-roomdoo
============================

[](#tbi-softwarelaravel-roomdoo)

[![tests](https://github.com/Tu-buen-camino/laravel-roomdoo/actions/workflows/tests.yml/badge.svg)](https://github.com/Tu-buen-camino/laravel-roomdoo/actions/workflows/tests.yml) [![PHP 8.2+](https://camo.githubusercontent.com/c9f64f714c636ba27a3bba6dfd52f98426832db1262747efa54b212d16943651/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253545382e322d626c7565)](https://www.php.net/releases/8.2/en.php) [![Laravel 11/12](https://camo.githubusercontent.com/5bd61ec377dc8f6b4d88d43749cfe4da6a6e79fe5f3471cc226b3c0353295a88/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c61726176656c2d313125323025374325323031322d726564)](https://laravel.com) [![License: PolyForm NC 1.0.0](https://camo.githubusercontent.com/468796fc0c7cef278039ac783d3f95c1eed9b09d2c9572c97e889c80b87b951e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d506f6c79466f726d2532304e6f6e636f6d6d65726369616c253230312e302e302d626c7565)](LICENSE) [![Commercial license available](https://camo.githubusercontent.com/056594e571614291ce50831c4ebb2c7af3eabfb289fd393eb9f30f7bcde63a84/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636f6d6d65726369616c2532306c6963656e73652d617661696c61626c652d6f72616e6765)](COMMERCIAL.md) [![Repo](https://camo.githubusercontent.com/60331a8168aad1a0b324d44dde854bbeda5e322a404660844c8b5590b4645798/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7265706f2d54752d2d6275656e2d2d63616d696e6f2532466c61726176656c2d2d726f6f6d646f6f2d3138313731373f6c6f676f3d676974687562)](https://github.com/Tu-buen-camino/laravel-roomdoo)

A Laravel client for the **Roomdoo PMS internal API** (`https://your-instance.host.roomdoo.com`). Wraps folios, reservations, extra services, charges, refunds, invoices, contacts, calendar and catalogs behind typed DTOs and a fluent facade.

- **Repository:**
- **Issues:**
- **Publisher:** TBI Consulting Group SL —
- **Commercial licensing:** [COMMERCIAL.md](COMMERCIAL.md) · `licensing@tbi-software.com`

> Source-available under the [PolyForm Noncommercial License 1.0.0](LICENSE) — free for personal, research, educational, charitable, and government use. A [commercial license](COMMERCIAL.md) is required for use in paid SaaS, commercial products, and for-profit internal tooling.

> **Reverse-engineered.** Roomdoo does not publish their internal API. This client was built by capturing SPA traffic against `your-instance.roomdoo.com`. Endpoints marked `@beta` were inferred from UI behavior and may break without notice. See [`docs/roomdoo-api.md`](docs/roomdoo-api.md) for the full endpoint reference and known quirks.

Table of contents
-----------------

[](#table-of-contents)

- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Quick start](#quick-start)
- [Resources](#resources)
    - [Instance and properties](#instance-and-properties)
    - [Catalog](#catalog)
    - [Calendar and availability](#calendar-and-availability)
    - [Rates (pricelists, availability plans, cancelation rules)](#rates-pricelists-availability-plans-cancelation-rules)
    - [Folios](#folios)
    - [Reservations](#reservations)
    - [Extra services](#extra-services)
    - [Charges, refunds and transactions](#charges-refunds-and-transactions)
    - [Invoices](#invoices)
    - [Contacts](#contacts)
    - [Checkin partners](#checkin-partners)
    - [Files](#files)
- [Cancelling a folio](#cancelling-a-folio)
- [Multi-property usage](#multi-property-usage)
- [Custom token providers](#custom-token-providers)
- [Error handling](#error-handling)
- [Raw responses](#raw-responses)
- [Beta endpoints](#beta-endpoints)
- [Testing](#testing)
- [Contributing](#contributing)
- [License](#license)

Requirements
------------

[](#requirements)

- PHP **8.2+**
- Laravel **11** or **12**
- A Roomdoo **JWT token** obtained from a logged-in session (cookie name `jwt` in the browser)

Installation
------------

[](#installation)

Install via Composer from Packagist:

```
composer require tbi-software/laravel-roomdoo
```

Then publish the config (optional but recommended):

```
php artisan vendor:publish --tag=roomdoo-config
```

Configuration
-------------

[](#configuration)

Set the following in your `.env`:

```
ROOMDOO_BASE_URL=https://your-instance.host.roomdoo.com
ROOMDOO_JWT=eyJhbGciOi...                # JWT captured from the browser cookie
ROOMDOO_PMS_PROPERTY_ID=2                # your default property id

# optional
ROOMDOO_AUTH_HEADER=cookie               # "cookie" (default) or "bearer"
ROOMDOO_TIMEOUT=30
ROOMDOO_RETRY_TIMES=0
ROOMDOO_RETRY_SLEEP_MS=200
ROOMDOO_LOG_ENABLED=false
ROOMDOO_LOG_CHANNEL=stack
```

The config file (`config/roomdoo.php`) also lets you wire a custom token provider class — useful in multi-tenant apps where each authenticated user has their own Roomdoo JWT.

Quick start
-----------

[](#quick-start)

```
use Tbi\Roomdoo\Facades\Roomdoo;

// look up an existing folio
$folio = Roomdoo::folios()->find(8293);
echo $folio->name;                       // "F/26050611"
echo $folio->amountTotal;                // 832.0

// register a cash charge
$txId = Roomdoo::folios()->charge(
    folioId: 8293,
    amount: 832.0,
    journalId: 11,                       // 11 = cash, from /api/account-journals
    date: '2026-05-22',
);

// create an invoice from the folio's sale lines and validate it
$saleLines = Roomdoo::folios()->saleLines(8293);
$invoiceId = Roomdoo::folios()->createInvoice(
    folioId: 8293,
    partnerId: 1187,
    saleLines: $saleLines->map->raw->all(),
);

$invoice = Roomdoo::invoices()->find($invoiceId);
Roomdoo::invoices()->validate(
    invoiceId: $invoiceId,
    date: '2026-05-22',
    partnerId: 1187,
    moveLines: $invoice->moveLines->map->raw->all(),
);
```

Resources
---------

[](#resources)

The facade exposes one resource per logical group. Every list-returning method returns an `Illuminate\Support\Collection` of typed DTOs. Every DTO carries `->raw` (the original API payload) and `->toArray()` for serialization.

### Instance and properties

[](#instance-and-properties)

```
// instance metadata and per-module feature flags
$instance = Roomdoo::instance()->get();
$features = Roomdoo::instance()->extraFeatures('pms');

// list properties (the `id` you pass to forProperty / ROOMDOO_PMS_PROPERTY_ID)
$properties = Roomdoo::properties()->all();     // Collection
$pmsProps   = Roomdoo::properties()->pms();
$links      = Roomdoo::properties()->links(2);
```

### Catalog

[](#catalog)

Read-only master data needed to build payloads for the rest of the API.

```
$rooms        = Roomdoo::catalog()->rooms();            // Collection
$products     = Roomdoo::catalog()->products();         // Collection
$journals     = Roomdoo::catalog()->accountJournals();  // Collection
$boardSvcs    = Roomdoo::catalog()->boardServices();    // Collection
$saleChannels = Roomdoo::catalog()->saleChannels();
$pricelists   = Roomdoo::catalog()->pricelists();
$countries    = Roomdoo::catalog()->countries();
$states       = Roomdoo::catalog()->countryStates(68);  // states of country 68
$zip          = Roomdoo::catalog()->zipAutocomplete('15001');
$products     = Roomdoo::catalog()->products();
```

Other methods: `languages, categories, idCategories, fiscalDocTypes, agencies, availabilityPlans, roomTypes, roomTypeClasses, amenities, amenityTypes, roomClosureReasons, boardServiceLines, extraBeds, notificationsReservationsToAssign`.

### Calendar and availability

[](#calendar-and-availability)

```
// daily prices for a room type over a date range
$prices = Roomdoo::calendar()->prices(
    pricelistId: 1,
    roomTypeId: 6,
    dateFrom: '2026-12-10',
    dateTo: '2026-12-12',
);

// availability for a room type
$avails = Roomdoo::calendar()->avails(
    availabilityFrom: '2026-12-10',
    availabilityTo: '2026-12-11',
    roomTypeId: 3,
);

// full planning grid
$grid = Roomdoo::calendar()->between(
    dateFrom: '2026-12-01',
    dateTo: '2026-12-31',
    availabilityPlanId: 1,
);
```

### Rates (pricelists, availability plans, cancelation rules)

[](#rates-pricelists-availability-plans-cancelation-rules)

Rate management is split into three entities — see [`docs/roomdoo-api.md` §15](docs/roomdoo-api.md#15-rates-pricelists-availability-plans-cancelation-rules) for the full reference.

```
// list pricelists (with cancelationRuleId, defaultAvailabilityPlanId,
// pmsPropertyIds, saleChannelIds)
$pricelists = Roomdoo::pricelists()->all();

// only those active in the planning's daily-management dropdown
$active = Roomdoo::pricelists()->all(isActiveOnDailyManagement: true);

$pricelist = Roomdoo::pricelists()->find(1);                 // detail
$cancellation = Roomdoo::cancelationRules()->all();
$plans = Roomdoo::availabilityPlans()->all();

// read prices + restrictions in one call (preferred by the planning view)
$grid = Roomdoo::calendar()->pricesRules(
    dateFrom: '2026-06-01',
    dateTo: '2026-06-30',
    availabilityPlanId: 1,
    pricelistId: 1,
);
foreach ($grid as $roomTypeBlock) {
    $roomTypeBlock->roomTypeId;
    foreach ($roomTypeBlock->dates as $day) {
        $day->price; $day->quota; $day->closed; $day->minStay; // …
    }
}

// read prices only
$items = Roomdoo::pricelists()->items(
    pricelistId: 1,
    dateFrom: '2026-06-01',
    dateTo: '2026-06-30',
);

// read restrictions only
$rules = Roomdoo::availabilityPlans()->rules(
    availabilityPlanId: 1,
    dateFrom: '2026-06-01',
    dateTo: '2026-06-30',
);
```

#### Writing prices

[](#writing-prices)

```
// upsert one or more items (pricelistItemId = -1 to create, real id to update)
Roomdoo::pricelists()->upsertItems(1, [
    ['pricelistItemId' => -1, 'roomTypeId' => 3, 'date' => '2026-12-01', 'price' => 120, 'pmsPropertyId' => 2, 'pricelistId' => 1],
    ['pricelistItemId' => 3566, 'roomTypeId' => 3, 'date' => '2026-05-21', 'price' => 95, 'pmsPropertyId' => 2, 'pricelistId' => 1],
]);

// bulk across many days / room types in one request
Roomdoo::pricelists()->batchChanges([
    ['pricelistItemId' => -1, 'roomTypeId' => 3, 'date' => '2026-12-01', 'price' => 120, 'pmsPropertyId' => 2, 'pricelistId' => 1],
    ['pricelistItemId' => -1, 'roomTypeId' => 3, 'date' => '2026-12-02', 'price' => 120, 'pmsPropertyId' => 2, 'pricelistId' => 1],
]);
```

#### Writing restrictions

[](#writing-restrictions)

Field semantics: `quota: -1` unlimited, `0` closed by quota, `N` allocated rooms. `minStay/maxStay`: `0` = no restriction. `closed: true` fully blocks the day for that room type.

```
Roomdoo::availabilityPlans()->upsertRules(1, [[
    'availabilityRuleId' => -1,
    'roomTypeId' => 3,
    'date' => '2026-05-21',
    'quota' => 5,
    'maxAvailability' => -1,
    'minStay' => 2,
    'closed' => false,
    'availabilityPlanId' => 1,
    'pmsPropertyId' => 2,
]]);

// single-rule POST variant (no envelope)
Roomdoo::availabilityPlans()->createRule(1, [
    'roomTypeId' => 3, 'date' => '2026-12-03', 'quota' => 10, 'pmsPropertyId' => 2,
]);

// bulk — unset fields MUST be sent as null (not 0) so the backend leaves them alone
Roomdoo::availabilityPlans()->batchChanges([
    [
        'availabilityRuleId' => -1, 'roomTypeId' => 3, 'date' => '2026-12-03',
        'quota' => 10, 'maxAvailability' => null,
        'minStay' => 2, 'maxStay' => null, 'minStayArrival' => null, 'maxStayArrival' => null,
        'closed' => false, 'closedDeparture' => null, 'closedArrival' => null,
        'pmsPropertyId' => 2, 'availabilityPlanId' => 1,
    ],
]);
```

> Master entities (`pricelist`, `availability-plan`, `cancelation-rule`) and the assignments between pricelists, channels and properties are managed in the Odoo backend — the SPA does not expose endpoints for those operations.

### Folios

[](#folios)

A folio groups one or more reservations and is the unit of billing.

```
// create a folio + reservations in one call
$folioId = Roomdoo::folios()->create([
    'pmsPropertyId' => 2,
    'checkin' => '2026-12-10',
    'checkout' => '2026-12-12',
    'saleChannelId' => 1,
    'pricelistId' => 1,
    'reservationType' => 'normal',
    'partnerName' => 'Test API Roomdoo',
    'partnerEmail' => 'testapi@example.com',
    'partnerPhone' => '600000000',
    'preferredCommunicationLang' => 'es_ES',
    'sendConfirmationEmail' => false,
    'reservations' => [[
        'roomTypeId' => 3,
        'preferredRoomId' => 13,
        'adults' => 2,
        'children' => 0,
        'reservationLines' => [
            ['date' => '2026-12-10', 'price' => 400.0, 'roomId' => 13],
            ['date' => '2026-12-11', 'price' => 400.0, 'roomId' => 13],
        ],
    ]],
]);

// search, find and sub-resources
$folios       = Roomdoo::folios()->search('Test API');
$folio        = Roomdoo::folios()->find($folioId);
$reservations = Roomdoo::folios()->reservations($folioId);
$saleLines    = Roomdoo::folios()->saleLines($folioId);
$transactions = Roomdoo::folios()->transactions($folioId);
$invoices     = Roomdoo::folios()->invoices($folioId);
$messages     = Roomdoo::folios()->messages($folioId);
```

### Reservations

[](#reservations)

```
$reservation     = Roomdoo::reservations()->find(9110);
$lines           = Roomdoo::reservations()->lines(9110);          // 1 per night
$services        = Roomdoo::reservations()->services(9110);
$checkinPartners = Roomdoo::reservations()->checkinPartners(9110);
$wizardStates    = Roomdoo::reservations()->wizardStates(9110);

// update reservation (e.g. room change) — @beta
Roomdoo::reservations()->update(9110, ['preferredRoomId' => 13]);
```

### Extra services

[](#extra-services)

```
// add breakfast (productId 7) on two dates
$serviceId = Roomdoo::services()->add(
    reservationId: 9110,
    productId: 7,
    serviceLines: [
        ['date' => '2026-12-11', 'priceUnit' => 8, 'quantity' => 2],
        ['date' => '2026-12-12', 'priceUnit' => 8, 'quantity' => 2],
    ],
);

// @beta — confirm shape against live API
Roomdoo::services()->update(9110, $serviceId, [...]);
Roomdoo::services()->delete(9110, $serviceId);

// @beta — used by the SPA to suggest default prices before adding
$defaults = Roomdoo::services()->defaultPrices(
    productId: 7, pricelistId: 1, dateFrom: '2026-12-11', dateTo: '2026-12-12',
);
```

### Charges, refunds and transactions

[](#charges-refunds-and-transactions)

Two different ways to "reverse" a charge depending on accounting policy:

```
// register a charge (returns the new transaction id)
$txId = Roomdoo::folios()->charge(
    folioId: 8293,
    amount: 832.0,
    journalId: 11,
    date: '2026-5-22',                  // YYYY-M-D for /charge and /refund
);

// register a refund (creates a new opposing transaction — audit-friendly)
Roomdoo::folios()->refund(
    folioId: 8293,
    amount: 832.0,
    journalId: 11,
    date: '2026-5-22',
    partnerId: 1187,
    reference: 'Cancelacion',
);

// void a transaction in place (sets amount=0 — original amount is lost)
Roomdoo::payments()->voidTransaction(
    transactionId: $txId,
    journalId: 11,
    date: '2026-05-22',                 // YYYY-MM-DD for /transactions/p/
    reference: 'F/26050611',
);

// edit a transaction (change method, amount, date, partner — all fields required)
Roomdoo::payments()->editTransaction(
    transactionId: $txId,
    journalId: 9,                       // switch cash → card
    amount: 500.0,
    date: '2026-05-22',
    transactionType: 'customer_inbound',
    reference: 'F/26050611',
    partnerId: 1187,
);

// cash register / cashbook
$cashbook = Roomdoo::payments()->cashRegister(journalId: 11);
```

> ⚠️ Note the **date format quirk**: `/charge` and `/refund` use `YYYY-M-D` (no zero-padding), while `/transactions/p/{id}` uses `YYYY-MM-DD`. The package passes dates through unmodified — pass them in the format each endpoint expects.

### Invoices

[](#invoices)

```
// create a proforma from a folio's sale-lines
$invoiceId = Roomdoo::folios()->createInvoice(
    folioId: 8293,
    partnerId: 1187,
    saleLines: Roomdoo::folios()->saleLines(8293)->map->raw->all(),
);

// fetch and validate (proforma -> posted)
$invoice = Roomdoo::invoices()->find($invoiceId);

Roomdoo::invoices()->validate(
    invoiceId: $invoiceId,
    date: '2026-05-22',
    partnerId: 1187,
    moveLines: $invoice->moveLines->map->raw->all(),
);

// list and filter
$invoices = Roomdoo::invoices()->list([
    'limit' => 80,
    'dateStart' => '2026-04-22',
    'dateEnd' => '2026-05-22',
]);

// @beta — buttons in the UI but not directly captured
Roomdoo::invoices()->updateDraft($invoiceId, [...]);
Roomdoo::invoices()->cancel($invoiceId);
Roomdoo::invoices()->refund($invoiceId);
Roomdoo::invoices()->sendMail($invoiceId);
$pdf = Roomdoo::invoices()->pdf($invoiceId);   // raw bytes
```

### Contacts

[](#contacts)

```
// create a company
$contact = Roomdoo::contacts()->create([
    'contactType' => 'company',
    'name' => 'TEST API SL',
    'street' => 'Calle Test 1',
    'zipCode' => '15001',
    'city' => 'A Coruña',
    'country' => 68,
    'fiscalIdNumber' => 'B12345674',
    'fiscalIdNumberType' => 'vat',
]);

// create an individual
Roomdoo::contacts()->create([
    'contactType' => 'individual',
    'firstname' => 'Jane',
    'lastname' => 'Doe',
    'email' => 'jane@example.com',
    'birthdate' => '1990-05-12',
    'fiscalIdNumber' => '12345678Z',
    'fiscalIdNumberType' => 'vat',
]);

// update
Roomdoo::contacts()->update($contact->id, ['name' => 'TEST']);

// paginated lists
$result = Roomdoo::contacts()->list(page: 1, pageSize: 50);
$result->items;            // Collection
$result->total;            // total available
$result->hasMorePages();   // bool

// other lists
Roomdoo::contacts()->customers();
Roomdoo::contacts()->agencies();
Roomdoo::contacts()->guests();
Roomdoo::contacts()->suppliers();

// free-text search across all partners
$results = Roomdoo::contacts()->searchPartners('test', limit: 20);
```

### Checkin partners

[](#checkin-partners)

```
// listing is done via the reservation resource
$guests = Roomdoo::reservations()->checkinPartners(9110);

// @beta — REST pattern inferred
Roomdoo::checkinPartners()->create(9110, [...]);
Roomdoo::checkinPartners()->update(9110, $checkinPartnerId, [...]);
Roomdoo::checkinPartners()->delete(9110, $checkinPartnerId);
```

### Files

[](#files)

```
$url   = Roomdoo::files()->imageUrl(123, accessToken: 'abc');
$bytes = Roomdoo::files()->downloadImage(123, accessToken: 'abc');
```

Cancelling a folio
------------------

[](#cancelling-a-folio)

```
Roomdoo::folios()->cancel(8293);
```

### What does NOT block cancellation

[](#what-does-not-block-cancellation)

Confirmed empirically — these all let `cancel()` succeed:

- **Positive charge balance.** The folio still cancels; `pendingAmount` goes negative (overpayment).
- **Registered refunds** (`Roomdoo::folios()->refund(...)`).
- **Voided transactions** (`Roomdoo::payments()->voidTransaction(...)`).
- **Down-payment invoices** in `state="posted"` (`isDownPayment` move lines). They remain valid.

### What DOES block cancellation

[](#what-does-block-cancellation)

```
HTTP 400 — "You cannot delete a sale order line once a invoice has been created from it."

```

A **full sales invoice** attached to the folio in `state="posted"` (i.e. `move_type="out_invoice"` with `isDownPayment=false`) blocks cancellation. Cancel or refund the invoice first via `Roomdoo::invoices()->cancel($id)` / `Roomdoo::invoices()->refund($id)` (both currently `@beta`).

### Recommended flow

[](#recommended-flow)

```
// 1. inspect existing invoices to detect blockers
$blocking = Roomdoo::folios()->invoices($folioId)
    ->filter(fn ($inv) => $inv->state === 'posted'
        && collect($inv->moveLines ?? [])->every(fn ($l) => empty($l->isDownPayment)));

// 2. if there are any, cancel/refund them first (beta)
foreach ($blocking as $inv) {
    Roomdoo::invoices()->cancel($inv->id);  // or ->refund($inv->id)
}

// 3. (optional) clean up the charge balance for a 0-balance cancellation
//    Otherwise cancellation succeeds with an overpayment.
foreach (Roomdoo::folios()->transactions($folioId) as $tx) {
    Roomdoo::payments()->voidTransaction(
        transactionId: $tx->id,
        journalId: $tx->journalId,
        date: now()->toDateString(),
        transactionType: $tx->transactionType,
        reference: $tx->reference,
        partnerId: $tx->partnerId,
    );
}

// 4. cancel
Roomdoo::folios()->cancel($folioId);
```

See [`docs/roomdoo-api.md` §14](docs/roomdoo-api.md#14-reservation-cancellation-flow) for the full diagnostic trail (test folios 8293, 8296, 8297, 8298).

Multi-property usage
--------------------

[](#multi-property-usage)

The default property comes from `ROOMDOO_PMS_PROPERTY_ID`. To call against another property in the same request:

```
$folios = Roomdoo::forProperty(5)->folios()->search('Smith');

// all subsequent calls on the returned instance use property 5
$forBranch = Roomdoo::forProperty(5);
$forBranch->catalog()->rooms();
$forBranch->folios()->find($id);
```

`pmsPropertyId` is auto-injected on the property-scoped endpoints (catalog, calendar, folio search, invoice list, customer/guest/supplier lists).

Custom token providers
----------------------

[](#custom-token-providers)

By default the JWT is read from `ROOMDOO_JWT`. For multi-tenant apps, where each user has their own Roomdoo session, implement `Tbi\Roomdoo\Client\JwtTokenProvider`:

```
namespace App\Roomdoo;

use Tbi\Roomdoo\Client\JwtTokenProvider;

class UserScopedTokenProvider implements JwtTokenProvider
{
    public function token(): ?string
    {
        return auth()->user()?->roomdoo_jwt;
    }
}
```

Then in `config/roomdoo.php`:

```
'token_provider' => \App\Roomdoo\UserScopedTokenProvider::class,
```

For closure-based use:

```
use Tbi\Roomdoo\Client\CallbackJwtTokenProvider;

app()->singleton(JwtTokenProvider::class, fn () =>
    new CallbackJwtTokenProvider(fn () => session('roomdoo_jwt'))
);
```

Error handling
--------------

[](#error-handling)

Non-2xx responses throw subclasses of `Tbi\Roomdoo\Exceptions\RoomdooException`:

ExceptionTriggers on`AuthenticationException`401, 403`NotFoundException`404`ValidationException`422`RateLimitException`429`ServerException`5xx`RoomdooException` (base)Anything else (incl. 400)Each exception exposes:

```
try {
    Roomdoo::folios()->cancel($folioId);
} catch (\Tbi\Roomdoo\Exceptions\RoomdooException $e) {
    $e->status;     // int — HTTP status
    $e->body;       // string — raw response body
    $e->errors;     // ?array — decoded JSON if any
    $e->response;   // ?Illuminate\Http\Client\Response — original response
    report($e);
}
```

> The Roomdoo backend occasionally returns Odoo-style error strings inside `200` responses on internal errors. If you observe surprising behaviour, log the `->raw` payload of the returned DTO before reporting a bug.

Raw responses
-------------

[](#raw-responses)

Every DTO carries the original payload under `->raw`:

```
$folio = Roomdoo::folios()->find(8293);
$folio->raw['externalReference'];
$folio->raw['createdBy'];
```

If a field is missing from the typed DTO, you can always read it from `->raw` without subclassing. Open an issue or PR to promote it into the DTO when useful.

Beta endpoints
--------------

[](#beta-endpoints)

These endpoints are exposed by the client but inferred (not captured directly). Confirm shape against the live API before using them in production. They are documented with `@beta` in their PHP doc-blocks.

- `PATCH /api/reservations/{id}` — `Roomdoo::reservations()->update()`
- `PATCH /api/folios/{id}/batch-changes` — `Roomdoo::folios()->batchChanges()`
- `PATCH/DELETE /api/reservations/{id}/services/{id}` — `Roomdoo::services()->update()/delete()`
- `GET /api/products/{id}/default-prices` — `Roomdoo::services()->defaultPrices()`
- `POST/PATCH/DELETE /api/reservations/{id}/checkin-partners[...]` — `Roomdoo::checkinPartners()->*`
- `POST /api/invoices/{id}/{send-mail|cancel|refund}` — `Roomdoo::invoices()->sendMail()/cancel()/refund()`
- `GET /api/invoices/{id}/pdf` — `Roomdoo::invoices()->pdf()`
- `PATCH /api/invoices/{id}` (edit draft) — `Roomdoo::invoices()->updateDraft()`
- `POST/DELETE /pmsApi/contacts/{id}/id-numbers[...]` — `Roomdoo::contacts()->addIdNumber()/deleteIdNumber()`

Testing
-------

[](#testing)

```
composer install
vendor/bin/pest
```

Tests use Laravel's `Http::fake()` together with JSON fixtures captured from a real Roomdoo session (`tests/Fixtures/*.json`). No network access is required.

To add your own integration tests:

```
use Illuminate\Support\Facades\Http;
use Tbi\Roomdoo\Facades\Roomdoo;

it('does the thing', function () {
    Http::fake([
        'your-instance.host.roomdoo.com/api/folios/*' => Http::response([/* ... */], 200),
    ]);

    $result = Roomdoo::folios()->find(8293);

    expect($result->id)->toBe(8293);
    Http::assertSent(fn ($r) => $r->hasHeader('Cookie', 'jwt=test-jwt-token'));
});
```

Links
-----

[](#links)

- **Source code:**
- **Issue tracker:**
- **API reference (reverse-engineered):** [`docs/roomdoo-api.md`](docs/roomdoo-api.md)
- **Contributing:** [`CONTRIBUTING.md`](CONTRIBUTING.md)
- **Changelog:** [`CHANGELOG.md`](CHANGELOG.md)
- **Security policy:** [`SECURITY.md`](SECURITY.md)
- **Commercial licensing:** [`COMMERCIAL.md`](COMMERCIAL.md) · `licensing@tbi-software.com`
- **Publisher:** [TBI Consulting Group SL](https://tbi-software.com)

Contributing
------------

[](#contributing)

PRs welcome at . Since this wraps a reverse-engineered API, the most valuable contributions confirm real request/response shapes. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for setup, conventions, the DCO sign-off and how to anonymise fixtures.

In short:

1. **Confirming a `@beta` endpoint.** Capture the request from the browser's network panel and update the corresponding `Resource` + add a test.
2. **Promoting `->raw` fields to typed DTO properties.** If your project keeps reading a specific field via `->raw`, open a PR.
3. **Adding fixtures.** Anonymise and add to `tests/Fixtures/` then write a test.

Conventional style:

- Resources own URL paths; DTOs own response shapes.
- New fields go into the DTO constructor as nullable, with a `null`-safe `fromArray()` cast.
- Beta endpoints must be marked `@beta` in the PHPDoc and listed in this README.

Security issues: please follow [`SECURITY.md`](SECURITY.md) — do not open a public issue. Release history is in [`CHANGELOG.md`](CHANGELOG.md).

License
-------

[](#license)

Dual-licensed by **TBI Consulting Group SL**:

1. [**PolyForm Noncommercial License 1.0.0**](LICENSE) — default. Free for personal, research, educational, charitable, public-safety, environmental and government use.
2. [**Commercial license**](COMMERCIAL.md) — required for paid SaaS, commercial products, internal tooling of for-profit organisations, redistribution and OEM use.

If you are unsure which applies to your use case, see [`COMMERCIAL.md`](COMMERCIAL.md) or contact `licensing@tbi-software.com`.

Contributions are accepted under the same dual-licensing scheme — see [`COMMERCIAL.md` → Contributions](COMMERCIAL.md#contributions).

###  Health Score

19

—

LowBetter than 9% of packages

Maintenance58

Moderate activity, may be stable

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity13

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/53398615?v=4)[Abraham Leiro](/maintainers/abrahampo1)[@abrahampo1](https://github.com/abrahampo1)

---

Top Contributors

[![abrahampo1](https://avatars.githubusercontent.com/u/53398615?v=4)](https://github.com/abrahampo1 "abrahampo1 (8 commits)")

---

Tags

laravellaravel-roomdooodooroomdoo

### Embed Badge

![Health badge](/badges/tbi-software-laravel-roomdoo/health.svg)

```
[![Health](https://phpackages.com/badges/tbi-software-laravel-roomdoo/health.svg)](https://phpackages.com/packages/tbi-software-laravel-roomdoo)
```

###  Alternatives

[exsyst/swagger

A php library to manipulate Swagger specifications

35916.4M7](/packages/exsyst-swagger)[hubspot/api-client

Hubspot API client

24016.2M20](/packages/hubspot-api-client)[pocketmine/bedrock-protocol

An implementation of the Minecraft: Bedrock Edition protocol in PHP

172445.0k18](/packages/pocketmine-bedrock-protocol)[botman/driver-telegram

Telegram driver for BotMan

93459.5k6](/packages/botman-driver-telegram)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
