# Get promotion by ID

`GET /api/configuration/promotions/{id}`

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

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

## Overview

Returns one promotion together with the promo codes attached to it and the discounts behind those
codes. A promotion is the campaign — a name, a validity window and an on/off flag; the promo codes
are the strings a customer actually enters; the discounts are what those codes are worth. This is
the only endpoint that gives you all three in one call. [Get all
promotions](get-all-promotions.md) lists campaigns with their code strings and
stops there.

Get the `id` from that listing. It is an integer allocated per tenant, so it means nothing
in another tenant or environment — do not hardcode it.

Three things to know before you build against the response:

- **`promotionType` is always `null` here.** This endpoint never populates it, even
  though every promotion has a type. If you need it, read it from [Get all
  promotions](get-all-promotions.md).
- **A promotion that does not exist returns `404`, not `400`,** with a plain-text body. A `400` from
  this endpoint means something else entirely — see the Errors table.
- Unlike the listing, whose `status` can be up to six hours old, this endpoint computes `status` at
  the moment you call.

The discounts are returned as one flat array for the whole promotion, not nested under each code.
Join them yourself using `promoCodes[].discountIds`.

## Endpoint

`GET {base_url}/api/configuration/promotions/{id}`

`{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 |
|------|------|----------|-------------|
| `id` | integer (int32) | Yes | The promotion's numeric identifier, as returned in the `id` field of [Get all promotions](get-all-promotions.md). Must be a whole number that fits in a 32-bit signed integer; anything else is rejected with `400` before the promotion is looked up. A well-formed but unknown id gives `404`. |

## Sample request

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

## Response fields

| Field | Type | Description |
|-------|------|-------------|
| `promoCodes` | array of object | Every promo code mapped to this promotion, with the ids of the discounts attached to each. Always an array and never `null`; a promotion with no codes yet returns `[]`, and in that case `discounts` is empty too. Each code carries its own validity window and active flag which are **independent of the promotion's** — a code can be switched off inside a campaign whose `status` is `Active`. This response does not include the code's own dates; fetch them from [Get promo code by ID or code](get-promo-code-by-id-or-code.md) if you need them. |
| `promoCodes[].id` | integer (int32) | The promo code's own numeric identifier, distinct from the promotion's `id`. This is what you pass to [Get promo code by ID or code](get-promo-code-by-id-or-code.md) for the full record. |
| `promoCodes[].code` | string | The string a customer types at checkout, at most 25 characters. Be aware that when you look a code up by its text rather than its id, the platform requires the text wrapped in curly braces in the path — `/api/configuration/promotion-codes/{SUMMER25}` — because an unwrapped segment is parsed as a numeric id. |
| `promoCodes[].active` | boolean | Whether this specific code may currently be redeemed, independent of the promotion's `status`. Defaults to `false`, but a code whose flag has somehow been left unset makes the whole request fail with `500` rather than returning `false`. |
| `promoCodes[].discountIds` | array of integer | The ids of the discounts this code applies. Empty when the code has been created but nothing has been attached to it yet — a valid state. Use these to look up entries in the top-level `discounts` array; the two are joined by id and by nothing else. |
| `discounts` | array of object | Every discount referenced by any of the codes above, de-duplicated and flattened into a single array for the whole promotion. It is **not** grouped by code, so a discount shared by two codes appears once. Empty when no code has any discounts. It can also be shorter than the union of `discountIds`: an id whose discount no longer exists is silently dropped here rather than reported, so match on id and treat a missing entry as "no longer configured". |
| `discounts[].id` | integer (int32) | The discount's identifier, matching the values in `promoCodes[].discountIds`. |
| `discounts[].discountText` | string | The operator's description of the discount, maximum 255 characters, intended for display alongside the price. `null` when the operator left it blank. |
| `discounts[].discountBanner` | string | A second, separate piece of promotional text for the discount, maximum 255 characters and also nullable. Held independently of `discountText`; the platform does not require either to be set. |
| `discounts[].regularAmount` | number (double) | The discount applied to the recurring subscription price. Held to two decimal places and never null — `0` means "no regular discount", which is the value used when none is configured rather than a hundred-per-cent reduction. **The unit is not in this response.** The discount type is a separate setting that this endpoint does not return; for discounts written through [Create a discount](create-a-discount.md) the type is set to percentage whenever the amount is above zero, so a non-zero value there is a percentage rather than a cash sum. |
| `discounts[].introAmount` | number (double) | The discount applied to the introductory period of a membership, where the membership type has one. Same precision, same defaulting to `0` and the same missing-unit caveat as `regularAmount`. It is independent of `regularAmount`; a discount can set one, both or neither. |
| `discounts[].oneOffAmount` | number (double) | The discount applied to the one-off joining charge. Same precision and caveats again. |
| `discounts[].membershipTypeId` | string | The membership type the discount applies to, maximum 15 characters. A discount is tied to exactly one membership type — creating a discount for several types produces several discounts with distinct ids, which is why one promo code frequently carries a list of near-identical discounts differing only in this field. |
| `discounts[].membershipTypeDescription` | string | The membership type's own description, included so you do not have to resolve `membershipTypeId` separately for display. |
| `id` | integer (int32) | The promotion's numeric identifier, echoing the value you requested. Tenant-local. |
| `promotionName` | string | The operator's name for the campaign. Mandatory and at most 50 characters. Not unique — nothing prevents two promotions sharing a name, so identify campaigns by `id`. |
| `validFrom` | string (date-time) | The date the campaign is intended to open. `null` when none was recorded. Unlike [Get all promotions](get-all-promotions.md), this endpoint returns the value as held rather than truncating it to midnight — values written through this API are already midnight, since writes drop the time, but anything written by other Gladstone tooling may carry one. **Nothing on this endpoint reads it:** `status` ignores `validFrom` entirely, so a campaign that has not started yet can still report `Active`. Compare it against the current date yourself. |
| `validTo` | string (date-time) | The date the campaign is intended to close, returned as held. `null` means no end date, which `status` treats as never expiring. This is the only date `status` considers. Writes guarantee `validFrom` is no later than `validTo`, so you will not see them inverted. |
| `status` | string (enum) | One of: `Active`, `Inactive`, `Expired`. Derived by the platform — the active flag itself is not returned, so this is your only view of it. `Active` means the flag is set and `validTo` is either absent or strictly later than today's UTC date. `Expired` means the flag is set but `validTo` is strictly earlier than today. `Inactive` is everything else, and it covers more than its name suggests: a campaign the operator switched off, an expired campaign that was *also* switched off (you never see `Expired` for those), and — because both comparisons are strict — an active campaign whose `validTo` falls on today. **A campaign on its final day reports `Inactive`.** Computed fresh on each call here, unlike the listing. Do not treat it as an authority on whether a code can be redeemed; derive that from the dates and the per-code `active` flag. |
| `promotionType` | string | **Always `null` on this endpoint.** The value exists — every promotion has one, `Membership` or `Booking`, defaulting to `Membership` — but this endpoint does not return it. Read it from [Get all promotions](get-all-promotions.md) instead, where it is populated and can be filtered on. |

## Sample response

```json title="200 OK"
{
  "promoCodes": [
    {
      "id": 12345,
      "code": "string",
      "active": true,
      "discountIds": [
        12345
      ]
    }
  ],
  "discounts": [
    {
      "id": 12345,
      "discountText": "string",
      "discountBanner": "string",
      "regularAmount": 10.5,
      "introAmount": 10.5,
      "oneOffAmount": 10.5,
      "membershipTypeId": "string",
      "membershipTypeDescription": "string"
    }
  ],
  "id": 12345,
  "promotionName": "string",
  "validFrom": "2026-08-14T19:00:00Z",
  "validTo": "2026-08-14T19:00:00Z",
  "status": "Active",
  "promotionType": "string"
}
```

## Errors

| Status | Meaning | When |
|--------|---------|------|
| `400` | Bad request | The `id` segment is not a whole 32-bit integer — a code string, a decimal or a number beyond `2147483647`. It is rejected before the lookup runs, so the body is a validation problem document (`application/problem+json`) with an `errors` map keyed by field name, rather than the plain-text message the `404` carries. This is **not** the response for an unknown promotion. |
| `403` | Forbidden | The caller is not entitled to read promotions. This applies to a member-scoped token and to an unauthenticated request alike — an anonymous call is refused with `403`, not `401`. |
| `404` | Not found | No promotion exists with that id. The body is the plain-text message `The promotion with the ID {id} does not exist`. |
| `500` | Internal server error | An unexpected server-side failure while reading the promotion, its codes or its discounts. The body is empty and carries no detail. |
