# Create a membership for a member

`POST /api/memberships`

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

HTML version: [Create a membership for a member](https://support.gladstonesoftware.cloud/reference/joining/create-a-membership-for-a-member)

## Overview

Creates memberships for one or more members from a **pricing quote**, and raises the unpaid sales
invoices that pay for them. It is the second step of the joining flow: the members must already
exist, and the quote must already have been obtained.

The order is fixed, and none of it is optional:

1. Get a pricing quote for the memberships you want to sell. The quote is returned signed.
2. Create or identify the members who will hold them.
3. Call this endpoint with the quote **unaltered** and a map from each quote line to a member id.
4. Pay the returned `salesInvoiceIds` with
   [Pay for sales invoices](../payments/pays-for-sales-invoices-using-a-specified-payment-provider-and-can-handle-paymen.md).

**The quote is signed and short-lived.** `signature` is a keyed hash over the entire quote object
with `signature` itself removed. Change any value inside `pricingQuote` — a price, a date, a
description, a discount — or drop any field, and the hash no longer matches and the call is
rejected with `The pricing quote is invalid.` You cannot adjust a price on the way through. Echo
the quote back exactly as you received it. The quote is also only valid for **one hour** from its
`timeStamp`, and its `timeStamp` may not be more than **ten minutes** in the future relative to
the server's clock.

**This call leaves work half-done by design.** A `201` means the memberships and their invoices
exist, not that anything has been paid. The invoices are created unpaid, and the membership is not
active until they are settled. If your process stops between this call and payment, you have
created a real membership with an outstanding balance against it — there is no expiry and no
rollback here, so reconcile rather than simply retrying. Retrying is not idempotent either: a
second call with the same quote creates a second set of memberships and invoices, unless a
duplicate or concurrency rule happens to block it.

The prices in the quote are what get billed. `memberships[].pricing.now.breakdown` becomes the
invoice lines, `memberships[].pricing.now.totalPrice.amount` becomes the invoice total, and the
first entry of `memberships[].pricing.recurring.schedule` sets up the first instalment.

## Endpoint

`POST {base_url}/api/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 |
|-------|------|----------|-------------|
| `isJuniorMembershipWithGuardian` | boolean | No | Set `true` when a guardian is buying a membership on behalf of a junior member, which relaxes one check: the order keys in `orderKeyToMemberIdMap` are then no longer required to appear in `pricingQuote.memberships`. Defaults to `false` when omitted, in which case every order key you map must exist in the quote. It also changes the wording of two error messages (`junior member` rather than `secondary member`), but does not otherwise loosen the age or eligibility rules — a junior still has to fall inside the price level's age range. |
| `primaryPaysInd` | boolean | No | For a linked member buying a direct debit membership, says whose bank details pay for it: `true` means the primary member's, `false` means the linked member's own. Defaults to `false` when omitted. It is only consulted when at least one membership in the quote is direct debit and the buyer is a linked member, and it decides which member's bank account is checked — get it wrong and the call fails with `422` and either `Primary member must have bank details as this is a linked member and Primary Pays flag is set to true` or `Member must have bank details as this is a linked member and Primary Pays flag is set to false`. |
| `pricingQuote` | object | Yes | The signed quote, returned whole by the pricing call. Treat it as opaque: it is hashed on receipt and the result compared against `signature`, so every field must survive the round trip unchanged — including ones your code does not care about. Dropping a nested object, rounding an amount or reformatting a date all produce the same `400` with `The pricing quote is invalid.` The safest implementation stores the quote as received and posts it back verbatim. |
| `pricingQuote.siteId` | string | Yes | The site the memberships are being sold at, as it appeared in the quote. It determines the site the new memberships are created against. Must not be empty (`Site ID cannot be empty`), and like everything else inside `pricingQuote` it must match the quote exactly or the signature check fails. |
| `pricingQuote.promotionCode` | string | No | The promotion code the quote was priced under, if any. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` It is used to record promotion usage against the memberships, so removing it because it looks cosmetic both invalidates the signature and would lose the promotion record. |
| `pricingQuote.overview` | object | Yes | The basket-level totals for the whole quote. Required (`Pricing quote overview cannot be empty`) even though this endpoint bills from the per-membership figures — it is part of the signed payload, so it cannot be trimmed out of the request to save bytes. |
| `pricingQuote.overview.now` | object | No | The total payable immediately across every membership in the quote, as opposed to the per-membership figure in `memberships[].pricing.now`. Useful for showing a basket total. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.now.amount` | string | No | The whole-basket amount payable now, as a decimal string with two decimal places, so parse it as a decimal rather than a float. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.now.currency` | string | No | The ISO 4217 currency code of the basket total, resolved from the site's configuration — do not assume `GBP`. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.now.discount` | object | No | The discount applied to the basket total, or `null` when the quote was priced without a promotion code. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.now.discount.priceBeforeDiscount` | string | No | What the basket would have cost without the discount, as a decimal string with two decimal places, so parse it as a decimal rather than a float. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.now.discount.discountValue` | string | No | The amount taken off the basket, as a decimal string with two decimal places, so parse it as a decimal rather than a float. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.now.discount.discountId` | integer (int32) | No | The discount record that was applied. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring` | object | No | The ongoing cost across every membership in the quote, once the initial payment is out of the way. `null` for a quote containing only one-off memberships. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.headlinePrice` | object | No | The headline recurring amount for the basket — the "£x a month" figure a shopper is agreeing to. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.headlinePrice.amount` | string | No | The recurring basket amount, as a decimal string with two decimal places, so parse it as a decimal rather than a float. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.headlinePrice.currency` | string | No | The ISO 4217 currency code of the recurring amount. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.headlinePrice.discount` | object | No | The discount applied to the recurring amount, or `null` when none applies. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.headlinePrice.discount.priceBeforeDiscount` | string | No | The recurring amount before the discount, as a decimal string with two decimal places, so parse it as a decimal rather than a float. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.headlinePrice.discount.discountValue` | string | No | The amount taken off each recurring payment, as a decimal string with two decimal places, so parse it as a decimal rather than a float. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.headlinePrice.discount.discountId` | integer (int32) | No | The discount record applied to the recurring amount. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.frequency` | string | No | How often the recurring payment is taken. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.schedule` | array of object | No | The forecast payment dates and amounts for the basket as a whole. Presentational here — the schedule that actually drives billing is the per-membership one in `memberships[].pricing.recurring.schedule`. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.schedule[].date` | string (date-time) | No | The date this scheduled basket payment falls due. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.schedule[].totalPrice` | object | No | The total due across the basket on this date. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.schedule[].totalPrice.amount` | string | No | The amount due on this date, as a decimal string with two decimal places, so parse it as a decimal rather than a float. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.schedule[].totalPrice.currency` | string | No | The ISO 4217 currency code for this scheduled payment. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.schedule[].totalPrice.discount` | object | No | The discount applied to this scheduled payment, or `null` when none applies. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.schedule[].breakdown` | array of object | No | How this scheduled basket payment splits across payment methods — one entry per method contributing to the date. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.schedule[].breakdown[].price` | object | No | The portion of this scheduled payment taken by the accompanying payment method, with the same `amount`, `currency` and `discount` shape used elsewhere in the quote. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.overview.recurring.schedule[].breakdown[].paymentMethod` | object | No | The payment method that portion is collected by, carrying its `id`, `description` and `translations`. The `id` matches a payment method from [List all payment methods](../payments/list-all-payment-methods.md). Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships` | array of object | Yes | One entry per membership being sold. Required and non-empty (`Memberships cannot be empty`). The entries are matched to members by `orderKey`, and the same membership type may appear more than once — subject to the duplicate and concurrency rules, which reject a second copy of a membership type that does not allow duplicates, or an overlapping membership of the same priority where the type prevents concurrency. |
| `pricingQuote.memberships[].orderKey` | integer (int32) | No | The line number of this membership within the quote, and the join key to `orderKeyToMemberIdMap` — it is how the platform decides which member receives which membership. Unless `isJuniorMembershipWithGuardian` is `true`, every `orderKey` you send in the map must appear here, or the call fails with `At least one order key from the mapping object does not relate to any order key in the given pricing quote.` Several quote lines may share an order key, in which case that member receives all of them. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].id` | string | No | The membership type being bought, as returned in `membershipTypes[].id` by [Get membership types grouped by tag](get-membership-types-grouped-by-tag.md). Every distinct id here must still exist when this call runs, or you get `At least one membership was not found in database.` Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].description` | string | No | The membership type's display name as it stood when the quote was priced. Carried for presentation. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].coolingOffPeriod` | integer (int32) | No | The cancellation cooling-off period configured against this membership type, carried through from the quote. `null` when the type has none. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].translations` | array of object | No | Localised versions of `description`, one per configured culture. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].translations[].type` | string | No | The record type the translation belongs to. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].translations[].culture` | string | No | The culture code the text is for, such as `en-GB`. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].translations[].text` | string | No | The translated text. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].startDate` | string (date-time) | No | When this membership begins. It is used for the overlap checks against the member's existing memberships, so it is not merely decorative — but it comes from the quote and cannot be changed here. To move the start date, get a new quote. Use [Get earliest start date for membership](get-earliest-start-date-for-membership.md) before quoting so the date is valid. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].endDate` | string (date-time) | No | When this membership ends. Also used for the overlap checks; for an auto-renewing membership type the platform treats the effective end as a hundred years later when testing for overlap, so an auto-renewing membership collides with almost anything of the same priority. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing` | object | No | What this individual membership costs, split into what is due now and what recurs. This is the part of the quote that becomes real money: its contents are turned directly into invoice lines and instalments. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.now` | object | No | What is payable immediately for this membership — joining fee plus any up-front portion of the subscription. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.now.totalPrice` | object | No | The total due now for this membership. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.now.totalPrice.amount` | string | No | The amount due now, as a decimal string with two decimal places, so parse it as a decimal rather than a float. This value becomes the total of the sales invoice raised for this membership, so it is the figure the member will be charged. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.now.totalPrice.currency` | string | No | The ISO 4217 currency code for the amount due now. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.now.totalPrice.discount` | object | No | The discount applied to the amount due now, or `null` when none applies. Its `discountId` is one of the sources used to record which discount the resulting membership was sold under. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.now.breakdown` | array of object | No | The individual products making up the amount due now — typically the subscription product plus any one-off joining products. **Each entry becomes a line on the sales invoice**, so this is the most load-bearing array in the quote. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.now.breakdown[].priceLevelId` | string | No | The price level this line was priced at — the pricing band the member qualified for. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.now.breakdown[].productId` | string | No | The product being charged for on this line. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.now.breakdown[].type` | string | No | What kind of charge this line is, which is how the platform tells the recurring subscription product apart from one-off joining products when building the invoice and the instalment. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.now.breakdown[].description` | string | No | The product's display name, used as the invoice line description. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.now.breakdown[].translations` | array of object | No | Localised versions of the line description, each with `type`, `culture` and `text`. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.now.breakdown[].price` | object | No | The amount for this line, with the same `amount`, `currency` and `discount` shape used elsewhere in the quote. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.recurring` | object | No | The ongoing cost of this membership after the initial payment. `null` for a one-off membership type. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.recurring.membershipId` | integer (int32) | No | Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.recurring.headlinePrice` | object | No | The headline recurring amount for this membership — the figure a shopper thinks of as the price. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.recurring.headlinePrice.amount` | string | No | The recurring amount, as a decimal string with two decimal places, so parse it as a decimal rather than a float. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.recurring.headlinePrice.currency` | string | No | The ISO 4217 currency code of the recurring amount. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.recurring.headlinePrice.discount` | object | No | The discount applied to the recurring amount, or `null` when none applies. Consulted when recording which discount the membership was sold under. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.recurring.frequency` | string | No | How often the recurring payment falls due for this membership. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.recurring.schedule` | array of object | No | The forecast future payments for this membership. **The first entry is used to set up the membership's instalment record**, so this array is functional rather than informational — an empty or reordered schedule changes what is billed as well as breaking the signature. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.recurring.schedule[].date` | string (date-time) | No | When this scheduled payment falls due. The date on the first entry becomes the membership's first instalment date. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.memberships[].pricing.recurring.schedule[].payments` | array of object | No | The payments due on this date, each carrying a `price` and the payment method collecting it. The discount on the first payment of the first entry is one of the sources used to record which discount the membership was sold under. Part of the signed quote: send it back exactly as the pricing quote gave it to you. Altering or omitting it breaks `signature` and the call fails with `The pricing quote is invalid.` |
| `pricingQuote.timeStamp` | string | Yes | When the quote was issued, as returned by the pricing call. It is the quote's expiry clock: the call is rejected once more than **one hour** has elapsed since this moment (`The time frame where the request is considered valid has expired.`), and also if it is more than **ten minutes** ahead of the server's UTC clock (`The time stamp of the request can not be in the future.`). Both comparisons are against the server's time, not yours, so leave a margin. Must not be empty, and must not be edited — it is inside the signature. |
| `pricingQuote.signature` | string | Yes | The keyed hash computed over the rest of the quote when it was issued. The platform recomputes it from the quote you send, with `signature` itself excluded, and rejects any mismatch with `The pricing quote is invalid.` It is opaque — do not decode, regenerate or re-case it, and do not attempt to sign a quote yourself; the key is not issued to integrators. If you are hitting this error, the cause is almost always that something in `pricingQuote` was normalised, rounded or dropped in transit. Must not be empty. |
| `orderKeyToMemberIdMap` | array of object | Yes | Which member receives which line of the quote. The quote itself names membership types and order keys but no members, so without this the platform does not know who is buying. Every member id here must be either the logged-in member or one of their linked members, or the whole call is rejected with `403`. The first entry's `memberId` is treated as the principal member for the duplicate and direct-debit checks, so order it deliberately. |
| `orderKeyToMemberIdMap[].orderKey` | integer (int32) | Yes | The quote line this member is taking, matching a `pricingQuote.memberships[].orderKey`. Must not be null — a null in either property of any entry fails with `The properties of the orderKeyToMemberIdMap object cannot be null.` Unless `isJuniorMembershipWithGuardian` is `true`, a key with no matching quote line fails with `At least one order key from the mapping object does not relate to any order key in the given pricing quote.` |
| `orderKeyToMemberIdMap[].memberId` | integer (int32) | Yes | The member who will hold the membership, as returned when the member was created. Must not be null. It must be the authenticated member or one of their linked members — anything else returns `403` with `The member logged in is only allowed to purchase memberships for themselves or their linked members.` A linked member cannot be given a membership type whose `applyToLinks` is set; that fails with `A secondary member cannot purchase a group membership.` (or `A junior member…` when `isJuniorMembershipWithGuardian` is `true`). |

