# 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`)

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

## 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 as `SUMMER25`, 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 `%7B` and `%7D` if 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](../../guides/authenticating-with-gladstone-apis.md).

## 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

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

```bash title="cURL — by code rather than ID"
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](get-site-by-id.md). |
| `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](get-membership-type-by-id.md). |
| `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

```json title="200 OK"
{
  "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. |
