Inventory & Products | Products | Stock | Set Stock (Advanced)

Advanced Stock Management Endpoint

This document provides guidance on using the set_stock API endpoint for comprehensive stock management operations. Unlike Set Products Stock which handles simple add/deduct operations, this endpoint supports transfers, absolute value adjustments, BBD changes, location moves, and more — all with full stockdiary audit trail.

Important Notes

  • All parameters marked as MANDATORY are required
  • A stockdiary entry is automatically created for every stock operation
  • This endpoint uses an action-based design — the action parameter determines the operation
  • For simple add/deduct operations, you can use either this endpoint (action=adjust) or Set Products Stock
  • Atomic transfers — the transfer action handles source deduction and destination addition in a single call

Supported Actions

Action Description Diary Entries
adjust Add/deduct stock by quantity with reason code. Use stock=0 to reset to zero (reason auto-detected). 1 entry
set Set stock to an absolute value 1 entry (or 0 if no change)
transfer Move stock between locations/shelves atomically 2 entries (OUT + IN)
modify_bbd Change best-before date on existing stock 2 entries (OUT old + IN new)
move Change location/shelf while keeping quantity 2 entries (OUT + IN)
set_default_location Set default location flag for a product 0 entries
delete Remove a stock entry entirely 1 entry (OUT)

Advanced Stock Management API

Endpoint and Method

  • Endpoint: https://easycms.fi/public_api/set_stock/
  • Method: POST only
  • Content-Type: application/json


Authentication Parameters (required for all actions)

Parameter Type Description
TOKEN (api_key) string Your unique API key in Authorization1 header. How to Create API Credentials
username string Your login username
password string Your login password
account integer Your specific account ID




Action: adjust — Add/Deduct Stock by Quantity

Same functionality as Set Products Stock but through this unified endpoint.

Parameters

Required Parameters

Parameter Type Description
action string Must be "adjust"
pid integer Product ID
location_id integer Location ID
shelf_id integer Shelf ID
stock decimal Quantity to add (positive) or deduct (negative). Use 0 to reset stock to zero (reason auto-detected).
reason integer Reason code (see Reason Codes table below)

Optional Parameters

Parameter Type Description
best_before_date date YYYY-MM-DD format
price decimal Sell price at time of movement
price_buy decimal Buy price at time of movement
supplier_id integer Supplier ID
reference string Reference text (max 255 chars)

Reason Codes

Stock Additions (IN)

Code Constant Description
1 IN_PURCHASE Stock received from purchase order
2 IN_REFUND Stock returned from customer refund
3 IN_MOVEMENT Stock moved in from another location
4 IN_SALE_CORRECTION Sale cancelled, stock returned
9 IN_ADJUSTMENT Stock adjustment (addition / correction)

Stock Deductions (OUT)

Code Constant Description
-1 OUT_SALE Stock sold to customer
-2 OUT_REFUND Stock refunded to supplier
-3 OUT_BREAK Stock broken/damaged
-4 OUT_MOVEMENT Stock moved to another location
-5 OUT_SAMPLE Stock used as sample
-6 OUT_FREE Stock given for free
-7 OUT_USED Stock used internally
-8 OUT_SUBTRACT Stock adjustment (deduction)
-9 OUT_ADJUSTMENT Stock adjustment (removal / correction)

Zero Stock Behavior (action=adjust, stock=0)

When stock=0 is passed to the adjust action:

Scenario Behavior
No reason provided, current stock > 0 Auto-sets reason to -9 (OUT_ADJUSTMENT) — resets to 0
No reason provided, current stock < 0 Auto-sets reason to 9 (IN_ADJUSTMENT) — resets to 0
No reason provided, current stock = 0 Returns success with "No change — stock is already at 0"
reason explicitly provided Uses the provided reason code (respects user intent)




Action: set — Set Stock to Absolute Value

Sets the stock quantity at a location/shelf to an exact value. The system calculates the difference and creates an adjustment diary entry (reason=9 for increase, reason=-9 for decrease).

Parameters

Parameter Type Required Description
action string YES Must be "set"
pid integer YES Product ID
location_id integer YES Location ID
shelf_id integer YES Shelf ID
stock decimal YES The new absolute stock value (must be >= 0)
best_before_date date NO YYYY-MM-DD format
reference string NO Reference text




Action: transfer — Move Stock Between Locations/Shelves

Atomically transfers a specified quantity of stock from one location/shelf to another. This is a single API call that handles both the source deduction and destination addition, including merge detection when the destination already has stock for the product.

Parameters

