# Create promo code

`POST /api/configuration/promotion-codes`

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

HTML version: [Create promo code](https://support.gladstonesoftware.cloud/reference/configuration/create-promo-code)

## Overview

Creates a promo code — the short string a customer types at checkout to pull in one or more
[discounts](create-a-discount.md) you have already configured. The promo code itself
carries no money off; it is a validity window plus a list of discount ids, and the discounts decide what is
actually reduced. You therefore have to create the discounts first.

The identifier is chosen by the platform, not by you: it is an automatically allocated integer, and
that integer is the **entire body** of the `201 Created` response — a bare JSON number such as `12345`, not
an object. Keep it, because it is what
[Update promo code](update-promo-code.md) takes in its path. The `Location` header
points back at this same collection URL rather than at the new promo code, so it is of no use to you.
If you lose the id and only have the code string, [Get promo code by ID or code](get-promo-code-by-id-or-code.md) will find it — but that
endpoint treats a bare path segment as a numeric id, so the code has to be wrapped in curly braces:
`/api/configuration/promotion-codes/{SUMMER24}`.

Retrying is safe in the ordinary case: `code` must be unique, compared case-insensitively, and a repeat
returns `409 Conflict` with a message that includes the id of the existing promo code — so you can recover
the id from the error. Be aware, though, that uniqueness is not guaranteed under concurrency: two identical requests arriving at
the same moment can both succeed and leave you
with two promo codes sharing a code. Nothing else on the request is checked for uniqueness.

The remaining validation is about time and is stricter than it first looks. `validFrom` must be today or
later and `validTo` must be strictly later than today, both compared as dates in UTC with the time of day
discarded, so a code cannot be created that starts and finishes on the same day, and no code can be
back-dated.

## Endpoint

`POST {base_url}/api/configuration/promotion-codes`

`{base_url}` is the environment URL issued to your integration. See [Authenticating with Gladstone APIs](../../guides/authenticating-with-gladstone-apis.md).

## Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `code` | string | Yes | The string customers actually type, up to 25 characters. It must contain no whitespace anywhere — not even in the middle — and must not match an existing promo code, compared case-insensitively, so `SUMMER24` clashes with `summer24`. It is stored exactly as you send it, but every lookup ignores case. |
| `validFrom` | string (date-time) | Yes | The first day the code can be redeemed. Only the date part is kept; any time of day you send is discarded, so the code is live from the very start of that day. It must be today or later in UTC — back-dating is rejected — and must not be after `validTo`. |
| `validTo` | string (date-time) | Yes | The expiry day. Only the date part is kept. It is exclusive where `validFrom` is inclusive: redemption fails as soon as the current UTC date reaches `validTo`, so set it to the day **after** the last day you want the code to work. It must be strictly later than today, which makes tomorrow the earliest value this endpoint accepts. |
| `maxUsages` | integer (int32) | Yes | How many times the code may be redeemed in total. Must be 0 or greater. `0` means unlimited, and the retrieval endpoints report `maxUsage` as `0` for an unlimited code. The separate remaining-usages count starts at the number you send, so an unlimited code starts life reporting `usagesRemaining` of `0`. |
| `active` | boolean | Yes | Whether the code is currently enabled. This is enforced at redemption: membership pricing rejects an inactive code with `Promotion code {code} is not currently active`, regardless of its dates. It also controls visibility on [Get all promo codes](get-all-promo-codes.md), which omits inactive codes unless you pass `includeInactive=true`. Send `false` to stage a campaign in advance and switch it on later with [Update promo code](update-promo-code.md). |
| `autoApply` | boolean | Yes | Stored against the promo code and returned by the retrieval endpoints. The field is mandatory — omitting it fails request validation — so send `false` unless you have been told otherwise. |
| `discountIds` | array of integer | Yes | The discounts this code switches on, by id. At least one is required, and every id must already exist or the whole request fails with `400` listing the ids it could not find. The ids come from the response of [Create a discount](create-a-discount.md) — which returns an **array** of ids, one per membership type, when the discount covers several — or from [Get all discounts](retrieves-all-active-discounts-with-their-configuration-details.md). The discounts are what determine the actual reduction and which membership types it applies to; a code whose discounts match none of the memberships being bought is rejected at pricing time, not here. |

## Sample request

```bash title="cURL"
curl -X POST '{base_url}/api/configuration/promotion-codes' \
  -H 'Authorization: Bearer {accessToken}' \
  -H 'X-Use-SSO: true' \
  -H 'Content-Type: application/json' \
  -d '{
  "code": "string",
  "validFrom": "2026-08-14T19:00:00Z",
  "validTo": "2026-08-14T19:00:00Z",
  "maxUsages": 1,
  "active": true,
  "autoApply": true,
  "discountIds": [
    12345
  ]
}'
```

## Sample response

```json title="201 Created"
1
```

## Errors

Every failure below returns the message as a plain string body, except request validation, which returns a
validation problem document (`application/problem+json`) with an `errors` map keyed by field name.

| Status | Meaning | When |
|--------|---------|------|
| `400` | Bad request | Request validation failed before any other check: `code` missing or longer than 25 characters, `validFrom` or `validTo` missing, `maxUsages` missing or negative, `active` or `autoApply` missing, or `discountIds` missing or empty. The messages are `The field 'Code' is a mandatory field`, `The field 'Code' can not exceed 25 characters`, `ValidFrom date must be provided`, `ValidTo date must be provided`, `MaxUsages must be supplied`, `Max Usages must be 0 or greater`, `Active must be supplied`, `AutoApply must be supplied` and `At least one discount ID must be supplied`. |
| `400` | Bad request | Malformed JSON, or a value of the wrong type for its field. |
| `400` | Bad request | `code` contains a space, tab or other whitespace character. Body: `Promo-code code can not have any blank spaces.`. |
| `400` | Bad request | `validTo` is today or earlier in UTC. Body: `The Valid To date must be a future date`. |
| `400` | Bad request | `validFrom` is before today in UTC. Body: `The Valid From date must be from today onwards`. |
| `400` | Bad request | `validTo` is earlier than `validFrom`. Body: `The Valid To date must be a future date and after the Valid From date`. |
| `400` | Bad request | One or more entries in `discountIds` do not exist. Body: `The following discount IDs do not exist and can not be mapped to a promo-code: {ids}`, listing exactly which ids were not found. Nothing is created. |
| `409` | Conflict | A promo code with the same `code` already exists, ignoring case. Body: `A promo-code with the code '{code}' already exists with an ID of {id}` — the existing id is in the message, so you can carry on without a second call. |
| `500` | Internal server error | Any unexpected server-side failure. The promo code and its discount links are saved in two separate steps, so a failure between them can leave a promo code with no discounts attached; check with [Get promo code by ID or code](get-promo-code-by-id-or-code.md) before retrying. |
