# Calculate membership pricing

`POST /api/pricing/memberships`

- **Success:** `200 OK`
- **Products:** Operator (`FOH`), Consumer (`CONSUMER`) (any one of these products, not all)
- **Features:** Online Bookings (`ONLINEBOOK`), Online Joining (`ONLINEJOIN`), Front of House Generic Access (`FOHGENERIC`) (any one of these features, not all)

HTML version: [Calculate membership pricing](https://support.gladstonesoftware.cloud/reference/joining/calculate-membership-pricing)

## Overview

Returns a signed pricing quote for one or more memberships that people want to join: what each membership costs today, its recurring payments, its start and end dates and its cooling-off period. Use it when someone has chosen a [membership type](../../guides/glossary.md) and you need the price to show them and the quote you must send back when you create the membership.

It is step 5 of the [Joining](../../guides/joining.md) flow. Take membership type ids from [Get membership types grouped by tag](get-membership-types-grouped-by-tag.md), price them here, then pass the whole response, unaltered, as `pricingQuote` to [Create members with memberships](create-members-with-memberships.md) (new members) or [Create a membership for a member](create-a-membership-for-a-member.md) (existing members). Those pages explain how long a quote stays valid.

**The response is signed.** `timeStamp` and `signature` cover the whole object, so store the response exactly as you received it and send it back verbatim. Change, round, reformat or drop any field, including ones you do not use, and the quote is rejected when you create the membership.

One request can price several people at once: each entry in `members` is one person, identified by an [order key](../../guides/glossary.md) you choose, with the memberships they want. Every membership line in the response carries that order key back.

## Endpoint

`POST {base_url}/api/pricing/memberships`

`{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 |
|-------|------|----------|-------------|
| `siteId` | string | Yes | The [site](../../guides/glossary.md) the memberships are being sold at. It must exist (`404 Site ID {siteId} does not exist`), and every requested membership type must be available there, or the request fails with `400` listing the ones that are not. It also sets the currency of every amount in the quote, and is echoed in the response as `siteId`. |
| `startDate` | string (date-time) | Yes | The date the memberships start, for example `2026-11-01T00:00:00Z`. Only the date part is used. It cannot be before today's date in UTC (`Start Date cannot be in the past`), before a membership type's earliest start date, after a membership type's fixed end date, or, for membership types with no future earliest start date, before the site's opening date. See **Errors** for each case. [Get earliest start date for membership](get-earliest-start-date-for-membership.md) returns a date that passes these checks. |
| `members` | array of object | Yes | One entry per person being priced. Each entry is priced separately and adds its own lines to `memberships` in the response. |
| `members[].memberId` | integer (int32) | No | The existing member this person is, when they already have a member record. A member's own [price level](../../guides/glossary.md) can then decide their price (see **Notes**). Omit it, or send `0`, for someone who is not yet a member, which is the usual case when joining. Must be `0` or greater, or the request fails validation. A positive value must be a member that exists, or the request fails with `404 At least one member in the request does not exist`. |
| `members[].orderKey` | integer (int16) | Yes | A number you choose to identify this person within the request, from `1` up. It must be unique across `members` (`The order key for each member request must be unique`); `0` or below fails with `OrderKeys must be greater than 0`. It is returned on each of this person's lines as `memberships[].orderKey`, and is how you later say which member receives which quote line. |
| `members[].memberships` | array of object | Yes | The memberships this person is buying. Must contain at least one entry, and every entry must have an `id`, or the request fails with `At least one member request contains null or has no memberships`. When one person buys two or more memberships, a single price level is chosen for all of them. |
| `members[].memberships[].id` | string | Yes | The membership type id, for example `ASUBDD0001`, from `membershipTypes[].id` on [Get membership types grouped by tag](get-membership-types-grouped-by-tag.md). Case-insensitive: it is upper-cased before use. A membership type that does not exist returns `404 At least one membership type in the request does not exist`. |
| `members[].memberships[].promotionCode` | string | No | A promotion code to discount the price. All or nothing: if any membership in the request carries a code, every membership in the request, for every person, must carry the same code, or the request fails with `400` (see **Errors**). Leading and trailing spaces are ignored. Omit it on every membership to price without a promotion. A membership type the code holds no discount for is priced without one. |

## Sample request

```bash title="cURL"
curl -X POST '{base_url}/api/pricing/memberships' \
  -H 'Authorization: Bearer ***' \
  -H 'X-Use-SSO: true' \
  -H 'Content-Type: application/json' \
  -d '{
  "siteId": "WIN",
  "startDate": "2026-11-01T00:00:00Z",
  "members": [
    {
      "memberId": 0,
      "orderKey": 1,
      "memberships": [
        {
          "id": "ASUBDD0001"
        }
      ]
    }
  ]
}'
```

## Response fields

Every amount is a string with exactly two decimal places, for example `"35.00"`: parse it as a decimal, not a float. Every `currency` is the currency configured for the requested site.

| Field | Type | Description |
|-------|------|-------------|
| `siteId` | string | The `siteId` you sent. |
| `promotionCode` | string | The promotion code the quote was priced under, as the tenant has set it up, without any leading or trailing spaces you sent. Null when no code was sent. |
| `overview` | object | Totals across every membership in the quote, for a basket summary. |
| `overview.now` | object | The total payable today across all memberships: the sum of every `memberships[].pricing.now.totalPrice.amount`. |
| `overview.now.amount` | string | The amount. |
| `overview.now.currency` | string | The currency code, for example `GBP`. |
| `overview.now.discount` | object | Always null on the overview. Discounts are reported on each membership's lines. |
| `overview.recurring` | object | The combined recurring payments of every membership that has them. Null when no membership in the quote has a recurring payment schedule, for example when every membership is paid for in full up front. |
| `overview.recurring.headlinePrice` | object | The combined regular recurring amount: the sum of each recurring membership's `pricing.recurring.headlinePrice`. Before any promotion discount; `discount` is null. |
| `overview.recurring.headlinePrice.amount` | string | The amount. |
| `overview.recurring.headlinePrice.currency` | string | The currency code. |
| `overview.recurring.headlinePrice.discount` | object | Always null here. |
| `overview.recurring.frequency` | string | How often recurring payments are taken, taken from the first recurring membership in the quote, for example `Monthly`. |
| `overview.recurring.schedule` | array of object | The first four recurring payment dates across all memberships, in date order. Payments on the same date are combined. |
| `overview.recurring.schedule[].date` | string (date-time) | The payment date. |
| `overview.recurring.schedule[].totalPrice` | object | The total due on that date, as `amount`, `currency` and `discount` (null). |
| `overview.recurring.schedule[].breakdown` | array of object | The individual payments due on that date. Same shape as `memberships[].pricing.recurring.schedule[].payments[]`. |
| `memberships` | array of object | One entry for each membership requested, for each person, in request order. |
| `memberships[].orderKey` | integer (int16) | The `orderKey` of the person this line was priced for. A person buying two memberships has two lines with the same key. |
| `memberships[].id` | string | The membership type id, upper-cased. |
| `memberships[].description` | string | The membership type's name, for display. |
| `memberships[].coolingOffPeriod` | integer (int32) | The cooling-off period, during which a new membership can be cancelled. It is one setting for the whole tenant, so every line in a quote carries the same value. Null when the tenant has not configured one. See the [DMCCA guide](../../guides/dmcca-required-api-updates.md) for how to present it. |
| `memberships[].translations` | array of object | Translations of the membership type's name, when the tenant has configured any. Each entry has `type`, `culture` (the language the text is in) and `text`. |
| `memberships[].startDate` | string (date-time) | The membership's start date: the date part of the `startDate` you sent. |
| `memberships[].endDate` | string (date-time) | The date the membership's term ends. A membership type with a fixed end date ends then. Otherwise, for a membership paid in full up front, it is the start date plus the membership type's duration in days or months, less one day. |
| `memberships[].pricing` | object | The price of this membership. |
| `memberships[].pricing.now` | object | What is payable today for this membership. |
| `memberships[].pricing.now.totalPrice` | object | The total payable today for this membership, including any one-off joining fees and any part-month charge, after any promotion discount. `discount` is always null here; discounts are on the `breakdown` lines. |
| `memberships[].pricing.now.totalPrice.amount` | string | The amount. |
| `memberships[].pricing.now.totalPrice.currency` | string | The currency code. |
| `memberships[].pricing.now.breakdown` | array of object | One line for each product that makes up the membership, with the amount of it payable today. |
| `memberships[].pricing.now.breakdown[].priceLevelId` | string | The [price level](../../guides/glossary.md) the membership was priced at. The same on every line of the membership. |
| `memberships[].pricing.now.breakdown[].includesFirstFullPayment` | boolean | Only meaningful on a `pro-rata` line: `true` when the amount due today includes a full month's payment, for example when `startDate` falls on the membership's regular payment day. `false` on every other line. |
| `memberships[].pricing.now.breakdown[].productId` | string | The product this line charges for. |
| `memberships[].pricing.now.breakdown[].type` | string | What kind of charge this is: `one-off` (a single charge such as a joining fee), `regular` (the membership's regular price) or `pro-rata` (a pro-rata direct debit membership's charge for the period up to its regular payment day). |
| `memberships[].pricing.now.breakdown[].description` | string | The product's name, for display. |
| `memberships[].pricing.now.breakdown[].translations` | array of object | Translations of the product's name. Same shape as `memberships[].translations`. |
| `memberships[].pricing.now.breakdown[].price` | object | The amount of this product payable today. |
| `memberships[].pricing.now.breakdown[].price.amount` | string | The amount, after any promotion discount. |
| `memberships[].pricing.now.breakdown[].price.currency` | string | The currency code. |
| `memberships[].pricing.now.breakdown[].price.discount` | object | The promotion discount applied to this line. Null when none applies. |
| `memberships[].pricing.now.breakdown[].price.discount.priceBeforeDiscount` | string | What the line would have cost without the discount. |
| `memberships[].pricing.now.breakdown[].price.discount.discountValue` | string | The amount taken off. |
| `memberships[].pricing.now.breakdown[].price.discount.discountId` | integer (int32) | The discount that was applied. |
| `memberships[].pricing.recurring` | object | The membership's recurring payments. Always present, but populated only for a direct debit or recurring card payment ([RCP](../../guides/glossary.md)) membership. For any other membership, `headlinePrice` and `frequency` are null and `schedule` is empty. |
| `memberships[].pricing.recurring.membershipId` | integer (int32) | Always null in this response, because the membership does not exist yet. |
| `memberships[].pricing.recurring.headlinePrice` | object | The regular recurring amount: the sum of the membership's regular products at its start date, before any promotion discount (`discount` is null). This is the "£x a month" figure. Discounted payments appear in `schedule`. |
| `memberships[].pricing.recurring.headlinePrice.amount` | string | The amount. |
| `memberships[].pricing.recurring.headlinePrice.currency` | string | The currency code. |
| `memberships[].pricing.recurring.frequency` | string | How often recurring payments are taken, for example `Monthly`. |
| `memberships[].pricing.recurring.schedule` | array of object | The recurring payments for this membership's term, by date. |
| `memberships[].pricing.recurring.schedule[].date` | string (date-time) | The payment date. |
| `memberships[].pricing.recurring.schedule[].payments` | array of object | The payments due on that date. |
| `memberships[].pricing.recurring.schedule[].payments[].price` | object | The payment amount, after any promotion discount, as `amount`, `currency` and `discount`. `discount` has the same fields as on a `breakdown` line and is null when no discount applies to that date. |
| `memberships[].pricing.recurring.schedule[].payments[].paymentMethod` | object | The [payment method](../../guides/glossary.md) the payment is taken by: the membership type's first configured payment method. |
| `memberships[].pricing.recurring.schedule[].payments[].paymentMethod.id` | string | The payment method id. |
| `memberships[].pricing.recurring.schedule[].payments[].paymentMethod.description` | string | The payment method's name, for display. |
| `memberships[].pricing.recurring.schedule[].payments[].paymentMethod.translations` | array of object | Translations of the payment method's name. Same shape as `memberships[].translations`. |
| `timeStamp` | string | When the quote was created, in UTC, in ISO 8601 round-trip format. Quote validity is measured from it. |
| `signature` | string | A Base64 signature over the rest of the quote. You cannot generate or check it; send it back unchanged with the quote. |

## Sample response

```json title="200 OK"
{
  "siteId": "WIN",
  "promotionCode": null,
  "overview": {
    "now": {
      "amount": "45.00",
      "currency": "GBP",
      "discount": null
    },
    "recurring": {
      "headlinePrice": {
        "amount": "35.00",
        "currency": "GBP",
        "discount": null
      },
      "frequency": "Monthly",
      "schedule": [
        {
          "date": "2026-12-01T00:00:00Z",
          "totalPrice": {
            "amount": "35.00",
            "currency": "GBP",
            "discount": null
          },
          "breakdown": [
            {
              "price": {
                "amount": "35.00",
                "currency": "GBP",
                "discount": null
              },
              "paymentMethod": {
                "id": "string",
                "description": "string",
                "translations": []
              }
            }
          ]
        }
      ]
    }
  },
  "memberships": [
    {
      "orderKey": 1,
      "id": "ASUBDD0001",
      "description": "string",
      "coolingOffPeriod": null,
      "translations": [],
      "startDate": "2026-11-01T00:00:00Z",
      "endDate": "2027-10-31T00:00:00Z",
      "pricing": {
        "now": {
          "totalPrice": {
            "amount": "45.00",
            "currency": "GBP",
            "discount": null
          },
          "breakdown": [
            {
              "priceLevelId": "string",
              "includesFirstFullPayment": false,
              "productId": "string",
              "type": "one-off",
              "description": "string",
              "translations": [],
              "price": {
                "amount": "10.00",
                "currency": "GBP",
                "discount": null
              }
            },
            {
              "priceLevelId": "string",
              "includesFirstFullPayment": false,
              "productId": "string",
              "type": "pro-rata",
              "description": "string",
              "translations": [],
              "price": {
                "amount": "35.00",
                "currency": "GBP",
                "discount": null
              }
            }
          ]
        },
        "recurring": {
          "membershipId": null,
          "headlinePrice": {
            "amount": "35.00",
            "currency": "GBP",
            "discount": null
          },
          "frequency": "Monthly",
          "schedule": [
            {
              "date": "2026-12-01T00:00:00Z",
              "payments": [
                {
                  "price": {
                    "amount": "35.00",
                    "currency": "GBP",
                    "discount": null
                  },
                  "paymentMethod": {
                    "id": "string",
                    "description": "string",
                    "translations": []
                  }
                }
              ]
            }
          ]
        }
      }
    }
  ],
  "timeStamp": "2026-10-15T09:30:00.0000000Z",
  "signature": "string"
}
```

## Errors

Except where a validation problem document is described, the error body carries the message shown.

| Status | Meaning | When |
|--------|---------|------|
| `400` | Bad request | A required field is missing or a value has the wrong type or range: a validation problem document (`application/problem+json`) with an `errors` map keyed by field name. Examples: no `siteId` (`The SiteId field is required`), no `startDate`, no `orderKey`, `orderKey` of `0` (`OrderKeys must be greater than 0`), a negative `memberId`, or a non-numeric `orderKey` (`Could not convert string to integer: …`). |
| `400` | Bad request | `startDate` is before today's date in UTC: `Start Date cannot be in the past`. |
| `400` | Bad request | A person has no memberships, or a membership has no `id`: `At least one member request contains null or has no memberships`. |
| `400` | Bad request | Two people share an order key: `The order key for each member request must be unique`. |
| `400` | Bad request | `startDate` is before the site's opening date, and none of the requested membership types has an earliest start date in the future: `The requested start date is before the Sites opening date`. |
| `400` | Bad request | A requested membership type is not sold at the site: `The following memberships ({ids}) are not available at the site {siteId}`. |
| `400` | Bad request | The promotion code is on some memberships but not all: `When applying a Promotion Code, this must be applied against all memberships in the request.` Different codes on different memberships: `Only one Promotion Code can be used in the request for all memberships`. |
| `400` | Bad request | The promotion code cannot be used: `Promotion code {code} is not a valid promotion code`, `Promotion code {code} is not currently active` (inactive, or its start date is in the future), `Promotion code {code} is expired`, or `Promotion code {code} is not applicable to any of the memberships selected`. |
| `400` | Bad request | The promotion code exists but none of its discounts can be used for these memberships at this site: `There are no active and applicable discounts related to the promocode {code} that can be applied to any of the memberships`. |
| `400` | Bad request | The price level found for the person cannot be bought for at least one product of the membership types, for example `The default price level {priceLevelId} for the site {siteId} does not allow to be purchased in at least one of the products of the membership types {ids}.` The equivalent messages for a price level taken from the member or the membership type name the member or the membership type instead. When no price level could be found at all, these messages are returned with `422`. |
| `404` | Not found | `siteId` does not exist: `Site ID {siteId} does not exist`. |
| `404` | Not found | A positive `memberId` is not a member: `At least one member in the request does not exist`. |
| `404` | Not found | A membership type id does not exist: `At least one membership type in the request does not exist`. |
| `422` | Unprocessable entity | `startDate` is before a membership type's earliest start date: `The requested start date is before one or more memberships earliest start date. Membership Type(s): {ids}`. Get a valid date from [Get earliest start date for membership](get-earliest-start-date-for-membership.md). |
| `422` | Unprocessable entity | A membership type's fixed end date has passed (`The fixed end date has already passed for membership type(s): {ids}.`), or `startDate` is after it (`The requested start date must be on or before the fixed end date. Membership Type(s): {ids}`). |
| `422` | Unprocessable entity | No usable price level can be found for a membership, or its products have no price that can be bought. Messages include `The membership type {id} and it's product {productId} has no prices with purchase allowed, so has not been configured correctly to provide a price.`, `The membership type {id} configured with the price level {priceLevelId} is not allowed for purchase.`, `The membership type {id} and its products have not been configured correctly to provide a price` and `Membership ID {id} has not been configured correctly to provide a price`. |
| `422` | Unprocessable entity | The membership type's setup is inconsistent: `The duration has not been correctly configured for membership type {id}`, `The ProRata should not be set for a RCP membership. MembershipId: {id}`, `The default start day should not be set for a RCP membership. MembershipId: {id}`, `The default start day should be set for a DD membership. MembershipId: {id}` or `The cut off day must be after the fixed start day for the membership with ID {id}`. |
| `500` | Internal server error | An unexpected server-side failure. No body is returned. |

Every `422` here, and the price level `400`, is a configuration problem at the tenant: retrying will not help. Show a generic "this membership cannot be bought online" message and report the text to the operator.

## Notes

- **Which price level is used.** For each person, the price level comes from the first of these that is set: the membership type's own price level, then the member's price level (only when `memberId` is greater than `0`), then the site's default price level. When a person buys two or more memberships, one price level is chosen for all of them. Every product in the membership must have a price at that level that can be bought, or the request fails (see **Errors**).
- **Prices are those in force on `startDate`**, not today's: a price change scheduled to take effect on or before the start date is already in the quote.
- **Validation stops at the first failure**, so fixing one error can reveal the next. The checks run in this order: missing or malformed fields; `startDate` in the past; empty memberships; duplicate order keys; site; members; membership types; fixed end dates; earliest start date and site opening date; promotion code; availability at the site; then price levels and membership type setup.
- **Nothing is reserved or created.** The quote is a price only; the membership is created when you send the quote to [Create members with memberships](create-members-with-memberships.md) or [Create a membership for a member](create-a-membership-for-a-member.md).
- For what a joining journey must show the buyer from this response (total price, recurring payments, cooling-off period), see [DMCCA: required API updates](../../guides/dmcca-required-api-updates.md).