## Sample request

```bash title="cURL"
curl -X POST '{base_url}/api/memberships' \
  -H 'Authorization: Bearer {accessToken}' \
  -H 'X-Use-SSO: true' \
  -H 'Content-Type: application/json' \
  -d '{
  "isJuniorMembershipWithGuardian": true,
  "primaryPaysInd": true,
  "pricingQuote": {
    "siteId": "string",
    "promotionCode": "string",
    "overview": {
      "now": {
        "amount": "string",
        "currency": "GBP",
        "discount": {
          "priceBeforeDiscount": "string",
          "discountValue": "string",
          "discountId": 12345
        }
      },
      "recurring": {
        "headlinePrice": {
          "amount": "string",
          "currency": "GBP",
          "discount": {
            "priceBeforeDiscount": "string",
            "discountValue": "string",
            "discountId": 12345
          }
        },
        "frequency": "string",
        "schedule": [
          {
            "date": "2026-08-14T19:00:00Z",
            "totalPrice": {
              "amount": "string",
              "currency": "GBP",
              "discount": {
                "priceBeforeDiscount": "string",
                "discountValue": "string",
                "discountId": 12345
              }
            },
            "breakdown": [
              {
                "price": {
                  "amount": "string",
                  "currency": "GBP",
                  "discount": {}
                },
                "paymentMethod": {
                  "id": "string",
                  "description": "string",
                  "translations": []
                }
              }
            ]
          }
        ]
      }
    },
    "memberships": [
      {
        "orderKey": 1,
        "id": "string",
        "description": "string",
        "coolingOffPeriod": 1,
        "translations": [
          {
            "type": "string",
            "culture": "string",
            "text": "string"
          }
        ],
        "startDate": "2026-08-14T19:00:00Z",
        "endDate": "2026-08-14T19:00:00Z",
        "pricing": {
          "now": {
            "totalPrice": {
              "amount": "string",
              "currency": "GBP",
              "discount": {
                "priceBeforeDiscount": "string",
                "discountValue": "string",
                "discountId": 12345
              }
            },
            "breakdown": [
              {
                "priceLevelId": "string",
                "productId": "string",
                "type": "string",
                "description": "string",
                "translations": [
                  {}
                ],
                "price": {
                  "amount": "string",
                  "currency": "GBP",
                  "discount": {}
                }
              }
            ]
          },
          "recurring": {
            "membershipId": 12345,
            "headlinePrice": {
              "amount": "string",
              "currency": "GBP",
              "discount": {
                "priceBeforeDiscount": "string",
                "discountValue": "string",
                "discountId": 12345
              }
            },
            "frequency": "string",
            "schedule": [
              {
                "date": "2026-08-14T19:00:00Z",
                "payments": [
                  {}
                ]
              }
            ]
          }
        }
      }
    ],
    "timeStamp": "string",
    "signature": "string"
  },
  "orderKeyToMemberIdMap": [
    {
      "orderKey": 1,
      "memberId": 12345
    }
  ]
}'
```

