Supercommerce API Docs
Full Module Docs

Customer Module

HTTP surface for the customer-side address book (storefront) and an admin customer picker. The customer identity itself lives in Better-Auth's user table; this module owns the…

HTTP surface for the customer-side address book (storefront) and an admin customer picker. The customer identity itself lives in Better-Auth's user table; this module owns the shopping-related customer data on top of that.

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

The address book is read by the order module at place-order (for the shippingAddress / billingAddress snapshot) and by the cart for the address-bound shipping rate. The customer picker is consumed by admin features that need to pin a customer (e.g. admin-create review).


Conventions

Authentication

Endpoint groupAuthPermission
GET/POST/PATCH/DELETE /store/addresses/**required (customer)
GET /admin/customersrequireduser: list

Storefront endpoints scope every read and write to the session's user id. Cross-user ids return 404 Not Found, never 403 — so existence of someone else's address can't be inferred.

Response envelope

Successful responses are wrapped by ResponseInterceptor:

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

The admin picker uses the picker envelope (items[] + pinned[]) — see catalog.md.

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Domain types

AddressResponse

type AddressResponse = {
  id: string;
  firstName: string;
  lastName: string;
  fullAddress: string;           // 1..500 chars
  city: string;                  // 1..80 chars
  pincode: string;               // Indian 6-digit, /^[1-9]\d{5}$/
  state: string;                 // 1..80 chars
  phone: string;                 // Indian, /^(\+91)?[6-9]\d{9}$/
  country: string;               // ISO-3166 alpha-2; default "IN"
  isDefault: boolean;
  createdAt: string;             // ISO
  updatedAt: string;
};

Phone and pincode validation is India-specific today; internationalization swaps these for a country-driven validator.

CustomerListItem

type CustomerListItem = {
  id: string;
  email: string;
  name: string;
  image: string | null;
  createdAt: string;             // ISO
};

Default address rule

A customer has at most one default address. The address book invariants:

TriggerBehavior
First-ever address (book is empty)Force-promoted to isDefault: true regardless of input
Subsequent create with isDefault: trueNew row becomes default; prior default flips to false
POST /:id/defaultSame — flip swap
Delete defaultNo auto-promotion — caller is left without a default until they explicitly pick one. By design — silent re-promotion can ship orders to a stale address

Storefront — /store/addresses

GET /store/addresses — List my addresses

Default address comes first; the rest are sorted by most-recently-touched.

Query

NameTypeDefaultConstraints
pageint1>= 1
limitint201..100

Response 200 — paginated envelope of AddressResponse[].


GET /store/addresses/:id — Get one of my addresses

Errors

StatusCodeWhen
404NOT_FOUNDAddress does not exist or belongs to another user

POST /store/addresses — Create an address

Body

{
  "firstName": "Ada",
  "lastName": "Lovelace",
  "fullAddress": "221B Baker Street",
  "city": "London",
  "pincode": "110001",                 // 6-digit, first digit 1–9
  "state": "Delhi",
  "phone": "+919876543210",            // +91 prefix optional
  "country": "IN",                     // ISO-3166 alpha-2; default "IN"
  "isDefault": false
}

Response 201AddressResponse. If this is the caller's first address, the response has isDefault: true even when the body sent false.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod (phone/pincode regex, missing required field)

PATCH /store/addresses/:id — Update an address

Body — partial CreateAddressInput. At least one field is required (no-op writes are rejected at the zod layer to avoid burning a 200).

{ "city": "Mumbai", "pincode": "400001" }

Response 200 — updated AddressResponse.

Errors

StatusCodeWhen
400VALIDATION_ERROREmpty body; any field fails its individual schema
404NOT_FOUNDAddress not owned by caller

DELETE /store/addresses/:id — Delete an address

Hard delete. Deleting the default address leaves the customer without a default until they explicitly pick one — no auto-promotion.

Response 204 No Content.

Errors

StatusCodeWhen
404NOT_FOUNDAddress not owned by caller

POST /store/addresses/:id/default — Set as default

Flips the chosen address to isDefault: true and the prior default (if any) to false. Idempotent — already-default is a no-op 200.

Response 200 — the new default AddressResponse.

Errors

StatusCodeWhen
404NOT_FOUNDAddress not owned by caller

Admin

GET /admin/customers — Customer picker

Required permission: user: list. Substring ILIKE search across email and name. Pass selectedIds=<csv> to receive a pinned block of already-applied customers (see picker responses in catalog.md).

Query — standard QueryDto (page, limit, search, selectedIds).

Response 200

{
  "data": {
    "items": [
      {
        "id": "01J9...",
        "email": "ada@example.com",
        "name": "Ada Lovelace",
        "image": "https://cdn.example/u/ada.png",
        "createdAt": "2026-01-12T08:30:00.000Z"
      }
    ],
    "pinned": []
  },
  "metadata": { "total": 1234, "items": 1, "perPage": 20, "currentPage": 1, "lastPage": 62 }
}

  • auth — owns the user table that the address book references via userId.
  • order — snapshots shippingAddress / billingAddress from this module at place-order. See order.md.
  • cart — uses the customer's default address for the shipping-rate quote when no override is supplied at checkout.

On this page