Create promotion

POST /api/configuration/promotions

Success: 201 Created

Overview

Builds a complete promotional offer in one call. The endpoint performs three steps in sequence: it creates one discount for every membership type you list, creates one promo code for every code string you supply, and finally creates the promotion record that ties them together. A promotion is the container an operator manages; the discount holds the money, and the promo codes are what a member types in to claim it.

You do not choose the identifier. On success you get 201 Created and a body containing the integer id the platform allocated for the promotion, plus the name you sent. Use that id with Retrieve promotion by id, Update promotion and Delete promotion.

The call is not idempotent and not all-or-nothing. There is no idempotency key, and promotion names are not checked for uniqueness, so two identical requests that carry no promoCodes produce two separate promotions with different ids. Promo code strings, by contrast, are unique across the whole account: repeat a request that carried codes and it fails at the second step with 400 and a message ending A promo-code with the code '{code}' already exists with an ID of {id}. Because the three steps are not all-or-nothing and nothing is rolled back, that failed retry still leaves a fresh set of discounts behind. If a call fails part-way, clean up before retrying rather than sending the same body again.

Everything the request references must already exist. discount.membershipTypeIds are validated against the membership types on the account, and discount.siteId, if you send one, against the sites.

Endpoint

POST {base_url}/api/configuration/promotions

{base_url} is the environment URL issued to your integration. See Authenticating with Gladstone APIs.

Request body

The body has two required top-level objects: promotion and discount. The table below describes the members of promotion; the members of discount follow underneath it. Send the wrapped form shown in the sample request, not the promotion fields unwrapped at the top level.

FieldTypeRequiredDescription
promotionNamestringYesThe promotion's display name, and how it is identified in Retrieve all promotions. Maximum 50 characters — longer values are rejected with The field 'PromotionName' can not exceed 50 characters. Not checked for uniqueness, so nothing stops two promotions sharing a name. Echoed back as name in the response.
validFromstring (date-time)NoThe date the promotion starts. Only the date part is kept; the time is discarded. Must be on or before validTo, otherwise the call fails with The validFrom date must be before the validTo date. Omitting the field is accepted and stores 0001-01-01; sending an explicit null is rejected as malformed JSON before any validation runs. When you supply promoCodes this date is copied on to each new promo code, and promo codes additionally require it to be today or later — so a back-dated validFrom works for a promotion with no codes and fails for one with codes.
validTostring (date-time)YesThe date the promotion stops being valid, and again only the date part is kept. Must be today (UTC) or later, or the call fails with The validTo date must be from today onwards. There is no way to leave it out: omit it and it defaults to 0001-01-01, which fails that check; send null and it is rejected as malformed JSON. When you supply promoCodes this date is copied on to each new promo code, which require it to be strictly after today — so a validTo of today succeeds for a promotion with no codes and fails with The Valid To date must be a future date for one with codes.
isActivebooleanNoWhether the offer is live. This one flag governs all three records: it overrides whatever you put in discount.isActive and applies it to every promo code it creates, so setting discount.isActive yourself has no effect. Defaults to false when omitted, which creates the promotion, discount and codes in a dormant state ready to be switched on later with Update promotion. An explicit null is rejected as malformed JSON.
promoCodesarray of stringNoThe codes a member enters to claim the offer. Each string is created as a new promo code linked to the discounts this call has just created — do not send codes that already exist, or the call fails. Codes may not contain whitespace (Promo-code code can not have any blank spaces.) and are unique across the account. Each code is created with unlimited usages, auto-apply off, and the promotion's own validFrom, validTo and isActive; if you need different settings per code, create them with Create promo code instead. Omit the field or send [] to create a promotion with no codes, which is valid — an offer applied by the operator rather than typed in by a member.
promotionTypestringNoWhat the promotion applies to: Membership or Booking, case-sensitive. Defaults to Membership when omitted or null; any other value fails with Invalid promotionType. Must be either Membership or Booking. It is the filter used by GET /api/configuration/promotions?promotionType=….

