Inventory
Stock levels per variant per location, and the append-only movement ledger behind them.
Paste a key and every snippet on this page switches from the placeholder to your key — and the Try it panel under each endpoint is ready to send. It is stored in this browser only and goes nowhere except, if you press Send, straight to the API host you pick there.
Adjust stock at a location
/ext/v1/inventory/adjustmentsinventory:writeThe staff adjustment path verbatim: row-locked, ledgered, activity-logged. A reason is always required here; on-hand can never go below zero or below the reserved quantity; damage must remove stock; purchase requires a unit_cost and folds into the moving-average cost.
Headers
| Name | Type | Description |
|---|---|---|
Idempotency-Keyrequired | string | REQUIRED. A unique value per logical request (a UUID is ideal); reuse it verbatim when retrying. A retry replays the original response with Idempotent-Replayed: true instead of running twice. |
Body parameters
| Name | Type | Description |
|---|---|---|
store_id | uuid | Target store. REQUIRED with a company-scoped key; with a store-scoped key it may only repeat the key's own store. |
location_idrequired | uuid | Location (warehouse) to adjust. |
variant_idrequired | uuid | The variant being adjusted. |
quantity_changerequired | number | Signed change to on-hand. Positive adds stock, negative removes; never zero. Cannot take on-hand below zero or below reservations — the same rules staff adjustments obey. |
type | stringdefault adjustment | Ledger category. purchase adds stock at a required unit_cost; damage must remove stock; default adjustment.purchasereturnadjustmentdamage |
reasonrequired | string | Stored on the ledger entry. Required. |
unit_cost | number | Per-unit cost of the moved units. REQUIRED for purchase (stock cannot enter at no cost); otherwise the variant's current cost values the movement. Write-only — cost never appears in any response (Q6). |
low_stock_threshold | number | Optionally update the low-stock threshold for this level. |
Request
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"
curl -sS -X POST "https://api.salafems.com/ext/v1/inventory/adjustments" \
-H "Authorization: Bearer $SALAF_API_KEY" \
-H "Idempotency-Key: e7a1…-your-uuid" \
-H "Content-Type: application/json" \
-d '{
"location_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"variant_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"quantity_change": -5,
"reason": "Cycle count correction — shelf B4."
}'const url = new URL("https://api.salafems.com/ext/v1/inventory/adjustments");
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SALAF_API_KEY}`,
"Idempotency-Key": "e7a1…-your-uuid",
"Content-Type": "application/json",
},
body: JSON.stringify({
"location_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"variant_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"quantity_change": -5,
"reason": "Cycle count correction — shelf B4."
}),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}
const object = await response.json();import os
import requests
payload = {
"location_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"variant_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"quantity_change": -5,
"reason": "Cycle count correction — shelf B4.",
}
response = requests.post(
"https://api.salafems.com/ext/v1/inventory/adjustments",
json=payload,
headers={
"Authorization": f"Bearer {os.environ['SALAF_API_KEY']}",
"Idempotency-Key": "e7a1…-your-uuid",
},
timeout=30,
)
if not response.ok:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']}")
obj = response.json()<?php
$key = getenv('SALAF_API_KEY');
$payload = json_encode([
'location_id' => '5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001',
'variant_id' => '5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001',
'quantity_change' => -5,
'reason' => 'Cycle count correction — shelf B4.',
]);
$ch = curl_init('https://api.salafems.com/ext/v1/inventory/adjustments');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Idempotency-Key: e7a1…-your-uuid', 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$response = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}https://api.salafems.com/ext/v1/inventory/adjustmentsStored in this browser only and never sent anywhere except to the API host you picked above — these pages are static files with no server behind them.
generating…A new value is generated whenever you edit this request, and kept while you do not — so sending twice without changing anything is a real retry and comes back Idempotent-Replayed: true instead of writing again.
Paste a key above to enable Send.
Responses
201The adjusted stock level, in the same shape asGET /v1/inventory/levelsrows.401Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.403The key's scopes do not cover this endpoint.404Unknown location or variant.422Validation failed —error.fieldsmaps each offending field to its messages.429Rate limit exceeded for this key. HonorRetry-Afterand theX-RateLimit-*headers.
Every failure uses the one error envelope — Errors lists each code and what to do with it.
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"location_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"variant_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"sku": "TSHIRT-RED-M",
"on_hand": 10,
"reserved": 7,
"available": 3,
"low_stock_threshold": 5,
"is_low_stock": false,
"status": "active",
"location": {
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"name": "Main Warehouse",
"code": "WH-01"
},
"variant": {
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"sku": "TSHIRT-RED-M",
"barcode": null,
"name": "Red / M",
"product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001"
},
"created_at": "2026-08-11T10:00:00.000Z",
"updated_at": "2026-08-11T10:00:00.000Z"
}List stock levels (one row per variant per location)
/ext/v1/inventory/levelsinventory:readEach row carries on_hand, reserved and the available difference — sell against available, not on_hand.
Query parameters
| Name | Type | Description |
|---|---|---|
limit | numberdefault 25max 100 | Page size. |
starting_after | string | Return the page AFTER this object id (the previous page's next_cursor). |
ending_before | string | Return the page BEFORE this object id. |
sort | stringdefault -created_at | Newest first by default. Only created_at is sortable — a cursor over a mutable key (like updated_at) cannot page reliably. Use updated_after to sync changes.created_at-created_at |
store_id | string | Restrict to one store. Required breadth control for company-scoped keys; on a store-scoped key it must match the key's own store. |
warehouse_id | string | Only levels at this location. |
variant_id | string | Only levels for this variant. |
low_stock | boolean | Only levels at or below the variant's low-stock threshold — the reorder query, answered server-side so an integrator does not page the whole catalog to find twelve rows. |
Request
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"
curl -sS "https://api.salafems.com/ext/v1/inventory/levels?limit=25" \
-H "Authorization: Bearer $SALAF_API_KEY"const url = new URL("https://api.salafems.com/ext/v1/inventory/levels");
Object.entries({
limit: "25",
}).forEach(([key, value]) => url.searchParams.set(key, value));
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.SALAF_API_KEY}`,
},
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}
const { data, meta } = await response.json();import os
import requests
response = requests.get(
"https://api.salafems.com/ext/v1/inventory/levels",
params={
"limit": "25",
},
headers={
"Authorization": f"Bearer {os.environ['SALAF_API_KEY']}",
},
timeout=30,
)
if not response.ok:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']}")
page = response.json()
rows, meta = page["data"], page["meta"]<?php
$key = getenv('SALAF_API_KEY');
$ch = curl_init('https://api.salafems.com/ext/v1/inventory/levels?limit=25');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$response = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
$rows = $response['data'];
$meta = $response['meta'];https://api.salafems.com/ext/v1/inventory/levelsStored in this browser only and never sent anywhere except to the API host you picked above — these pages are static files with no server behind them.
Query parameters
Blank fields are left out of the request.
Paste a key above to enable Send.
Responses
200A cursor page of stock levels.401Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.403The key's scopes do not cover this endpoint.422Validation failed —error.fieldsmaps each offending field to its messages.429Rate limit exceeded for this key. HonorRetry-Afterand theX-RateLimit-*headers.
Every failure uses the one error envelope — Errors lists each code and what to do with it.
{
"data": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"location_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"variant_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"sku": "TSHIRT-RED-M",
"on_hand": 10,
"reserved": 7,
"available": 3,
"low_stock_threshold": 5,
"is_low_stock": false,
"status": "active",
"location": {
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"name": "Main Warehouse",
"code": "WH-01"
},
"variant": {
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"sku": "TSHIRT-RED-M",
"barcode": null,
"name": "Red / M",
"product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001"
},
"created_at": "2026-08-11T10:00:00.000Z",
"updated_at": "2026-08-11T10:00:00.000Z"
}
],
"meta": {
"has_more": true,
"next_cursor": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001"
}
}List stock movements (the append-only ledger behind the levels)
/ext/v1/inventory/movementsinventory:readEvery quantity change with before/after readings — reservations, sales, adjustments, transfers. Costs never appear here.
Query parameters
| Name | Type | Description |
|---|---|---|
limit | numberdefault 25max 100 | Page size. |
starting_after | string | Return the page AFTER this object id (the previous page's next_cursor). |
ending_before | string | Return the page BEFORE this object id. |
sort | stringdefault -created_at | Newest first by default. Only created_at is sortable — a cursor over a mutable key (like updated_at) cannot page reliably. Use updated_after to sync changes.created_at-created_at |
store_id | string | Restrict to one store. Required breadth control for company-scoped keys; on a store-scoped key it must match the key's own store. |
warehouse_id | string | Only movements at this location. |
variant_id | string | Only movements for this variant. |
Request
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"
curl -sS "https://api.salafems.com/ext/v1/inventory/movements?limit=25" \
-H "Authorization: Bearer $SALAF_API_KEY"const url = new URL("https://api.salafems.com/ext/v1/inventory/movements");
Object.entries({
limit: "25",
}).forEach(([key, value]) => url.searchParams.set(key, value));
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.SALAF_API_KEY}`,
},
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}
const { data, meta } = await response.json();import os
import requests
response = requests.get(
"https://api.salafems.com/ext/v1/inventory/movements",
params={
"limit": "25",
},
headers={
"Authorization": f"Bearer {os.environ['SALAF_API_KEY']}",
},
timeout=30,
)
if not response.ok:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']}")
page = response.json()
rows, meta = page["data"], page["meta"]<?php
$key = getenv('SALAF_API_KEY');
$ch = curl_init('https://api.salafems.com/ext/v1/inventory/movements?limit=25');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$response = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
$rows = $response['data'];
$meta = $response['meta'];https://api.salafems.com/ext/v1/inventory/movementsStored in this browser only and never sent anywhere except to the API host you picked above — these pages are static files with no server behind them.
Query parameters
Blank fields are left out of the request.
Paste a key above to enable Send.
Responses
200A cursor page of movements.401Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.403The key's scopes do not cover this endpoint.422Validation failed —error.fieldsmaps each offending field to its messages.429Rate limit exceeded for this key. HonorRetry-Afterand theX-RateLimit-*headers.
Every failure uses the one error envelope — Errors lists each code and what to do with it.
{
"data": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"location_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"variant_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"type": "adjustment",
"quantity": -2,
"quantity_before": 12,
"quantity_after": 10,
"reference_type": "manual",
"reference_id": null,
"note": "Damaged in transit",
"created_at": "2026-08-11T10:00:00.000Z"
}
],
"meta": {
"has_more": true,
"next_cursor": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001"
}
}