Skip to content

Inventory & stock ​

Ambikly tracks stock per product and, for variable products, per variation. Stock moves when an order is created, not when it is paid, and comes back when an order is cancelled, fails or is fully refunded. This page covers every rule.

Managed versus unmanaged stock ​

Each product row carries manage_stock, stock_quantity and stock_status.

manage_stockWhat decides availabilityWhat happens on an order
On (default)stock_quantity > 0The quantity is decremented
Offstock_status must be exactly instockNothing — the quantity is never touched

Turn tracking off for anything you never run out of: a service, a made-to-order item, a print-on-demand product. Then set the stock status by hand when you do need to close sales.

A product is purchasable only when its status is published and it is in stock by the rule above.

Digital products never track stock ​

The digital product editor sends manage_stock = 0, stock_quantity = null and stock_status = instock on every save. The stock service also skips any order line flagged is_downloadable, so a digital sale never moves a number. There is nothing to configure.

Setting stock on a product ​

  1. Open a physical product in Ambikly → Products.
  2. Click the Inventory & stock row to open its drawer.
  3. Set Track stock levels, the quantity, and a low-stock threshold if you want one.
  4. Click Save in the drawer, then Save changes on the product.
FieldStored asNotes
Track stock levelsmanage_stockOn by default for new products.
Quantity in stockstock_quantity, intOnly shown when tracking is on.
Low stock alertProduct meta low_stock_thresholdPer-product override. Clearing the field deletes the meta row and falls back to the global value.
Stock statusstock_statusinstock, outofstock or onbackorder.
When sold outnothingSee the backorders section below.

Variations carry their own manage_stock, stock_quantity and stock_status. See Variations.

Stock status values ​

ValueMeaning with tracking onMeaning with tracking off
instockMaintained automatically — set whenever the quantity is above 0Product is purchasable
outofstockMaintained automatically — set whenever the quantity reaches 0Product is not purchasable
onbackorderOverwritten by the next stock movementProduct is not purchasable

With tracking on, you rarely set the status yourself: every decrement and restore recalculates it in the same SQL statement.

Thresholds ​

SettingWhereDefault
Global low-stock thresholdAmbikly → Settings → Store → Low Stock Threshold5
Per-product thresholdProduct editor → Inventory & stock → Low stock alertFalls back to the global value

The threshold drives the low-stock email alert only. It does not gate purchases, and it is not what the Products list's "Low" badge uses — that badge is hard-coded to highlight anything below 10.

Careful: three other fields in the same Settings panel are stored but never read by anything: Enable Stock Management, Out of Stock Threshold and Hold Stock (minutes). Turning Enable Stock Management off does not disable per-product tracking, no out-of-stock threshold other than zero is honored, and there is no stock-holding or reservation window at checkout. Only Low Stock Threshold has a reader.

When stock moves ​

EventWhat happens
ambikly_order_createdStock is decremented for every non-downloadable line item
Status changes from pending, processing, on-hold or completed to cancelled or failedStock is restored, once
ambikly_order_refunded with payment status refundedStock is restored, once

Stock leaves your inventory the moment the order row exists, before payment clears. A customer who abandons a redirect gateway has already taken the stock until the order is moved to failed or cancelled.

Partial refunds do not restore stock. Only a full refund does.

What is skipped ​

Both the decrement and the restore skip a line item when:

  • the item is flagged is_downloadable, or
  • the item has no product_id.

Variations ​

When a line item carries a variation_id, the variation row is adjusted first and then the parent product row, both by the same quantity. A variable product therefore holds a parent-level total alongside its per-variation counts.

The atomic update ​

Each adjustment is a single conditional UPDATE, with no read-then-write gap:

sql
UPDATE wp_ambikly_products
   SET stock_quantity = stock_quantity - 3,
       stock_status = CASE WHEN stock_quantity <= 0 THEN 'outofstock' ELSE 'instock' END
 WHERE id = 42 AND manage_stock = 1 AND stock_quantity >= 3

Three things fall out of that statement:

  1. manage_stock = 1 is part of the WHERE clause. An unmanaged product is never touched, whatever the order says.
  2. stock_quantity >= 3 is part of the WHERE clause. If concurrent checkouts have already taken the stock, the row does not match and nothing is written. MySQL's own row lock serializes concurrent decrements, so two simultaneous orders cannot both read the same starting quantity and clobber each other.
  3. The status is recalculated in the same statement. MySQL evaluates multi-column assignments left to right, so the CASE sees the already-decremented quantity.

When the conditional update matches nothing, Ambikly re-reads the row. If the product does not manage stock, that is the normal early-out. If it does manage stock but the quantity is insufficient, the row is clamped to 0 / outofstock and the shortfall is reported back.

Because this path bypasses the model layer, the product's cached model is explicitly invalidated afterwards, so nothing later in the same request serves a stale quantity.

Overselling is recorded, not hidden ​

If any line on an order had to be clamped, Ambikly writes an order note:

Stock warning: this order oversold on Blue Hoodie (concurrent orders exceeded available stock). Verify inventory before fulfilling.

The order is not blocked — the decrement still applies, floored at zero. The note exists so you find out before you ship. A clean decrement instead logs Stock decremented for order #1024.

Restores run once per order ​

The first restore sets stock_restored in the order's meta. Any later status transition that would restore again returns immediately. Without that flag, a processing → cancelled → processing → cancelled cycle, a double-click or a retried request would each add the quantity back a second time and inflate on-hand stock.

