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)
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:
- Get a pricing quote for the memberships you want to sell. The quote is returned signed.
- Create or identify the members who will hold them.
- Call this endpoint with the quote unaltered and a map from each quote line to a member id.
- Pay the returned
salesInvoiceIdswith Pay for sales invoices.
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.
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. 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. 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 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
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 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
{
"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. |