Create members with memberships
POST /api/memberships/add-members-with-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 a primary member, optionally a set of linked members alongside them, and the memberships they are buying — all from one signed pricing quote. It is the single call behind a self-service join: everything from "here are my details" to "here are the invoices to pay" happens here.
Get a pricing quote first. The quote is signed and must come back unaltered; change anything
inside pricingQuote and the call fails with The pricing quote is invalid. It is valid for
one hour from its timeStamp and may not be more than ten minutes ahead of the server's
UTC clock. siteId at the top level must equal pricingQuote.siteId.
What this call does and does not finish. It is presented as one operation, but it runs as a sequence of steps: create the members, link them, write user fields, bank details, contact details, addresses, marketing consents and signed agreements, then create the memberships and confirm the members. If any step fails, the members created so far are deleted and you get the failing step's status back. That rollback can itself fail — when it does you are left with members that exist but have no memberships, and they must be cleaned up by hand.
What the rollback never covers is the money. A 201 means members, memberships and unpaid
sales invoices exist. Nothing is paid, and no membership is active, until you settle
membersCreated[].salesInvoiceIds with
Pay for sales invoices
— one call per member, since that endpoint is scoped to a single memberId. Stop between the two
and you have real members holding real memberships with an outstanding balance. The call is not
idempotent: retrying creates a second set of everything, or fails on duplicate-member validation.
Order keys are how the quote lines are matched to the people in this request. Each member
carries an orderKey that must correspond to a pricingQuote.memberships[].orderKey, so the
platform knows who is buying what. A member with orderKey 0 or absent is created but given no
membership.
Endpoint
POST {base_url}/api/memberships/add-members-with-memberships
{base_url} is the environment URL issued to your integration. See Authenticating with Gladstone APIs.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
siteId | string | Yes | The site the members are being created at and the memberships sold at. Maximum 15 characters and must not be empty. It must be identical to pricingQuote.siteId — a mismatch is the first thing checked and fails with The site ID of the member must match the site ID in the membership pricing quote. It also overwrites every profile.siteId in the request. |
primaryMember | object | Yes | PrimaryMember cannot be empty. |
primaryMember.orderKey | integer (int32) | No | Which line of the pricing quote this primary member is buying, matching a pricingQuote.memberships[].orderKey. At least one member in the request must carry a non-zero order key, or the call fails with At least one orderKey for the Primary or Linked members must be specified. Order keys must be unique across the primary and all linked members (The orderKeys for the Primary and Linked members cannot be repeated.). Omitting it, or sending 0, creates the primary member without any membership — useful for a child who is only being added to the account. Unless hasGuardian puts the request into guardian mode, every non-zero key must exist in the quote. |
primaryMember.profile | object | Yes | Profile cannot be empty. |
primaryMember.profile.titleId | string | No | Title ID cannot be more than 15 characters. |
primaryMember.profile.firstName | string | Yes | First name cannot be more than 25 characters. |
primaryMember.profile.email | string | No | The primary member's email address. Effectively required: an empty value or one over 255 characters is rejected with The primary member's email must be supplied and be less than 256 characters., despite the field being optional in the schema. It is also used for the duplicate-member check, so an address already on file can fail the whole request. |
primaryMember.profile.lastName | string | Yes | Last name cannot be more than 25 characters. |
primaryMember.profile.dateOfBirth | string (date-time) | No | Date of Birth cannot be in the future. |
primaryMember.profile.siteId | string | No | Ignored. Whatever you send here is overwritten with the top-level siteId before the member is created, so there is no way to place a member at a different site from the one the quote was priced for. |
primaryMember.profile.pendingConfirmation | boolean | No | Marks the member as awaiting confirmation at the moment they are created. It does not survive a successful call: once the memberships are created this endpoint confirms every member it made, so a 201 always leaves confirmed members. It matters only in that a member left behind by a failed rollback may still be in the pending state. |
primaryMember.profile.memberLanguage | string | No | Ignored. Overwritten with the top-level memberLanguage before the member is created. Set the language once at the top level; there is no way to give members in the same request different languages. |
primaryMember.addressDetails | object | No | The primary member's addresses, up to two. The whole object is optional — omit it and no address is recorded and no address validation runs at all. But once you supply it, exactly one of address1 and address2 must be marked isPrimary, or you get The primary member needs to have exactly one primary contact address. |
primaryMember.addressDetails.address1 | object | No | The primary member's first address. Sending it entirely empty while marking it primary is rejected — The primary contact address of the primary member cannot be empty. Individual fields are limited to 75 characters, except postalCode and country at 25. |
primaryMember.addressDetails.address1.addressLine1 | string | Yes | AddressLine1 cannot be more than 75 characters. |
primaryMember.addressDetails.address1.addressLine2 | string | No | AddressLine2 cannot be more than 75 characters. |
primaryMember.addressDetails.address1.addressLine3 | string | No | AddressLine3 cannot be more than 75 characters. |
primaryMember.addressDetails.address1.townCity | string | Yes | TownCity cannot be more than 75 characters. |
primaryMember.addressDetails.address1.region | string | No | Region cannot be more than 75 characters. |
primaryMember.addressDetails.address1.postalCode | string | Yes | PostalCode cannot be more than 25 characters. |
primaryMember.addressDetails.address1.country | string | No | Country cannot be more than 25 characters. |
primaryMember.addressDetails.address1.isPrimary | boolean | No | Marks this as the primary member's main address. Exactly one address must carry true whenever addressDetails is supplied; both true or neither true fails validation. Defaults to false when omitted, so a single address object with this left out will fail. |
primaryMember.addressDetails.address2 | object | No | A second address for the primary member, such as a term-time or correspondence address. Optional. Subject to the same emptiness and length rules as address1, and it counts towards the exactly-one-primary rule. |
primaryMember.addressDetails.address2.addressLine1 | string | Yes | AddressLine1 cannot be more than 75 characters. |
primaryMember.addressDetails.address2.addressLine2 | string | No | AddressLine2 cannot be more than 75 characters. |
primaryMember.addressDetails.address2.addressLine3 | string | No | AddressLine3 cannot be more than 75 characters. |
primaryMember.addressDetails.address2.townCity | string | Yes | TownCity cannot be more than 75 characters. |
primaryMember.addressDetails.address2.region | string | No | Region cannot be more than 75 characters. |
primaryMember.addressDetails.address2.postalCode | string | Yes | PostalCode cannot be more than 25 characters. |
primaryMember.addressDetails.address2.country | string | No | Country cannot be more than 25 characters. |
primaryMember.addressDetails.address2.isPrimary | boolean | No | Marks this as the primary member's main address instead of address1. Only one of the two may be true. |
primaryMember.contactDetails | object | No | The primary member's phone numbers. The whole object is optional — omit it and no numbers are recorded and the primary-number rule is not applied. Supply it and exactly one of telephone1, telephone2 and mobile must be marked isPrimary: zero gives The primary member needs to have one primary contact phone number. and more than one gives The primary member cannot have multiple primary contact phone numbers. Note the rule counts flags, not numbers: an entry flagged primary with a blank number satisfies validation but records nothing. |
primaryMember.contactDetails.telephone1 | object | No | A landline or general contact number for the primary member. |
primaryMember.contactDetails.telephone1.number | string | No | The number itself, as free text. Only entries with a non-empty number are saved, so a blank number here means nothing is recorded even though the entry may still count towards the primary-flag rule. |
primaryMember.contactDetails.telephone1.isPrimary | boolean | No | Marks this as the primary member's main contact number. Exactly one of the three numbers must carry true whenever contactDetails is supplied. Defaults to false. |
primaryMember.contactDetails.telephone2 | object | No | A second landline or alternative contact number for the primary member. |
primaryMember.contactDetails.telephone2.number | string | No | The number itself, as free text. A blank number records nothing. |
primaryMember.contactDetails.telephone2.isPrimary | boolean | No | Marks this as the primary member's main contact number. Only one number across the three may be true. |
primaryMember.contactDetails.mobile | object | No | A mobile number for the primary member. |
primaryMember.contactDetails.mobile.number | string | No | The number itself, as free text. A blank number records nothing. |
primaryMember.contactDetails.mobile.isPrimary | boolean | No | Marks the mobile as the primary member's main contact number. Only one number across the three may be true. |
primaryMember.bankDetails | object | No | Bank details for collecting direct debit payments. Whether it is required is decided from the quote, not from a flag you set: if any membership's recurring schedule names a BACS direct debit payment method other than the operator's default recurring-card method, omitting this fails with Membership types "{ids}" require the primary member to provide bank details. Only the primary member's bank details are ever taken — linked members cannot supply their own here — and they are only saved when the quote actually needs them, so sending them for a card-only purchase is silently ignored. accountNumber must be exactly 8 digits and sortCode exactly 6, both as strings; a leading zero dropped by a JSON number type is a common cause of rejection. |
primaryMember.bankDetails.accountName | string | Yes | Maximum 50 characters. |
primaryMember.bankDetails.accountNumber | string | Yes | Value must be a string of 8 digits. |
primaryMember.bankDetails.sortCode | string | Yes | Value must be a string of 6 digits. |
primaryMember.userFields | array of object | No | Values for the operator's custom member fields. Optional; omit or send an empty array when there are none. Each entry must carry both an id and a value — there is no way to send a field with a deliberately blank value through this endpoint. |
primaryMember.userFields[].userFieldId | integer (int32) | Yes | User field ID cannot be empty. |
primaryMember.userFields[].value | string | Yes | User field value cannot be empty. |
primaryMember.optInMarketing | boolean | No | A single switch that opts the primary member in to every marketing campaign and every communication channel the operator has configured, at siteId. There is no per-channel or per-campaign control here: it is all or nothing. Defaults to false, which records no consent at all rather than an explicit refusal. |
primaryMember.signedAgreements | object | No | Which agreements the primary member has accepted, and the object that makes the call succeed or fail: privacyPolicy and termsAndConditions must both be true. Omitting the object entirely is treated the same as refusing, and fails with The primary member needs to agree to the privacy policy and terms and conditions. The date of acceptance is recorded by the platform at the time of the call; you cannot backdate it. |
primaryMember.signedAgreements.privacyPolicy | boolean | No | Whether the primary member has accepted the privacy policy. Must be true — an omitted or false value fails the whole request before any member is created. |
primaryMember.signedAgreements.termsAndConditions | boolean | No | Whether the primary member has accepted the terms and conditions. Must be true — an omitted or false value fails the whole request before any member is created. |
primaryMember.signedAgreements.healthCommitmentStatement | boolean | No | Whether the primary member has accepted the health commitment statement. Genuinely optional, unlike the other two: false or omitted is accepted and simply records no acceptance. |
linkedMembers | array of object | No | Additional members created alongside the primary member and linked to them, such as family on a joint membership. Optional; omit or send an empty array for a single-member join. Every linked member is subject to the same agreement, contact and address rules as the primary. They are linked to the primary member as part of this call, and if any later step fails they are deleted along with the primary. A linked member cannot be given a membership type that applies to links — that is what the primary member's membership does. |
linkedMembers[].orderKey | integer (int32) | No | Which line of the pricing quote this linked member is buying, matching a pricingQuote.memberships[].orderKey. At least one member in the request must carry a non-zero order key, or the call fails with At least one orderKey for the Primary or Linked members must be specified. Order keys must be unique across the primary and all linked members (The orderKeys for the Primary and Linked members cannot be repeated.). Omitting it, or sending 0, creates the linked member without any membership — useful for a child who is only being added to the account. Unless hasGuardian puts the request into guardian mode, every non-zero key must exist in the quote. |
linkedMembers[].profile | object | Yes | Profile cannot be empty. |
linkedMembers[].profile.titleId | string | No | Title ID cannot be more than 15 characters. |
linkedMembers[].profile.firstName | string | Yes | First name cannot be more than 25 characters. |
linkedMembers[].profile.email | string | No | The linked member's email address. Optional only for members young enough: if this is empty, the platform compares the member's dateOfBirth against the operator's age-of-online-account setting and rejects the request with One or more linked members are missing an email address. if they are old enough to hold their own account. A member with no dateOfBirth and no email is treated as old enough and so fails. When supplied it must be a well-formed address, or you get The email address "{email}"is not a valid email address. |
linkedMembers[].profile.lastName | string | Yes | Last name cannot be more than 25 characters. |
linkedMembers[].profile.dateOfBirth | string (date-time) | No | Date of Birth cannot be in the future. |
linkedMembers[].profile.siteId | string | No | Ignored. Whatever you send here is overwritten with the top-level siteId before the member is created, so there is no way to place a member at a different site from the one the quote was priced for. |
linkedMembers[].profile.pendingConfirmation | boolean | No | Marks the member as awaiting confirmation at the moment they are created. It does not survive a successful call: once the memberships are created this endpoint confirms every member it made, so a 201 always leaves confirmed members. It matters only in that a member left behind by a failed rollback may still be in the pending state. |
linkedMembers[].profile.memberLanguage | string | No | Ignored. Overwritten with the top-level memberLanguage before the member is created. Set the language once at the top level; there is no way to give members in the same request different languages. |
linkedMembers[].addressDetails | object | No | The linked member's addresses, up to two. The whole object is optional — omit it and no address is recorded and no address validation runs at all. But once you supply it, exactly one of address1 and address2 must be marked isPrimary, or you get The linked member does not have a primary contact address. or … contains multiple primary contact addresses., each naming the offending orderKey. |
linkedMembers[].addressDetails.address1 | object | No | The linked member's first address. Sending it entirely empty while marking it primary is rejected — The linked member cannot have an address with empty values. Individual fields are limited to 75 characters, except postalCode and country at 25. |
linkedMembers[].addressDetails.address1.addressLine1 | string | Yes | AddressLine1 cannot be more than 75 characters. |
linkedMembers[].addressDetails.address1.addressLine2 | string | No | AddressLine2 cannot be more than 75 characters. |
linkedMembers[].addressDetails.address1.addressLine3 | string | No | AddressLine3 cannot be more than 75 characters. |
linkedMembers[].addressDetails.address1.townCity | string | Yes | TownCity cannot be more than 75 characters. |
linkedMembers[].addressDetails.address1.region | string | No | Region cannot be more than 75 characters. |
linkedMembers[].addressDetails.address1.postalCode | string | Yes | PostalCode cannot be more than 25 characters. |
linkedMembers[].addressDetails.address1.country | string | No | Country cannot be more than 25 characters. |
linkedMembers[].addressDetails.address1.isPrimary | boolean | No | Marks this as the linked member's main address. Exactly one address must carry true whenever addressDetails is supplied; both true or neither true fails validation. Defaults to false when omitted, so a single address object with this left out will fail. |
linkedMembers[].addressDetails.address2 | object | No | A second address for the linked member, such as a term-time or correspondence address. Optional. Subject to the same emptiness and length rules as address1, and it counts towards the exactly-one-primary rule. |
linkedMembers[].addressDetails.address2.addressLine1 | string | Yes | AddressLine1 cannot be more than 75 characters. |
linkedMembers[].addressDetails.address2.addressLine2 | string | No | AddressLine2 cannot be more than 75 characters. |
linkedMembers[].addressDetails.address2.addressLine3 | string | No | AddressLine3 cannot be more than 75 characters. |
linkedMembers[].addressDetails.address2.townCity | string | Yes | TownCity cannot be more than 75 characters. |
linkedMembers[].addressDetails.address2.region | string | No | Region cannot be more than 75 characters. |
linkedMembers[].addressDetails.address2.postalCode | string | Yes | PostalCode cannot be more than 25 characters. |
linkedMembers[].addressDetails.address2.country | string | No | Country cannot be more than 25 characters. |
linkedMembers[].addressDetails.address2.isPrimary | boolean | No | Marks this as the linked member's main address instead of address1. Only one of the two may be true. |
linkedMembers[].contactDetails | object | No | The linked member's phone numbers. The whole object is optional — omit it and no numbers are recorded and the primary-number rule is not applied. Supply it and exactly one of telephone1, telephone2 and mobile must be marked isPrimary: zero gives The linked member does not have a primary contact phone number. and more than one gives The linked member contains multiple primary contact phone numbers., each naming the offending orderKey. Note the rule counts flags, not numbers: an entry flagged primary with a blank number satisfies validation but records nothing. |
linkedMembers[].contactDetails.telephone1 | object | No | A landline or general contact number for the linked member. |
linkedMembers[].contactDetails.telephone1.number | string | No | The number itself, as free text. Only entries with a non-empty number are saved, so a blank number here means nothing is recorded even though the entry may still count towards the primary-flag rule. |
linkedMembers[].contactDetails.telephone1.isPrimary | boolean | No | Marks this as the linked member's main contact number. Exactly one of the three numbers must carry true whenever contactDetails is supplied. Defaults to false. |
linkedMembers[].contactDetails.telephone2 | object | No | A second landline or alternative contact number for the linked member. |
linkedMembers[].contactDetails.telephone2.number | string | No | The number itself, as free text. A blank number records nothing. |
linkedMembers[].contactDetails.telephone2.isPrimary | boolean | No | Marks this as the linked member's main contact number. Only one number across the three may be true. |
linkedMembers[].contactDetails.mobile | object | No | A mobile number for the linked member. |
linkedMembers[].contactDetails.mobile.number | string | No | The number itself, as free text. A blank number records nothing. |
linkedMembers[].contactDetails.mobile.isPrimary | boolean | No | Marks the mobile as the linked member's main contact number. Only one number across the three may be true. |
linkedMembers[].bankDetails | object | No | Ignored. Only primaryMember.bankDetails is used; bank details sent against a linked member are validated for format but never saved and never used to satisfy a direct debit requirement. If a linked member's membership needs a direct debit, put the details on the primary member. |
linkedMembers[].bankDetails.accountName | string | Yes | Maximum 50 characters. |
linkedMembers[].bankDetails.accountNumber | string | Yes | Value must be a string of 8 digits. |
linkedMembers[].bankDetails.sortCode | string | Yes | Value must be a string of 6 digits. |
linkedMembers[].userFields | array of object | No | Values for the operator's custom member fields. Optional; omit or send an empty array when there are none. Each entry must carry both an id and a value — there is no way to send a field with a deliberately blank value through this endpoint. |
linkedMembers[].userFields[].userFieldId | integer (int32) | Yes | User field ID cannot be empty. |
linkedMembers[].userFields[].value | string | Yes | User field value cannot be empty. |
linkedMembers[].optInMarketing | boolean | No | A single switch that opts the linked member in to every marketing campaign and every communication channel the operator has configured, at siteId. There is no per-channel or per-campaign control here: it is all or nothing. Defaults to false, which records no consent at all rather than an explicit refusal. |
linkedMembers[].signedAgreements | object | No | Which agreements the linked member has accepted, and the object that makes the call succeed or fail: privacyPolicy and termsAndConditions must both be true. Omitting the object entirely is treated the same as refusing, and fails with All the linked members need to agree to the privacy policy and terms and conditions. The date of acceptance is recorded by the platform at the time of the call; you cannot backdate it. |
linkedMembers[].signedAgreements.privacyPolicy | boolean | No | Whether the linked member has accepted the privacy policy. Must be true — an omitted or false value fails the whole request before any member is created. |
linkedMembers[].signedAgreements.termsAndConditions | boolean | No | Whether the linked member has accepted the terms and conditions. Must be true — an omitted or false value fails the whole request before any member is created. |
linkedMembers[].signedAgreements.healthCommitmentStatement | boolean | No | Whether the linked member has accepted the health commitment statement. Genuinely optional, unlike the other two: false or omitted is accepted and simply records no acceptance. |
pricingQuote | object | Yes | Pricing quote cannot be empty. |
pricingQuote.siteId | string | Yes | The site the quote was priced for. It must equal the top-level siteId exactly, or the request is rejected up front with The site ID of the member must match the site ID in the membership pricing quote. Must not be empty. 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.promotionCode | string | No | The promotion code the quote was priced under, if any. Read when creating the memberships to record promotion usage, so removing it both loses that record and breaks 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.overview | object | Yes | Pricing quote overview cannot be empty. |
pricingQuote.overview.now | object | No | The total payable immediately across the whole basket, as opposed to the per-membership figures below. 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 the whole basket 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 figure the shopper is agreeing to pay each period. 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 — the schedule that drives billing is the per-membership one below. 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 | Memberships cannot be empty. |
pricingQuote.memberships[].orderKey | integer (int32) | No | The line number of this membership within the quote, and the join key to the orderKey on primaryMember and linkedMembers — it is how the platform decides who receives which membership. Unless hasGuardian puts the request into guardian mode, every non-zero member orderKey must appear here or the call fails with The orderKeys for the primary and linked members don't match the orderKeys in the given pricing 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[].id | string | No | The membership type being bought, as returned in membershipTypes[].id by Get membership types grouped by tag. 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. 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. 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, fixed by the quote. Used for the overlap checks against memberships the member already holds, so it is functional rather than decorative — but it cannot be changed here. To move the start date, get a new quote, and use Get earliest start date for membership to find a date that will be accepted. 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 effective end is treated as a hundred years later, so it 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 turn 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 becomes the total of the sales invoice raised for this membership, so it is the figure the member will actually 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. 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. Each entry becomes a line on the sales invoice. 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 separates the recurring subscription product from one-off joining products when building the invoice. 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. 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. 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 sets up the membership's instalment record, and the whole array is scanned to decide whether bank details are needed — see primaryMember.bankDetails. 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 paymentMethod collecting it. The payment method ids here are what decide whether primaryMember.bankDetails is compulsory. 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 expiry clock: the request is rejected once more than one hour has passed (The time frame where the request is considered valid has expired.), or 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 use the server's time, not yours. Must not be empty, and is inside the signature so it cannot be adjusted. |
pricingQuote.signature | string | Yes | The keyed hash computed over the rest of the quote when it was issued. The platform recomputes it from what you send, with signature itself excluded, and rejects any mismatch with The pricing quote is invalid. Opaque — do not decode, regenerate or re-case it; the signing key is not issued to integrators. If you hit this error, something inside pricingQuote was normalised, rounded or dropped in transit. Must not be empty. |
hasGuardian | boolean | No | Declares that the primary member is a guardian buying on behalf of the linked members rather than for themselves. Defaults to false. Setting it true only takes effect when there is at least one linked member, siteId matches the quote, and every linked member's order key is the one the quote's memberships carry; when all of that holds, the check that member order keys appear in the quote is skipped and the members are linked in guardian mode. In every other case a true here has no effect — so do not rely on it to bypass order key validation. |
memberLanguage | string | No | The language to record against every member created by this call. It overwrites any memberLanguage set on an individual profile, so all members in one request get the same value. |
Sample request
curl -X POST '{base_url}/api/memberships/add-members-with-memberships' \
-H 'Authorization: Bearer {accessToken}' \
-H 'X-Use-SSO: true' \
-H 'Content-Type: application/json' \
-d '{
"siteId": "string",
"primaryMember": {
"orderKey": 1,
"profile": {
"titleId": "string",
"firstName": "string",
"email": "member@example.com",
"lastName": "string",
"dateOfBirth": "2026-08-14T19:00:00Z",
"siteId": "string",
"pendingConfirmation": true,
"memberLanguage": "string"
},
"addressDetails": {
"address1": {
"addressLine1": "string",
"addressLine2": "string",
"addressLine3": "string",
"townCity": "string",
"region": "string",
"postalCode": "string",
"country": "string",
"isPrimary": true
},
"address2": {
"addressLine1": "string",
"addressLine2": "string",
"addressLine3": "string",
"townCity": "string",
"region": "string",
"postalCode": "string",
"country": "string",
"isPrimary": true
}
},
"contactDetails": {
"telephone1": {
"number": "string",
"isPrimary": true
},
"telephone2": {
"number": "string",
"isPrimary": true
},
"mobile": {
"number": "string",
"isPrimary": true
}
},
"bankDetails": {
"accountName": "string",
"accountNumber": "string",
"sortCode": "string"
},
"userFields": [
{
"userFieldId": 12345
}
],
"optInMarketing": true,
"signedAgreements": {
"privacyPolicy": true,
"termsAndConditions": true,
"healthCommitmentStatement": true
}
},
"linkedMembers": [
{
"orderKey": 1,
"profile": {
"titleId": "string",
"firstName": "string",
"email": "member@example.com",
"lastName": "string",
"dateOfBirth": "2026-08-14T19:00:00Z",
"siteId": "string",
"pendingConfirmation": true,
"memberLanguage": "string"
},
"addressDetails": {
"address1": {
"addressLine1": "string",
"addressLine2": "string",
"addressLine3": "string",
"townCity": "string",
"region": "string",
"postalCode": "string",
"country": "string",
"isPrimary": true
},
"address2": {
"addressLine1": "string",
"addressLine2": "string",
"addressLine3": "string",
"townCity": "string",
"region": "string",
"postalCode": "string",
"country": "string",
"isPrimary": true
}
},
"contactDetails": {
"telephone1": {
"number": "string",
"isPrimary": true
},
"telephone2": {
"number": "string",
"isPrimary": true
},
"mobile": {
"number": "string",
"isPrimary": true
}
},
"bankDetails": {
"accountName": "string",
"accountNumber": "string",
"sortCode": "string"
},
"userFields": [
{
"userFieldId": 12345
}
],
"optInMarketing": true,
"signedAgreements": {
"privacyPolicy": true,
"termsAndConditions": true,
"healthCommitmentStatement": 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"
},
"hasGuardian": true,
"memberLanguage": "string"
}'Response fields
| Field | Type | Description |
|---|---|---|
membersCreated | array of object | One entry per member this call created, including any created without a membership. Match entries to your request by orderKey, except for members you sent with no order key — those come back with orderKey 0 and can only be identified by elimination, so keep the creation order if you need to tell them apart. |
membersCreated[].orderKey | integer (int32) | The order key you sent for this member, echoed back. 0 for a member created without a membership, and 0 for a primary member sent without one — so it is not a reliable identifier on its own. |
membersCreated[].memberId | integer (int32) | The platform's id for the newly created member. This is the value you pass as memberId when paying their invoices, and the handle you need if a later failure leaves the account to be cleaned up. Persist it before doing anything else. |
membersCreated[].bankAccountReference | string | The reference for the bank account stored against the primary member, returned only when bank details were both supplied and actually needed by the quote. null on every linked member, and null on the primary member when the purchase needed no direct debit — which is not an error. |
membersCreated[].salesInvoiceIds | array of integer | The unpaid invoices raised for this member. Pass them, together with this entry's memberId, to Pay for sales invoices — that endpoint is scoped to one member, so a family join needs one payment call per member with a non-empty list here. Empty for a member created without a membership. |
membershipsCreated | array of object | One entry per membership created, which is the view you need when a single member bought more than one. A member appears once here per membership but only once in membersCreated. |
membershipsCreated[].memberId | integer (int32) | The member this membership belongs to, matching a membersCreated[].memberId. |
membershipsCreated[].membershipIsDirectDebit | boolean | Whether this membership collects by direct debit. Even when true, the initial invoice still has to be paid through the payment endpoint — the direct debit governs the recurring collections that follow, and is why bank details were required for the call to succeed. |
membershipsCreated[].membershipId | integer (int32) | The platform's reference for the membership record 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 union of these across a member equals that member's membersCreated[].salesInvoiceIds. |
timeStamp | string | When this response was produced, in the same form as the pricing quote's timestamp. It is covered by signature and exists so the response can be shown to be fresh rather than replayed. |
signature | string | A keyed hash over this response, computed the same way the pricing quote is signed. It lets a downstream step confirm the response has not been tampered with. You cannot verify it yourself — the signing key is not issued to integrators — so treat it as data to pass on rather than to check. |
Sample response
{
"membersCreated": [
{
"orderKey": 1,
"memberId": 12345,
"bankAccountReference": "string",
"salesInvoiceIds": [
12345
]
}
],
"membershipsCreated": [
{
"memberId": 12345,
"membershipIsDirectDebit": true,
"membershipId": 12345,
"salesInvoiceIds": [
12345
]
}
],
"timeStamp": "string",
"signature": "string"
}Errors
| Status | Meaning | When |
|---|---|---|
400 | Bad request | The site ID of the member must match the site ID in the membership pricing quote. — checked first, before anything else. |
400 | Bad request | The pricing quote is invalid. — the recomputed signature does not match, because something inside pricingQuote was altered, reformatted or dropped. |
400 | Bad request | The time frame where the request is considered valid has expired. — more than an hour since pricingQuote.timeStamp. Get a fresh quote. |
400 | Bad request | The time stamp of the request can not be in the future. — the quote's timestamp is over ten minutes ahead of the server's UTC clock. |
400 | Bad request | The primary member's email must be supplied and be less than 256 characters. |
400 | Bad request | One or more linked members are missing an email address. — a linked member with no email is at or above the operator's age-of-online-account threshold. A linked member with no dateOfBirth and no email also trips this. |
400 | Bad request | The email address "{email}"is not a valid email address — a linked member's address is malformed. |
400 | Bad request | At least one orderKey for the Primary or Linked members must be specified. or The orderKeys for the Primary and Linked members cannot be repeated. |
400 | Bad request | The orderKeys for the primary and linked members don't match the orderKeys in the given pricing quote. — skipped only when hasGuardian puts the request into guardian mode. |
400 | Bad request | Contact number rules: The primary member needs to have one primary contact phone number., The primary member cannot have multiple primary contact phone numbers., or the linked-member equivalents naming an orderKey. Only applied when contactDetails is present. |
400 | Bad request | Address rules: The primary member needs to have exactly one primary contact address., The primary contact address of the primary member cannot be empty., or the linked-member equivalents naming an orderKey. Only applied when addressDetails is present. |
400 | Bad request | The primary member needs to agree to the privacy policy and terms and conditions. or All the linked members need to agree to the privacy policy and terms and conditions. |
400 | Bad request | Membership types "{ids}" require the primary member to provide bank details — the quote's recurring schedule names a BACS direct debit payment method and primaryMember.bankDetails is absent. |
400 | Bad request | The primary member's memberCounter must be 0 — an internal field was populated on the request. Do not send memberCounter. |
400 | Bad request | Field validation failures: Site ID cannot be empty, Site ID cannot be more than 15 characters, PrimaryMember cannot be empty, Profile cannot be empty, Pricing quote cannot be empty, First name cannot be empty, First name cannot be more than 25 characters, Last name cannot be empty, Last name cannot be more than 25 characters, Title ID cannot be more than 15 characters, Date of Birth cannot be in the future, AddressLine1 cannot be more than 75 characters and its siblings, Value must be a string of 8 digits., Value must be a string of 6 digits., User field ID cannot be empty, User field value cannot be empty. |
400 | Bad request | A later step rejected the data — for example a member that duplicates one already on file, or a membership rule such as a duplicate or concurrent membership. The message describes what was refused, and the members created so far have been deleted. |
422 | Unprocessable entity | A later step could not complete: a member's contact details could not be updated because of a concurrent change, or a membership could not be created. Also returned when the rollback could not identify a primary member to remove, which means members may remain. |
500 | Internal server error | An unexpected server-side failure, or a failure whose rollback itself failed. In the second case members have been created and not removed: look them up by the details you submitted and delete them before retrying, or you will hit duplicate-member validation. The body is empty in this case. |
503 | Service unavailable | A service this call depends on could not be reached — including Failed to retrieve payment method configuration. when the bank details requirement cannot be determined. Members created so far are rolled back. |