discount is required — omit the object entirely and the call fails with Discount is required. Its members are:

Sample request

cURL
curl -X POST '{base_url}/api/configuration/promotions' \
  -H 'Authorization: Bearer {accessToken}' \
  -H 'X-Use-SSO: true' \
  -H 'Content-Type: application/json' \
  -d '{
  "promotion": {
    "promotionName": "Summer 2026 joining offer",
    "validFrom": "2026-06-01T00:00:00Z",
    "validTo": "2026-08-31T00:00:00Z",
    "isActive": true,
    "promoCodes": [
      "SUMMER26"
    ],
    "promotionType": "Membership"
  },
  "discount": {
    "membershipTypeIds": [
      "GYMPLUS"
    ],
    "discountRegularAmount": 10,
    "discountIntroductoryAmount": 0,
    "discountOneOffAmount": 100,
    "discountUntilUnit": 3,
    "discountBanner": "Summer offer",
    "discountText": "10% off for your first three months and no joining fee",
    "isActive": true,
    "siteId": null
  }
}'

Sample response

201 Created
{
  "id": 4127,
  "name": "Summer 2026 joining offer"
}

Errors

Every failure below is returned as a 400, with the underlying message wrapped in the text Error creating promotion code: {message}, whichever of the three steps failed. Read the trailing message, not the prefix — it says "promotion code" even when the discount or the promotion itself was the problem.

StatusMeaningWhen
400Missing top-level objectDiscount is required or Promotion is required. These come back as a validation problem document (application/problem+json) with an errors map keyed by field name, before any other check.
400Promotion name missing or too longPromotionName is required, or The field 'PromotionName' can not exceed 50 characters. Also returned as a validation problem document.
400Discount amounts missingOne of Discount could not be created as the DiscountRegularAmount is not greater than or equal to 0, …DiscountIntroductoryAmount…, …DiscountOneOffAmount… or …DiscountUntilUnit… — each fires when that field is absent or null. Send 0 for "no discount".
400Discount amount out of rangeDiscount could not be created as the DiscountRegularAmount cannot be greater than 100 (and the equivalents for the introductory and one-off amounts). The amounts are percentages.
400No discount amount setDiscount could not be created as no discount amounts have been provided — all three amounts were 0.
400discountUntilUnit set without a regular discountDiscount could not be created as DiscountUntilUnit should only be greater than 0 when DiscountRegularAmount is greater than 0.
400Membership types missing, duplicated or unknownDiscount could not be created as no membership types have been provided, …as duplicate MembershipTypeIds have been provided, or …as at least one of the membership types does not exist.
400Unknown siteIdDiscount could not be created as siteId does not exist. Only fires for a non-blank id that no site matches; a blank or omitted siteId means "all sites" and passes.
400Promo code already existsA promo-code with the code '{code}' already exists with an ID of {id}. Codes are unique across the account and this endpoint always creates them, never reuses them.
400Promo code contains whitespacePromo-code code can not have any blank spaces.
400Dates rejected by the promo code stepThe Valid To date must be a future date (a validTo of today), The Valid From date must be from today onwards (a back-dated validFrom), or The Valid To date must be a future date and after the Valid From date. These only occur when promoCodes is non-empty, because the codes inherit the promotion's dates and are validated more strictly than the promotion.
400Promotion dates invalidThe validTo date must be from today onwards, or The validFrom date must be before the validTo date.
400Unrecognised promotionTypeInvalid promotionType. Must be either Membership or Booking.
400Malformed JSONSending null for validFrom, validTo or isActive is rejected as malformed JSON, for example Error converting value {null} to type 'System.DateTime'. Path 'Promotion.validFrom'. Omit the field instead of nulling it.
403ForbiddenThe access token does not satisfy this endpoint's requirements — the FOH product and the FOHGENERIC feature — or its scope is neither api nor partner.
500Internal server errorAn unexpected server-side failure in any of the three steps. The body is empty. Because the steps are sequential, a 500 may still have created the discounts.

This page as markdown · All pages · llms.txt