A restore also logs Stock restored for order #1024.

Backorders ​

Careful: backorders are not implemented in the free core.

The product editor's When sold out dropdown (Block purchase / Allow, warn customer / Allow silently) is sent on save as backorders, but there is no such column and nothing reads the value.

The Enable Backorders toggle in Ambikly → Settings → Store is stored and never read.

The onbackorder stock status saves and round-trips, but it does not make a product purchasable — with tracking off, only instock is purchasable, and with tracking on the quantity decides. With tracking on, the next stock movement overwrites onbackorder anyway.

If you need to keep selling past zero today, turn Track stock levels off for that product and set the status by hand.

Low-stock and out-of-stock alerts ​

Alerts fire on ambikly_order_paid, after the decrement has already happened on order creation. For each line item on the paid order Ambikly:

  1. Skips the item if it is downloadable or has no product.
  2. Loads the product and skips it if Track stock levels is off.
  3. Resolves the threshold: the product's own low_stock_threshold meta if set, otherwise the global Settings value.
  4. Sends an out of stock alert when the quantity is 0 or below, or a low stock alert when the quantity is above 0 and at or below the threshold.

The emails ​

LevelSubject
Out of stock[Your Store] Out of stock: Blue Hoodie
Low stock[Your Store] Low stock: Blue Hoodie (3 left)

The body reads Stock level for "Blue Hoodie" (SKU AK-1042) is now 3. followed by a Manage product link straight into the product editor. Both are sent as HTML from the store's configured From name and address.

Careful: these two alerts are hard-coded. They are not among the four editable transactional templates in Ambikly → Emails and cannot be re-worded or re-designed there. See Emails.

The throttle ​

Before sending, Ambikly checks a transient named ambikly_low_stock_sent_{level}_{productId} and skips the send if it exists. After sending, it sets that transient for 6 hours.

The key includes the level, so one product can emit at most one low-stock email and one out-of-stock email per 6 hours. A flurry of orders on the same product produces one alert, not one per order.

A successful send fires do_action('ambikly_inventory_alert_sent', $level, $product, $qty).

Who receives them ​

Resolution order:

  1. ambikly_settings_inventory → recipient
  2. ambikly_settings_emails → admin_email
  3. WordPress's own admin_email option
Careful: the inventory settings group has no admin screen at all. Its recipient, send_low_stock and send_out_of_stock values can only be written directly to the database. Left alone, both alert types are on and mail goes to the address from Ambikly → Settings → Emails, or failing that WordPress's admin email.

If no recipient resolves at all, the scan exits without sending. Any error inside the scan is logged and swallowed, so a mail failure never breaks an order.

Editing stock in bulk ​

There is no bulk stock editor on the Products screen — its bulk actions are limited to status changes, export and delete.

Two practical routes:

  • CSV round-trip. Export the catalog, edit stock_quantity, stock_status and manage_stock in a spreadsheet, and re-import. Rows match by SKU. See Import & export (CSV).
  • The API. PUT /ambikly/v1/products/{id} accepts stock_quantity, stock_status, manage_stock and low_stock_threshold. For variations, PUT /ambikly/v1/products/{product_id}/variations/{id}.
Careful: a CSV import or a PUT writes the quantity directly. Neither goes through the atomic adjust path, so an import that lands at the same moment as a checkout can overwrite a decrement. Import stock changes when the store is quiet.

Reading stock ​

WhereWhat it shows
Products list, Stock columnIn Stock / Out of Stock / Low (n) / In Stock (n) — the "Low" cut-off is hard-coded at 10
Product editor, Inventory row summary"N units in stock", "Stock tracking on", or "Not tracking stock", plus an Out of stock badge at zero
GET /ambikly/v1/productsstock_quantity, stock_status, manage_stock and the resolved low_stock_threshold
Storefront product cardAdd to Cart is disabled and relabelled Out of stock
Storefront product pageSame, plus schema.org availability of InStock or OutOfStock

For developers ​

Ambikly\Services\StockService::adjust($productId, $delta, $variationId = 0) is public so add-ons can use the same oversell-safe path instead of writing their own quantity updates. A negative delta decrements, a positive one restores. It returns false only when a decrement had to be clamped short.

The Bundles Pro add-on uses it to move stock on a bundle's component products rather than on the bundle wrapper row.

See Hooks & filters for ambikly_order_created, ambikly_order_paid, ambikly_order_status_changed, ambikly_order_refunded and ambikly_inventory_alert_sent.

Troubleshooting ​

SymptomCause
Stock dropped before the customer paidBy design. The decrement runs on order creation. Move abandoned orders to failed or cancelled to get the stock back.
Stock did not come back after a cancellationThe transition must come from pending, processing, on-hold or completed. Cancelling an already-cancelled or already-failed order does nothing.
Stock did not come back after a refundOnly a full refund restores. Partial refunds do not.
Restore ran only once across several cancel cyclesCorrect. The stock_restored order-meta flag guarantees one restore per order.
Quantity went negativeIt cannot. A decrement past zero is clamped to zero and an oversold order note is written.
Low-stock email never arrivedTracking is off for that product, the quantity is above the threshold, the 6-hour throttle is still live, no recipient resolved, or the order never reached paid.
Changing the global threshold changed nothingCheck for a per-product Low stock alert value — it wins over the global.
A product shows Low in the list but sends no alertThe list badge uses a fixed cut-off of 10; alerts use the threshold.

Next ​