Parameter Type Required Description
action string YES Must be "transfer"
pid integer YES Product ID
location_id integer YES SOURCE Location ID
shelf_id integer YES SOURCE Shelf ID
dest_location_id integer YES DESTINATION Location ID
dest_shelf_id integer YES DESTINATION Shelf ID
stock decimal YES Quantity to transfer (must be > 0)
best_before_date date NO New BBD for destination (YYYY-MM-DD)
reference string NO Reference text

Logic:

  1. Validates source has sufficient stock
  2. Deducts from source → stockdiary OUT_MOVEMENT (reason=-4)
  3. Adds to destination → stockdiary IN_MOVEMENT (reason=4)
  4. If destination already has stock for this product, quantities are summed (merged)




Action: modify_bbd — Change Best-Before Date

Changes the best-before date on an existing stock entry. Creates two diary entries following the admin pattern: OUT with old BBD + IN with new BBD.

Parameters

Parameter Type Required Description
action string YES Must be "modify_bbd"
pid integer YES Product ID
location_id integer YES Location ID
shelf_id integer YES Shelf ID
best_before_date date YES New BBD (YYYY-MM-DD)
reference string NO Reference text




Action: move — Change Location/Shelf Keeping Quantity

Moves stock to a different location and/or shelf while keeping the quantity intact. If the destination already has stock for this product, the quantities are merged (combined) and the source row is removed.

Parameters

Parameter Type Required Description
action string YES Must be "move"
pid integer YES Product ID
location_id integer YES CURRENT Location ID
shelf_id integer YES CURRENT Shelf ID
new_location_id integer NO* NEW Location ID (defaults to current if not provided)
new_shelf_id integer NO* NEW Shelf ID (required if new_location_id not changing)
reference string NO Reference text

*At least one of new_location_id or new_shelf_id must be different from the current values.

Logic:

  1. Creates OUT_MOVEMENT diary from old location (reason=-4)
  2. Updates or merges the stockcurrent row to the new location/shelf
  3. Creates IN_MOVEMENT diary to new location (reason=4)
  4. If destination already has this product, stocks are combined and source row deleted




Action: set_default_location — Set Default Location Flag

Sets a specific location/shelf as the default for a product. All other locations for this product are reset to default_location=0.

Parameters

Parameter Type Required Description
action string YES Must be "set_default_location"
pid integer YES Product ID
location_id integer YES Location ID to set as default
shelf_id integer YES Shelf ID to set as default




Action: delete — Remove Stock Entry Entirely

Deletes a stock entry from a location/shelf. Creates an OUT_MOVEMENT diary entry with the full stock quantity before removing the row.

Parameters

Parameter Type Required Description
action string YES Must be "delete"
pid integer YES Product ID
location_id integer YES Location ID
shelf_id integer YES Shelf ID
reference string NO Reference text




Call Examples in Different Languages

The following examples demonstrate the transfer action. Change the action and parameters for other operations.


# Transfer 50 units from location 1/shelf 5 to location 2/shelf 3
curl -X POST 'https://easycms.fi/public_api/set_stock' \
-H 'Authorization1: TOKEN' \
-H 'Content-Type: application/json' \
-d '{
  "username": "USERNAME",
  "password": "PASSWORD",
  "account": "ACCOUNT_ID",
  "action": "transfer",
  "pid": 12345,
  "location_id": 1,
  "shelf_id": 5,
  "dest_location_id": 2,
  "dest_shelf_id": 3,
  "stock": 50,
  "reference": "TRANSFER-2024-001"
}'

# Set stock to absolute value of 200
curl -X POST 'https://easycms.fi/public_api/set_stock' \
-H 'Authorization1: TOKEN' \
-H 'Content-Type: application/json' \
-d '{
  "username": "USERNAME",
  "password": "PASSWORD",
  "account": "ACCOUNT_ID",
  "action": "set",
  "pid": 12345,
  "location_id": 1,
  "shelf_id": 5,
  "stock": 200,
  "reference": "STOCKTAKE-2024"
}'

# Add stock via adjust action
curl -X POST 'https://easycms.fi/public_api/set_stock' \
-H 'Authorization1: TOKEN' \
-H 'Content-Type: application/json' \
-d '{
  "username": "USERNAME",
  "password": "PASSWORD",
  "account": "ACCOUNT_ID",
  "action": "adjust",
  "pid": 12345,
  "location_id": 1,
  "shelf_id": 5,
  "stock": 100,
  "reason": 1,
  "best_before_date": "2025-12-31",
  "price_buy": 15.50,
  "reference": "PO-2024-001"
}'

