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>" }
}
}| Field | Meaning |
|---|---|
enabled | The store sells to members only |
gate | checkout: anyone can browse, only members can buy. browse: you should hide the products and basket from non-members yourself |
requireVerification | Applying needs a selfie and an ID document |
applicationNote | What 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.
| Field | Rule |
|---|---|
firstName, lastName | Required |
dateOfBirth | YYYY-MM-DD, a real date, applicant 18 or older |
idType | sa_id (the default) or passport |
idNumber | SA ID: 13 digits with a valid check digit. Passport: the passport format |
addressLine, city | Required. province, postalCode and country are optional |
over18 | Must be true |
consent | Must be true: the applicant agreed to the store keeping their details, selfie and ID |
applicationNote | Optional |
selfie | A file, image/*, 12 MB at most |
idDoc | A 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
pendingoractivereturns200with the current status and leaves the documents under review alone. Arejectedorrevokedshopper 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.
| Situation | Answer |
|---|---|
| No shopper credential | 401 shopper_required |
| A shopper who is not an active member | 403 not_a_member |
| An active member | The 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.