Back in Stock Module — Storefront
HTTP surface for back-in-stock ("notify me") requests on a sold-out variant. Signed-in shoppers and — when the operator allows it — guests with just an email address can ask to be told when…
HTTP surface for back-in-stock ("notify me") requests on a sold-out variant. Signed-in shoppers and — when the operator allows it — guests with just an email address can ask to be told when a variant can be ordered again. The message itself is a catalogued, operator-editable template (email + in-app + push), not an ad-hoc send.
Source:
api-modules/back-in-stock/src/controllers/store-stock-notification.controller.ts.This is an optional plugin. When
BackInStockModule.forRoot()is removed fromapp.module.ts, every route below disappears, the inventory listener stops enqueueing restock sweeps, and the notify worker no longer registers.
Conventions
Authentication
| Endpoint | Auth |
|---|---|
POST /store/stock-notification/subscribe | optional (customer session or guest email) |
GET /store/stock-notification/check-subscription | optional |
GET /store/stock-notification | required (customer session) |
DELETE /store/stock-notification/:id | required (customer session) |
The subscriber's identity is the email address, not the user id — a guest may subscribe with nothing else, and a guest who later signs up keeps the request they already made (the account is attached to the existing row).
- Signed in: the session address is used. Passing a different
emailis 403EMAIL_NOT_YOURS, so a session can't sign someone else up. - Anonymous:
emailis required (400EMAIL_REQUIRED), and the operator must haveback_in_stock.guest_enabledon.
Operator settings
Both public keys are readable from GET /store/settings/back_in_stock so the product page can decide whether to render the button before calling anything:
| Key | Public | Effect |
|---|---|---|
enabled | yes | Master switch. Off ⇒ subscribe returns 403 and no one is notified. |
guest_enabled | yes | Off ⇒ only signed-in customers may subscribe. |
max_active_per_email | no | How many variants one address may wait on at once (default 50). |
expiry_days | no | Waiting requests older than this are dropped by a daily sweep (default 180; 0 disables). |
Response envelope
{ "data": <payload>, "message": "Success", "statusCode": 200 }Lists use the standard metadata: { total, limit, offset, hasMore } shape.
Error envelope
statusCode | errorCode / code | When |
|---|---|---|
| 400 | EMAIL_REQUIRED | Anonymous caller with no email |
| 400 | TOO_MANY_STOCK_SUBSCRIPTIONS | The address is already at max_active_per_email |
| 403 | BACK_IN_STOCK_DISABLED | The operator turned the feature off |
| 403 | BACK_IN_STOCK_GUEST_DISABLED | Anonymous caller while guest_enabled is off |
| 403 | EMAIL_NOT_YOURS | Signed-in caller passed someone else's address |
| 404 | NOT_FOUND | Unknown variant, or a subscription that isn't the caller's |
| 409 | VARIANT_ALREADY_AVAILABLE | The variant can already be ordered |
Domain types
type StockSubscription = {
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
};A request is never hard-deleted. It moves to notified once the message goes out (so the same address isn't re-mailed on every subsequent restock), cancelled when withdrawn, or expired by the daily sweep.
Endpoints
POST /store/stock-notification/subscribe — Ask to be notified
Body
| Field | Type | Notes |
|---|---|---|
variantId | string | The sold-out variant |
email | string? | Required when anonymous; must match the session address when signed in |
Idempotent. Subscribing again while a request is still waiting returns the existing one and emits nothing. Subscribing after being notified creates a fresh request — the shopper is asking about the next restock too.
Rejected with 409 when the variant is already orderable (including backorder / untracked variants, which are always orderable) — there is nothing to wait for. Check stock.isOrderable on GET /store/products/:slug before offering the button.
Response 201 — StockSubscription.
GET /store/stock-notification/check-subscription — Already waiting?
Query
| Name | Type | Notes |
|---|---|---|
variantId | string | Required |
email | string? | Required when anonymous; must match the session address when signed in |
Response 200
{
"data": {
"isSubscribed": true,
"subscriptionId": "sub_123" // null unless the caller has a session
}
}subscriptionId is the withdraw handle, so it is only returned to a caller who proved they own the address by signing in.
GET /store/stock-notification — My requests
Session required. Paginated (limit / offset, standard QueryDto), newest first, optionally filtered by status. Scoped to the session's email address.
Response 200 — StockSubscription[] + pagination metadata.
DELETE /store/stock-notification/:id — Withdraw a request
Session required. Scoped by email as well as id, so a guessed id belonging to someone else is a 404, not a silent cancel. Returns 204.
How the notification is sent
A restock (inventory.stock.adjusted with a positive delta) or a policy change (inventory.policy.updated — turning on backorder, turning off tracking) enqueues one sweep per variant. The worker re-checks that the variant is genuinely orderable, then walks the waiting rows oldest-first, in batches.
Each subscriber gets:
- Email — resolved through the operator-editable catalog as
back_in_stock.available|customer. Editable under Admin → Templates → Email Templates, including its enable toggle. Guests receive this too; it is the only channel that can reach them. - In-app + push — for subscribers with an account, dispatched through the notification orchestrator (which respects their per-category preferences). Editable under Admin → Templates → In-App / Push Notifications.
Template variables: {{product_title}}, {{product_slug}}, {{product_url}}, {{variant_sku}}, {{variant_id}}, {{product_id}}. product_url is absolute when the operator has set storefront_urls.store_url (or the SEO canonical base), and relative otherwise.
Related modules
Affiliate Module — Storefront
HTTP surface for the customer-facing affiliate plugin. Customers apply to join, generate trackable referral links, see their commission balance + payout history, and (anonymous…
Banner Module — Storefront
HTTP surface for storefront banner reads. The customer-facing app fetches active, platform-targeted promotional banners attached to a catalog entity (category / brand / tag /…