SDK Reference

Commerce

The coupon/discount system (company/coupon/), the package-bundle system (company/package/), Product and its Feature union, and the standalone GroupPricing class.

Coupon system

Four classes work together, all under company/coupon/:

  • Coupon (coupon.ts) — the reusable discount definition (percent off, duration).
  • PromotionalCode (promotionalCode.ts) — a specific redemption code tied to a coupon, optionally scoped to one member.
  • Discount (discount.ts) — a record that a coupon was actually applied somewhere (a product, package, subscription, or checkout).
  • CouponModifier (modifier.ts) — scopes a coupon to a specific target (product/service/class/tag) so it only applies there.

Coupon

Mirrors Stripe's coupon duration semantics.

FieldTypeNotes
id, companyIDstring
namestring?
percentOffnumber
durationCouponDuration = "once" | "forever" | "repeating""once" = first invoice only, "forever" = life of subscription, "repeating" = durationInMonths months
durationInMonthsnumber?only meaningful when duration === "repeating"
timesRedeemednumber
stripeCouponIDstring?full data only

Full data (CouponData) adds createdAt/updatedAt over the lazy shape.

ts
static create(company: Company, coupons: CreateCouponData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<Coupon[]>>

CreateCouponData = { name: string; percentOff: number; duration: CouponDuration; durationInMonths?: number }.

Endpoint: /company/{cID}/coupons. viewHref: /company/{cID}/coupons/{id}.

Instance methods — the parent → children convenience-wrapper pattern used across the SDK:

ts
coupon.promotionalCodes(opts: QueryRequest) // PromotionalCode.query filtered by couponID
coupon.modifiers(opts: QueryRequest)        // CouponModifier.query filtered by couponID
coupon.discounts(opts: QueryRequest)        // Discount.query filtered by couponID

PromotionalCode

A specific redemption code tied to a couponID, optionally scoped to one memberID.

FieldTypeNotes
id, companyID, couponIDstring
memberIDstring | null?optional single-member scoping
codestring
activeboolean
maxRedemptionsnumber | null?
timesRedeemednumber
restrictionFirstTimeTransationbooleanfield name as written in source (typo'd — missing the second s in Transaction; kept verbatim since it's the literal wire field)
restrictionMinimumAmountnumber
expiresAtDate | null?
stripePromotionCodeIDstring?full data only
ts
static create(company: Company, codes: CreatePromotionalCodeData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<PromotionalCode[]>>

Endpoint: /company/{cID}/promotionalCodes. viewHref: /company/{cID}/coupons/codes/{id}.

promotionalCode.coupon getter — lazily resolves and caches the parent Coupon from couponID.

ts
promotionalCode.discounts(opts: QueryRequest)
Gotcha:Filters Discount.query by the code's own couponID — not a promotionalCodeID filter, since Discount (as modeled client-side pre-2024) originally had no promotionalCodeIDfield. This means it returns all discounts recorded against the code's parent coupon, not only ones redeemed through this specific code.

Discount

A read-only-ish record of a coupon applied somewhere: exactly one of productID (required), packageID/subscriptionID/checkoutID/couponID/promotionalCodeID is meaningfully set to say where it landed.

FieldTypeNotes
id, companyIDstring
productIDstring | null?required per the interface doc comment, though typed optional
packageIDstring | null?
subscriptionIDstring | null?
checkoutIDstring | null?
couponIDstring | null?
promotionalCodeIDstring | null?
amountOffnumber
stripeDiscountIDstring?full data only
ts
static create(company: Company, products: CreateDiscountData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<Discount[]>>
Note:Static creator is create (lowercase), matching the other coupon classes — the products parameter name on create() is a copy/paste leftover, not a hint about the payload shape (CreateDiscountData has no products field).

Endpoint: /company/{cID}/discounts. viewHref: /company/{cID}/discounts/{id}.

CouponModifier

Scopes a Coupon to a specific target: exactly one of productID/serviceID/classID/tagID is set.

FieldTypeNotes
id, companyID, couponIDstring
productIDstring | null?
serviceIDstring | null?
classIDstring | null?
tagIDstring | null?
ts
static create(company: Company, coupons: CreateCouponModifierData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<CouponModifier[]>>

Static creator is create (lowercase) — all four coupon classes use the same lowercase create/query casing, unlike some other domain classes elsewhere in the SDK that use Create/Query.

Endpoint: /company/{cID}/couponModifiers. viewHref: /company/{cID}/coupons/modifiers/{id}.

Package system

A bundle of products/services/class instances sold together at one price, split across two classes under company/package/. The relationship is one-directional: PackageLines point back at their Package via packageID, but neither Product nor Checkout references Package back.

Package

FieldTypeNotes
id, companyIDstring
namestring | null?
pricenumberthe actual sale price of the bundle
descriptionstring | null?full data only
valuedPricenumberfull data only — pre-discount sum of the lines' standalone prices, shown alongside price so the savings are visible
ts
static create(company: Company, packages: CreatePackageData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<Package[]>>

CreatePackageData = { name?, description?, valuedPrice?, price? }.

Endpoint: /company/{cID}/packages. Has icon (lucide Package) and viewHref: /company/{cID}/packages/{id}.

ts
pkg.lines(opts: QueryRequest) // PackageLine.query filtered by packageID

PackageLine

One bundled item within a Package — references a Product, Service, or ClassInstance.

FieldTypeNotes
idstring
packageIDstring | null?
productIDstring | null?exactly one of productID/serviceID/instanceID is normally set
serviceIDstring | null?
instanceIDstring | null?
quantitynumber
amountnumber
discountnumberfull data only — amount shaved off this line's standalone price when sold as part of the bundle

CreatePackageLineData = Pick<LazyPackageLineData, "quantity"> & { packageID: string; productID?; serviceID?; instanceID?; amount?; discount? }. instanceID is modeled on the type but not exposed in the create form — that form only lets a caller pick Product or Service.

ts
static create(company: Company, lines: CreatePackageLineData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<PackageLine[]>>

Endpoint: /company/{cID}/packages/lines.

Related-entity getters, each cached in a private field so the same instance is returned on every call:

ts
line.package  // Package | undefined, from packageID
line.product  // Product | undefined, from productID
line.service  // Service | undefined, from serviceID
line.instance // ClassInstance | undefined, from instanceID

Product

A sellable item (physical or plan). company/product/product.ts. What actually happens on purchase — one-time charge, subscription, group grant, tag grant, credits, document generation — is layered on separately via Feature rows, not stored directly on the product.

FieldTypeNotes
id, companyIDstring
namestring
descriptionstring?
visibleOnlineboolean
pricenumber?"Sticker" field only — actual purchase pricing lives on the product's pricing-type Feature(s)
skustring?
stocknumber?
stripeProductIDstring?
createdAt / updatedAtDatepresent on LazyProductData itself — ProductData is currently identical to it, kept as its own type for parity with other resources' lazy/full split
ts
static create(company: Company, products: CreateProductData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<Product[]>>

CreateProductData = Pick<LazyProductData, "name" | "visibleOnline"> & { description?, price?, stock?, sku? } — a product can be created bare (name + visibility) with pricing/behavior added afterward via Feature rows.

Endpoint: /company/{cID}/products. Has icon (lucide Package) and viewHref: /company/{cID}/products/{id}.

Parent → children convenience wrappers (same pattern as coupon.promotionalCodes()):

ts
product.features(opts: QueryRequest)                              // Feature.query filtered by productID
product.GetFeatures({ limit?, offset?, lazy? })                    // paginated-defaults variant, same filter
product.GetSubscriptions({ limit?, offset?, lazy? })                // Subscription.query filtered by productID
product.GetDiscounts({ limit?, offset?, lazy? })                    // Discount.query filtered by productID
product.GetModifiers({ limit?, offset?, lazy? })                    // CouponModifier.query filtered by productID
product.GetListOfTagAssignments({ limit?, offset?, lazy? })         // TagAssignment.query, resourceType "product"

Media (product photo, via the generic per-resource media system — see the overview page):

ts
product.UploadPhoto(file: File | Blob, contentType?: string)
product.GetPhoto(): Promise<string | undefined>

Tagging:

ts
product.AssignTag(tag: Tag) // creates a TagAssignment { tagID, resourceID: this.id, resourceType: "product" }

Feature

company/product/feature.ts. A purchase-time effect attached to a Product — grants pricing, group membership, a tag, credits, or a generated document when the product is bought. A single product can carry several features at once (e.g. a pricing feature plus a group feature that grants access when that price resolves).

ts
export type FeatureType = "pricing" | "group" | "tag" | "credits" | "document";
export type BillingUnit = "day" | "week" | "month" | "year"; // reused by PayrollSchedule

FeatureData is a client-side flattening of the discriminated union below into one shape — only the field group matching featureType is meaningful for a given row. CreateFeatureDatamirrors this: one flat "optional everything" interface (not a true TS union) keyed by featureType, so callers populate only the fields relevant to the chosen type; Feature.create/save branch on featureType internally to pick out the relevant subset before sending to the backend — passing fields for the wrong type is a silent no-op, not a type error.

Variant shapes

Note:Group/tag/credits/document features share one expiry model, driven by two optional fields — resolveAfterAmount/resolveAfterUnit and permanent — that resolve to exactly one of three mutually exclusive modes: (1) resolveAfterAmount/resolveAfterUnit set → expires a fixed duration after grant; (2) permanent: true→ never expires, and is skipped from invoice tracking entirely regardless of any recurring pricing on the product; (3) neither set (the default) → tracks the earliest current period-end among the product's recurring pricing features. A PricingFeature itself has no permanent field — it can only be immediate (neither field set) or resolve-after — and a DocumentFeature has no resolveAfterAmount/resolveAfterUnit — it only expires (i.e. gets re-issued) by tracking or is permanent.

PricingFeature (featureType: "pricing") — sets the price (one-time or recurring) the buyer pays.

FieldType
amountnumber
stripePriceIDstring
recurring{ unit: BillingUnit; amount: number }?
resolveAfterAmountnumber | null?
resolveAfterUnitBillingUnit | null?

GroupFeature (featureType: "group") — adds the buyer to groupID on purchase.

FieldType
groupIDstring
resolveAfterAmount / resolveAfterUnitas above
permanentboolean | null?

TagFeature (featureType: "tag") — applies tagID to the buyer on purchase.

FieldType
tagIDstring
resolveAfterAmount / resolveAfterUnitas above
permanentboolean | null?

CreditsFeature (featureType: "credits") — grants a Credit on purchase. See Members for Credit's own realm rules, which this mirrors exactly.

FieldTypeNotes
classID / classTagIDstring?class realm
serviceID / serviceTagIDstring?service realm
doorID / locationIDstring?door/location realm
amountnumber | null?null/unset means the granted credit is unlimited — no usage cap, never exhausted
giftableboolean
resolveAfterAmount / resolveAfterUnitas above
permanentboolean | null?

The realm fields scope the credit to exactly one of class/service/door-location, or none for a fully generic credit — enforced server-side by validateCreditRealm(); the frontend does not re-validate this.

DocumentFeature (featureType: "document") — generates a MemberDocument from documentTemplateID for the buyer on purchase.

FieldType
documentTemplateIDstring
permanentboolean | null?

No resolveAfterAmount/resolveAfterUnit— unlike group/tag/credits grants, a document doesn't expire on a fixed duration. It's still subject to the other two expiry modes shared with the rest of the feature types though: permanent: truemeans the issued document is never re-issued regardless of any recurring pricing on the product, and unset (the default) means it's re-issued whenever the earliest current period-end among the product's recurring pricing features rolls over.

Methods

ts
static async create(company: Company, features: CreateFeatureData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static async query(company: Company, data: QueryRequest): Promise<SimpleResult<Feature[]>>

Feature.create accepts an array (the underlying param is always an array — pass a single-element array for one feature):

ts
await Feature.create(company, [{
  featureType: "pricing",
  productID: product.id,
  amount: 49.99,
  recurring: { unit: "month", amount: 1 },
  resolveAfterAmount: 1,
  resolveAfterUnit: "month",
}]);

await Feature.create(company, [{ featureType: "group", productID: product.id, groupID: "..." }]);
await Feature.create(company, [{ featureType: "tag", productID: product.id, tagID: "..." }]);

// credits: grants a Credit scoped to at most one realm — class, service,
// or door/location — or none for a fully generic credit
await Feature.create(company, [{ featureType: "credits", productID: product.id, classID: "..." }]);
await Feature.create(company, [{ featureType: "credits", productID: product.id, serviceTagID: "..." }]);

await Feature.create(company, [
  { featureType: "pricing", productID: product.id, amount: 49.99 },
  { featureType: "group", productID: product.id, groupID: "..." },
]);

Endpoint: /company/{cID}/products/features. save()/create() both branch on f.featureType, guarding each non-pricing variant's required field (groupID/tagID/documentTemplateID) with a 400 short-circuit if missing.

Type guards

ts
export function isPricingFeature(f: FeatureData): f is FeatureData & PricingFeature
export function isGroupFeature(f: FeatureData): f is FeatureData & GroupFeature
Gotcha:Only these two guards currently exist — there is no isTagFeature/isCreditsFeature/isDocumentFeature; narrow those manually via f.featureType === "tag" etc.

Display metadata

BillingUnitOptions — label/value pairs for BillingUnit ("Day"|"Week"|"Month"|"Year").

FeatureTypeOptions — one entry per FeatureType with title, value, icon (a rendered lucide icon element), and description, used by the feature-type picker in the create-feature form.

Convenience accessors

Return falsy/empty defaults for non-pricing features rather than throwing:

ts
feature.IsRecurring: boolean       // !!this._data?.recurring
feature.RecurringUnit: BillingUnit | ""
feature.RecurringAmount: number    // defaults to 0

Related-entity getters

Each cached in a private field:

ts
feature.product         // Product | undefined, from productID
feature.group           // Group | undefined, from groupID

GroupPricing

company/groupPricing.ts. A standalone class that replaced the old per-class AddGroupPricing/GetGroupPricing/RemoveGroupPricing methods, shared across the class/service pricing types.

Gotcha:Product has no group-pricing relationship at all — only Class/Service do.
FieldTypeNotes
idstring
companyIDstring?
groupIDstring
classIDstring?exactly one of classID/serviceID is set — determines which pricing realm/endpoint this row targets
serviceIDstring?
discountTypeDiscountType = "free" | "percent" | "fixed"free waives the price entirely
discountAmountnumber?required (in practice) for percent/fixed
ts
IsServicePricing(): boolean // this._data?.serviceID != undefined
IsClassPricing(): boolean   // this._data?.classID != undefined
ts
static async create(company: Company, pricingType: "services" | "classes", data: CreateGroupPricingData[]): Promise<SimpleResult<EventData>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static async query(company: Company, data: QueryRequest): Promise<SimpleResult<GroupPricing[]>>
Gotcha:create() requires the explicit pricingType parameter (since a not-yet-constructed row has no classID/serviceID to infer the endpoint from), while save()/delete()/fetch() derive it from the instance's own data via a protected GetTypeEndpoint() helper — which throws if neither classID nor serviceID is set on the instance. query() itself posts to the endpoint-agnostic /company/{cID}/groupPricing (no services/classes segment), unlike create/save/delete/fetch.

Endpoints: /company/{cID}/services/groupPricing or /company/{cID}/classes/groupPricing (create/save/delete/fetch, chosen per-row); /company/{cID}/groupPricing (query).

Related-entity getters, each cached in a private field:

ts
pricing.group    // Group | undefined, from groupID
pricing.service   // Service | undefined, from serviceID
pricing.class     // Class | undefined, from classID