# Update a discount

`PUT /api/configuration/discounts/{id}`

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

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

## Overview

Rewrites a membership discount: which membership type it applies to, the three percentage rates it offers, how long the regular rate runs for, the marketing text shown with it, whether it is live, and whether it is restricted to one site.

**This is a full replacement, not a patch.** Every field in the body is written, and there is no "leave this alone" — a field you omit takes its JSON default. Two of those defaults are destructive: **omitting `isActive` sets it to false and switches the discount off**, and omitting `siteId` sets it to null, which widens a site-specific discount to every site. Read the discount first, change what you mean to change, and send the whole object back.

The percentage amounts also drive a derived flag you cannot set directly. Each amount you send as zero records that discount kind as "none" rather than as a nil percentage, and the discount is marked perpetual only when `discountRegularAmount` is above zero **and** `discountUntilUnit` is zero. You cannot make a discount perpetual without a regular rate.

Successful updates return **`204 No Content` with an empty body** — no representation of the updated discount comes back, so re-read it if you need to confirm what was stored. A discount id that does not exist returns `404`, and every business-rule failure returns `400` with a plain-text sentence naming the rule.

One wrinkle in the validation messages: several are worded "Discount could not be **created**…" even though this is an update, because the request body is shared with [Create a discount](create-a-discount.md). Match on the rule, not the verb.

Changes are visible immediately, so [Retrieves all active discounts](retrieves-all-active-discounts-with-their-configuration-details.md) reflects the change on its next call.

## Endpoint

