Supercommerce API Docs
Admin API

Back in Stock Module — Admin

Admin surface for back-in-stock ("notify me") requests — who is waiting on which sold-out variant, and withdrawing a request on a shopper's behalf. The demand signal behind a sold-out…

Admin surface for back-in-stock ("notify me") requests — who is waiting on which sold-out variant, and withdrawing a request on a shopper's behalf. The demand signal behind a sold-out SKU: how many people, and since when.

Source: api-modules/back-in-stock/src/controllers/admin-stock-notification.controller.ts.

This is an optional plugin — removing BackInStockModule.forRoot() from app.module.ts removes these routes along with the storefront surface. The customer-facing side is documented in Back in Stock — Storefront.


Conventions

Authentication

All endpoints: BetterAuthGuard + PermissionsGuard.

EndpointPermission
GET /admin/stock-notificationsstockNotification: read
GET /admin/stock-notifications/demandstockNotification: read
GET /admin/stock-notifications/summarystockNotification: read
GET /admin/stock-notifications/demand/:variantIdstockNotification: read
POST /admin/stock-notifications/demand/:variantId/notify-vendorstockNotification: notify-vendor
DELETE /admin/stock-notifications/:idstockNotification: delete

Configuration

The feature's behaviour is settings-driven, not endpoint-driven — see the back_in_stock group under Store Settings (enabled, guest_enabled, max_active_per_email, expiry_days). The message wording lives under Templates: back_in_stock.available|customer for email, and the matching in-app / push templates.

Admin UI

Storefront → Back in Stock (@sc/admin-stock-notifications), gated on stockNotification: read; the withdraw row-action is gated on stockNotification: delete. Removing the module's line from apps/admin/modules/registry.ts drops the nav entry and route.

The index page leads with four headline tiles (waiting, variants awaited, unique subscribers, notified all-time), a daily requests-raised trend, then the ranked demand table. Selecting a row navigates to /stock-notifications/:variantId — a full page with that variant's own tiles, its own interest-over-time chart, and the list of who is waiting. That list drops the product column, since the page already names it.

Every demand row carries a Notify vendor action (and the variant page a prominent button), gated on stockNotification: notify-vendor.


Endpoints

GET /admin/stock-notifications — List requests

Standard QueryDto pagination (limit / offset), newest first, plus:

QueryTypeNotes
status"waiting" | "notified" | "cancelled" | "expired"Filter by lifecycle state
variantIdstringEveryone waiting on one variant
productIdstringEveryone waiting on any variant of one product
emailstringPartial, case-insensitive match

Response 200 — paginated rows:

type AdminStockSubscription = {
  id: string;
  variantId: string;
  productId: string;
  productTitle: string | null;
  productSlug: string | null;
  variantSku: string | null;
  status: "waiting" | "notified" | "cancelled" | "expired";
  notifiedAt: string | null;   // ISO
  createdAt: string;           // ISO
  email: string;               // admin-only
  userId: string | null;       // admin-only; null for a guest subscriber
};

Rows are never hard-deleted, so notified rows stay as a record of demand that converted, and expired rows show requests the daily sweep dropped after expiry_days.

GET /admin/stock-notifications/demand — Awaited variants, ranked

The restock-decision view: one row per variant with at least one shopper waiting, joined to that variant's live stock so both halves arrive in one call. Paginated with the standard limit / offset.

QueryTypeNotes
rank"waiting" | "oldest" | "recent" | "guests"Ordering. Default waiting (most people waiting); oldest surfaces who has been queued longest; guests finds demand coming from shoppers with no account
unorderableOnlyboolean?Keep only variants a restock would actually release — excludes anything already orderable (in stock, backorder, untracked)
type StockDemandRow = {
  variantId: string;
  productId: string;
  productTitle: string | null;
  productSlug: string | null;
  variantSku: string | null;
  waitingCount: number;      // shoppers still waiting
  notifiedCount: number;     // already told, all-timethe demand that converted
  guestCount: number;        // of the waiting, how many have no account
  oldestRequestedAt: string | null;   // ISO; the longest-waiting request
  newestRequestedAt: string | null;   // ISO
  availableQuantity: number | null;   // null when untracked
  stockStatus: "in_stock" | "low_stock" | "out_of_stock" | "backorder" | "untracked";
};

Rows are grouped per variant, not per product — two sizes of the same product are two decisions. availableQuantity here is the operator-facing number (on-hand − reserved − safety stock); it is not subject to the storefront's show_stock_quantity toggle.

GET /admin/stock-notifications/summary — Headline totals + trend

QueryTypeNotes
daysintTrend window, 7–180, default 30
type StockDemandSummary = {
  totalWaiting: number;
  variantsAwaited: number;      // distinct variants with someone waiting
  uniqueSubscribers: number;    // distinct email addresses waiting
  guestWaiting: number;
  totalNotified: number;
  requestedInWindow: number;
  notifiedInWindow: number;
  windowDays: number;
  trend: Array<{ date: string; requested: number; notified: number }>;
};

trend is dense — days with no activity come back as zeroes, so it can be plotted without gap-filling on the client.

GET /admin/stock-notifications/demand/:variantId — One variant

The same StockDemandRow for a single variant, plus that variant's own daily trend — what the variant page renders. Accepts the same days window as /summary (7–180, default 30).

type VariantDemandDetail = StockDemandRow & {
  windowDays: number;
  requestedInWindow: number;
  notifiedInWindow: number;
  trend: Array<{ date: string; requested: number; notified: number }>;
};

404 when no request has ever been raised for that variant.

POST /admin/stock-notifications/demand/:variantId/notify-vendor — Nudge the vendor

Tells the variant's vendor how many shoppers are queued on it. Fans out to every member of that vendor through the notification orchestrator, so each one gets it on the channels they've kept switched on (email + push, with in-app riding the push intent).

The wording is not in the code path — it resolves through back_in_stock.vendor_alert|vendor_member in the editable catalogs, so the operator can rewrite or disable it under Templates. Variables available: {{waiting_count}}, {{guest_count}}, {{waited_days}}, {{product_title}}, {{variant_sku}}, {{product_id}}, {{variant_id}}, {{inventory_url}}.

The call to action points at the vendor dashboard's inventory row for that variant (/inventory/:productId/:variantId) — where they can actually add stock — not the storefront product page. The in-app and push templates use that relative path directly; the email needs an absolute URL, so it prefixes admin.notifications.vendor_app_url. Leave that setting blank and the email still sends, just without a working button.

Rate-limited per variant — one alert per variant per 12 hours, so repeated clicks (or two admins working the same list) can't spam a vendor. A second attempt inside the window is 409 VENDOR_ALERT_ON_COOLDOWN. The cooldown starts only after a successful send.

Response 200

{
  "data": {
    "variantId": "var_123",
    "vendorId": "ven_456",
    "waitingCount": 12,
    "cooldownHours": 12
  }
}
StatusCodeWhen
404NOT_FOUNDNobody is waiting on that variant, or it doesn't exist
409VENDOR_ALERT_ON_COOLDOWNAlready alerted within the last 12 hours

DELETE /admin/stock-notifications/:id — Withdraw a request

Marks a waiting request cancelled (no row is removed). Returns 204. A request that is already notified, cancelled, or expired is left as it is.


On this page