# Modify best-before date
curl -X POST 'https://easycms.fi/public_api/set_stock' \
-H 'Authorization1: TOKEN' \
-H 'Content-Type: application/json' \
-d '{
  "username": "USERNAME",
  "password": "PASSWORD",
  "account": "ACCOUNT_ID",
  "action": "modify_bbd",
  "pid": 12345,
  "location_id": 1,
  "shelf_id": 5,
  "best_before_date": "2026-06-30",
  "reference": "BBD-UPDATE"
}'

# Move stock to a different shelf (keep quantity)
curl -X POST 'https://easycms.fi/public_api/set_stock' \
-H 'Authorization1: TOKEN' \
-H 'Content-Type: application/json' \
-d '{
  "username": "USERNAME",
  "password": "PASSWORD",
  "account": "ACCOUNT_ID",
  "action": "move",
  "pid": 12345,
  "location_id": 1,
  "shelf_id": 5,
  "new_shelf_id": 10,
  "reference": "SHELF-REARRANGE"
}'

# Set default location
curl -X POST 'https://easycms.fi/public_api/set_stock' \
-H 'Authorization1: TOKEN' \
-H 'Content-Type: application/json' \
-d '{
  "username": "USERNAME",
  "password": "PASSWORD",
  "account": "ACCOUNT_ID",
  "action": "set_default_location",
  "pid": 12345,
  "location_id": 1,
  "shelf_id": 5
}'

# Delete a stock entry
curl -X POST 'https://easycms.fi/public_api/set_stock' \
-H 'Authorization1: TOKEN' \
-H 'Content-Type: application/json' \
-d '{
  "username": "USERNAME",
  "password": "PASSWORD",
  "account": "ACCOUNT_ID",
  "action": "delete",
  "pid": 12345,
  "location_id": 1,
  "shelf_id": 5,
  "reference": "REMOVE-EMPTY"
}'

# Reset stock to zero (reason auto-detected from current stock direction)
curl -X POST 'https://easycms.fi/public_api/set_stock' \
-H 'Authorization1: TOKEN' \
-H 'Content-Type: application/json' \
-d '{
  "username": "USERNAME",
  "password": "PASSWORD",
  "account": "ACCOUNT_ID",
  "action": "adjust",
  "pid": 12345,
  "location_id": 1,
  "shelf_id": 5,
  "stock": 0,
  "reference": "STOCKTAKE-RESET"
}'

$curl = curl_init();
curl_setopt_array($curl, array(
  CURLOPT_URL => "https://easycms.fi/public_api/set_stock",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_POSTFIELDS => json_encode([
    'username' => 'USERNAME', 
    'password' => 'PASSWORD', 
    'account' => 'ACCOUNT_ID',
    'action' => 'transfer',
    'pid' => 12345,
    'location_id' => 1,
    'shelf_id' => 5,
    'dest_location_id' => 2,
    'dest_shelf_id' => 3,
    'stock' => 50,
    'reference' => 'TRANSFER-2024-001'
  ]),
  CURLOPT_HTTPHEADER => array(
    "Authorization1: TOKEN",
    "Content-Type: application/json"
  ),
));
$response = curl_exec($curl);
curl_close($curl);
echo $response;

import requests
import json