`PUT {base_url}/api/configuration/discounts/{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 id of the discount to update, as returned in the `discountId` field of [Retrieves all active discounts](retrieves-all-active-discounts-with-their-configuration-details.md). An id that does not exist returns `404` with the body `Discount with ID {id} does not exist`. Note that a single discount id can cover several discounted products, so updating one id can affect more than one discounted product. |

## Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `membershipTypeId` | string | Yes | The single membership type the discount applies to, taken from the `id` of [Get all membership types with optional filtering](get-all-membership-types-with-optional-filtering.md). Note the difference from [Create a discount](create-a-discount.md), which takes a `membershipTypeIds` **array** — on update you set exactly one, and changing it moves the discount rather than adding a second. **Stored upper-cased whatever you send**, so compare case-insensitively. Must be present and non-empty, and must exist: an unknown value fails with `400` and `Discount could not be updated as {membershipTypeId} does not exist`. |
| `discountRegularAmount` | number (double) | Yes | The percentage off the recurring subscription price, as a number between `0` and `100` inclusive — `10.5` means 10.5% off, not £10.50. Required: omit it and the request is rejected with `400`. `0` is accepted and means "no regular discount", which also records the discount kind as none rather than as a zero percentage. Outside the range, `400`. This is the only amount that can be paired with `discountUntilUnit`, and the only one that can make a discount perpetual. |
| `discountIntroductoryAmount` | number (double) | Yes | The percentage off the introductory (joining) price, between `0` and `100` inclusive. Required — omitting it is a `400`. `0` means no introductory discount and records that kind as none. Independent of the regular and one-off amounts: a discount may offer any combination, but not all three as zero. |
| `discountOneOffAmount` | number (double) | Yes | The percentage off one-off product prices, between `0` and `100` inclusive. Required — omitting it is a `400`. `0` means no one-off discount. **At least one of the three amounts must be above zero**, or the request fails with `Discount could not be updated as no discount amounts have been provided`. |
| `discountUntilUnit` | integer (int32) | Yes | How many billing units the regular discount runs for before the full price resumes. Required and must be `0` or greater; a negative value is a `400`. `0` combined with a `discountRegularAmount` above zero marks the discount **perpetual** — it never expires. `0` with no regular discount simply means the setting is unused. Only set this above zero when you are also setting a regular discount, since nothing else consumes it. |
| `discountBanner` | string | No | Short marketing text displayed alongside the discount in consumer-facing journeys. Maximum 510 characters, rejected with `400` beyond that. **An omitted, null, empty or whitespace-only value is stored as null**, clearing whatever banner the discount had — whitespace is trimmed before the check, so `" "` clears it just as an omission does. |
| `discountText` | string | No | Longer descriptive text for the discount, shown to members alongside the banner. Maximum 510 characters, rejected with `400` beyond that. Same clearing behaviour as `discountBanner`: omitted, null, empty or whitespace-only is stored as null. |
| `isActive` | boolean | No | Whether the discount is live and applied to qualifying prices. **Because a missing boolean defaults to false, omitting this field switches the discount off** — the single most common way to break a working discount with this endpoint. Always send it explicitly. Setting it false is how you retire a discount; there is no separate delete step required for that. |
| `siteId` | string | No | Restricts the discount to one site, using the site code from [Get all sites with optional filtering](get-all-sites-with-optional-filtering.md). **Null or omitted means the discount applies at every site** — so leaving it out of the body silently removes a site restriction rather than preserving it. An empty string is not the same as null: `""` is rejected with `400` and `Discount could not be updated as siteId does not exist`, and so is any code that does not match an existing site. |

## Sample request

```bash title="cURL"
curl -X PUT '{base_url}/api/configuration/discounts/{id}' \
  -H 'Authorization: Bearer {accessToken}' \
  -H 'X-Use-SSO: true' \
  -H 'Content-Type: application/json' \
  -d '{
  "membershipTypeId": "string",
  "discountRegularAmount": 10.5,
  "discountIntroductoryAmount": 10.5,
  "discountOneOffAmount": 10.5,
  "discountUntilUnit": 1,
  "discountBanner": "string",
  "discountText": "string",
  "isActive": true,
  "siteId": "string"
}'
```

## Sample response

```text title="204 No Content"
(empty body)
```

## Errors

| Status | Meaning | When |
|--------|---------|------|
| `204` | No content | Success. The body is empty; nothing about the updated discount is returned. |
| `400` | Bad request | A required field is missing — `membershipTypeId`, `discountRegularAmount`, `discountIntroductoryAmount`, `discountOneOffAmount` or `discountUntilUnit` — returned as a validation problem document (`application/problem+json`) with an `errors` map keyed by field name. The messages are worded "could not be created" because the request body is shared with the create endpoint. |
| `400` | Bad request | `membershipTypeId` is empty, or names a membership type that does not exist. Body: `Discount could not be updated as no membership type ID has been provided` or `Discount could not be updated as {membershipTypeId} does not exist`. |
| `400` | Bad request | Any of the three amounts is negative or above 100. Body names the offending field, for example `Discount could not be updated as the DiscountRegularAmount cannot be greater than 100`. |
| `400` | Bad request | All three amounts are zero. Body: `Discount could not be updated as no discount amounts have been provided`. A discount must actually discount something. |
| `400` | Bad request | `discountUntilUnit` is negative. Body: `Discount could not be updated as the DiscountUntilUnit is not greater than or equal to 0`. |
| `400` | Bad request | `siteId` is an empty string, or a site code that does not exist. Body: `Discount could not be updated as siteId does not exist`. Send null, or omit the field, if you want the discount to apply to all sites. |
| `400` | Bad request | `discountBanner` or `discountText` exceeds 510 characters. Body: `The field 'DiscountBanner' can not exceed 510 characters`. |
| `404` | Not found | No discount exists with the supplied id. Body: `Discount with ID {id} does not exist`. The id is looked up before the business rules are checked, so a `404` tells you nothing about whether your amounts, membership type or site id were valid — fix the id and expect to see those errors next. |
| `500` | Internal server error | An unexpected server-side failure, including a failure while saving. The body is empty. Validation runs before anything is written, so a `500` here means the write itself failed — re-read the discount to see whether it landed. |
