ShopEazydevelopers
Recipes

Members-only store

Let shoppers apply for membership, and let a merchant's server approve them.

A store can sell to members only. Shoppers apply, the merchant approves, and only active members can buy. This recipe covers the shopper side with a publishable key, and the review side with a secret key.

You need a shopper credential for the shopper steps. Read Shoppers first.

1. Read the gate

curl https://api.shopeazy.co/v1/store \
  -H "Authorization: Bearer sez_pub_..."
{
  "data": {
    "membership": {
      "enabled": true,
      "gate": "checkout",
      "requireVerification": false,
      "applicationNote": "Tell us why you would like to join."
    },
    "auth": { "tenantId": "<tenant id>" }
  }
}
FieldMeaning
enabledThe store sells to members only
gatecheckout: anyone can browse, only members can buy. browse: you should hide the products and basket from non-members yourself
requireVerificationApplying needs a selfie and an ID document
applicationNoteWhat the merchant asks applicants to tell them

The API always enforces membership when an order is placed, whatever the gate. The gate is for your UI.

import { membersOnly, membershipGate } from '@shopeazy/storefront-client';

if (membersOnly(store.membership)) {
  const gate = membershipGate(store.membership); // 'checkout' or 'browse'
}

2. Sign the shopper in

Sign the shopper in with the Firebase client SDK, in the store's tenant: set auth.tenantId = store.auth.tenantId before you sign in. Then exchange the Firebase ID token for a 14-day shopper session. The shopper's email must be verified.

curl https://api.shopeazy.co/v1/store/shopper/session \
  -H "Authorization: Bearer sez_pub_..." \
  -H "Content-Type: application/json" \
  -d '{ "idToken": "<firebase id token>" }'

Keep the sessionToken in an httpOnly cookie. See Shoppers for the details.

3. Check where the shopper stands

curl https://api.shopeazy.co/v1/store/me \
  -H "Authorization: Bearer sez_pub_..." \
  -H "X-Shopper-Session: <session token>"
{
  "data": {
    "shopper": { "uid": "shopper_123", "email": "sipho@example.co.za", "name": "Sipho Dlamini", "phone": null },
    "membership": {
      "status": "pending",
      "appliedAt": 1792636800000,
      "decidedAt": null,
      "applicationNote": "Regular buyer at the market.",
      "decisionNote": null
    },
    "canCheckout": false
  }
}

membership is null if the shopper has not applied. status is pending, active, rejected or revoked. decisionNote is the merchant's message on an approval, a decline or a revoke, and the console tells the merchant the shopper sees it. canCheckout is true only for an active member of a members-only store. KYC details and documents are never returned.

4. Apply

A simple store

Send JSON. Both fields are optional. name defaults to the shopper's name, then their email.

curl https://api.shopeazy.co/v1/store/members/apply \
  -H "Authorization: Bearer sez_pub_..." \
  -H "X-Shopper-Session: <session token>" \
  -H "Content-Type: application/json" \
  -d '{ "applicationNote": "Regular buyer at the market." }'

The answer is 201 with { "data": { "status": "pending" } }. The store's auto-approve setting applies, so the status can be active at once.

const { status } = await shopeazy.asShopperSession(token).members.apply({ applicationNote });

An ID-verified store

When requireVerification is true, send multipart/form-data. A JSON body is 422 verification_required.

FieldRule
firstName, lastNameRequired
dateOfBirthYYYY-MM-DD, a real date, applicant 18 or older
idTypesa_id (the default) or passport
idNumberSA ID: 13 digits with a valid check digit. Passport: the passport format
addressLine, cityRequired. province, postalCode and country are optional
over18Must be true
consentMust be true: the applicant agreed to the store keeping their details, selfie and ID
applicationNoteOptional
selfieA file, image/*, 12 MB at most
idDocA file, image/* or application/pdf, 12 MB at most

The typed client builds the form for you:

await shopeazy.asShopperSession(token).members.apply({
  firstName, lastName, dateOfBirth: '1990-01-31', idType: 'sa_id', idNumber,
  addressLine, city, over18: true, consent: true,
  selfie, // a Blob, resize to about 1200px
  idDoc,  // a Blob, an image or a PDF. Resize images to about 1600px
});

The whole request must fit about 4.5 MB

That is the hosting body limit. Resize photos in the browser before you send them: the selfie to about 1200px and the ID to about 1600px, as JPEG. A file over 12 MB is 413 file_too_large.

  • A verified application is always pending, whatever the auto-approve setting.
  • The files go to the store's private storage with no public link. The merchant reviews them in the console.
  • Applying again while pending or active returns 200 with the current status and leaves the documents under review alone. A rejected or revoked shopper may apply again with new documents.
  • A broken field is 422 validation_error, and the message names the field.
  • Applying to a store that is open to everyone is 422 validation_error.

5. Let members buy

An order on a members-only store needs a shopper credential. Send the session when you place the order, as in Headless checkout.

SituationAnswer
No shopper credential401 shopper_required
A shopper who is not an active member403 not_a_member
An active memberThe order is placed, and linked to the shopper

Show the shopper where they stand from GET /v1/store/me, and offer Apply when membership is null.

6. Review applications from your server

Use a secret key. It carries members:manage. Never send it from a browser.

curl "https://api.shopeazy.co/v1/store/members?status=pending" \
  -H "Authorization: Bearer sez_sk_..."

status is pending, active, rejected, revoked or all. The answer is a page, { items, nextCursor }, of members:

{
  "data": {
    "items": [
      {
        "uid": "shopper_123",
        "status": "pending",
        "name": "Sipho Dlamini",
        "email": "sipho@example.co.za",
        "phone": null,
        "applicationNote": "Regular buyer at the market.",
        "appliedAt": 1792636800000,
        "decidedAt": null,
        "decisionNote": null,
        "verification": null
      }
    ],
    "nextCursor": null
  }
}

On an ID-verified store, verification holds the applicant's details. List rows carry no document links. To see the selfie and ID, read one member:

curl https://api.shopeazy.co/v1/store/members/shopper_123 \
  -H "Authorization: Bearer sez_sk_..."

That answer has short-lived signed selfieUrl and idDocUrl links.

Then decide. The optional note is shown to the shopper as decisionNote.

curl -X POST https://api.shopeazy.co/v1/store/members/shopper_123/approve \
  -H "Authorization: Bearer sez_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "note": "Welcome to the club." }'

Use /decline or /revoke in place of /approve for the other decisions.

const pending = await shopeazy.members.page({ status: 'pending' });
await shopeazy.members.approve(uid, { note: 'Welcome to the club.' });

Merchants can also review applications in the console.

On this page