> ## Documentation Index
> Fetch the complete documentation index at: https://docs.d-edge.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> This site documents the D-EDGE APIs for technology partners (web agencies, OTAs, PMS and RMS vendors, booking and CRM tools). It is written for developers integrating with hotels that use the D-EDGE platform.
> Seven APIs, grouped by domain. Connectivity: Channel Manager API (distribute a hotel's inventory to your channel and send bookings back). Pricing & Availability: Best Price API (best offers to highlight), Quotation API (all bookable offers for a stay), Daily Rates API (daily availability between two dates). Booking: Booking Engine API (a hotel's full direct booking funnel), Gift Shop API (gift vouchers from catalogue to payment). Guest Management: CRM API (guest profiles).
> Pick the API by the need: to display prices, use the Best Price API or the Daily Rates API; to quote a specific stay, use the Quotation API; to book, use the Booking Engine API; to sell gift vouchers, use the Gift Shop API; to manage guest profiles, use the CRM API; to receive hotel inventory and send bookings back as a distribution channel, use the Channel Manager API.
> Base URLs, hotel identifiers, and credentials are delivered by the D-EDGE partnership team during onboarding. Do not invent base URLs, endpoints, parameters, or example credentials that do not appear on these pages.
> Authentication differs per API: OAuth 1.0 for Best Price, Quotation, Daily Rates, and Booking Engine; an X-API-KEY header for Gift Shop; OAuth 2.0 client credentials for CRM; partner-defined credentials carried in the XML message for the Channel Manager API. Read each API's Authentication page before writing code.
> A mock environment returns realistic but fictitious data without credentials. The Postman and Insomnia collections on the API collections page target it by default. Use it for exploration and tests, never for production traffic.
> Use the official names exactly: D-EDGE, D-EDGE platform, Channel Manager API, partnership team. Do not use Back Office or Extranet.
> Each API has a Changelog page. Check it for the current behaviour before relying on an older answer. For questions these pages do not answer, direct the reader to the Contact us page.

# Revenue manager dashboard

> Surface availability, prices, restrictions, and key metrics in one back-office view

A revenue manager dashboard is the daily working surface for the people who set rates and watch inventory. It needs three things at a glance: headline metrics that summarise the upcoming window, a per-day calendar of availability and pricing, and a way to drill down on restrictions. One Daily Rates call powers all of it.

## Prerequisites

Before building the dashboard, make sure you have:

