Cart Module — Vendor surface
Vendor-facing HTTP endpoints for browsing carts that contain the vendor's own products and for vendor-scoped cart funnel analytics. There are no write operations: a vendor cannot…
Vendor-facing HTTP endpoints for browsing carts that contain the vendor's own products and for vendor-scoped cart funnel analytics. There are no write operations: a vendor cannot mutate a customer's cart. Both surfaces are deliberately stripped of cross-vendor data, cart_token (the storefront bearer handle), and customer_id (PII) — a multi-vendor cart never leaks one vendor's volume to another.
Source:
api-modules/cart/src/controllers/vendor-cart.controller.ts,api-modules/cart/src/controllers/vendor-cart-analytics.controller.ts.
Conventions
Authentication
All endpoints require a Better-Auth bearer session.
Authorization: Bearer <session-token>The vendor scope is read from the session via resolveActiveVendorId(session) — it prefers session.session.activeOrganizationId (better-auth canonical) and falls back to activeVendorId. If neither is set, the request fails with 400 ACTIVE_VENDOR_REQUIRED.
Tenant scoping
Every read scopes to the active vendor's id:
GET /vendor/cartsonly returns carts that contain at least one line owned by the active vendor.GET /vendor/carts/:idreturns 404 when the cart exists but contains no lines owned by the active vendor — the cart's existence is itself confidential to non-participating vendors.- Analytics endpoints filter every metric to the active vendor's lines/events; the
vendorIdin each response carries the caller's id (nevernullon this surface).
Response envelope
Successful responses are wrapped by ResponseInterceptor:
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* optional, e.g. pagination */ }
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR, ACTIVE_VENDOR_REQUIRED |
| 401 | UNAUTHORIZED |
| 404 | NOT_FOUND |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Money
All monetary fields (vendorSubtotal, unitPrice, allocations[].amount, …) are integer subunits (paise / cents / eurocents) — single currency per tenant.
Domain types
CartVendorListItem
type CartVendorListItem = {
cartId: string;
status: "active" | "abandoned" | "converted" | "discarded";
platform: "WEB" | "IOS" | "ANDROID";
vendorLineCount: number; // ONLY this vendor's lines
vendorSubtotal: number; // ONLY this vendor's lines (subunits)
lastActivityAt: string; // ISO
createdAt: string; // ISO
};CartVendorView
Vendor-scoped projection of CartResponse. Strips cartToken, customerId, other vendors' bags, the global cartTotals, and limits appliedCouponAllocations to this vendor's allocation amount only.
type CartVendorView = {
cartId: string;
status: "active" | "abandoned" | "converted" | "discarded";
platform: "WEB" | "IOS" | "ANDROID";
vendorBag: {
vendorId: string;
lines: CartLineResponse[]; // see cart.md for full shape
subtotal: number; // subunits
discountAllocated: number; // subunits
totalBeforeShippingAndTax: number;
shipping: { cost: number } | null;
};
appliedCouponAllocations: Array<{
code: string;
amount: number; // allocation against THIS vendor (subunits)
}>;
lastActivityAt: string;
createdAt: string;
};FunnelResponse
type FunnelBucket = {
bucketAt: string; // ISO start-of-bucket
cartsCreated: number;
itemsAdded: number;
itemsRemoved: number;
couponsApplied: number;
giftsAttached: number;
checkoutsPrepared: number;
cartsAbandoned: number;
uniqueActiveCarts: number;
};
type FunnelResponse = {
granularity: "hourly" | "daily";
vendorId: string; // active vendor's id
buckets: FunnelBucket[];
totals: Omit<FunnelBucket, "bucketAt">;
};AbandonmentResponse
type AbandonmentResponse = {
vendorId: string;
cartsCreated: number;
cartsAbandoned: number;
checkoutsPrepared: number;
abandonmentRate: number; // abandoned / created, 0..1 with 2 decimals
checkoutRate: number; // prepared / created, 0..1 with 2 decimals
};TopVariantsResponse
type TopVariantsResponse = {
vendorId: string;
items: Array<{
variantId: string;
vendorId: string;
removedCount: number;
}>;
};Vendor cart browse
Base path: /vendor/carts.
GET /vendor/carts — List vendor's carts
Returns carts containing at least one line owned by the active vendor.
Query
| Name | Type | Default | Notes |
|---|---|---|---|
status | "active" | "abandoned" | "converted" | "discarded"? | — | Filter by cart lifecycle status |
customerId | string? | — | Filter to a specific customer |
vendorId | string? | — | Ignored / redundant — scope is already the active vendor |
activeSince | string? (ISO datetime) | — | Inclusive lower bound for last_activity_at |
limit | int | 50 | 1..500 |
offset | int | 0 | >= 0 |
Response 200 — paginated envelope of CartVendorListItem[].
Errors
| Status | Code | When |
|---|---|---|
| 400 | ACTIVE_VENDOR_REQUIRED | Session has no active vendor |
| 400 | VALIDATION_ERROR | Query fails zod (bad enum, out-of-range pagination) |
GET /vendor/carts/:id — Vendor-scoped detail
Returns a CartVendorView for a single cart.
Path params
| Name | Type | Notes |
|---|---|---|
id | string (UUID) | Must contain at least one line owned by the active vendor |
Response 200 — envelope of CartVendorView.
{
"data": {
"cartId": "01J9...",
"status": "active",
"platform": "WEB",
"vendorBag": {
"vendorId": "01J9...",
"lines": [ /* CartLineResponse[] for THIS vendor only */ ],
"subtotal": 125000,
"discountAllocated": 12500,
"totalBeforeShippingAndTax": 112500,
"shipping": { "cost": 4900 }
},
"appliedCouponAllocations": [
{ "code": "WELCOME10", "amount": 12500 }
],
"lastActivityAt": "2026-05-07T10:00:00.000Z",
"createdAt": "2026-05-07T09:00:00.000Z"
},
"message": "Success",
"statusCode": 200
}Errors
| Status | Code | When |
|---|---|---|
| 400 | ACTIVE_VENDOR_REQUIRED | Session has no active vendor |
| 404 | NOT_FOUND | Cart does not exist or contains no lines owned by the active vendor |
Vendor cart analytics
Base path: /vendor/cart-analytics. Every endpoint accepts the common range query and reports metrics filtered to the active vendor's lines/events.
Common query — AnalyticsRangeQuery
| Name | Type | Default | Notes |
|---|---|---|---|
from | string? (ISO datetime) | 30 days ago | Inclusive lower bound |
to | string? (ISO datetime) | now | Exclusive upper bound |
granularity | "hourly" | "daily" | "daily" | Bucket size |
GET /vendor/cart-analytics/funnel — Vendor-scoped funnel rollup
Response 200 — FunnelResponse (vendor's vendorId populated).
{
"data": {
"granularity": "daily",
"vendorId": "01J9...",
"buckets": [ /* FunnelBucket[] */ ],
"totals": { "cartsCreated": 1234, "itemsAdded": 5678, "itemsRemoved": 432, "couponsApplied": 89, "giftsAttached": 21, "checkoutsPrepared": 678, "cartsAbandoned": 345, "uniqueActiveCarts": 901 }
}
}GET /vendor/cart-analytics/abandonment — Abandonment + checkout rates
Response 200 — AbandonmentResponse.
GET /vendor/cart-analytics/top-abandoned-variants — Most-removed variants
Additional query
| Name | Type | Default | Notes |
|---|---|---|---|
limit | int | 10 | 1..100 |
Response 200 — TopVariantsResponse (items are scoped to variants owned by the active vendor).
GET /vendor/cart-analytics/coupon-usage — Coupons applied to carts containing my products
Response 200
{
"data": { "vendorId": "01J9...", "couponsApplied": 89 }
}GET /vendor/cart-analytics/gift-attach-rate — Free-gift attach rate
Response 200
{
"data": {
"vendorId": "01J9...",
"cartsCreated": 1234,
"giftsAttached": 21,
"rate": 0.02
}
}rate = giftsAttached / cartsCreated (0 when cartsCreated is 0), rounded to two decimal places.
Related modules
cart(full) — full storefront + admin cart surface, including theCartLineResponseandCartVendorBagshapes referenced above. Seecart.md.catalog— variant ownership is determined byproduct.vendorId; the vendor scope check uses these joins.order—checkoutsPreparedevents arrive at order place-time; admin analytics shares the same underlying rollup tables.
Vendor Module — Admin
HTTP surface for the platform-admin side of vendor onboarding: list / inspect vendor applications, approve them (provisioning a Better-Auth organization + vendor profile) or…
Catalog Module — Vendor surface
Vendor-facing CRUD over the vendor's own products, variants, options, and tabs, plus the vendor-side "request a new taxonomy entry" workflow (new brand, category, tag, or…