url = "https://easycms.fi/public_api/set_stock"
headers = {
    "Authorization1": "TOKEN",
    "Content-Type": "application/json"
}
payload = {
    'username': 'USERNAME', 
    'password': 'PASSWORD', 
    'account': 'ACCOUNT_ID',
    'action': 'transfer',
    'pid': 12345,
    'location_id': 1,
    'shelf_id': 5,
    'dest_location_id': 2,
    'dest_shelf_id': 3,
    'stock': 50,
    'reference': 'TRANSFER-2024-001'
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
print(response.text)

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

HttpClient client = HttpClient.newHttpClient();
String jsonPayload = """
    {
        "username": "USERNAME",
        "password": "PASSWORD",
        "account": "ACCOUNT_ID",
        "action": "transfer",
        "pid": 12345,
        "location_id": 1,
        "shelf_id": 5,
        "dest_location_id": 2,
        "dest_shelf_id": 3,
        "stock": 50,
        "reference": "TRANSFER-2024-001"
    }
    """;
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://easycms.fi/public_api/set_stock"))
    .headers("Authorization1", "TOKEN", "Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
    .build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

const https = require('https');

const payload = JSON.stringify({
  username: 'USERNAME', 
  password: 'PASSWORD', 
  account: 'ACCOUNT_ID',
  action: 'transfer',
  pid: 12345,
  location_id: 1,
  shelf_id: 5,
  dest_location_id: 2,
  dest_shelf_id: 3,
  stock: 50,
  reference: 'TRANSFER-2024-001'
});

const options = {
  hostname: 'easycms.fi',
  path: '/public_api/set_stock',
  method: 'POST',
  headers: {
    'Authorization1': 'TOKEN',
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(payload)
  }
};

const req = https.request(options, (res) => {
  let data = '';
  res.on('data', (chunk) => { data += chunk; });
  res.on('end', () => { console.log(data); });
});
req.on('error', (e) => { console.error(e); });
req.write(payload);
req.end();

import okhttp3.OkHttpClient
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody

fun main() {
    val client = OkHttpClient()

    val jsonPayload = """
        {
            "username": "USERNAME",
            "password": "PASSWORD",
            "account": "ACCOUNT_ID",
            "action": "transfer",
            "pid": 12345,
            "location_id": 1,
            "shelf_id": 5,
            "dest_location_id": 2,
            "dest_shelf_id": 3,
            "stock": 50,
            "reference": "TRANSFER-2024-001"
        }
    """.trimIndent()

    val requestBody = jsonPayload.toRequestBody("application/json".toMediaType())

    val request = Request.Builder()
        .url("https://easycms.fi/public_api/set_stock")
        .post(requestBody)
        .addHeader("Authorization1", "TOKEN")
        .build()

    client.newCall(request).execute().use { response ->
        if (!response.isSuccessful) throw IOException("Unexpected code $response")
        println(response.body?.string())
    }
}




Handling Endpoint Results

When you make a request to the endpoint, you receive a JSON response containing various keys and values. Here's an explanation of the response keys and their meanings:


{
  "INFO": {
    "start": 0,
    "limit": 50,
    "count": 0,
    "total_count": 0,
    "docs": "https://cms.helpostikotisivut.fi/DOCUMENTATION/bt5/set_stock.php"
  },
  "OUTPUT": {
    "response_type": "success",
    "action": "transfer",
    "status": "success",
    "message": "Stock transferred successfully",
    "data": {
      "pid": 12345,
      "source": {
        "location_id": 1,
        "shelf_id": 5,
        "previous_stock": 100,
        "new_stock": 50
      },
      "destination": {
        "location_id": 2,
        "shelf_id": 3,
        "previous_stock": 20,
        "new_stock": 70,
        "is_new_entry": false
      },
      "transferred_qty": 50,
      "best_before_date": null,
      "stockdiary": {
        "out_id": "uuid-out-entry",
        "in_id": "uuid-in-entry"
      }
    }
  }
}
    

{
  "INFO": { ... },
  "OUTPUT": {
    "response_type": "success",
    "action": "set",
    "status": "success",
    "message": "Stock set to absolute value",
    "data": {
      "pid": 12345,
      "location_id": 1,
      "shelf_id": 5,
      "previous_stock": 85,
      "new_stock": 200,
      "stock_change": 115,
      "reason": 9,
      "reason_text": "ADJUSTMENT_IN",
      "stockdiary_id": "uuid-diary-entry",
      "best_before_date": null,
      "is_new_entry": false
    }
  }
}
    

{
  "INFO": { ... },
  "OUTPUT": {
    "response_type": "success",
    "action": "adjust",
    "status": "success",
    "message": "Stock updated successfully",
    "data": {
      "pid": 12345,
      "location_id": 1,
      "shelf_id": 5,
      "previous_stock": 100,
      "stock_change": 50,
      "new_stock": 150,
      "reason": 1,
      "reason_text": "IN_PURCHASE",
      "stockdiary_id": "uuid-diary-entry",
      "best_before_date": "2025-12-31",
      "is_new_entry": false
    }
  }
}
    

{
  "INFO": { ... },
  "OUTPUT": {
    "response_type": "success",
    "action": "delete",
    "status": "success",
    "message": "Stock entry deleted successfully",
    "data": {
      "pid": 12345,
      "location_id": 1,
      "shelf_id": 5,
      "deleted_stock": 75,
      "best_before_date": "2025-06-30",
      "stockdiary_id": "uuid-diary-entry"
    }
  }
}
    

{
  "INFO": { ... },
  "OUTPUT": {
    "response_type": "error",
    "action": "transfer",
    "message": "Insufficient stock at source. Current: 30, Requested: 50",
    "error_code": "INSUFFICIENT_STOCK",
    "data": {
      "pid": 12345,
      "source": {
        "location_id": 1,
        "shelf_id": 5,
        "current_stock": 30,
        "requested_transfer": 50
      }
    }
  }
}
    

Error Codes

Error Code Description
MISSING_ACTION The action parameter is required
INVALID_ACTION Unknown action. Valid: adjust, set, transfer, modify_bbd, move, set_default_location, delete
MISSING_PID pid parameter is required
INVALID_PID pid must be a positive integer
PRODUCT_NOT_FOUND Product with specified pid does not exist
MISSING_LOCATION_ID location_id is required for this action
INVALID_LOCATION_ID location_id must be a positive integer
INVALID_LOCATION Location does not exist
MISSING_SHELF_ID shelf_id is required for this action
INVALID_SHELF_ID shelf_id must be a non-negative integer
INVALID_SHELF Shelf does not exist or doesn't belong to this location
MISSING_STOCK stock parameter is required for this action
STOCK_MUST_BE_POSITIVE Transfer qty must be positive
MISSING_REASON reason parameter is required for adjust action
INVALID_REASON Invalid reason code
MISSING_DEST_LOCATION dest_location_id is required for transfer action
MISSING_DEST_SHELF dest_shelf_id is required for transfer action
INVALID_DEST_LOCATION Destination location does not exist
INVALID_DEST_SHELF Destination shelf does not exist
SOURCE_EQUALS_DESTINATION Source and destination location/shelf cannot be the same
INSUFFICIENT_STOCK Not enough stock at source for transfer/deduction
STOCK_ENTRY_NOT_FOUND No stock entry exists at specified location/shelf
MISSING_BBD best_before_date is required for modify_bbd action
INVALID_BBD_FORMAT best_before_date must be in YYYY-MM-DD format
MISSING_NEW_LOCATION At least one of new_location_id or new_shelf_id must differ from current
INVALID_NEW_LOCATION New location does not exist
INVALID_NEW_SHELF New shelf does not exist or doesn't belong to new location
INTERNAL_ERROR An unexpected error occurred

Business Logic

Transfer Flow (action=transfer)

  1. Validate source has sufficient stock: stock >= transfer_qty
  2. Validate source ≠ destination
  3. Deduct from source tbl_stockcurrent row
  4. Create stockdiary entry: reason=-4 (OUT_MOVEMENT), stock=-qty, at source
  5. Add to destination: if row exists, sum quantities; if not, create new row
  6. Create stockdiary entry: reason=4 (IN_MOVEMENT), stock=+qty, at destination

Set Absolute Value Flow (action=set)

  1. Get current stock at location/shelf
  2. Calculate difference = new_value - old_value
  3. If difference = 0: return success with "no change" message
  4. Update tbl_stockcurrent.stock to new absolute value
  5. Create stockdiary entry: reason=9 (positive diff) or reason=-9 (negative diff)

Modify BBD Flow (action=modify_bbd)

  1. Get current stock at location/shelf
  2. Create OUT_MOVEMENT diary: reason=-4, stock=-current_qty, with OLD BBD
  3. Update tbl_stockcurrent.best_before_date to new value
  4. Create IN_MOVEMENT diary: reason=4, stock=+current_qty, with NEW BBD

Move Flow (action=move)

  1. Get current stock at location/shelf
  2. Create OUT_MOVEMENT diary: reason=-4, stock=-current_qty, OLD location
  3. Check if destination location+shelf already has this product:
    • YES: Combine (sum) stocks, remove source row (merge)
    • NO: Update existing row's location_id and/or shelf_id
  4. Create IN_MOVEMENT diary: reason=4, stock=+current_qty, NEW location

Best Practices

  1. Use transfer instead of two separate calls — the atomic transfer action ensures data integrity
  2. Use set for stocktaking — set stock to exact counted values rather than calculating differences manually
  3. Always provide reference — include transfer IDs, adjustment notes, or reasons for traceability
  4. Use adjust for transactional movements — sales, purchases, returns with appropriate reason codes
  5. Check stock before transfers — use get_products_stock or get_stockcurrent to verify available stock
  6. Handle merge detection — when moving/transferring to a location that already has stock, quantities are combined automatically

Comparison with set_products_stock

Feature set_products_stock set_stock
Add/deduct by reason ✅ ✅ (action=adjust)
Set absolute value ❌ ✅ (action=set)
Transfer (atomic) ❌ (needs 2 calls) ✅ (action=transfer)
Modify BBD ❌ ✅ (action=modify_bbd)
Move location/shelf ❌ ✅ (action=move)
Set default location ❌ ✅ (action=set_default_location)
Delete stock entry ❌ ✅ (action=delete)
Stock diary audit ✅ ✅ (all actions)
New entry auto-create ✅ ✅ (adjust only)

Note: Set Products Stock remains unchanged and fully functional. The set_stock endpoint is a superset that provides all advanced stock management operations.