# Products

Products are the sellable catalog items that you expose to guests. A product combines a unit type, rate configuration, target inventory goals, and optional services into one offer that can be used in your booking flows.

Products sit under [Product Groups](/catalog/product-groups/). Product groups define shared commercial controls, while products represent the individual offers within those groups.

## Key Concepts

### Product composition

A product combines:

- A **unit type** (`unitType`)
- A **product group** (`productGroup`)
- A **rate** (`rate`) with interval and rate calendars
- **Target inventory settings** (`targetUnitCount`, `targetOccupancyPercentage`)
- Optional and included **services**

### Product types

`CatalogProductProductTypeEnum` defines the product category:

| Value | Description |
| --- | --- |
| `MULTI_FAMILY` | Product configured for multi-family use cases |
| `SHORT_STAY` | Product configured for short stay use cases |
| `STUDENT` | Product configured for student use cases |

### Rate intervals

`CatalogRateIntervalEnum` controls the pricing interval:

| Value | Description |
| --- | --- |
| `MONTHLY` | Monthly pricing |
| `NIGHTLY` | Nightly pricing |
| `WEEKLY` | Weekly pricing |

## GraphQL API

### Types

#### `Product`

| Field | Type | Description |
| --- | --- | --- |
| `id` | `ID!` | Product identifier |
| `targetUnitCount` | `Int!` | Target number of units for this product |
| `targetOccupancyPercentage` | `Float!` | Target occupancy percentage for this product |
| `productGroup` | `ProductGroup!` | Product group the product belongs to |
| `productGroupId` | `ID!` | Product group identifier, deprecated in favor of `productGroup.id` |
| `unitType` | `UnitType` | Unit type linked to the product |
| `rate` | `ProductRate` | Rate linked to the product |
| `services` | `ProductServiceConnection!` | Services currently selected for the product |
| `eligibleServices` | `ServiceConnection!` | Services that can be added to the product |
| `allocatedUnits` | `AllocatedUnitConnection!` | Units allocated to the product |

#### `ProductRate`

| Field | Type | Description |
| --- | --- | --- |
| `id` | `ID!` | Product rate identifier |
| `interval` | `CatalogRateIntervalEnum!` | Rate interval (`MONTHLY`, `NIGHTLY`, `WEEKLY`) |
| `productId` | `ID` | Related product identifier |

#### `ProductService`

| Field | Type | Description |
| --- | --- | --- |
| `id` | `ID!` | Product service identifier |
| `service` | `Service!` | Referenced service |
| `serviceOffering` | `CatalogProductServiceOfferingEnum!` | Whether the service is included or optional |
| `createdAt` | `ISO8601DateTime!` | Creation timestamp |
| `updatedAt` | `ISO8601DateTime!` | Last update timestamp |

### Queries

#### Get a product

```graphql
query Product($id: ID!, $first: Int, $after: String) {
  catalog {
    product(id: $id) {
      id
      targetUnitCount
      targetOccupancyPercentage
      productGroup {
        id
        internalReference
      }
      unitType {
        id
        code
      }
      rate {
        id
        interval
        calendars(first: $first, after: $after) {
          nodes {
            id
            startDate
            endDate
            amountCents
            currency
          }
          totalCount
          pageInfo {
            hasNextPage
            endCursor
          }
        }
      }
      services(first: $first, after: $after) {
        nodes {
          id
          serviceOffering
          service {
            id
            name
            amountCents
            amountCurrency
            applicationUnit
          }
        }
        totalCount
      }
      eligibleServices(first: $first, after: $after) {
        nodes {
          id
          name
          amountCents
          amountCurrency
          applicationUnit
        }
        totalCount
      }
    }
  }
}
```

```json
{
  "id": "product-id",
  "first": 20
}
```

#### List products in a product group

```graphql
query ProductGroupProducts($id: ID!, $first: Int, $after: String) {
  catalog {
    productGroup(id: $id) {
      id
      internalReference
      products(first: $first, after: $after) {
        nodes {
          id
          targetUnitCount
          targetOccupancyPercentage
          unitType {
            id
            code
          }
          rate {
            id
            interval
          }
        }
        totalCount
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
  }
}
```

```json
{
  "id": "product-group-id",
  "first": 20
}
```

### Mutations

#### Create a product

```graphql
mutation CreateProduct($input: CreateProductMutationInput!) {
  catalog {
    product {
      create(input: $input) {
        id
      }
    }
  }
}
```

```json
{
  "input": {
    "product": {
      "name": "Standard Studio Product",
      "productGroupId": "product-group-id",
      "productType": "STUDENT",
      "targetUnitCount": 20,
      "targetOccupancyPercentage": 95,
      "unitTypeId": "unit-type-id",
      "rate": {
        "amountCents": 125000,
        "currency": "GBP",
        "interval": "WEEKLY"
      }
    }
  }
}
```

Required fields for `product` in `CreateProductMutationInput`:

| Field | Type |
| --- | --- |
| `name` | `String!` |
| `productGroupId` | `ID!` |
| `productType` | `CatalogProductProductTypeEnum!` |
| `targetUnitCount` | `Int!` |
| `unitTypeId` | `ID!` |
| `rate` | `CreateRateInput!` |

#### Update a product

```graphql
mutation UpdateProduct($input: UpdateProductMutationInput!) {
  catalog {
    product {
      update(input: $input) {
        id
      }
    }
  }
}
```

```json
{
  "input": {
    "productId": "product-id",
    "product": {
      "targetUnitCount": 24,
      "targetOccupancyPercentage": 97.5
    }
  }
}
```

Required fields for `UpdateProductMutationInput`:

| Field | Type |
| --- | --- |
| `productId` | `ID!` |
| `product` | `ProductUpdateInput!` |

#### Delete a product

```graphql
mutation DeleteProduct($input: DeleteProductMutationInput!) {
  catalog {
    product {
      delete(input: $input) {
        id
      }
    }
  }
}
```

```json
{
  "input": {
    "productId": "product-id"
  }
}
```

Required fields for `DeleteProductMutationInput`:

| Field | Type |
| --- | --- |
| `productId` | `ID!` |

> **Tip:** The Catalog product namespace also supports service management (`addServices`, `removeService`, `updateService`) and unit allocation (`allocateUnits`, `unallocateUnits`), plus rate updates through `catalog.rate.update`.

## MCP Tools

If you use MCP, these tools are relevant:

- [get_product](/mcp/tools/catalog/products/get_product/)
- [get_product_group](/mcp/tools/catalog/product-groups/get_product_group/)

See [Tools](/mcp/tools/) for setup and full tool coverage.

## Booking date bounds

Products do not own contract date boundaries directly — they inherit them from their parent product group. The `contractEarliestStartDate`, `contractLatestStartDate`, `contractEarliestEndDate`, and `contractLatestEndDate` fields on `ProductGroup` control the date range a booking made against any product in the group can occupy.

In particular, `contractEarliestStartDate` and `contractLatestEndDate` are the outer bounds, while `contractLatestStartDate` and `contractEarliestEndDate` allow a student to start late or end early within those bounds. See [Contract windows](/catalog/product-groups/#contract-windows) on the Product Group doc for full details.

## Relationships

- A product belongs to a [Product Group](/catalog/product-groups/)
- A product references a [Unit Type](/spaces-and-inventory/unit-types/)
- A product includes rate information that works with [Pricing](/pricing/pricing/)
- Product details are used as part of the [Ecommerce](/ecommerce/flow/) booking flow
