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

# Consume a deployment's public API

> Build a storefront, mobile app, or partner integration against a Voyant deployment's own /v1/public/* surface: the public TypeScript SDK, React hooks, raw REST, and the server-only admin client.

This guide is about consuming the API of a Voyant deployment you (or your
customer) run, from your own website, mobile app, or backend. The deployment
exposes the catalog browse, pricing, and booking flows under a public surface,
and you drive them from outside.

## Mental model

The single most important thing to get right is which surface you are calling.
Your deployment exposes two of them:

| Surface        | Who calls it                    | Auth                      | Base URL               |
| -------------- | ------------------------------- | ------------------------- | ---------------------- |
| `/v1/public/*` | Browsers, mobile apps, partners | None                      | Your deployment domain |
| `/v1/admin/*`  | Your own backend, staff tools   | A `voy_` API key (bearer) | Your deployment domain |

The base URL is **your deployment's own domain**, for example
`https://operator.example.com`, not `api.voyant.travel`. The hosted
[Cloud](/docs/services/overview), [Connect](/docs/connect/overview), and [Data](/docs/data/overview)
SDKs in the SDKs tab are the opposite case: they target `api.voyant.travel` with
a bearer key. The storefront and public surface described here run on your app
and need no key. See the note in
[API routes](/docs/platform/fundamentals/api-routes#rest-reference).

That split maps to two clients:

<CardGroup cols={2}>
  <Card title="@voyant-travel/storefront-sdk" icon="globe">
    The public client. Safe to ship to a browser or mobile bundle. Carries no
    secret. Talks to `/v1/public/*`.
  </Card>

  <Card title="@voyant-travel/admin-react" icon="lock">
    The admin client. **Server only.** Carries a `voy_` API key. Talks to
    `/v1/admin/*`. Never embed it in a client bundle.
  </Card>
</CardGroup>

If your website needs admin data, your own backend calls the admin client and
proxies the result. The API key stays on your server, per
[Authentication](/docs/sdks/authentication).

## Browse and book from a TypeScript app

Install the public SDK and point it at your deployment domain. There is no key
to pass.

```bash theme={null}
pnpm add @voyant-travel/storefront-sdk
```

```ts theme={null}
import { createVoyantStorefrontClient } from "@voyant-travel/storefront-sdk"

const voyant = createVoyantStorefrontClient({
  baseUrl: "https://operator.example.com",
  // Optional: a custom `fetcher` and default `headers`. The default fetcher
  // sends `credentials: "include"` so the checkout-capability cookie flows.
})
```

The client groups operations into four facades: `storefront` (catalog reads),
`booking` (the raw session lifecycle), `bookingEngine` (the recommended,
flow-oriented wrapper), and `checkout` (payment collection).

<Steps>
  <Step title="Browse the catalog">
    Read settings, then departures, availability, and pricing for a product. All
    of these are plain reads on the `storefront` facade.

    ```ts theme={null}
    const settings = await voyant.storefront.getSettings()

    const departures = await voyant.storefront.listProductDepartures(productId)
    const availability = await voyant.storefront.getProductAvailability(productId)

    const departure = await voyant.storefront.getDeparture(departureId)
    const quote = await voyant.storefront.previewDeparturePrice(departureId, {
      // pax / occupancy input for the price preview
    })
    ```

    Marketing-style offers are also available with
    `voyant.storefront.listProductOffers(productId)` and
    `voyant.storefront.getOfferBySlug(slug)`.
  </Step>

  <Step title="Reserve a booking session">
    Prefer the `bookingEngine` facade. `reserve` opens a public booking session
    and returns a canonical snapshot whose `engine.state` you can gate UI on.

    ```ts theme={null}
    const booking = await voyant.bookingEngine.reserve({
      sellCurrency: "EUR",
      items: [
        {
          title: "Danube tour",
          availabilitySlotId: "slot_123",
          quantity: 2,
          totalSellAmountCents: 24000,
        },
      ],
    })

    const sessionId = booking.session.sessionId
    ```

    When you already have a selected departure and quote and want the combined
    payload in one call (session, availability, repricing, payment plan,
    allocation, and the checkout capability attached at
    `session.checkoutCapability`), use `voyant.booking.bootstrapSession(...)`
    instead. An async bootstrap returns `428` if you do not supply an
    idempotency key; see the next step.
  </Step>

  <Step title="Follow the server's next action">
    Do not derive engine state on the client. The server publishes what to do
    next, and a host renders it rather than inventing it.

    Recoverable errors come back in an envelope carrying `code`, `message`,
    `recoverable`, and `nextAction`. `nextAction` is drawn from a closed set
    (`bookingEngineNextActions`):

    ```ts theme={null}
    import { bookingEngineNextActions } from "@voyant-travel/storefront-sdk"
    // "configure_contract_template" | "restart_reservation"
    // | "choose_another_departure"  | "correct_traveler_payload"
    // | "choose_another_payment_method" | "retry_payment_start"
    // | "poll_payment_status" | "contact_operator"
    ```

    Map each value to a recovery affordance in your UI. Because the set is
    closed and server-owned, what you render and what the server enforces stay
    one derivation instead of two that drift.
  </Step>

  <Step title="Collect travelers and reprice">
    Update travelers and reprice as the customer fills the form. Pass an
    idempotency key on writes so retries do not double-apply.

    ```ts theme={null}
    await voyant.bookingEngine.updateTravelers(
      sessionId,
      { travelers: [{ firstName: "Ada", lastName: "Lovelace" }] },
      { idempotencyKey: crypto.randomUUID() },
    )

    await voyant.bookingEngine.reprice(sessionId, {
      /* final occupancy */
    })
    ```

    Every write method accepts a trailing `StorefrontRequestOptions` with
    `headers` and `idempotencyKey`. The key is sent as the `Idempotency-Key`
    header.
  </Step>

  <Step title="Take payment and confirm">
    Drive payment through the engine, gating each step on state, then confirm.

    ```ts theme={null}
    if (canRunBookingEngineAction(state, "start_payment")) {
      // payment methods take the booking id from the engine snapshot
      const payment = await voyant.bookingEngine.startPayment(bookingId, {
        method: "card",
      })
      // hand the customer off to the provider using the returned payment payload,
      // then poll the snapshot for the result
    }

    const confirmed = await voyant.bookingEngine.confirm(sessionId)
    ```

    Re-read the snapshot any time with
    `voyant.bookingEngine.getSnapshot(sessionId)`. Lower-level payment helpers
    (`previewPayment`, `bootstrapPayment`) and the `checkout` facade
    (`previewCollection`, `initiateCollection`, `bootstrapCollection`) are there
    when you need finer control.
  </Step>
</Steps>

### Errors

Failed calls throw `VoyantStorefrontApiError` with `status`, `body`, and a
`normalizedError` (the parsed `{ code, message, details }` envelope). Branch on
`normalizedError.code`, not on the message string.

```ts theme={null}
import { VoyantStorefrontApiError } from "@voyant-travel/storefront-sdk"

try {
  await voyant.bookingEngine.confirm(sessionId)
} catch (err) {
  if (err instanceof VoyantStorefrontApiError) {
    if (err.normalizedError?.code === "reservation_expired") {
      // restart the reservation
    }
  }
}
```

There is no checkout login. The checkout surface is protected by a short-lived,
server-issued checkout-capability cookie attached to the session, not a user
account.

## Build a React website

For a React site, layer the hooks packages on top of the same public contract.
`@voyant-travel/storefront-react` covers the storefront and booking flows;
`@voyant-travel/catalog-react` adds catalog search and a booking-engine hook
family.

```bash theme={null}
pnpm add @voyant-travel/storefront-react @tanstack/react-query
```

Both providers re-export one shared context, so you wrap your app once with a
provider and a TanStack Query `QueryClientProvider` (v5 is a peer dependency).

```tsx theme={null}
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { VoyantStorefrontProvider } from "@voyant-travel/storefront-react/provider"

const queryClient = new QueryClient()

export function App({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      <VoyantStorefrontProvider baseUrl="https://operator.example.com">
        {children}
      </VoyantStorefrontProvider>
    </QueryClientProvider>
  )
}
```

The provider takes `{ baseUrl, fetcher?, children }`. Inside the tree, read with
hooks:

```tsx theme={null}
import {
  useStorefrontSettings,
  useStorefrontProductDepartures,
  useStorefrontDeparture,
  useStorefrontProductOffers,
} from "@voyant-travel/storefront-react/hooks"

function ProductPage({ productId }: { productId: string }) {
  const settings = useStorefrontSettings()
  const departures = useStorefrontProductDepartures(productId)
  // ...render
}
```

For dynamic catalog browse and a hook-based booking flow, add
`@voyant-travel/catalog-react`. It exposes `VoyantCatalogProvider` (same
`{ baseUrl, fetcher }` context, so it composes with the storefront provider),
discovery hooks `useCatalogSearch`, `usePackageSearch`, `usePackageDetail`,
`useCatalogSlots`, and, under `@voyant-travel/catalog-react/booking-engine`, the
booking hooks `useBookingDraft`, `useBookingQuote`, and `useBookingCommit`.
These hooks can target `/v1/admin` or `/v1/public` depending on the `baseUrl`
and the route they call.

## Mobile or any other language: raw REST

The SDK is a convenience, not a requirement. Everything is plain HTTP and JSON
under `/v1/public/*`, so a native mobile app or a backend in any language can
call it directly.

**Routes.** Public routes are organized by business capability, not by the
frontend that calls them: `/v1/public/bookings`, `/v1/public/products`,
`/v1/public/pricing`, and so on. The full set is documented in the
**Storefront API** reference in the Platform navigation, with your app domain
as the server URL.

**Success envelope.** Reads return `{ "data": ... }`. List endpoints add
pagination alongside `data`.

```json theme={null}
{ "data": { "id": "dep_123", "title": "Danube tour" } }
```

**Error envelope.** Errors return one shape on every route. Branch on `code`,
not on `error`.

```json theme={null}
{
  "error": "Human-readable message",
  "code": "invalid_request",
  "requestId": "req_…",
  "details": {}
}
```

`requestId` is also returned in the `X-Request-Id` response header on success
and failure alike. Keep it in logs and support requests.

**Idempotency.** Writes accept an `Idempotency-Key` request header so a retry
does not create a duplicate. Async bootstrap endpoints reject a write with
`428` if the key is missing.

```http theme={null}
POST /v1/public/bookings/sessions HTTP/1.1
Host: operator.example.com
Content-Type: application/json
Idempotency-Key: 9f1c2e7a-...
```

**CORS.** Browser callers must be allowlisted. The deployment's
`CORS_ALLOWLIST` env decides which origins get credentialed CORS headers: list
exact origins or a single-label wildcard such as `https://*.example.com`. A bare
`*` is rejected for credentialed requests. Native mobile apps do not run a
browser CORS preflight, so the allowlist does not affect them.

**Caching.** `GET /v1/public/*` responses that the route marks
`Cache-Control: public, s-maxage=...` and that carry no `Set-Cookie` are served
from a shared cache. To force a fresh read, send `Cache-Control: no-cache` on
your request.

## Read admin data from your own server

When you need data only the admin surface exposes (full bookings, finance, staff
operations), call it from your backend with `@voyant-travel/admin-react` and a
`voy_` API key. This client is **server only**. Never ship it or the key to a
browser or mobile bundle; a public site reaches admin data by proxying through
its own backend.

```bash theme={null}
pnpm add @voyant-travel/admin-react
```

```ts theme={null}
import { createAdminClient } from "@voyant-travel/admin-react/client"

const admin = createAdminClient({
  baseUrl: "https://operator.example.com",
  auth: { type: "apiKey", apiKey: process.env.VOYANT_API_KEY! }, // voy_ key, server-side
  idempotencyKey: (op) => `${op}:${crypto.randomUUID()}`,
})

const { data, total } = await admin.bookings.list({ status: "on_hold", limit: 20 })
const booking = await admin.bookings.get({ id: "book_123" })
```

Non-2xx responses throw `AdminApiError` carrying `status`, `code`, and
`requestId`. Use `admin.capabilities()` to discover which modules and operations
a given deployment enables. Mint and scope `voy_` keys per
[Authentication](/docs/sdks/authentication), and grant each key only the scopes it
uses.

## A working example

There is no standalone example app to clone. The living reference is the
operator application's storefront route group at
`starters/operator/src/routes/(storefront)/`, which consumes these same public
contracts, plus the
[`@voyant-travel/storefront-sdk` README](https://github.com/voyant-travel/voyant/tree/main/packages/storefront-sdk).

## Next steps

<CardGroup cols={2}>
  <Card title="Public API" icon="globe" href="/docs/platform/public-api">
    The customer-facing surface, its packages, and where checkout fits.
  </Card>

  <Card title="API routes" icon="route" href="/docs/guides/consume-the-api">
    The admin/public split, the error envelope, and the route conventions.
  </Card>

  <Card title="SDKs overview" icon="cubes" href="/docs/sdks/overview">
    The hosted Cloud, Connect, and Data clients and their shared shape.
  </Card>

  <Card title="SDK authentication" icon="key" href="/docs/sdks/authentication">
    How `voy_` API keys, scopes, and server-only token rules work.
  </Card>
</CardGroup>