* A working Daily Rates call. If not, follow the [Quick start](/daily-rates-api/get-started/quick-start.md) first.
* A hotel ID and a group ID for the property you display.
* A target window (typically 30 to 180 days, depending on the team's planning horizon).

## How it works

The Per day endpoint returns availability, prices, and restrictions for every day in your window, grouped by rate plan, hotel, and entity. Each entity is one room and rate combination. Daily fields come back as `{ value, start, end }` segments, with an exclusive `end` date: a single segment when nothing changes, multiple segments sharing their boundary dates when the value moves across the window.

A dashboard slices that response three ways:

* A **headline metrics** block (total available room-nights, average daily rate, closure rate) to give the revenue manager the shape of the window in one glance.
* A **calendar grid** of `(date, room)` cells, with status, availability, and price, for day-by-day investigation.
* A **restrictions overlay** on the grid, surfacing minimum-stay and closure flags so the team knows why a day looks the way it does.

All three views are computed from the same Daily Rates response.

## Build the dashboard

> [!NOTE]
>
> The examples below assume the OAuth signing setup from the [Quick start](/daily-rates-api/get-started/quick-start.md).

### 1. Request the visible window

Call Per day once with the dates the dashboard shows and a `layout` that includes the operational fields. For a back-office surface, request `Status`, `Availability`, `TotalPrice`, `MinimumStayOnArrival`, and the closure flags.

```bash cURL theme={null}
curl --request POST \
  --url https://{apiBaseUrl}/Planning/2018A/DailyRates/PerDay \
  --header 'Authorization: OAuth ...' \
  --header 'Content-Type: application/json' \
  --data '{
    "beginDate": "2026-12-07",
    "endDate": "2027-02-05",
    "guests": [{"adults": 2}],
    "filter": {
      "ratePlans": [
        {
          "hotels": {"exceptions": [<hotelId>], "default": "Excluded"},
          "groupId": <groupId>
        }
      ],
      "currencies": {"exceptions": ["EUR"], "default": "Excluded"}
    },
    "layout": {
      "levels": [
        {
          "name": "ArticleRate",
          "properties": [
            "Status",
            "Availability",
            "TotalPrice",
            "MinimumStayOnArrival",
            "ClosedOnArrival",
            "ClosedOnDeparture",
            "Occupancies"
          ]
        }
      ]
    }
  }'
```

```javascript Node.js theme={null}
const url = "https://{apiBaseUrl}/Planning/2018A/DailyRates/PerDay";

const response = await fetch(url, {
  method: "POST",
  headers: { ...authHeaders, "Content-Type": "application/json" },
  body: JSON.stringify({
    beginDate: "2026-12-07",
    endDate: "2027-02-05",
    guests: [{ adults: 2 }],
    filter: {
      ratePlans: [
        {
          hotels: { exceptions: [<hotelId>], default: "Excluded" },
          groupId: <groupId>,
        },
      ],
      currencies: { exceptions: ["EUR"], default: "Excluded" },
    },
    layout: {
      levels: [
        {
          name: "ArticleRate",
          properties: [
            "Status",
            "Availability",
            "TotalPrice",
            "MinimumStayOnArrival",
            "ClosedOnArrival",
            "ClosedOnDeparture",
            "Occupancies",
          ],
        },
      ],
    },
  }),
});
const data = await response.json();
```

```python Python theme={null}
response = session.post(
    "https://{apiBaseUrl}/Planning/2018A/DailyRates/PerDay",
    json={
        "beginDate": "2026-12-07",
        "endDate": "2027-02-05",
        "guests": [{"adults": 2}],
        "filter": {
            "ratePlans": [
                {
                    "hotels": {"exceptions": [<hotelId>], "default": "Excluded"},
                    "groupId": <groupId>,
                }
            ],
            "currencies": {"exceptions": ["EUR"], "default": "Excluded"},
        },
        "layout": {
            "levels": [
                {
                    "name": "ArticleRate",
                    "properties": [
                        "Status",
                        "Availability",
                        "TotalPrice",
                        "MinimumStayOnArrival",
                        "ClosedOnArrival",
                        "ClosedOnDeparture",
                        "Occupancies",
                    ],
                }
            ]
        },
    },
)
data = response.json()
```

```php PHP theme={null}
$response = $client->post('Planning/2018A/DailyRates/PerDay', [
    'json' => [
        'beginDate' => '2026-12-07',
        'endDate'   => '2027-02-05',
        'guests'    => [['adults' => 2]],
        'filter'    => [
            'ratePlans' => [
                [
                    'hotels'  => ['exceptions' => [<hotelId>], 'default' => 'Excluded'],
                    'groupId' => <groupId>,
                ],
            ],
            'currencies' => ['exceptions' => ['EUR'], 'default' => 'Excluded'],
        ],
        'layout' => [
            'levels' => [
                [
                    'name'       => 'ArticleRate',
                    'properties' => [
                        'Status',
                        'Availability',
                        'TotalPrice',
                        'MinimumStayOnArrival',
                        'ClosedOnArrival',
                        'ClosedOnDeparture',
                        'Occupancies',
                    ],
                ],
            ],
        ],
    ],
]);
$data = json_decode($response->getBody(), true);
```

```csharp C# theme={null}
var request = new RestRequest("/DailyRates/PerDay", Method.Post);
request.AddJsonBody(new
{
    beginDate = "2026-12-07",
    endDate = "2027-02-05",
    guests = new[] { new { adults = 2 } },
    filter = new
    {
        ratePlans = new[]
        {
            new
            {
                hotels  = new { exceptions = new[] { <hotelId> }, @default = "Excluded" },
                groupId = <groupId>
            }
        },
        currencies = new { exceptions = new[] { "EUR" }, @default = "Excluded" }
    },
    layout = new
    {
        levels = new[]
        {
            new
            {
                name = "ArticleRate",
                properties = new[]
                {
                    "Status",
                    "Availability",
                    "TotalPrice",
                    "MinimumStayOnArrival",
                    "ClosedOnArrival",
                    "ClosedOnDeparture",
                    "Occupancies"
                }
            }
        }
    }
});

var response = await client.ExecuteAsync(request);
var data = JsonDocument.Parse(response.Content!).RootElement;
```

```java Java theme={null}
String body = mapper.writeValueAsString(Map.of(
    "beginDate", "2026-12-07",
    "endDate", "2027-02-05",
    "guests", List.of(Map.of("adults", 2)),
    "filter", Map.of(
        "ratePlans", List.of(Map.of(
            "hotels", Map.of("exceptions", List.of(<hotelId>), "default", "Excluded"),
            "groupId", <groupId>
        )),
        "currencies", Map.of("exceptions", List.of("EUR"), "default", "Excluded")
    ),
    "layout", Map.of(
        "levels", List.of(Map.of(
            "name", "ArticleRate",
            "properties", List.of(
                "Status",
                "Availability",
                "TotalPrice",
                "MinimumStayOnArrival",
                "ClosedOnArrival",
                "ClosedOnDeparture",
                "Occupancies"
            )
        ))
    )
));

HttpPost request = new HttpPost("https://{apiBaseUrl}/Planning/2018A/DailyRates/PerDay");
request.setEntity(new StringEntity(body, "UTF-8"));
request.setHeader("Content-Type", "application/json");
consumer.sign(request);

JsonNode data;
try (CloseableHttpResponse response = client.execute(request)) {
    data = mapper.readTree(EntityUtils.toString(response.getEntity()));
}
```

### 2. Pivot segments into per-day cells

For each entity, walk every temporal segment and emit one cell per `(roomId, date)` pair. Carry the operational fields onto the cell so both the metrics block and the grid can read them without a second lookup.

```javascript Node.js theme={null}
function eachDay(start, end) {
  const out = [];
  let d = new Date(start);
  const endD = new Date(end);
  while (d < endD) {
    out.push(d.toISOString().slice(0, 10));
    d.setUTCDate(d.getUTCDate() + 1);
  }
  return out;
}

function buildGrid(data) {
  const grid = new Map();
  for (const rp of data.ratePlans) {
    for (const hotel of rp.hotels) {
      for (const entity of hotel.entities) {
        const cells = new Map();
        for (const field of ["status", "availability", "totalPrice",
                             "minimumStayOnArrival", "closedOnArrival", "closedOnDeparture"]) {
          for (const segment of entity[field] ?? []) {
            for (const date of eachDay(segment.start, segment.end)) {
              if (!cells.has(date)) cells.set(date, { date });
              cells.get(date)[field] = segment.value;
            }
          }
        }
        const key = entity.roomId;
        if (!grid.has(key)) grid.set(key, []);
        grid.get(key).push(...cells.values());
      }
    }
  }
  return grid;
}

const grid = buildGrid(data);
```

```python Python theme={null}
from datetime import date, timedelta

def each_day(start, end):
    d, end_d = date.fromisoformat(start), date.fromisoformat(end)
    while d < end_d:
        yield d.isoformat()
        d += timedelta(days=1)

def build_grid(data):
    grid = {}
    fields = ["status", "availability", "totalPrice",
              "minimumStayOnArrival", "closedOnArrival", "closedOnDeparture"]
    for rp in data["ratePlans"]:
        for hotel in rp["hotels"]:
            for entity in hotel["entities"]:
                cells = {}
                for field in fields:
                    for segment in entity.get(field, []):
                        for d in each_day(segment["start"], segment["end"]):
                            cells.setdefault(d, {"date": d})[field] = segment["value"]
                grid.setdefault(entity["roomId"], []).extend(cells.values())
    return grid

grid = build_grid(data)
```

```php PHP theme={null}
function buildGrid(array $data): array {
    $fields = ['status', 'availability', 'totalPrice',
               'minimumStayOnArrival', 'closedOnArrival', 'closedOnDeparture'];
    $grid = [];
    foreach ($data['ratePlans'] as $rp) {
        foreach ($rp['hotels'] as $hotel) {
            foreach ($hotel['entities'] as $entity) {
                $cells = [];
                foreach ($fields as $field) {
                    foreach ($entity[$field] ?? [] as $segment) {
                        $d = new DateTime($segment['start']);
                        $endD = new DateTime($segment['end']);
                        while ($d < $endD) {
                            $key = $d->format('Y-m-d');
                            $cells[$key]['date'] = $key;
                            $cells[$key][$field] = $segment['value'];
                            $d->modify('+1 day');
                        }
                    }
                }
                $grid[$entity['roomId']] = array_merge(
                    $grid[$entity['roomId']] ?? [],
                    array_values($cells)
                );
            }
        }
    }
    return $grid;
}

$grid = buildGrid($data);
```

```csharp C# theme={null}
static IEnumerable<string> EachDay(string start, string end)
{
    var d = DateOnly.Parse(start);
    var endD = DateOnly.Parse(end);
    while (d < endD) { yield return d.ToString("yyyy-MM-dd"); d = d.AddDays(1); }
}

var grid = new Dictionary<int, List<Dictionary<string, object>>>();
var fields = new[] { "status", "availability", "totalPrice",
                     "minimumStayOnArrival", "closedOnArrival", "closedOnDeparture" };

foreach (var rp in data.GetProperty("ratePlans").EnumerateArray())
foreach (var hotel in rp.GetProperty("hotels").EnumerateArray())
foreach (var entity in hotel.GetProperty("entities").EnumerateArray())
{
    var cells = new Dictionary<string, Dictionary<string, object>>();
    foreach (var field in fields)
    {
        if (!entity.TryGetProperty(field, out var segments)) continue;
        foreach (var segment in segments.EnumerateArray())
        {
            foreach (var date in EachDay(
                segment.GetProperty("start").GetString()!,
                segment.GetProperty("end").GetString()!))
            {
                if (!cells.ContainsKey(date)) cells[date] = new() { ["date"] = date };
                cells[date][field] = segment.GetProperty("value").ToString();
            }
        }
    }
    var roomId = entity.GetProperty("roomId").GetInt32();
    if (!grid.ContainsKey(roomId)) grid[roomId] = new();
    grid[roomId].AddRange(cells.Values);
}
```

```java Java theme={null}
static List<String> eachDay(String start, String end) {
    var out = new ArrayList<String>();
    var d = LocalDate.parse(start);
    var endD = LocalDate.parse(end);
    while (d.isBefore(endD)) { out.add(d.toString()); d = d.plusDays(1); }
    return out;
}

var fields = List.of("status", "availability", "totalPrice",
                     "minimumStayOnArrival", "closedOnArrival", "closedOnDeparture");
Map<Integer, List<Map<String, Object>>> grid = new HashMap<>();

for (JsonNode rp : data.get("ratePlans")) {
    for (JsonNode hotel : rp.get("hotels")) {
        for (JsonNode entity : hotel.get("entities")) {
            Map<String, Map<String, Object>> cells = new HashMap<>();
            for (String field : fields) {
                JsonNode segments = entity.get(field);
                if (segments == null) continue;
                for (JsonNode segment : segments) {
                    for (String date : eachDay(
                        segment.get("start").asText(),
                        segment.get("end").asText())) {
                        cells.computeIfAbsent(date, k -> new HashMap<>()).put("date", date);
                        cells.get(date).put(field, segment.get("value").asText());
                    }
                }
            }
            int roomId = entity.get("roomId").asInt();
            grid.computeIfAbsent(roomId, k -> new ArrayList<>()).addAll(cells.values());
        }
    }
}
```

### 3. Compute the headline metrics

Aggregate the per-day cells into the dashboard's KPIs. Three metrics cover most revenue-management views:

* **Available room-nights**: sum of `availability` across cells where the room is open. Tells the team how much capacity is up for sale across the window.
* **Average daily rate**: mean of `totalPrice` across cells where the room is open. The pricing pulse of the window.
* **Closure rate**: share of cells where `status` is not `Available`, or `closedOnArrival` / `closedOnDeparture` is true. A high number signals heavy restriction pressure.

```javascript Node.js theme={null}
function computeKpis(grid) {
  let availableRoomNights = 0;
  let priceSum = 0;
  let priceCount = 0;
  let closedCells = 0;
  let totalCells = 0;

  for (const cells of grid.values()) {
    for (const cell of cells) {
      totalCells++;
      const isClosed =
        cell.status !== "Available" ||
        cell.closedOnArrival === true ||
        cell.closedOnDeparture === true;
      if (isClosed) {
        closedCells++;
      } else {
        availableRoomNights += cell.availability ?? 0;
        if (cell.totalPrice != null) {
          priceSum += Number(cell.totalPrice);
          priceCount++;
        }
      }
    }
  }

  return {
    availableRoomNights,
    adr: priceCount > 0 ? priceSum / priceCount : null,
    closureRate: totalCells > 0 ? closedCells / totalCells : 0,
  };
}

const kpis = computeKpis(grid);
```

```python Python theme={null}
def compute_kpis(grid):
    available_room_nights = 0
    price_sum = 0.0
    price_count = 0
    closed_cells = 0
    total_cells = 0

    for cells in grid.values():
        for cell in cells:
            total_cells += 1
            is_closed = (
                cell.get("status") != "Available"
                or cell.get("closedOnArrival") is True
                or cell.get("closedOnDeparture") is True
            )
            if is_closed:
                closed_cells += 1
            else:
                available_room_nights += cell.get("availability", 0)
                if cell.get("totalPrice") is not None:
                    price_sum += float(cell["totalPrice"])
                    price_count += 1

    return {
        "availableRoomNights": available_room_nights,
        "adr": price_sum / price_count if price_count else None,
        "closureRate": closed_cells / total_cells if total_cells else 0,
    }

kpis = compute_kpis(grid)
```

```php PHP theme={null}
function computeKpis(array $grid): array {
    $availableRoomNights = 0;
    $priceSum = 0.0;
    $priceCount = 0;
    $closedCells = 0;
    $totalCells = 0;

    foreach ($grid as $cells) {
        foreach ($cells as $cell) {
            $totalCells++;
            $isClosed = ($cell['status'] ?? null) !== 'Available'
                || ($cell['closedOnArrival'] ?? false) === true
                || ($cell['closedOnDeparture'] ?? false) === true;
            if ($isClosed) {
                $closedCells++;
            } else {
                $availableRoomNights += $cell['availability'] ?? 0;
                if (isset($cell['totalPrice'])) {
                    $priceSum += (float) $cell['totalPrice'];
                    $priceCount++;
                }
            }
        }
    }

    return [
        'availableRoomNights' => $availableRoomNights,
        'adr'                 => $priceCount > 0 ? $priceSum / $priceCount : null,
        'closureRate'         => $totalCells > 0 ? $closedCells / $totalCells : 0,
    ];
}

$kpis = computeKpis($grid);
```

```csharp C# theme={null}
long availableRoomNights = 0;
decimal priceSum = 0;
int priceCount = 0;
int closedCells = 0;
int totalCells = 0;

foreach (var cells in grid.Values)
{
    foreach (var cell in cells)
    {
        totalCells++;
        var status = cell.GetValueOrDefault("status")?.ToString();
        var coa = cell.GetValueOrDefault("closedOnArrival")?.ToString();
        var cod = cell.GetValueOrDefault("closedOnDeparture")?.ToString();
        var isClosed = status != "Available" || coa == "True" || cod == "True";
        if (isClosed)
        {
            closedCells++;
        }
        else
        {
            if (cell.TryGetValue("availability", out var avail))
                availableRoomNights += Convert.ToInt64(avail);
            if (cell.TryGetValue("totalPrice", out var price))
            {
                priceSum += Convert.ToDecimal(price);
                priceCount++;
            }
        }
    }
}

var kpis = new
{
    AvailableRoomNights = availableRoomNights,
    Adr = priceCount > 0 ? priceSum / priceCount : (decimal?)null,
    ClosureRate = totalCells > 0 ? (double)closedCells / totalCells : 0
};
```

```java Java theme={null}
long availableRoomNights = 0;
BigDecimal priceSum = BigDecimal.ZERO;
int priceCount = 0;
int closedCells = 0;
int totalCells = 0;

for (var cells : grid.values()) {
    for (var cell : cells) {
        totalCells++;
        boolean isClosed = !"Available".equals(cell.get("status"))
            || Boolean.TRUE.equals(cell.get("closedOnArrival"))
            || Boolean.TRUE.equals(cell.get("closedOnDeparture"));
        if (isClosed) {
            closedCells++;
        } else {
            Object avail = cell.get("availability");
            if (avail instanceof Number n) availableRoomNights += n.longValue();
            Object price = cell.get("totalPrice");
            if (price != null) {
                priceSum = priceSum.add(new BigDecimal(price.toString()));
                priceCount++;
            }
        }
    }
}

Map<String, Object> kpis = Map.of(
    "availableRoomNights", availableRoomNights,
    "adr", priceCount > 0
        ? priceSum.divide(BigDecimal.valueOf(priceCount), 2, RoundingMode.HALF_UP)
        : null,
    "closureRate", totalCells > 0 ? (double) closedCells / totalCells : 0.0
);
```

### 4. Render the grid with operational cues

Display each room as a row, each day as a column. The cell value combines availability count and price; the cell colour signals status. Use the on-arrival restrictions for tooltips so revenue managers see why a day looks the way it does.

| Cell state | Trigger |
| - | - |
| Open, neutral | `status = Available`, no closure flags |
| Open with badge | `closedOnArrival = true` or `closedOnDeparture = true` |
| Restricted | `minimumStayOnArrival > 1`, surface the value in a corner badge |
| Closed | `status = NotAvailable` |
| Sold out | `availability = 0` while `status = Available` (rare; flag for review) |

### 5. Refresh on demand

A revenue manager dashboard is a working surface, not a published page. Refresh on tab focus or on an explicit reload action so the team always sees the latest snapshot. Keep a short cache (one to two minutes) for navigation responsiveness, but invalidate it the moment the user takes an action that should change the grid.

## Tips

* **Compute KPIs from the same payload**. The metrics block and the grid both read the per-day cells produced in step 2. One pivot, two views, no extra call.
* **Pivot once, render many times**. The grid is cheap to re-render across filter changes (room subset, status filter, date sub-window). Build it once per refresh and slice in memory.
* **Show segment boundaries**. Where a price changes mid-window, the API returns two segments. Surface the boundary visually in the calendar (a thin separator, a colour shift) so revenue managers can spot rate changes at a glance.
* **Cap the window**. 30 to 180 days is plenty for an operational view. Longer windows are slow and rarely useful, since data this far out drifts before revenue managers act on it.
* **Keep restrictions in the tooltip, not the cell**. The cell shows price and availability. Hover surfaces the restriction reason. A grid that tries to render every restriction inline gets unreadable fast.

## Related

> **[Reference](/daily-rates-api/build/reference/per-day.md)**
>
> Full schema for the Per day endpoint.

> **[Best practices](/daily-rates-api/build/best-practices.md)**
>
> Caching, performance, and resilience patterns.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.