## Response fields

| Field | Type | Description |
|-------|------|-------------|
| `salesInvoiceIds` | array of integer | Every sales invoice raised by this call, across all memberships and members, flattened into one list. **These invoices are unpaid.** Pass them straight to [Pay for sales invoices](../payments/pays-for-sales-invoices-using-a-specified-payment-provider-and-can-handle-paymen.md) as `salesInvoiceIds` — that call requires them to still be unpaid and to belong to the member you name. Persist them before doing anything else: if you lose them, you have memberships with outstanding balances and no handle on them. |
| `membershipsCreated` | array of object | The same information broken down per membership, so you can tell which invoice belongs to which member. Use this rather than the flat `salesInvoiceIds` when the quote covered several members, because the payment call is scoped to a single `memberId` and you will need one payment per member. |
| `membershipsCreated[].memberId` | integer (int32) | The member this membership was created for, echoing one of the ids you sent in `orderKeyToMemberIdMap`. |
| `membershipsCreated[].membershipIsDirectDebit` | boolean | Whether this membership collects by direct debit. When `true` the initial invoice still has to be paid through the payment endpoint; the direct debit governs the recurring collections that follow, and it is why the member needed bank details on file for the call to succeed at all. |
| `membershipsCreated[].membershipId` | integer (int32) | The platform's reference for the membership record that was created. This is a new membership instance, not the membership type id you sent in the quote. |
| `membershipsCreated[].salesInvoiceIds` | array of integer | The invoices raised for this membership alone — the subset of the top-level `salesInvoiceIds` attributable to it. Group these by `memberId` to build the payment calls. |

