# Update price level

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

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

HTML version: [Update price level](https://support.gladstonesoftware.cloud/reference/configuration/update-price-level)

## Overview

Rewrites a price level — the banding a member or visitor is placed in, which determines which prices
apply to them. This call sets the price level's name, its age band, its colours, who may be assigned
it, the price levels it transitions to, its translations and its per-site or per-activity alternative
names. It does not set any actual prices; those are held against products and activities.

**This is a full replacement, and one of the more dangerous updates in this API.** Everything below
is written on every call, and three things are worth knowing before you use it:

1. **Fields you omit are cleared.** Every field is written unconditionally, so an
   omitted `description`, `infringementPriceLevel` or `nextPriceLevel` is written as null, and an
   omitted boolean is written as `false`. Read the price level first with
   `GET /api/configuration/price-levels` and send every field back.
2. **Settings this endpoint does not expose are reset.** Every update resets several settings the
   request cannot carry: the Connect non-members, force booking payment, force payment
   via Connect and kiosk non-members indicators are all set to `0`, the loyalty point threshold is
   set to `0`, and the smart card reference is set to an empty string. If your estate uses any of
   those, updating a price level through this endpoint silently turns them off.
3. **`backColourHex` and `textColourHex` must be present.** An empty string is understood as
   "no colour", but `null` — including the null you get by omitting the field — fails with `500`.

`translations` and `alternativeDescriptions` are also full-set replacements: the existing translations
are deleted and rebuilt, and alternative descriptions for any site or activity missing from your
request are deleted.

Validation of the whole request happens before anything is written. The write itself is not atomic:
the price level record and its translations are saved first, then the alternative descriptions in
separate steps, so a failure part-way through can leave the two inconsistent.

On success the response is `204 No Content` and the change is visible on the next read.

## Endpoint

`PUT {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 | Identifier of the price level to rewrite, up to 15 characters, as listed by `GET /api/configuration/price-levels`. It is converted to upper case before the record is looked up. If no price level matches, the request fails with `404` and the body `Record cannot be found`. The identifier is not writable. |

## Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `description` | string | No | The price level's name, as shown wherever price levels are listed. Maximum 255 characters (`The price level description cannot be more than 255 characters`); an empty string is rejected (`A blank description is not allowed`), but null is accepted and **clears the name**. Written on every call. |
| `ageRangeStart` | integer (int32) | No | Youngest age this price level applies to. Used when a member's date of birth is checked against the price level they are being given, and by age-based transitions. Range 0 to 32767 (`AgeRangeStart has to be greater or equal to 0`). Must not be greater than `ageRangeEnd`, or the request fails with `The starting age range is greater than the ending age range`. Omitting it writes `0`. |
| `ageRangeEnd` | integer (int32) | No | Oldest age this price level applies to, on the same 0–32767 range (`AgeRangeEnd has to be greater or equal to 0`). **Omitting it writes `0`** — which, with any non-zero `ageRangeStart`, then fails the start-versus-end check. Always send both ages together. |
| `backColourHex` | string | Yes | Background colour used when the price level is displayed, as `#RRGGBB` — the same form `GET /api/configuration/price-levels` returns. The value you read back is a normalised hex string rather than exactly what you sent. Send an empty string to store no colour. **Do not omit it and do not send null:** the request fails with `500`. |
| `textColourHex` | string | Yes | Foreground colour used for the price level's text, in the same `#RRGGBB` form and with the same normalisation and the same null trap as `backColourHex`. Empty string means no colour. |
| `membersAllowed` | boolean | No | Whether this price level may be used for members. Used to filter price levels — the member-only listing returns just those with this set. Omitting it or sending null writes `false`, removing the price level from member selection. |
| `nonMembersAllowed` | boolean | No | Whether this price level may be used for non-members — casual visitors rather than members. Used the same way for the non-member listing. Omitting it writes `false`. |
| `webNonMembersAllowed` | boolean | No | Whether non-members may be given this price level through the public web channel, as opposed to only at the counter. Listings can filter on it in combination with `membersAllowed`. Omitting it writes `false` and quietly withdraws the price level from online journeys. |
| `infringementPriceLevel` | string | No | The price level a member is moved to when they breach terms — for example debt or missed payments. Give the identifier of another price level, up to 15 characters; it must already exist or the request fails with `400 PriceLevel {id} does not exist`. Empty and null are accepted and clear the setting, so omitting it removes the infringement route. Do not confuse it with `nextPriceLevel`. |
| `nextPriceLevel` | string | No | The price level this one automatically transitions to, used for progressions such as a junior band becoming an adult band once the age range is exceeded. Give the identifier of another price level, up to 15 characters; it must already exist or the request fails with `400 PriceLevel {id} does not exist`. Cleared when omitted or null. |
| `translations` | array of object | No | The complete set of localised text for this price level. The existing translations are deleted and rebuilt from this array on every call, so omitting it removes every translation; there is no way to amend a single one. No validation is applied to the entries. |
| `translations[].type` | string | No | Which piece of text this entry localises. Not validated. |
| `translations[].culture` | string | No | The culture the text is written in, as a `language-REGION` culture code such as `cy-GB`. Not validated, so an unrecognised culture is accepted rather than rejected and the text simply never surfaces. |
| `translations[].text` | string | No | The translated text itself, for the `type` and `culture` in the same entry. Not length-validated here, unlike `description`. |
| `alternativeDescriptions` | array of object | No | Overrides for the price level's name at particular sites or on particular activities — for example the same band shown under a local name at one centre. This is a full-set replacement: overrides for any site or activity not named in your request are deleted, and omitting the array or sending it empty deletes them all. Each entry must supply `alternativeDescription` and at least one of `siteIds` or `activityIds`, and the names must be unique within the array, compared case-insensitively and trimmed. |
| `alternativeDescriptions[].alternativeDescription` | string | No | The alternative name to show. Mandatory within an entry — an entry without one fails with `400 One or more alternative descriptions does not have a description`. Maximum 25 characters (`AlternativeDescriptions cannot be more than 25 characters`), much shorter than `description`. Two entries with the same name, ignoring case and surrounding spaces, are rejected. |
| `alternativeDescriptions[].activityIds` | array of string | No | Activities this alternative name applies to. Each identifier must exist or the request fails with `400 Some or all of the activities supplied are not valid`, and no activity may appear twice **across the whole array** (`Duplicate Activity IDs are not allowed across alternative descriptions`) — one activity cannot have two alternative names. Ids are converted to upper case. Each activity gets its own override, independently of `siteIds`: the two lists are not combined into site-and-activity pairs. |
| `alternativeDescriptions[].siteIds` | array of string | No | Sites this alternative name applies to. Each identifier must exist or the request fails with `400 Some or all of the sites supplied are not valid`, and no site may appear twice across the whole array (`Duplicate Site IDs are not allowed across alternative descriptions`). Ids are converted to upper case, and each site gets its own override. An entry needs at least one site or activity, or the request fails with `One or more alternative descriptions does not have a activity ID or site ID defined`. |

## Sample request

```bash title="cURL"
curl -X PUT '{base_url}/api/configuration/price-levels/{priceLevelId}' \
  -H 'Authorization: Bearer {accessToken}' \
  -H 'X-Use-SSO: true' \
  -H 'Content-Type: application/json' \
  -d '{
  "description": "string",
  "ageRangeStart": 1,
  "ageRangeEnd": 1,
  "backColourHex": "string",
  "textColourHex": "string",
  "membersAllowed": true,
  "nonMembersAllowed": true,
  "webNonMembersAllowed": true,
  "infringementPriceLevel": "string",
  "nextPriceLevel": "string",
  "translations": [
    {
      "type": "string",
      "culture": "string",
      "text": "string"
    }
  ],
  "alternativeDescriptions": [
    {
      "alternativeDescription": "string",
      "activityIds": [
        "string"
      ],
      "siteIds": [
        "string"
      ]
    }
  ]
}'
```

## Errors

| Status | Meaning | When |
|--------|---------|------|
| `400` | Bad request | Validation failed: `description` blank or over 255 characters, an age outside 0–32767, or an `alternativeDescription` over 25 characters. The body is a validation error object keyed by field name. |
| `400` | Bad request | `ageRangeStart` is greater than `ageRangeEnd` — `The starting age range is greater than the ending age range`. |
| `400` | Bad request | `nextPriceLevel` or `infringementPriceLevel` names a price level that does not exist — `PriceLevel {id} does not exist`. |
| `400` | Bad request | An alternative description entry has no name — `One or more alternative descriptions does not have a description`. |
| `400` | Bad request | An alternative description entry names neither a site nor an activity — `One or more alternative descriptions does not have a activity ID or site ID defined`. |
| `400` | Bad request | Two alternative descriptions share a name — `Duplicate alternateDescriptions within the same PriceLevel is not allowed. Please ensure each alternateDescription is unique`. |
| `400` | Bad request | A site or activity appears in more than one alternative description — `Duplicate Site IDs are not allowed across alternative descriptions` or `Duplicate Activity IDs are not allowed across alternative descriptions`. |
| `400` | Bad request | A site or activity does not exist — `Some or all of the sites supplied are not valid` or `Some or all of the activities supplied are not valid`. |
| `404` | Not found | No price level matches the path identifier — body is the string `Record cannot be found`. |
| `409` | Conflict | The update conflicts with an existing record — `The record updating contains a key already in use`. |
| `500` | Internal server error | `backColourHex` or `textColourHex` was null or omitted. Also any other unexpected server-side failure. No detail is returned. |
