Covers a company user's profile (Member), their check-in history (Visit), credit balances (Credit/CreditUse), signed waivers (MemberDocument), in-app notifications (Notification), on-file payment methods, product subscriptions, and the account-credit ledger. All classes here live under zensile-sdk/company/member/. See the Introduction for DataStructure<T>/SimpleResult<T>/EventData<T>/QueryRequest<T>/CheckInResponse shapes.
company/member/member.ts. A company user's profile row — referred to as User throughout the app UI, but the class/table is Member/members, distinct from the global User account it's optionally linked to via userID (an invited-but-not-yet-linked member can exist without one).
roleID/role field on Member — company-level staff role assignment is a Staff concern (see Company), not a Member field.interface LazyMemberData {
id: string;
customID?: string;
userID?: string;
companyID: string;
userEmail: string;
firstName?: string;
lastName?: string;
}
interface MemberData extends LazyMemberData {
mobilePhone?: string;
homePhone?: string;
workPhone?: string;
addressLine1?: string;
addressLine2?: string;
city?: string;
state?: string;
country?: string;
postalCode?: string;
birthday?: string; // date-only, YYYY-MM-DD
gender?: MemberGender; // "male"|"female"|"non_binary"|"other"|"prefer_not_to_say"
emergencyName?: string;
emergencyRelationship?: string;
emergencyPhone?: string;
emergencyEmail?: string;
referredBy?: string;
notes?: string;
alerts?: string;
accountCredit: number;
stripeCustomerID?: string;
mindbodyClientId?: string;
createdAt: Date;
updatedAt: Date;
}MemberGender/MemberGenderOptions are defined in types.ts (not member.ts) to avoid a circular import, and re-exported from member.ts. MemberGenderOptions is the shared display-label array — always use it over a hand-rolled enum list.
CreateMemberData mirrors MemberData's editable fields (all the contact/address/emergency/notes fields above) plus sendInvite?: boolean, which optionally emails the new member an account-linking invite.
Member.create(company: Company, members: CreateMemberData[]): Promise<EventData<string[]>>
member.save(): Promise<SimpleResult<EventData>> // PATCH; normalizes birthday to YYYY-MM-DD before sending
member.delete(): Promise<SimpleResult<EventData>>
Member.query(company: Company, data: QueryRequest): Promise<SimpleResult<Member[]>>Endpoint: /company/{cID}/members (/query for search).
member.user — lazily resolves the linked User account (undefined if userIDisn't set), cached in a private field for stable useData subscriptions./company/{cID}/members/{id}.member.name — convenience "First Last" string."undefined" for whichever name is unset.There is no profileKey column on members — the photo is tracked via the generic media system (resourceType: "user") instead:
member.UploadPhoto(file: File | Blob, contentType?: string): Promise<SimpleResult<EventData>> // default contentType "image/jpeg"
member.GetPhoto(): Promise<string | undefined> // temporary public download URL<img> on lazy MemberData — check the resolved photo URL instead.Thin delegates to the standalone functions in ./paymentMethod — prefer these instance methods (see Member-level PaymentMethod below for the underlying functions):
member.GetPaymentMethods(): Promise<SimpleResult<PaymentMethodData[]>>
member.SetupPaymentMethod(): Promise<SimpleResult<SetupPaymentMethodResult>>
member.DetachPaymentMethod(paymentMethodID: string): Promise<SimpleResult<EventData>>
member.SetDefaultPaymentMethod(paymentMethodID: string): Promise<SimpleResult<EventData>>Each queries a related resource type filtered by this member's id (the pattern used throughout the SDK, e.g. coupon.promotionalCodes(), staff.rates()):
| Method | Wraps |
|---|---|
member.groups(opts) | GroupMember.query, filtered by memberID |
member.checkouts(opts) | Checkout.query, filtered by memberID |
member.payments(opts) | Resolves the member's checkout ids first (up to 500), then Payment.query with checkoutID in [...] — payments has no memberID column |
member.invoices(opts) | Same checkout-id-resolution pattern as payments(), then Invoice.query with checkoutID in [...] |
member.credits(opts) | Credit.query, filtered by memberID |
member.subscriptions(opts) | Subscription.query, filtered by memberID |
member.accountCreditTransactions(opts) | AccountCreditTransaction.query, filtered by memberID |
member.documents(opts) | MemberDocument.query, filtered by memberID |
member.notifications(opts) | Notification.query, filtered by memberID |
member.visits(opts) | this.company.QueryVisits(...), filtered by memberID |
member.tagAssignments(opts) | TagAssignment.query, filtered by resourceType: "user" + resourceID: this.id |
member.classBookings(opts) | Booking.query, filtered by memberID |
Both payments() and invoices() are async (they await the checkout lookup first); the rest return the query promise directly.
member.AssignTag(tag: Tag): Promise<EventData<string[]>> // creates a "user"-scoped TagAssignment
member.createNewCheckout(): Promise<EventData<string[]>> // Checkout.create(company, [{ memberID: this.id }])company/member/visit.ts. A check-in event: a member passing through a specific door.
save() is always rejected.interface LazyVisitData {
id: string;
memberID: string;
companyID: string;
doorID: string;
locationID: string;
}
interface VisitData extends LazyVisitData {
createdAt: Date;
}Visit has no working create() static as an entry point in app code — check-ins go through the standalone CreateVisits() function (documented alongside Visitsince it's the only way to produce one):
interface CreateVisitData {
memberID: string;
doorID: string; // locationID is derived server-side from doorID, not supplied
}
CreateVisits({ company, visits: CreateVisitData[] }): Promise<SimpleResult<CheckInResponse>>Per the Introduction's CheckInResponse shape, check result.status:
200 — clean check-in.202 — check-in succeeded but the member has an alert on their profile (result.data.alerts[]) — always surface this to the operator, never treat it as failure.Credit for the visit's location before rejecting with a 403. See Credit below for the realm-scoping rules this draws from.Visit.query(company: Company, data: QueryRequest): Promise<SimpleResult<Visit[]>>
visit.save(): Promise<SimpleResult<EventData>> // always rejected — { status: 400, message: "Visits cannot be edited." }
visit.delete(): Promise<SimpleResult<EventData>>visit.door / visit.user — related-entity getters, cached in a private field.visit.VisitDate: Date — createdAt, falling back to new Date()if data hasn't loaded.visit.IsToday: boolean — whether VisitDate falls on today's calendar day.company.QueryVisits(opts: QueryRequest) — convenience wrapper delegating to Visit.query.Endpoint: /company/{cID}/visits (/query for search).
company/member/credit.ts. A member's balance of usable credits — normally granted by selling a credits-type product Feature via checkout (see Commerce), or by a manual staff grant via Credit.create().
A credit scopes to at most one realm — class (classID/classTagID), service (serviceID/serviceTagID), or door/location (doorID/locationID) — or none of the six fields, in which case it's fully generic and usable anywhere.
validateCreditRealm() at every write path (feature creation, manual grants) — the frontend does not re-validate this.interface LazyCreditData {
id: string;
memberID: string;
companyID?: string;
sourceCheckoutID?: string;
sourceFeatureID?: string;
sourceInvoiceID?: string;
classID?: string;
classTagID?: string;
serviceID?: string;
serviceTagID?: string;
doorID?: string;
locationID?: string;
amount?: number | null; // null/unset = unlimited, no usage cap, never exhausted
usedAmount: number;
expiresAt?: Date;
giftable: boolean;
originalMemberID: string | null;
}
interface CreditData extends LazyCreditData {
createdAt: Date;
updatedAt: Date;
}CreateCreditData (payload for the manual staff credit-grant path) accepts all realm fields directly, plus memberID, amount? (omit/null for unlimited), usedAmount?, expiresAt?, giftable? (defaults to false server-side) — so door/service punch-passes can be handed out without a checkout. MemberUpdateCreditData (Pick<LazyCreditData, "id" | "memberID">) is the shape a non-staff member may PATCH on their own credit — just the recipient, via giftCredit() below.
Credit.create(company: Company, credits: CreateCreditData[]): Promise<EventData<string[]>>
credit.save(): Promise<SimpleResult<EventData>> // PATCH — amount, usedAmount, expiresAt, etc.
credit.delete(): Promise<SimpleResult<EventData>>
Credit.query(company: Company, data: QueryRequest): Promise<SimpleResult<Credit[]>>Endpoint: /company/{cID}/members/credits (/query for search).
All cached in a private field for stable useData subscriptions:
| Getter | Resolves from |
|---|---|
credit.user | memberID → Member |
credit.checkout | sourceCheckoutID → Checkout |
credit.feature | sourceFeatureID → Feature |
credit.tag | classTagID → Tag |
credit.class | classID → Class |
credit.service | serviceID → Service |
credit.serviceTag | serviceTagID → Tag |
credit.door | doorID → Door |
credit.location | locationID → Location |
credit.IsUnlimited: boolean // amount == null
credit.uses(opts: QueryRequest): Promise<SimpleResult<CreditUse[]>> // CreditUse.query filtered by creditID
credit.ApplyToBooking(booking: Booking): Promise<EventData<string[]>>
credit.giftCredit(recipientMemberID: string): Promise<SimpleResult<EventData>>ApplyToBooking() manually attributes this credit to an unpaid class booking — the credit-based counterpart to paying for a booking via checkout — by calling CreditUse.create() under the hood. Only eligible for class-realm or fully generic credits; service- and door/location-scoped credits are rejected server-side.
giftCredit() reassigns the credit to another member (e.g. "bring a guest"). Requires giftable: true; fails 400 if not giftable or the recipient doesn't exist, 403if the caller isn't authorized to reassign it. On success it clears the cached _user getter and updates local data via setData.
member.GetListOfCredits-style access is via member.credits(opts) (see Member above), the convenience wrapper used by UserDisplay's Credits sub-resource list in the app.
company/member/credit/use.ts. A reversible redemption record tying a Credit to the class booking or appointment it was applied to.
interface CreditUseData {
id: string;
creditID: string;
classInstanceID?: string;
classBookingID?: string;
appointmentID?: string;
usedAt?: string | null;
}Class-realm credits (classID/classTagID) apply to a class booking via classBookingID; service-realm credits (serviceID/serviceTagID) apply to an appointment via appointmentIDinstead. Door/location-scoped credits aren't eligible for either and have no CreditUse flow.
interface CreateCreditUseData {
creditID: string;
classBookingID?: string;
appointmentID?: string;
}CreditUse.create(company: Company, uses: CreateCreditUseData[]): Promise<EventData<string[]>>
creditUse.save(): Promise<SimpleResult<EventData>> // PATCH, e.g. usedAt
creditUse.delete(): Promise<SimpleResult<EventData>> // reverses a single attribution
CreditUse.deleteMany(company: Company, ids: string[]): Promise<SimpleResult<EventData>> // bulk reversal
CreditUse.query(company: Company, data: QueryRequest): Promise<SimpleResult<CreditUse[]>>Endpoint: /company/{cID}/members/credits/uses (/query for search). Detail route: /company/{cID}/creditUses/{id}.
Prefer Credit.ApplyToBooking() over calling CreditUse.create() directly for the booking case.
IsUnlimited), and be class-realm or fully generic (service- and door/location-scoped credits are excluded).usedAmount and reverts the associated booking to unpaid only if it's still sitting at booked— left alone if it's since moved on (checked in, canceled, etc.). There is no deletedAt on credit_uses — this is a real hard delete, not the soft-delete pattern used by most other resources in this SDK.Getters creditUse.instance / .booking / .appointment resolve ClassInstance/Booking/Appointment from the corresponding id fields, each cached in a private field.
company/member/document.ts. A single issued document for a company user — generated automatically when a document-type product Feature resolves (see Commerce), never hand-created. Its template (DocumentTemplate — the reusable waiver/agreement definition) is documented in Company; MemberDocument is a signed instance of that template for one member, tracking the merge-filled PDF, its signature state, and (once signed) the final signed PDF.
type MemberDocumentStatus = "unsigned" | "signed" | "void";
interface LazyMemberDocumentData {
id: string;
companyID: string;
memberID: string;
documentTemplateID: string;
status: MemberDocumentStatus;
}
interface MemberDocumentData extends LazyMemberDocumentData {
sourceCheckoutID?: string | null;
sourceFeatureID?: string | null;
sourceInvoiceID?: string | null;
s3Key: string; // merge-filled, unsigned PDF
signedS3Key?: string | null; // final PDF, written once signed
signedAt?: Date | null;
signerName?: string | null;
signerIPAddress?: string | null;
createdAt: Date;
updatedAt: Date;
}unsigned also serves as the terminal state for templates with requiresSignature: false. sourceCheckoutID/sourceFeatureID/sourceInvoiceID trace the purchase that generated the document (all unset if issued another way).
Mirrors AccountCreditTransaction's read-only pattern — create/save/delete are all rejected (400). The only mutation is Sign():
MemberDocument.query(company: Company, data: QueryRequest): Promise<SimpleResult<MemberDocument[]>>
// create/save/delete all reject with { status: 400, message: "..." }
document.Sign(signatureImage: string, signerName: string): Promise<SimpleResult<EventData>>
document.GetDownloadUrl(): Promise<string | undefined>Sign() posts { memberDocumentID, signatureImage, signerName } to /members/documents/sign, which stamps the captured signature (e.g. a PNG data URL from a signature pad) onto the unsigned PDF, flattens the form, and marks the document signed.
reload() (or swap in a fresh instance, matching StaffTimeBlock's Clock Out resource-swap trick) to pick up the resulting signedS3Key/status/signedAt.GetDownloadUrl() resolves whichever of s3Key/signedS3Key is current to a temporary public download URL via POST /media/download-url with { type: "member-document", memberDocumentID }.
Non-staff callers querying documents are scoped server-side to their own memberID rows. Getters: document.member, .documentTemplate, .checkout, .feature, .invoice (each cached in a private field); IsUnsigned/IsSigned/IsVoid status helpers. member.documents(opts) is the parent → children convenience wrapper. Endpoint: /company/{cID}/members/documents.
company/member/notification.ts. A read-only notification for a Member, written exclusively by the backend (createNotification(), mirroring logEvent()).
interface LazyNotificationData {
id: string;
companyID: string;
memberID: string;
type: string;
title: string;
isRead: boolean;
}
interface NotificationData extends LazyNotificationData {
message: string;
resourceType?: string; // polymorphic — not a typed FK
resourceID?: string;
readAt?: Date;
createdAt: Date;
updatedAt: Date;
}create/save/delete are all rejected (400), mirroring CompanyEvent/AccountCreditTransaction. The one exception is MarkRead():
Notification.query(company: Company, data: QueryRequest): Promise<SimpleResult<Notification[]>>
notification.MarkRead(): Promise<SimpleResult<EventData>> // idempotent — no-ops if already readMarkRead() hits the cross-company /users/notifications/markRead endpoint (mirrors GET /users/notifications) rather than a company-scoped URL, since ownership is resolved server-side from the caller's own memberID rows, not a companyID path param. On success it updates local data via setData({ ..., isRead: true, readAt: new Date() }).Getters: notification.member (cached); IsRead/IsUnread booleans. member.notifications(opts) is the parent → children convenience wrapper. company.QueryNotifications(opts) also wraps Notification.query. Endpoint: /company/{cID}/notifications.
company/member/paymentMethod.ts. Standalone functions for a Member's on-file Stripe payment methods — distinct from the company's own on-file payment method used for platform subscription billing (company/paymentMethod.ts, see Company).
interface PaymentMethodData {
id: string;
brand?: string;
last4?: string;
expMonth?: number;
expYear?: number;
isDefault: boolean;
}
interface SetupPaymentMethodResult {
clientSecret: string;
setupIntentID: string;
}
ListPaymentMethods(company: Company, userID: string): Promise<SimpleResult<PaymentMethodData[]>>
SetupPaymentMethod(company: Company, userID: string): Promise<SimpleResult<SetupPaymentMethodResult>>
DetachPaymentMethod(company: Company, userID: string, paymentMethodID: string): Promise<SimpleResult<EventData>>
SetDefaultPaymentMethod(company: Company, userID: string, paymentMethodID: string): Promise<SimpleResult<EventData>>SetupPaymentMethod starts a Stripe SetupIntent — clientSecret/setupIntentID are used to confirm it client-side (Stripe Elements).
member.GetPaymentMethods(), member.SetupPaymentMethod(), member.DetachPaymentMethod(), member.SetDefaultPaymentMethod() delegate to these functions but are the intended public interface. The instance methods build their own endpoint URLs with ?id=/body { id } rather than calling these functions directly; both hit the same /company/{cID}/members/paymentMethods* routes.company/member/subscription.ts. A company user's subscription to a product — distinct from the company's own Stripe billing plan (company/subscription.ts's CompanySubscriptionData/company.GetSubscription() etc., documented in Company).
Subscription (this class) is the member-facing one; company.GetSubscription() is the platform-billing one.type SubscriptionStatus = "active" | "past_due" | "canceled" | "paused" | "incomplete" | "trialing" | "unpaid";
interface SubscriptionData {
id: string;
checkoutID: string;
companyID: string;
memberID: string;
stripeSubscriptionID: string;
productID?: string | null;
status: SubscriptionStatus;
currentPeriodStart?: number | null; // unix seconds
currentPeriodEnd?: number | null; // unix seconds
cancelAtPeriodEnd: boolean;
cancelAt?: number | null; // unix seconds
canceledAt?: Date | null;
trialStart?: number | null; // unix seconds
trialEnd?: number | null; // unix seconds
createdAt?: Date | null;
updatedAt?: Date | null;
deletedAt?: Date | null;
}currentPeriodStart/currentPeriodEnd/cancelAt/trialStart/trialEnd are unix seconds (Stripe's native format), not Date — convert with the exported helper:function toDate(unixSeconds: number | null): Date | nullsubscription.save(): Promise<SimpleResult<EventData>> // PATCH, e.g. cancelAtPeriodEnd
Subscription.create(): Promise<SimpleResult<string[]>> // ALWAYS rejects: { status: 400, message: "Cannot create subscription please sell a product." }
subscription.delete(): Promise<SimpleResult<EventData>> // cancels the subscription
Subscription.query(company: Company, data: QueryRequest): Promise<SimpleResult<Subscription[]>>Subscription row is created only as the side effect of selling a subscription-type product through checkout.Getters subscription.product / subscription.user lazily resolve and cache the related Product/Member. member.subscriptions(opts) is the parent → children convenience wrapper. Endpoint: /company/{cID}/members/subscriptions.
company/member/accountCreditTransaction.ts. A read-only ledger entry for a Member.accountCredit balance change. Written exclusively by the backend (member create/update, checkout processing) — mirrors CompanyEvent's read-only pattern (see Company).
type AccountCreditTransactionSource = "admin_adjustment" | "checkout_payment";
interface LazyAccountCreditTransactionData {
id: string;
companyID: string;
memberID: string;
amount: number;
balanceAfter: number;
source: AccountCreditTransactionSource;
}
interface AccountCreditTransactionData extends LazyAccountCreditTransactionData {
checkoutID?: string; // only set for "checkout_payment" entries
invoiceID?: string; // only set for "checkout_payment" entries
createdByUserID?: string; // only meaningful for "admin_adjustment" entries
note?: string; // only meaningful for "admin_adjustment" entries
createdAt: Date;
updatedAt: Date;
}AccountCreditTransaction.query(company: Company, data: QueryRequest): Promise<SimpleResult<AccountCreditTransaction[]>>
// create/save/delete all reject with { status: 400, message: "..." }Getters: transaction.member, .checkout, .invoice, .createdByUser (each cached in a private field); IsCredit/IsDebit booleans over amount's sign. Detail route: /company/{cID}/accountCreditTransactions/{id}. member.accountCreditTransactions(opts)is the parent → children convenience wrapper (surfaced as a Credit History sub-resource list on the app's UserDisplay). Endpoint: /company/{cID}/members/accountCredit/transactions.