# Staging catalog previews

> Build a TEST directory, room grid, and preview drawer from explicitly approved catalog content.


Build a staging directory, browse a group's rooms, and open a room-preview drawer
using the same catalog methods your production site uses. Published content remains
the default. A TEST key alone does not expose unpublished content.

This guide describes the source-pinned SDK candidate and its matching API extension.
Confirm that your API deployment supports `view=staging` on all four reads below.
Installing the candidate does not enable preview access by itself. See
[SDK evaluation](https://developers.kismet.travel/guides/private-evaluation.md) for installation.

## What you can build

| UI task | REST operation | SDK method on `client` |
| --- | --- | --- |
| Directory or group selector | [GET groups](https://developers.kismet.travel/api/reference/list-bookable-product-groups.md) | `bookableProductGroup.list()` |
| Group heading and content | [GET group](https://developers.kismet.travel/api/reference/get-bookable-product-group.md) | `bookableProductGroup.get()` |
| Room grid in a group | [GET rentals](https://developers.kismet.travel/api/reference/list-vacation-rentals.md) | `vacationRental.list()` |
| Room gallery and details | [GET rental](https://developers.kismet.travel/api/reference/get-vacation-rental.md) | `vacationRental.get()` |

These are optional views on existing operations, not a separate preview API.
The same flow works for a building directory or another approved curated group.
Use `semanticType: 'BUILDING'` only when your UI needs buildings; omit it for
all eligible classifications. Classification is separate from STANDARD/DROP
grouping behavior. [Catalog classification](https://developers.kismet.travel/guides/catalog-group-classification.md).

A room-preview drawer displays content, not checkout. You own that presentation.
Use the supported [Kismet Fixtures](https://developers.kismet.travel/fixtures.md) integration for trusted guest or
checkout experiences; this read extension does not install or activate them.

## Set up access

1. [Create an active TEST installation and credential](https://developers.kismet.travel/guides/developer-access.md)
   with collection-wide `vacation_rentals.read`. A LIVE credential cannot request
   `view=staging`. No additional pricing or booking capability is needed for this
   content preview.
2. Ask a collection manager to approve the specific groups and source inventory
   for preview. These are two independent, expiring approvals, not capabilities
   you can add by changing a URL or enabling Developer mode.
3. Keep the preview site out of search indexes and shared caches. Preview approval
   permits shareable storefront content; it is not a confidentiality boundary.
   If the site needs restricted access, protect it separately.

### Manager approval through MCP

Sign in with Kismet and use `get_fixture_status` to inspect the current draft
rules and `staging.inventory_preview`. An authorized manager can use
`set_fixture_rules` with `staging_group_slugs` and
`staging_inventory_collections` to approve the intended scope for seven days.

The tool requires Fixture-management permission on the destination collection and
manage access to each source collection. A `DEVELOPER` role alone does not grant
Fixture writes. See [MCP authentication and access](https://developers.kismet.travel/mcp.md#authentication-and-access).

**Preserve the complete existing draft rule set.** `set_fixture_rules` replaces
`rules`; do not pass an empty array unless clearing those rules is intended.
First preview the request without `confirm`, review its scope and diff, then
repeat with `confirm: true`. Omitting an approval field preserves it; passing
`[]` revokes that approval. Renewing group approval does not renew source-inventory
approval. No production Fixture publish is needed to approve a preview.

Only same-collection STANDARD groups qualify, excluding guest-save and DROP groups.
An unpublished room must belong to an approved group and have active membership
in the installation collection, plus active, directory-visible membership in an
approved public, active, non-draft source collection. The room must remain active,
non-deleted, and have an unpublished content object.

Cross-collection links may remain hidden from the installation's public directory:
the two explicit staging approvals authorize their preview without changing that
visibility flag. Published rooms still require ordinary directory visibility.
Approval does not create groups, change membership, or publish content.

## Read with the SDK

Run this in a server-only module using a source-pinned SDK build that includes
`CatalogReadOptions`. Keep the server credential in server environment variables,
never in a `NEXT_PUBLIC_` variable.

```ts
import { createKismetClient } from '@kismet-tech/sdk/server';

const client = createKismetClient({
  apiKey: process.env.KISMET_DEVELOPER_API_KEY!,
  baseUrl: process.env.KISMET_API_BASE_URL ?? 'https://api.ksmt.app/v1',
  // Prevent framework fetch caches from retaining expiring preview content.
  fetchImpl: (input, init) => fetch(input, { ...init, cache: 'no-store' }),
});
const view = 'staging' as const;

const groups = await client.bookableProductGroup.list({
  view, semanticType: 'BUILDING', limit: 50,
});

// Example selection. In your UI, use the group chosen by the visitor.
const selectedGroup = groups.data[0];
if (selectedGroup) {
  const group = await client.bookableProductGroup.get(selectedGroup.id, { view });
  const rooms = await client.vacationRental.list({
    view, group: group.slug, minGuests: 2, limit: 50,
  });

  // An empty list is a valid result. Do not request an undefined room ID.
  const selectedRoom = rooms.data[0];
  if (selectedRoom) {
    const room = await client.vacationRental.get(selectedRoom.id, { view });
    // Render room.media.gallery and the detail fields in your preview drawer.
    // A list row is a summary with a hero, not a substitute for this detail read.
  }
}
```

If you use a publishable key directly in a browser, its exact origin must be
authorized on the TEST installation. Browser origin restrictions do not make
preview content confidential.

### Pagination and filters

Keep `view` and every filter when following `page.nextCursor`. Group listing
also supports `semanticType`; room listing supports `group`, `minBedrooms`,
`minBathrooms`, `minGuests`, and `petsAllowed`. These select catalog content
and capacity, not dated availability.

```ts
const roomQuery = { view, group: 'example-building', minGuests: 2, limit: 50 };
const firstPage = await client.vacationRental.list(roomQuery);
if (firstPage.page.nextCursor) {
  const nextPage = await client.vacationRental.list({
    ...roomQuery, cursor: firstPage.page.nextCursor,
  });
}
```

Use a returned group slug instead of the example value. If the selection changes,
start again without a cursor. Eligibility and filters are checked before paging.
Group `links.self` and `links.bookableProducts` preserve staging view;
`memberCount` counts eligible members, not available rooms for particular dates.

## Read with REST

Set `KISMET_API_BASE_URL=https://api.ksmt.app/v1` and a TEST server key in
`KISMET_DEVELOPER_API_KEY`. The key already carries its collection: read the
collection slug from `GET /developer/credential` and export it as
`COLLECTION_SLUG` for the examples below.
Use returned identifiers for `GROUP_ID`, `GROUP_SLUG`, and `ROOM_ID`.
These examples run on your server or in a local terminal, not in browser code.

```sh
curl --get "$KISMET_API_BASE_URL/developer/credential" \
  --header "Authorization: Bearer $KISMET_DEVELOPER_API_KEY"
# Copy the collection "slug" from the response:
export COLLECTION_SLUG=your-collection-slug

curl --get "$KISMET_API_BASE_URL/developer/collections/$COLLECTION_SLUG/bookable-product-groups" \
  --header "Authorization: Bearer $KISMET_DEVELOPER_API_KEY" \
  --data-urlencode "view=staging" --data-urlencode "semanticType=BUILDING"

curl --get "$KISMET_API_BASE_URL/developer/bookable-product-groups/$GROUP_ID" \
  --header "Authorization: Bearer $KISMET_DEVELOPER_API_KEY" \
  --data-urlencode "view=staging"

curl --get "$KISMET_API_BASE_URL/developer/collections/$COLLECTION_SLUG/vacation-rentals" \
  --header "Authorization: Bearer $KISMET_DEVELOPER_API_KEY" \
  --data-urlencode "view=staging" --data-urlencode "group=$GROUP_SLUG" \
  --data-urlencode "minGuests=2"

curl --get "$KISMET_API_BASE_URL/developer/vacation-rentals/$ROOM_ID" \
  --header "Authorization: Bearer $KISMET_DEVELOPER_API_KEY" \
  --data-urlencode "view=staging"
```

## Interpret publication and expiry

A staging view includes eligible published content as well as approved unpublished
content. Do not mark every result as unpublished merely because you requested
`view=staging`. A staging-only room has publication metadata like this:

```json
{
  "publishedAt": null,
  "updatedAt": "2026-09-24T00:00:00.000Z",
  "visibility": "staging",
  "stagingExpiresAt": "2026-09-30T00:00:00.000Z"
}
```

`stagingExpiresAt` is the earlier of the group and source-inventory approvals.
A staging-only group's expiry describes its group approval, so the group can
remain visible after permission to preview its unpublished rooms expires.
Ordinary published responses retain their existing shape; absence of staging
metadata is not a missing publication date.

Show a preview label for staging-only content. API responses use
`Cache-Control: private, no-store`; propagate that policy through your BFF.
Do not statically export or put preview HTML in ISR/shared caches. Set noindex on
your own preview pages. Remove expired preview content from the UI and refetch;
a timestamp does not override earlier revocation.

## Empty results and errors

The SDK raises `KismetApiError` with `status`, `code`, and `requestId`.
Do not silently substitute local fixture data or retry a denied request with
broader access.

| Result | Meaning and UI response |
| --- | --- |
| Empty group or room list | No eligible content matches the current scope and filters. Show an empty preview state, not “sold out.” |
| `403 CAPABILITY_NOT_GRANTED` | Check the TEST installation and required capability. Developer mode does not convert a LIVE key. |
| `403 RESOURCE_NOT_GRANTED` | The detail is not accessible, including after preview revocation. Remove it and reload the selection. |
| `403 ORIGIN_NOT_ALLOWED` | The browser origin is not authorized for the publishable key. Check the exact scheme, host, and port. |
| `400 INVALID_GROUP` | The selected group is not accessible in this collection/view. Refresh the group selector. |
| `400 INVALID_CURSOR` | The cursor no longer belongs to the eligible filtered set. Restart from the first page with the same intended view and filters. |
| `401` | Check credential validity and expiry; do not fall back to anonymous access. |

Missing or expired preview approvals restore published-only eligibility.
Revoking the inventory approval can remove unpublished rooms while leaving the
separately approved group visible. It does not remove genuinely published content.

## Pricing, checkout, and promotion

This extension does not add rates, dated availability, unpublished-room review,
policy or calendar subresource reads, quotes, booking writes, or public publication.
Staging-only rooms do not receive `pricing.nightlyFrom`, even with `rates.read`.
Published rooms retain their existing grant-controlled behavior.

Keep “Book” actions unavailable for a content-only preview. Do not turn a missing
price into zero or describe a capacity filter as an availability check.
[Group pricing](https://developers.kismet.travel/guides/group-stay-pricing.md) and managed checkout have separate
contracts and eligibility.

Before promotion, publish the intended catalog through the normal approval process,
switch to the default published view, and verify the same directory, room list,
and detail flow with the appropriate LIVE installation. Verify pricing and checkout
separately. Never carry a preview allowlist or a TEST credential into production.