## Sample response

```json title="201 Created"
{
  "salesInvoiceIds": [
    12345
  ],
  "membershipsCreated": [
    {
      "memberId": 12345,
      "membershipIsDirectDebit": true,
      "membershipId": 12345,
      "salesInvoiceIds": [
        12345
      ]
    }
  ]
}
```

## Errors

| Status | Meaning | When |
|--------|---------|------|
| `400` | Bad request | `The pricing quote is invalid.` — the recomputed signature does not match. Something inside `pricingQuote` was altered, reformatted or omitted. |
| `400` | Bad request | `The time frame where the request is considered valid has expired.` — more than an hour has passed since `pricingQuote.timeStamp`. Get a fresh quote; the old one cannot be revived. |
| `400` | Bad request | `The time stamp of the request can not be in the future.` — `pricingQuote.timeStamp` is more than ten minutes ahead of the server's UTC clock. |
| `400` | Bad request | `The properties of the orderKeyToMemberIdMap object cannot be null.` — an entry has a null `orderKey` or `memberId`. |
| `400` | Bad request | `At least one order key from the mapping object does not relate to any order key in the given pricing quote.` — only checked when `isJuniorMembershipWithGuardian` is not `true`. |
| `400` | Bad request | A required field is missing: `Site ID cannot be empty`, `Pricing quote overview cannot be empty`, `Memberships cannot be empty`, `TimeStamp cannot be empty` or `Signature cannot be empty`. |
| `400` | Bad request | `At least one membership was not found in database.` — a membership type named in the quote no longer exists, or the quote lists ids that do not all resolve. |
| `400` | Bad request | `The products of the memberships were not found.` — the membership types exist but have no products configured, so nothing can be invoiced. |
| `400` | Bad request | `A secondary member cannot purchase a group membership.` (or `A junior member cannot purchase a group membership.`) — a linked member was mapped to a membership type whose `applyToLinks` is set. |
| `400` | Bad request | `The member does not meet the age range of the price level {priceLevelId} to be issued with the membership type {description}.` — the member's date of birth falls outside the price level's age range. Members with no date of birth recorded skip this check entirely. |
| `400` | Bad request | `One or more memberships cannot be created as they do not allow duplicate memberships` — either the quote contains the same non-duplicable membership type twice, or the member already holds an active one. |
| `400` | Bad request | `One or more of the memberships "{ids}" cannot be created as duplicate concurrent memberships or concurrent memberships with the same priority cannot be specified`, or `… cannot be created as concurrent memberships already exist with the same priority` — overlapping dates on membership types that prevent concurrency. Note that an auto-renewing membership is treated as running a hundred years, so it overlaps almost everything of the same priority. |
| `400` | Bad request | `Direct debit memberships must have the same start day and duration.` — the new direct debit memberships are incompatible with ones the member already pays for. |
| `403` | Forbidden | `The member logged in is only allowed to purchase memberships for themselves or their linked members.` — a `memberId` in `orderKeyToMemberIdMap` is neither the authenticated member nor one of their links. |
| `422` | Unprocessable entity | Bank details are missing or unusable for a direct debit membership: `Primary member must have bank details as this is a linked member and Primary Pays flag is set to true`, `Member must have bank details as this is a linked member and Primary Pays flag is set to false`, `Member does not have any bank accounts and one or more requested memberships require a bank account`, `Member does not have any suitable bank accounts for one or more of the requested memberships`, or `Member has no active bank accounts that are required for one or more of the requested memberships`. Collect bank details first and retry with the same quote if it has not expired. |
| `422` | Unprocessable entity | `Unable to process this request due to a configuration issue` — the memberships could not be saved. Nothing is created. |
| `500` | Internal server error | An unexpected server-side failure. The memberships and invoices are saved together, so a `500` while saving leaves nothing behind — but a `500` after they are saved may not. Check for the memberships before retrying. |
