Create price level
POST /api/configuration/price-levels
- Products: Operator (
FOH) - Features: Front of House Generic Access (
FOHGENERIC)
Overview
Creates a price level. A price level is a named pricing category — "Adult", "Junior", "Corporate" and so on — that members and casual customers are assigned to, and that activity and membership prices are then quoted against.
A price level is identified by a short string id. You choose that id yourself by sending id, or you omit
id and one is allocated for you: the endpoint takes the highest existing id beginning PLVL, adds one, and
zero-pads the number to a total length of 15 characters, giving ids of the form PLVL00000000001.
A successful call returns 201 Created. The body is a small JSON object whose only property is the id that
was actually used — the one you sent, upper-cased and stripped of spaces, or the one allocated for you:
{ "id": "PLVL00000000001" }Read that property case-insensitively, and take the id from it rather than assuming the value you sent was
stored verbatim. The endpoint also sets a Location header.
Retrying is only safe when you choose the id yourself. If you send an id that already exists the request
is rejected with 409 Conflict and nothing is created, so a retry after a timeout is harmless. If you omit
id, every retry allocates a fresh id and creates another price level — there is no idempotency key and no
duplicate detection on description.
Uniqueness is enforced on the id only: id must be unique across all price levels, but two price levels may
carry the same description. Within a single request, each alternativeDescriptions[].alternativeDescription
must be distinct, and no site id or activity id may appear more than once across the whole
alternativeDescriptions array.
Two of the fields on this request are routinely confused, and they do different things:
infringementPriceLevel is the price level a member is moved to when they breach the terms of their
agreement — falling into debt, missing payments — whereas nextPriceLevel is the price level a member
progresses to automatically, typically when they age out of one band into another (junior to adult). Setting
one does not affect the other.
Creation is not all-or-nothing. The price level is saved first, and any
alternativeDescriptions are saved afterwards, one entry at a time. All validation runs before anything is
written, so this rarely bites, but if saving the alternative descriptions fails mid-way the price level itself
still exists and the call returns 500. Retrieve the price level before retrying rather than assuming
nothing was created.
Endpoint
POST {base_url}/api/configuration/price-levels
{base_url} is the environment URL issued to your integration. See Authenticating with Gladstone APIs.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
id | string | No | The identifier you want the price level to have, up to 15 characters. It is upper-cased and has spaces stripped before it is stored, so gym adult is saved and must later be referenced as GYMADULT. It must not already be in use — a clash is rejected with 409. Omit the field and the endpoint allocates the next id in the PLVL… sequence instead, and returns it in the response body. |
description | string | Yes | The name of the price level as staff and customers see it, 1–255 characters. Descriptions are not required to be unique, so nothing stops you creating two price levels called "Adult". Use alternativeDescriptions if a particular site or activity needs to show a different name for this same price level. |
ageRangeStart | integer (int32) | No | Lower bound of the age band this price level covers, in whole years. Despite the error message ("AgeRangeStart has to be greater than 0"), the accepted range is 0 to 32767 inclusive, and 0 is valid. Defaults to 0 when omitted. Must not be greater than ageRangeEnd, or the request is rejected with 400. |
ageRangeEnd | integer (int32) | No | Upper bound of the age band, in whole years, on the same 0–32767 scale as ageRangeStart. Defaults to 0 when omitted — so a request that sets only ageRangeStart to a non-zero value is rejected, because the start would then exceed the end. |
backColourHex | string | No | Background colour used when the price level is shown in Gladstone's own front-of-house screens. Send a hex string such as #E0E0E0; a value that cannot be parsed as a colour fails the request with 500 rather than 400. Send null or "" to get the default #E0E0E0. The default is applied only when the field is actually present in the JSON: send it explicitly as null rather than leaving the key out. |
textColourHex | string | No | Foreground (text) colour used alongside backColourHex on the same screens, with identical parsing and identical handling of null and empty values. Its default is #000000. |
membersAllowed | boolean | No | Marks the price level as applying to members. Defaults to false when omitted. Price levels with this set are the ones returned by GET /api/configuration/price-levels?filter=Member. |
nonMembersAllowed | boolean | No | Marks the price level as applying to non-members — casual customers with no membership. Defaults to false, and is independent of membersAllowed; a price level may have both, one, or neither. Price levels with this set are returned by GET /api/configuration/price-levels?filter=Non-Member. |
webNonMembersAllowed | boolean | No | Marks the price level as available to non-members booking through the consumer web channel, as opposed to at the centre. Defaults to false. It is a separate flag from nonMembersAllowed, and is what the webNonMembers=true query parameter on the list endpoint filters on. |
infringementPriceLevel | string | No | The id of the price level a member on this price level is moved to when they breach the terms of their agreement — for example falling into debt or missing payments. Must be the id of a price level that already exists, matched exactly as stored (upper case), or the request fails with 400. Defaults to empty, meaning no infringement level is configured. Do not confuse this with nextPriceLevel: this one is punitive, that one is progression. |
nextPriceLevel | string | No | The id of the price level a member progresses to automatically as part of normal lifecycle, most commonly an age-based transition such as junior to adult. Must be the id of an existing price level, matched exactly as stored (upper case), or the request fails with 400. Defaults to empty, meaning no onward level is configured. There is no cycle detection, and you cannot point it at the price level you are creating in this request, because the target must already exist. |
translations | array of object | No | Translations of this price level's description into other cultures, for bilingual sites. Omit or send an empty array if you only need the description. The entries are stored as supplied and are not validated, so mistakes here surface as a 500 rather than a 400. |
translations[].type | string | No | The name of the field being translated. Use description — that is the only property the price level retrieval and list endpoints read back, so an entry with any other value is stored but never returned. |
translations[].culture | string | No | The culture code the translation is for, for example cy-GB. It must be a culture code the platform already knows about; an unrecognised code fails the request with 500. |
translations[].text | string | No | The translated text itself — what description should read as in that culture. |
alternativeDescriptions | array of object | No | Per-site and per-activity overrides of the price level's name, for cases where the same price level is called something different at a particular centre or for a particular activity. Omit or send an empty array if no overrides are needed. All entries are validated before the price level is written, so an invalid entry means nothing at all is created. |
alternativeDescriptions[].alternativeDescription | string | No | The name to display instead of description for the sites and activities listed in this entry, up to 25 characters — a much tighter limit than description's 255. It must not be blank, and comparing case-insensitively and ignoring surrounding whitespace, it must differ from every other alternativeDescription in the same request. |
alternativeDescriptions[].activityIds | array of string | No | The activities this override applies to. Each id must be an activity that already exists, and no activity id may be repeated anywhere in the alternativeDescriptions array — including across two different entries. Ids are upper-cased before use. May be empty, but only if siteIds is populated: every entry needs at least one site or activity. |
alternativeDescriptions[].siteIds | array of string | No | The sites (centres) this override applies to. Each id must be a site that already exists, and no site id may be repeated anywhere in the alternativeDescriptions array. Ids are upper-cased before use. May be empty, but only if activityIds is populated. |
Sample request
curl -X POST '{base_url}/api/configuration/price-levels' \
-H 'Authorization: Bearer {accessToken}' \
-H 'X-Use-SSO: true' \
-H 'Content-Type: application/json' \
-d '{
"id": "string",
"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
Error responses carry the reason as the response body. Request validation failures return a validation
problem document (application/problem+json) with an errors map keyed by field name; every other failure
returns the message below as a plain string.
| Status | Meaning | When |
|---|---|---|
400 | Bad request | Request validation failed before any other check: id longer than 15 characters, description missing, empty or longer than 255 characters, ageRangeStart or ageRangeEnd outside 0–32767, or an alternativeDescription longer than 25 characters. The body is a validation document listing the offending fields. |
400 | Bad request | Malformed JSON, or a value of the wrong type for its field. |
400 | Bad request | ageRangeStart is greater than ageRangeEnd. Body: 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. Body: PriceLevel {id} does not exist. Remember ids are stored upper-cased. |
400 | Bad request | An alternativeDescriptions entry has a blank or missing alternativeDescription. Body: One or more alternative descriptions does not have a description. |
400 | Bad request | An alternativeDescriptions entry supplies neither siteIds nor activityIds. Body: One or more alternative descriptions does not have a activity ID or site ID defined. |
400 | Bad request | Two entries in alternativeDescriptions use the same text, compared case-insensitively and trimmed. Body: Duplicate alternateDescriptions within the same PriceLevel is not allowed. Please ensure each alternateDescription is unique. |
400 | Bad request | The same site id appears more than once across alternativeDescriptions, whether inside one entry or spread across several. Body: Duplicate Site IDs are not allowed across alternative descriptions. |
400 | Bad request | The same activity id appears more than once across alternativeDescriptions. Body: Duplicate Activity IDs are not allowed across alternative descriptions. |
400 | Bad request | One or more ids in siteIds do not match an existing site. Body: Some or all of the sites supplied are not valid. The message does not say which id was wrong. |
400 | Bad request | One or more ids in activityIds do not match an existing activity. Body: Some or all of the activities supplied are not valid. |
409 | Conflict | The id you supplied is already in use by another price level. Body: The PriceLevel : {id} already exists. Nothing is created, so this is safe to treat as "already done". |
409 | Conflict | A concurrent request created a price level with the same id at the same moment. Body: This entry already exists in the database. |
500 | Internal server error | backColourHex or textColourHex could not be parsed as a colour. This is a bad request in everything but status code, so validate the hex yourself before sending. |
500 | Internal server error | A translations[].culture is not a culture code the platform recognises, so the translation cannot be saved. |
500 | Internal server error | Any other unexpected server-side failure, including one while saving the alternative descriptions — in which case the price level itself may already exist. |