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…
HTTP surface for the platform-admin side of vendor onboarding: list / inspect vendor applications, approve them (provisioning a Better-Auth organization + vendor profile) or reject them with a reason; and list / inspect existing approved vendors via the vendor directory. The vendor-self application submit / "my applications" endpoints live in the same module but are out of scope here (see vendor/vendor.md).
Source:
api-modules/vendor/src/controllers/vendor-admin.controller.ts,api-modules/vendor/src/controllers/vendor-management.controller.ts. The vendor-selfvendor-application.controller.tsbelongs to the vendor folder, not this one.Approving an application provisions a Better-Auth organization (the vendor's tenant within the platform) and creates the vendor profile row. The applicant becomes the organization's first
ownermember.
Conventions
Authentication
All endpoints require a Better-Auth admin session and a role granting the matching vendor:* permission.
| Endpoint group | Permission |
|---|---|
GET /admin/vendor/applications, GET /admin/vendor/applications/:id | vendor: view |
POST /admin/vendor/applications/:id/approve, POST /admin/vendor/applications/:id/reject | vendor: approve |
GET /admin/vendors, GET /admin/vendors/:id | vendor: view |
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 |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN |
| 404 | NOT_FOUND |
| 409 | CONFLICT, UNIQUE_VIOLATION (slug taken at approval time) |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Application lifecycle
vendor_application.status values:
| Status | Set by | Reversible |
|---|---|---|
pending | POST /vendor/applications (vendor-self) | yes — admin acts on it |
approved | POST /admin/vendor/applications/:id/approve | no |
rejected | POST /admin/vendor/applications/:id/reject | no — applicant must submit a new application |
A user may have at most one application in pending (enforced on the vendor-self submit side).
Domain types
ApplicationResponse
type ApplicationStatus = "pending" | "approved" | "rejected";
type ApplicationResponse = {
id: string;
userId: string; // applicant's user id
businessName: string;
slug: string; // lowercase alnum + hyphens; unique on approval
businessEmail: string;
businessPhone: string;
businessDescription: string;
status: ApplicationStatus;
rejectionReason: string | null;
reviewedBy: string | null; // admin user id
reviewedAt: string | null; // ISO
createdAt: string;
updatedAt: string;
};Vendor profile (admin reads)
The admin vendor list returns the Better-Auth organization joined with the vendor profile plus a team-member count. Exact shape lives in VendorProfileService; at minimum:
type VendorProfileSummary = {
id: string; // == organization id (the vendor id used everywhere else)
businessName: string;
slug: string;
businessEmail: string;
businessPhone: string;
businessDescription: string;
memberCount: number;
createdAt: string;
updatedAt: string;
members?: Array<{ // only on the detail endpoint
userId: string;
email: string;
name: string;
role: string; // "owner" | "admin" | "member"
joinedAt: string;
}>;
};Applications
Base path: /admin/vendor/applications.
GET /admin/vendor/applications — List applications
Required permission: vendor: view. Standard QueryDto (page / limit / search / filters[]).
Response 200 — paginated envelope of ApplicationResponse[].
GET /admin/vendor/applications/:id — Application detail
Required permission: vendor: view. Returns ApplicationResponse plus the applicant's user details (name, email, image) — joined in the service.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
POST /admin/vendor/applications/:id/approve — Approve
Required permission: vendor: approve. Allowed only from pending.
Side effects
- Validates
sluguniqueness against the organization slug column. On collision → 409UNIQUE_VIOLATION. - Creates a new Better-Auth organization with
slug = application.slugandname = businessName. - Adds the applicant as the organization's
ownermember. - Creates the
vendor_profilerow pointing at the organization. - Stamps
status="approved",reviewedBy = current admin user id,reviewedAt. - Emits
vendor.application.approved(consumed by notifications — sends a welcome email).
The applicant's existing session does not automatically gain access to the new organization — they need to renew the session so Better-Auth surfaces the new activeOrganizationId.
Response 200 — updated ApplicationResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown application id |
| 409 | CONFLICT | Application is not pending |
| 409 | UNIQUE_VIOLATION | slug already taken by another organization since submission |
POST /admin/vendor/applications/:id/reject — Reject
Required permission: vendor: approve. Allowed only from pending.
Body
{ "reason": "Required documents not provided" }| Field | Type | Constraints |
|---|---|---|
reason | string | 1..2000 |
Side effects
- Stamps
status="rejected",rejectionReason,reviewedBy,reviewedAt. - Emits
vendor.application.rejected(consumed by notifications — sends an email with the reason).
Response 200 — updated ApplicationResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Empty / oversized reason |
| 404 | NOT_FOUND | Unknown application id |
| 409 | CONFLICT | Application is not pending |
Vendor directory
Base path: /admin/vendors.
GET /admin/vendors — List approved vendors
Required permission: vendor: view. Standard QueryDto. Returns approved vendors with profile + member count.
Response 200 — paginated envelope of VendorProfileSummary[] (without the members[] field).
GET /admin/vendors/:id — Vendor detail
Required permission: vendor: view. Returns the full VendorProfileSummary with members[] populated.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown vendor id |
Domain events
Emitted via EventEmitter2. Listeners include the notifications module (onboarding / rejection emails).
| Event | Fired when |
|---|---|
vendor.application.approved | POST /admin/vendor/applications/:id/approve |
vendor.application.rejected | POST /admin/vendor/applications/:id/reject |
Related modules
admin-rbac— gates every endpoint viavendor:view/vendor:approve. Seeadmin-rbac.md.auth— owns the Better-Authuser+organization+membertables. Approval creates an organization row.settings— vendor self-service settings (shipping, tax, etc.) are scoped to the organization id created here. The platform-admin override endpoints insettings.md/shipping.md/tax.mduse the same vendor id.notifications— consumes thevendor.application.*events to email the applicant.
Tax Module — Admin
HTTP surface for the platform-admin override of any vendor's flat-tax config — the list of tax lines (e.g. { type: "GST", rate: 18 }) that the flat tax provider applies to that…
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…