Supercommerce API Docs
Full Module Docs

Vendor Module

HTTP surface for vendor onboarding (a user applies to become a vendor; an admin reviews + approves/rejects) and vendor directory reads (admin lists all approved vendors with team…

HTTP surface for vendor onboarding (a user applies to become a vendor; an admin reviews + approves/rejects) and vendor directory reads (admin lists all approved vendors with team membership).

Source: api-modules/vendor (registered via VendorModule.forRoot() in apps/api/src/app.module.ts).

Approving an application provisions a Better-Auth organization (the vendor's tenant within the platform) and creates the vendor profile row. The applying user is added as the organization's first member; the session token they renew next will surface the new activeOrganizationId, which downstream modules consume via resolveActiveVendorId(session).


Conventions

Authentication

Endpoint groupAuthPermission
POST /vendor/applicationsrequired (any logged-in user)
GET /vendor/applications/myrequired (any logged-in user)
GET /admin/vendor/applications, GET /admin/vendor/applications/:idrequiredvendor: view
POST /admin/vendor/applications/:id/approve|rejectrequiredvendor: approve
GET /admin/vendors, GET /admin/vendors/:idrequiredvendor: view

The application submit endpoint requires a session because the applicant becomes the first member of the new organization — there is no "anonymous vendor signup" flow.

Response envelope

Successful responses are wrapped by ResponseInterceptor:

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* optional, e.g. pagination */ }
}

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND
409CONFLICT, UNIQUE_VIOLATION (slug already taken), pending application exists
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Lifecycle

vendor_application.status values:

StatusSet byReversible
pendingPOST /vendor/applicationsyes — admin acts on it
approvedPOST /admin/vendor/applications/:id/approveno
rejectedPOST /admin/vendor/applications/:id/rejectno — applicant must submit a new application

A user may have at most one application in pending. Submitting again while a pending one exists returns 409 CONFLICT. Rejected applications stay on file; a fresh submission creates a new row.


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 underlying Better-Auth organization joined with the vendor profile and a team-member count. Exact shape lives in VendorProfileService.findAll / findByIdWithDetails — at minimum:

type VendorProfileSummary = {
  id: string;                      // == organization id (the vendor id)
  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;
  }>;
};

Vendor — applications

Base path: /vendor/applications. Requires a logged-in user.

POST /vendor/applications — Submit an application

The applicant's userId and userEmail are taken from the session; the body carries business details only.

Body

{
  "businessName": "Acme Bakery",
  "slug": "acme-bakery",
  "businessEmail": "hello@acme-bakery.example",
  "businessPhone": "+919876543210",
  "businessDescription": "Artisan sourdough since 2018."
}
FieldTypeConstraints
businessNamestring1..255 chars
slugstring1..255 chars; lowercase alphanumeric + hyphens; uniqueness checked at approval time, not submission
businessEmailstringRFC 5322 email
businessPhonestring1..50 chars
businessDescriptionstring1..2000 chars

Response 201ApplicationResponse with status="pending".

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod (invalid email, slug regex, etc.)
409CONFLICTUser already has a pending application

GET /vendor/applications/my — My application history

Returns every application the caller has submitted (ordered most recent first). Use this to show a "Resubmit" prompt after a rejection or to surface review status to the applicant.

Response 200 — array of ApplicationResponse.


Admin — applications

Base path: /admin/vendor/applications. Permissions on the vendor:* resource.

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).

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/vendor/applications/:id/approve — Approve

Required permission: vendor: approve. Allowed only from pending.

Side effects

  • Validates slug uniqueness against the organization slug column. On collision → 409 UNIQUE_VIOLATION.
  • Creates a new Better-Auth organization with slug = application.slug, name = businessName.
  • Adds the applicant as the organization's owner member.
  • Creates the vendor_profile row pointing at the organization.
  • Stamps status="approved", reviewedBy, 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 (re-login or hit /api/auth/session/refresh) so Better-Auth surfaces the new activeOrganizationId.

Response 200 — updated ApplicationResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown application id
409CONFLICTApplication is not pending
409UNIQUE_VIOLATIONSlug taken since submission (admin should bounce back to applicant for a new slug)

POST /admin/vendor/applications/:id/reject — Reject

Required permission: vendor: approve. Allowed only from pending.

Body

{ "reason": "Required documents not provided" }
FieldConstraints
reason1..2000 chars

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

StatusCodeWhen
404NOT_FOUNDUnknown application id
409CONFLICTApplication is not pending

Admin — 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.


GET /admin/vendors/:id — Vendor detail

Required permission: vendor: view. Returns the full VendorProfileSummary with members[] populated.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown vendor id

Domain events

Emitted via EventEmitter2. Listeners include the notifications module (onboarding email, rejection email).

EventFired when
vendor.application.submittedPOST /vendor/applications
vendor.application.approvedPOST /admin/vendor/applications/:id/approve
vendor.application.rejectedPOST /admin/vendor/applications/:id/reject

  • auth — owns the Better-Auth user + organization + member tables. Approval here creates an organization row.
  • admin-rbac — provides PermissionsGuard + vendor:view / vendor:approve permissions. See admin-rbac.md.
  • settings — vendor self-service settings (shipping, tax, etc.) are scoped to the organization id created here. See settings.md.
  • notifications — consumes the vendor.application.* events to email the applicant. See notifications.md.

On this page