Skip to content
Cartly Developers

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 locations
  • POST /admin/locations — create a location
  • PUT /admin/locations/:id — update name, address, active flag
  • DELETE /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

bash
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

typescript
// 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

typescript
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

typescript
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.