Inventory & Locations
Track stock across multiple warehouses, transfer inventory between locations, and route order fulfillment to the right source.
Overview
Cartly supports distributed inventory across multiple physical locations — warehouses, retail stores, and dropship fulfillment centers. Each inventory location has its own stock levels per product variant. When an order is fulfilled, you choose which location ships the goods.
Key Concepts
- inventory_location — a named physical location with a type (
warehouse,retail,dropship) - inventory_level — available, reserved, and incoming quantities for a variant × location pair
- inventory_reservation — temporary hold placed during checkout; auto-released after 15 minutes if payment is not completed
The legacy variant.inventory field is kept in sync automatically as the sum of available quantities across all locations via SyncLegacyField. Existing integrations that read variant.inventory continue to work without changes.
Locations API
Manage inventory locations with full CRUD:
GET /admin/locations— list all locationsPOST /admin/locations— create a locationPUT /admin/locations/:id— update name, address, active flagDELETE /admin/locations/:id— delete (only when all stock levels are zero)
Location type values: "warehouse", "retail", "dropship". The type is display-only and does not affect fulfillment logic. Mark a location as is_default: true to pre-select it in the fulfillment dialog.
Locations API Examples
curl -X POST https://cartly.pro/admin/locations \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "East Coast Warehouse",
"address": "123 Fulfillment Ave, Newark, NJ",
"type": "warehouse",
"active": true,
"is_default": false
}'Inventory Levels
Stock is tracked per variant × location pair. Each level has three fields:
- available — quantity ready to sell (decremented on fulfillment)
- reserved — quantity held during active checkouts
- incoming — expected stock from purchase orders (informational only)
Inventory Levels API
// Set stock for a variant at a specific location
await fetch(`/admin/inventory/${variantId}/${locationId}`, {
method: "PUT",
headers: {
"Authorization": `Bearer ${ADMIN_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ quantity: 150 }),
});Stock Transfer
Move inventory between locations atomically. The transfer decrements the source location and increments the destination in a single database transaction — no partial states are possible. If the source location does not have enough available stock, the request returns 422 Unprocessable Entity.
Transfer Stock Between Warehouses
await fetch("/admin/inventory/transfer", {
method: "POST",
headers: {
"Authorization": `Bearer ${ADMIN_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
variant_id: "uuid-of-variant",
from_location_id: "uuid-of-source",
to_location_id: "uuid-of-destination",
quantity: 20,
}),
});Fulfillment with Location
Pass an optional location_id when fulfilling an order to specify which warehouse ships the goods. When omitted, the default location is used. When your shop has more than one active location, the admin fulfillment dialog shows a location picker.
Fulfill from a Specific Location
await fetch(`/admin/orders/${orderId}/fulfill`, {
method: "POST",
headers: {
"Authorization": `Bearer ${ADMIN_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
tracking_number: "1Z999AA10123456784",
location_id: "uuid-of-warehouse", // optional; omit to use default
notify_customer: true,
}),
});Reservations & Cleanup
When a customer enters checkout, Cartly reserves the ordered quantities at their intended fulfillment location. Reservations prevent overselling during concurrent checkouts.
- Reservations expire automatically after 15 minutes of inactivity.
- A background ticker calls
CleanupExpiredReservations()every 5 minutes, releasing held stock back to available. - On successful payment, reservations are converted to fulfilled stock decrements.
- On order cancellation, reservations are released immediately.
Reservations are fully lifecycle-managed by the checkout and order workflows — there is no API to manually manage them.