Get promo code by ID or code
GET /api/configuration/promotion-codes/{promoCodeOrId}
Success: 200 OK
- Products: Operator (
FOH) - Features: Front of House Generic Access (
FOHGENERIC)
Overview
Returns one promo code together with every discount mapped to it. A promo code is the string a customer types at checkout; the discounts attached to it are the actual price reductions, each one bound to a single membership type. One code can carry many discounts, so expect an array even for a simple campaign.
The single most important detail is how you address the record. The same path segment accepts two different things, and it decides between them by looking for curly braces:
- A bare number is treated as the promo code's numeric ID —
/api/configuration/promotion-codes/42. - A value wrapped in curly braces is treated as the code itself —
/api/configuration/promotion-codes/{SUMMER25}. The braces are stripped before the lookup and are not part of the code. The match is case-insensitive. - Anything else is rejected with
400, including a bare code such asSUMMER25, because it is neither a number nor brace-wrapped. Some HTTP clients and gateways will not send literal braces in a path; encode them as%7Band%7Dif yours refuses.
This endpoint is a configuration read, not a validation call. It tells you what the operator has set up; it does not decide whether the code may be used right now. active, validFrom, validTo and usagesRemaining are all returned as configured — nothing is filtered or expired for you, so a code that has run out or has not started still returns 200. Apply those checks yourself, or use the pricing calls, which apply the discount properly.
Note also that discounts here are described but not evaluated: no money is calculated, and nothing is reserved or consumed.
Endpoint
GET {base_url}/api/configuration/promotion-codes/{promoCodeOrId}
{base_url} is the environment URL issued to your integration. See Authenticating with Gladstone APIs.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
promoCodeOrId | string | Yes | Either the promo code's numeric ID, or the code itself wrapped in curly braces — 42 or {SUMMER25}. Codes are maximum 25 characters and matched case-insensitively, so {summer25} and {SUMMER25} find the same record. A value that is neither numeric nor brace-wrapped is a 400, not a 404. When nothing matches, the 404 message tells you which of the two lookups ran, which is a quick way to confirm the braces were received. |
Sample request
curl -X GET '{base_url}/api/configuration/promotion-codes/{promoCodeOrId}' \
-H 'Authorization: Bearer {accessToken}' \
-H 'X-Use-SSO: true'curl -X GET '{base_url}/api/configuration/promotion-codes/%7BSUMMER25%7D' \
-H 'Authorization: Bearer {accessToken}' \
-H 'X-Use-SSO: true'Response fields
| Field | Type | Description |
|---|---|---|
maxUsage | integer (int32) | How many times the code may be redeemed in total. 0 means unlimited, not "none left". For a limited code this is the original allowance, not the remaining balance; compare it with usagesRemaining to see how much has been taken up. |
usagesRemaining | integer (int32) | How many redemptions are left. Meaningless when maxUsage is 0 (unlimited), where its value has no meaning. For a limited code, 0 here means the code is exhausted — the endpoint still returns it, so check this yourself rather than assuming a 200 implies availability. |
autoApply | boolean | Whether the operator intends this code to be applied automatically rather than typed in by the customer. This is a declaration of intent for your checkout to honour; the endpoint does not apply anything. |
discounts | array of object | Every discount mapped to this code, one entry per discount. null, not [], when the code has no discounts mapped — guard for null before iterating. Each entry targets exactly one membership type, so a campaign covering three membership types appears here as three entries. |
discounts[].appId | string | The single application this discount is scoped to, maximum 25 characters. Null when blank. Superseded in practice by appIds, which carries the same idea as a list. |
discounts[].startDate | string (date-time) | The date the discount itself becomes available, which is independent of the promo code's validFrom. Null when the discount has no start bound. Both windows must be satisfied for the discount to be usable — a code that is in date can still carry a discount that is not. |
discounts[].endDate | string (date-time) | The date the discount stops being available, independent of the promo code's validTo. Null when the discount never expires; see also isPerpetual. |
discounts[].siteId | string | The site the discount is restricted to, maximum 15 characters. Null means the discount is not restricted to a site, so do not filter it out when you have no site match — treat null as "applies everywhere". Resolve the code with Get site by ID. |
discounts[].membershipStartDate | string (date-time) | Restricts the discount to memberships whose start date is this date, taken from the discount's subscription start date. Null when there is no such restriction. Distinct from startDate, which is about when the discount may be applied rather than when the membership begins. |
discounts[].appIds | array of string | The applications the discount is available in, configured as a comma-separated string. Two consequences: a discount whose value is blank yields a single-element list containing an empty string rather than an empty list, and a discount where the value was never set at all causes the whole request to fail with 500. |
discounts[].discountSecondLanguageText | string | The customer-facing discount text in the operator's second language, maximum 255 characters. Null when not translated. Use it in place of discountText when rendering in that language. |
discounts[].discountBanner | string | Short promotional banner text for the discount, maximum 255 characters, for display above a basket or checkout. Null when the operator has set none. |
discounts[].regularDiscountDetails | object | The reduction applied to the ongoing, recurring price of the membership. Always present as an object, even when no regular discount is configured — in that case its type is none. |
discounts[].regularDiscountDetails.type | string | How the regular reduction is expressed. The platform's own discount tooling only ever writes percentage or none, and only those two are honoured by the consumer journey — see applicable. Any other value indicates a discount created outside that tooling. |
discounts[].regularDiscountDetails.value | number (double) | The size of the regular reduction, held to two decimal places. When type is percentage this is a percentage, so 10.5 means 10.5%, not £10.50. Defaults to 0 when no regular discount is configured. |
discounts[].introductoryDiscountDetails | object | The reduction applied to the introductory period of the membership, before the regular price takes over. Always present as an object; type is none when there is no introductory discount. Read discountUntilUnit to see how long the introductory period runs. |
discounts[].introductoryDiscountDetails.type | string | How the introductory reduction is expressed — percentage or none for discounts created through the platform's own tooling. |
discounts[].introductoryDiscountDetails.value | number (double) | The size of the introductory reduction, to two decimal places; a percentage when type is percentage. Defaults to 0. |
discounts[].oneOffDiscountDetails | object | The reduction applied to one-off charges — joining fees and similar — rather than to the recurring subscription. Always present as an object; type is none when there is no one-off discount. |
discounts[].oneOffDiscountDetails.type | string | How the one-off reduction is expressed — percentage or none for discounts created through the platform's own tooling. |
discounts[].oneOffDiscountDetails.value | number (double) | The size of the one-off reduction, to two decimal places; a percentage when type is percentage. Defaults to 0. |
discounts[].discountUntilUnit | integer (int32) | How many billing units the introductory discount runs for before the regular discount takes over. Defaults to 0. |
discounts[].discountToNearestValue | number (double) | The increment the discounted price is rounded to, defaulting to 0.01 — that is, to the penny. A value of 1.00 rounds discounted prices to whole units of currency. Never null. |
discounts[].reason | string | The operator's internal note explaining why the discount exists, maximum 4000 characters. Null when blank. This is back-office text — do not show it to customers. |
discounts[].isPerpetual | boolean | Whether the discount continues for the life of the membership rather than for a fixed number of units. When true, discountUntilUnit does not bound it. Defaults to false. |
discounts[].overrideWebPromotion | boolean | Whether this discount's text should replace the membership type's own web promotion copy in a consumer-facing journey. Reports false when unset, so false means "do not override" in both the unset and the explicitly-off case. |
discounts[].active | boolean | Whether the operator has this discount switched on. Inactive discounts are still returned, so filter on this yourself — a 200 with discounts in it does not mean any of them are live. Independent of the promo code's own active. |
discounts[].discountId | integer (int32) | The discount's numeric identifier. Use it to correlate with discount-management endpoints; it is not the promo code's promoId. |
discounts[].membershipTypeId | string | The single membership type this discount reduces, maximum 15 characters. Every discount targets exactly one — a campaign spanning several types produces one discount per type, all mapped to the same promo code. Resolve it with Get membership type by ID. |
discounts[].membershipTypeDescription | string | The membership type's display name, looked up for convenience so you do not need a second call. Null when the mapping between the discount and the membership type cannot be resolved, which is a data problem worth surfacing rather than rendering as an empty label. |
discounts[].discountText | string | The customer-facing description of the discount in the default language, maximum 255 characters. Use discountSecondLanguageText for the second language. |
discounts[].applicable | boolean | Whether the consumer web journey can apply this discount at all. Computed by the platform: it is true only when every one of the three discount types is either percentage or none, at least one of them is not none, and the discount has no product-level overrides attached. A false here is the usual explanation for a discount that looks correctly configured but never appears at checkout. Note that the product-level overrides that can force it to false are not themselves returned by this endpoint, so applicable: false on an otherwise all-percentage discount points at those. |
promoId | integer (int32) | The promo code's numeric identifier — the value you can pass back as promoCodeOrId without braces. Assigned by the platform; never supplied by you. |
code | string | The code a customer types, maximum 25 characters. Case is preserved as the operator entered it, but lookups ignore case. |
validFrom | string (date-time) | The first date the code may be used. Null when the code has no start bound. The time is truncated to midnight when set through the platform's own tooling. This is the code's window; each discount has its own startDate, and both must be satisfied. |
validTo | string (date-time) | The last date the code may be used. Null when the code has no end bound. Expired codes are still returned by this endpoint — compare against today yourself. |
active | boolean | Whether the operator has the code switched on. Reports false when unset, so false covers both "explicitly disabled" and "never enabled". Independent of the per-discount active flags: a live code can carry only inactive discounts. |
Sample response
{
"maxUsage": 1,
"usagesRemaining": 1,
"autoApply": true,
"discounts": [
{
"appId": "string",
"startDate": "2026-08-14T19:00:00Z",
"endDate": "2026-08-14T19:00:00Z",
"siteId": "string",
"membershipStartDate": "2026-08-14T19:00:00Z",
"appIds": [
"string"
],
"discountSecondLanguageText": "string",
"discountBanner": "string",
"regularDiscountDetails": {
"type": "string",
"value": 10.5
},
"introductoryDiscountDetails": {
"type": "string",
"value": 10.5
},
"oneOffDiscountDetails": {
"type": "string",
"value": 10.5
},
"discountUntilUnit": 1,
"discountToNearestValue": 10.5,
"reason": "string",
"isPerpetual": true,
"overrideWebPromotion": true,
"active": true,
"discountId": 12345,
"membershipTypeId": "string",
"membershipTypeDescription": "string",
"discountText": "string",
"applicable": true
}
],
"promoId": 12345,
"code": "string",
"validFrom": "2026-08-14T19:00:00Z",
"validTo": "2026-08-14T19:00:00Z",
"active": true
}Errors
Every error body is a bare string, not a JSON error object.
| Status | Meaning | When |
|---|---|---|
400 | Bad request | The path segment is neither a number nor wrapped in curly braces. The message names the fix: {value} is not a valid ID. If trying to search by code please put code in curly brackets e.g. {PROMOCODE}. Sending a bare code such as SUMMER25 lands here, not on a 404. |
404 | Not found | The lookup ran but matched nothing: Unable to locate promo code with ID of {value} for a numeric lookup, or Unable to locate promo code with code of {value} for a brace-wrapped one. The wording tells you which lookup was performed, so use it to confirm your braces survived the client and any gateway. |
500 | Internal server error | An unexpected server-side failure. The response body is empty. The known cause other than a platform fault is a mapped discount whose application list has never been set: one such discount fails the whole request even though the promo code and its other discounts are fine. |