# Get price level by ID

`GET /api/configuration/price-levels/{priceLevelId}`

- **Success:** `200 OK`
- **Products:** Operator (`FOH`)
- **Features:** Front of House Generic Access (`FOHGENERIC`)

HTML version: [Get price level by ID](https://support.gladstonesoftware.cloud/reference/configuration/get-price-level-by-id)

## Overview

Returns a single price level, together with the site- and activity-specific alternative
descriptions configured against it.

A price level is the operator-defined pricing band a person is charged under — adult, junior,
concession, staff and so on. **It holds no money.** Prices are held separately, keyed
on product, [time band](../../guides/glossary.md) and price level together, so a price level is one axis of
a price lookup rather than a price in its own right. Nothing in this response is an amount. What a
price level does carry is eligibility (`membersAllowed`, `nonMembersAllowed`,
`webNonMembersAllowed`), an age band, and pointers to two other price levels — `nextPriceLevel` for
automatic progression such as a junior ageing into adult pricing, and `infringementPriceLevel` for
the band a member is moved onto when they breach terms. A member record carries a price level id,
and that is what ties a person to the prices they are charged.

Get the id from [Get all price levels](get-all-price-levels.md). Ids are
operator-defined and not comparable across tenants, so never hardcode one. This call gives you two
things the list does not: the `alternativeDescriptions` block, and a `translations` array that is
actually populated.

**A price level id that does not exist returns `404`, not `400`.** The body is the plain-text
message `Unable to locate price level with id of {priceLevelId}` — not JSON, and not a
problem-details object.

One thing this response cannot tell you: a price level has an inactive flag which is
neither filtered on nor returned. A price level the operator has retired still comes back as a
normal `200`, indistinguishable from a live one.

## Endpoint

`GET {base_url}/api/configuration/price-levels/{priceLevelId}`

`{base_url}` is the environment URL issued to your integration. See [Authenticating with Gladstone APIs](../../guides/authenticating-with-gladstone-apis.md).

## Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `priceLevelId` | string | Yes | The id of the price level to read, maximum 15 characters. Take it from the `id` field of [Get all price levels](get-all-price-levels.md) rather than constructing it. Ids are uppercase with spaces stripped, because that is how the create endpoint normalises them, so send the value exactly as the platform returned it. No inactive filtering is applied — a retired price level is still returned. |

## Sample request

```bash title="cURL"
curl -X GET '{base_url}/api/configuration/price-levels/{priceLevelId}' \
  -H 'Authorization: Bearer {accessToken}' \
  -H 'X-Use-SSO: true'
```

## Response fields

| Field | Type | Description |
|-------|------|-------------|
| `alternativeDescriptions` | array of object | Labels that replace `description` for particular sites or activities — an operator running the same band under a different name at one site, for example. Empty array when none are configured, which is the common case. Each entry is one distinct label text, with the activities and sites it applies at collected under it, so a label used at three sites appears once with three entries in `sites`. This block is exclusive to this endpoint; the list endpoint does not return it. Overrides scoped to an activity *group* rather than an individual activity are deliberately excluded and will not appear here. |
| `alternativeDescriptions[].alternativeDescription` | string | The replacement label itself, at most 25 characters — noticeably shorter than `description`, so expect an abbreviation rather than a full phrase. |
| `alternativeDescriptions[].activities` | array of object | The activities this label applies to. Empty when the override is scoped only to sites. When both `activities` and `sites` are populated, they were configured as separate overrides sharing the same label text, not as an activity-and-site intersection. |
| `alternativeDescriptions[].activities[].id` | string | The activity's id, maximum 15 characters, matching the activity ids used elsewhere in the Configuration API. |
| `alternativeDescriptions[].activities[].name` | string | The activity's own description, included so you do not have to look the activity up separately for display. |
| `alternativeDescriptions[].sites` | array of object | The sites this label applies to. Empty when the override is scoped only to activities. |
| `alternativeDescriptions[].sites[].siteId` | string | The site's id, maximum 15 characters and uppercase. |
| `alternativeDescriptions[].sites[].name` | string | The site's own description, for display. |
| `alternativeDescriptions[].translations` | array of object | Localised versions of `alternativeDescription`. Duplicates are removed before the array is built, so each culture appears once even when several overrides share the label. Empty array when no translations are configured. Note this is a different set of translations from the top-level `translations` field, which localises `description`. |
| `alternativeDescriptions[].translations[].type` | string | The property the `text` replaces. |
| `alternativeDescriptions[].translations[].culture` | string | The culture the `text` applies to, at most 20 characters. Must be a code the platform knows about — unknown cultures are rejected when translations are written — so you will only see recognised codes here. |
| `alternativeDescriptions[].translations[].text` | string | The translated label for that culture. No culture filtering happens server-side: you receive every configured culture and pick the one you want. |
| `id` | string | The price level's own identifier, maximum 15 characters and unique within the tenant. This is the value you pass back as `priceLevelId`, and the value that appears as a member's price level elsewhere in the API. Treat it as opaque — a value such as `ADULT` at one operator says nothing about another. |
| `description` | string | The operator's own label for the band, intended for display. Maximum 255 characters and never blank: an empty description is rejected when the price level is written. Localise it using `translations` where you have a non-default culture. |
| `ageRangeStart` | integer (int32) | Lower bound in years of the age band this price level covers. Always present, and `0` means "no meaningful lower bound" rather than "applies from birth". Must be less than or equal to `ageRangeEnd`; writes reject the reverse with *The starting age range is greater than the ending age range*, so you will not see it violated here. |
| `ageRangeEnd` | integer (int32) | Upper bound in years of the age band. Always present. Read it together with `ageRangeStart`, and with `nextPriceLevel` where the operator uses age progression — this is the boundary at which a member moves on. |
| `backColourHex` | string | Background colour the operator has assigned for showing this price level on staff-facing screens, as `#RRGGBB`. It is converted from a legacy integer colour value, so an unset colour comes back as an **empty string, not `null`**. Purely cosmetic; nothing behavioural depends on it. The create endpoint substitutes `#E0E0E0` when no colour is sent, so recently created levels are rarely blank. |
| `textColourHex` | string | Foreground colour for the same display, as `#RRGGBB`, with the same empty-string-when-unset behaviour as `backColourHex`; the create endpoint defaults it to `#000000`. It is meant to be legible against `backColourHex`, but the platform does not check that it is. |
| `membersAllowed` | boolean | Whether members may be charged at this price level. Derived from a legacy numeric indicator where **only the value 1 counts as true**, so `false` means "not enabled" and cannot be distinguished from "never configured". This is what `filter=Member` selects on in [Get all price levels](get-all-price-levels.md). |
| `nonMembersAllowed` | boolean | Whether non-members — casual, pay-as-you-go visitors — may be charged at this price level. Derived the same way as `membersAllowed`, with the same "unset becomes `false`" behaviour. Independent of `webNonMembersAllowed`: a level can allow non-members at the counter without allowing them online. |
| `infringementPriceLevel` | string | The `id` of the price level a member is moved onto when they breach the terms of their membership — falling into debt or missing payments, for example. `null` when the operator has configured none, in which case no automatic move happens. The value is validated against existing price levels when it is set, so it always resolves. Do not confuse it with `nextPriceLevel`; the two exist for entirely different reasons. |
| `nextPriceLevel` | string | The `id` of the price level a member progresses to automatically, typically on ageing out of the band defined by `ageRangeStart`/`ageRangeEnd`. `null` when no progression is configured, meaning the member stays on this level indefinitely. Also validated to exist when set. |
| `webNonMembersAllowed` | boolean | Whether non-members may be charged at this level through online channels specifically. Never null and defaults to `false`, so it is always present. Treat it as a narrowing of `nonMembersAllowed` rather than a substitute for it — it is the flag the `webNonMembers=true` query parameter filters on in [Get all price levels](get-all-price-levels.md). |
| `translations` | array of object | Localised alternatives to `description`, and the main reason to call this endpoint rather than the list. Only the description is translated — no other field on the price level has translations — and the array is empty when the operator has configured none. |
| `translations[].type` | string | The property the translated `text` replaces. Always `description` here, because that is the only property this endpoint returns translations for. |
| `translations[].culture` | string | The culture the `text` applies to, at most 20 characters and drawn from the platform's configured culture list. Every configured culture is returned; there is no way to ask for one. |
| `translations[].text` | string | The translated description for that culture. |

## Sample response

```json title="200 OK"
{
  "alternativeDescriptions": [
    {
      "alternativeDescription": "string",
      "activities": [
        {
          "id": "string",
          "name": "string"
        }
      ],
      "sites": [
        {
          "siteId": "string",
          "name": "string"
        }
      ],
      "translations": [
        {
          "type": "string",
          "culture": "string",
          "text": "string"
        }
      ]
    }
  ],
  "id": "string",
  "description": "string",
  "ageRangeStart": 1,
  "ageRangeEnd": 1,
  "backColourHex": "string",
  "textColourHex": "string",
  "membersAllowed": true,
  "nonMembersAllowed": true,
  "infringementPriceLevel": "string",
  "nextPriceLevel": "string",
  "webNonMembersAllowed": true,
  "translations": [
    {
      "type": "string",
      "culture": "string",
      "text": "string"
    }
  ]
}
```

## Errors

| Status | Meaning | When |
|--------|---------|------|
| `400` | Bad request | Not returned by this endpoint: the only lookup failure is the not-found case below. Do not treat `400` as your missing-price-level signal — see the `404` row. |
| `404` | Not found | No price level exists with that id. The body is the plain-text message `Unable to locate price level with id of {priceLevelId}`, echoing the id exactly as you sent it. |
| `500` | Internal server error | An unexpected server-side failure while reading the price level, its alternative descriptions or their translations. The body is empty and carries no diagnostic detail. |
