This document provides guidance on using the API to create or update stock transfers in the API server's database through the set_stock_transfer endpoint. This involves two steps: API authentication and stock transfer creation/update.
This API call is essential for creating new stock transfers or updating existing ones in the system.
To create or update a stock transfer, follow the steps below using the set_stock_transfer API call. The process involves providing credentials for API login and specific details for the stock transfer.
https://easycms.fi/public_api/set_stock_transfer/For stock transfer operations, you need to provide two sets of parameters - one for API authentication and the other for the stock transfer details.
TOKEN (api_key): Your unique API key for authentication. How to Create API Credentials.username: Your login username.password: Your login password.account: Your specific account ID.from_location_id: Source Location ID (Integer) - Required. The ID of the location where stock is being transferred from.to_location_id: Destination Location ID (Integer) - Required. The ID of the location where stock is being transferred to.transfer_date: Transfer Date (String) - The date of the transfer. Defaults to current date if not provided.status: Status (Integer) - The status of the transfer. Defaults to 1 (draft) if not provided.transfer_lines: Array of transfer line data - Required for creation.id: Transfer ID (Integer) - Required for update. The ID of the transfer to update.transfer_lines: Array of transfer line data (optional for update).id: Transfer ID (Integer) - The unique identifier for the transfer (for updates only).reference: Reference Number (Integer) - The reference number for the transfer (auto-generated for new transfers).from_location_id: Source Location ID (Integer) - The ID of the location where stock is being transferred from.to_location_id: Destination Location ID (Integer) - The ID of the location where stock is being transferred to.title: Title (String) - The title of the transfer.note: Note (String) - General notes for the transfer.admin_note: Admin Note (String) - Administrative notes.delivery_instructions: Delivery Instructions (String) - Special delivery instructions.transfer_date: Transfer Date (String) - The date of the transfer.delivery_date: Delivery Date (String) - The expected or actual delivery date.total: Total (Float) - The total quantity of items transferred.discount: Discount (Float) - Any discount applied.status: Status (Integer) - The status of the transfer:
1 = Draft100 = Sent110 = Preparing120 = Shipped200 = Received-1 = CancelledEach item in the transfer_lines array can contain:
pid: Product ID (Integer) - Required. The identifier of the product.quantity: Ordered Quantity (Float) - Required. The quantity to transfer. Read-only once the transfer is received (status 200) — re-sending the current value is fine, but changing it skips the line with an error. Use received_quantity to report actual received amounts.received_quantity: Received Quantity (Float) - Optional (v3.16+). The actual amount received, for transfers in received status (200). Received ≠ ordered is fully supported (over/under receipt); the confirm step (set_transfer_confirm) moves this amount, not the ordered quantity.unit_price: Unit Price (Float) - The unit price for the product.unit_price_buy: Unit Price Buy (Float) - The purchase price.product_name: Product Name (String) - The name of the product.product_title: Product Title (String) - The title of the product.line_note: Line Note (String) - Notes specific to this line.best_before_date or bbd: Best Before Date (String) - The expiry date if applicable. Accepted formats: YYYY-MM-DD (recommended), DD.MM.YYYY, MM/DD/YYYY, YYYY-MM-DD HH:MM:SS, ISO-8601. The value is normalized to a date literal before storing (v3.16 — previously raw epoch values were silently stored as 0000-00-00). Empty or unparseable values are ignored (the existing line value is kept). Send the bbd before confirming the line — the confirm step copies the line's bbd into the stock records.barcode: Barcode (String) - The product barcode.vat_percent: VAT Percent (Float) - The VAT percentage.vat_percent_buy: VAT Percent Buy (Float) - The VAT percentage for buying.discount: Discount (Float) - Line-level discount.
When a stock transfer's status is changed to 200 (received):
received_quantity = quantity pre-filledreceived_quantity (over/under receipt is supported)set_transfer_confirm to move stockset_transfer_confirm without transfer_line_id to confirm all lines at onceThis ensures accurate stock tracking -- the person receiving goods explicitly confirms what was actually received.
Confirmed-line safety rules (v3.16+): re-sending transfer_lines on a received (status 200) transfer follows these rules, so duplicate/idempotent submissions are safe:
changes.transfer_lines.lines_skipped_confirmed). The line's confirmed stock is never touched by a re-send.received_quantity → the previous confirmed stock movement is reversed, the line is un-confirmed and staged with the new quantity — call set_transfer_confirm again to apply the corrected amount.quantity (the ordered amount) on a received transfer → rejected per line with an error: ordered quantity is read-only once received. Use received_quantity.received_quantity wins; without it, any already-entered received quantity is preserved (the ordered quantity never overwrites an entered actual count).Changing the status field on an UPDATE now runs the same stock-movement engine as the CMS editor. The API and CMS behave identically — there is no longer a difference between "changing status via the admin UI" and "changing status via API".
Validation: Only the canonical status values are accepted on UPDATE: 1, 100, 110, 120, 200, -1. Any other value returns 400 INVALID_STATUS.
What each transition does to stock:
| Transition | Stock effect |
|---|---|
Draft → Sent (1 → 100/110/120) |
Reserves stock at source: writes tbl_stockcoming rows (stock leaves source's availability, expected at destination). Live stock (tbl_stockcurrent) is not touched yet. |
Sent → Received (100/110/120 → 200) |
Pre-fills received_quantity = quantity on all lines, sets arrival_date = today. Stock movement is deferred — call set_transfer_confirm to move it. |
Draft → Received (1 → 200) |
Same as Sent → Received: pre-fills lines, defers movement to set_transfer_confirm. |
Received → Draft/Sent (200 → 1/100) |
Reverses the confirmed stock movements (puts stock back at source, removes from destination), resets received_confirmed = 0. |
Sent → Draft (100 → 1) |
Removes the tbl_stockcoming reservations. |
Any → Cancelled (→ -1) |
Rolls back all stock effects as if returning to draft, then marks the transfer as deleted. |
Reason codes used in stock diary: +4 (IN_MOVEMENT, stock arrives at destination) and -4 (OUT_MOVEMENT, stock leaves source). These are the same codes the CMS uses.
Recommended mobile flow:
set_stock_transfer (status defaults to 1) — no stock touchedset_stock_transfer UPDATE with status: 100 — stock reserved at source (in transit)set_stock_transfer UPDATE with status: 200 — lines pre-filled (received_quantity = quantity), movement deferredset_stock_transfer UPDATE with transfer_lines: [{pid, quantity: <ordered>, received_quantity: <actual>}] — keep quantity at the ordered value; only set received_quantity to what physically arrived. Omit lines you are not changing, and never re-send a line after confirming it.set_transfer_confirm with id — stock actually moves to destination (per line with transfer_line_id, or all pending lines at once)Steps 2 and 3 can be combined if the destination confirms receipt immediately, but the confirm step (set_transfer_confirm) is always required to move stock to tbl_stockcurrent.
# Create a new stock transfer
curl -X POST 'https://easycms.fi/public_api/set_stock_transfer' \
-H 'Authorization1: TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"username": "USERNAME",
"password": "PASSWORD",
"account": "ACCOUNT_ID",
"from_location_id": 1,
"to_location_id": 2,
"transfer_date": "2024-01-15 10:30:00",
"status": 1,
"title": "Monthly Stock Transfer",
"note": "Regular monthly transfer",
"delivery_instructions": "Handle with care",
"transfer_lines": [
{
"pid": 12345,
"quantity": 100,
"unit_price": 15.50,
"product_name": "Product Name",
"barcode": "1234567890123",
"best_before_date": "2025-12-31"
},
{
"pid": 12346,
"quantity": 50,
"unit_price": 22.00,
"product_name": "Another Product",
"boxcode": "BOX001"
}
]
}'
# Update an existing stock transfer
curl -X POST 'https://easycms.fi/public_api/set_stock_transfer' \
-H 'Authorization1: TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"username": "USERNAME",
"password": "PASSWORD",
"account": "ACCOUNT_ID",
"id": 123,
"status": 200,
"note": "Transfer completed"
}'
$curl = curl_init();
$payload = [
'username' => 'USERNAME',
'password' => 'PASSWORD',
'account' => 'ACCOUNT_ID',
'from_location_id' => 1,
'to_location_id' => 2,
'transfer_date' => '2024-01-15 10:30:00',
'status' => 1,
'title' => 'Monthly Stock Transfer',
'note' => 'Regular monthly transfer',
'delivery_instructions' => 'Handle with care',
'transfer_lines' => [
[
'pid' => 12345,
'quantity' => 100,
'unit_price' => 15.50,
'product_name' => 'Product Name',
'barcode' => '1234567890123',
'best_before_date' => '2025-12-31'
],
[
'pid' => 12346,
'quantity' => 50,
'unit_price' => 22.00,
'product_name' => 'Another Product',
'boxcode' => 'BOX001'
]
]
];
curl_setopt_array($curl, [
CURLOPT_URL => "https://easycms.fi/public_api/set_stock_transfer",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
"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_transfer"
headers = {
"Authorization1": "TOKEN",
"Content-Type": "application/json"
}
payload = {
"username": "USERNAME",
"password": "PASSWORD",
"account": "ACCOUNT_ID",
"from_location_id": 1,
"to_location_id": 2,
"transfer_date": "2024-01-15 10:30:00",
"status": 1,
"title": "Monthly Stock Transfer",
"note": "Regular monthly transfer",
"delivery_instructions": "Handle with care",
"transfer_lines": [
{
"pid": 12345,
"quantity": 100,
"unit_price": 15.50,
"product_name": "Product Name",
"barcode": "1234567890123",
"best_before_date": "2025-12-31"
},
{
"pid": 12346,
"quantity": 50,
"unit_price": 22.00,
"product_name": "Another Product",
"boxcode": "BOX001"
}
]
}
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;
public class Main {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String jsonPayload = """
{
"username": "USERNAME",
"password": "PASSWORD",
"account": "ACCOUNT_ID",
"from_location_id": 1,
"to_location_id": 2,
"transfer_date": "2024-01-15 10:30:00",
"status": 1,
"title": "Monthly Stock Transfer",
"note": "Regular monthly transfer",
"delivery_instructions": "Handle with care",
"transfer_lines": [
{
"pid": 12345,
"quantity": 100,
"unit_price": 15.50,
"barcode": "1234567890123"
}
]
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://easycms.fi/public_api/set_stock_transfer"))
.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 = {
username: 'USERNAME',
password: 'PASSWORD',
account: 'ACCOUNT_ID',
from_location_id: 1,
to_location_id: 2,
transfer_date: '2024-01-15 10:30:00',
status: 1,
title: 'Monthly Stock Transfer',
note: 'Regular monthly transfer',
delivery_instructions: 'Handle with care',
transfer_lines: [
{
pid: 12345,
quantity: 100,
unit_price: 15.50,
product_name: 'Product Name',
barcode: '1234567890123',
bbd: '2025-12-31'
}
]
};
const data = JSON.stringify(payload);
const options = {
hostname: 'easycms.fi',
path: '/public_api/set_stock_transfer',
method: 'POST',
headers: {
'Authorization1': 'TOKEN',
'Content-Type': 'application/json',
'Content-Length': data.length
}
};
const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => { responseData += chunk; });
res.on('end', () => { console.log(responseData); });
});
req.on('error', (e) => { console.error(e); });
req.write(data);
req.end();
import React, { useEffect, useState } from 'react';
function App() {
const [responseData, setResponseData] = useState('');
useEffect(() => {
const createTransfer = async () => {
try {
const response = await fetch('https://easycms.fi/public_api/set_stock_transfer', {
method: 'POST',
headers: {
'Authorization1': 'TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'USERNAME',
password: 'PASSWORD',
account: 'ACCOUNT_ID',
from_location_id: 1,
to_location_id: 2,
transfer_date: '2024-01-15 10:30:00',
status: 1,
title: 'Monthly Stock Transfer',
note: 'Regular monthly transfer',
delivery_instructions: 'Handle with care',
transfer_lines: [
{
pid: 12345,
quantity: 100,
unit_price: 15.50,
barcode: '1234567890123'
}
]
})
});
const data = await response.json();
setResponseData(JSON.stringify(data, null, 2));
} catch (error) {
console.error(error);
}
};
createTransfer();
}, []);
return <pre>{responseData}</pre>;
}
export default App;
import okhttp3.OkHttpClient
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
fun main() {
val client = OkHttpClient()
val jsonPayload = """
{
"username": "USERNAME",
"password": "PASSWORD",
"account": "ACCOUNT_ID",
"from_location_id": 1,
"to_location_id": 2,
"transfer_date": "2024-01-15 10:30:00",
"status": 1,
"title": "Monthly Stock Transfer",
"note": "Regular monthly transfer",
"delivery_instructions": "Handle with care",
"transfer_lines": [
{
"pid": 12345,
"quantity": 100,
"unit_price": 15.50,
"barcode": "1234567890123"
}
]
}
""".trimIndent()
val requestBody = jsonPayload.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("https://easycms.fi/public_api/set_stock_transfer")
.post(requestBody)
.addHeader("Authorization1", "TOKEN")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
println(response.body?.string())
}
}
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization1", "TOKEN");
var payload = new
{
username = "USERNAME",
password = "PASSWORD",
account = "ACCOUNT_ID",
from_location_id = 1,
to_location_id = 2,
transfer_date = "2024-01-15 10:30:00",
status = 1,
title = "Monthly Stock Transfer",
note = "Regular monthly transfer",
delivery_instructions = "Handle with care",
transfer_lines = new[]
{
new
{
pid = 12345,
quantity = 100,
unit_price = 15.50,
barcode = "1234567890123"
}
}
};
var jsonContent = new StringContent(
JsonSerializer.Serialize(payload),
Encoding.UTF8,
"application/json"
);
var response = await httpClient.PostAsync(
"https://easycms.fi/public_api/set_stock_transfer",
jsonContent
);
if (response.IsSuccessStatusCode)
{
var responseBody = await response.Content.ReadAsStringAsync();
Console.WriteLine(responseBody);
}
else
{
Console.WriteLine($"Error: {response.StatusCode}");
}
}
}
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:
Response Structure:
- `status`: Response status - "success" or "error"
- `message`: Human-readable message describing the result
- `data`: Object containing the created/updated transfer data
- `id`: The transfer ID
- `transfer`: The complete transfer object with all fields
- `changes`: Object tracking what was changed
- For CREATE:
- `transfer_created`: Object with new transfer details (id, reference, from_location_id, to_location_id)
- `transfer_lines`: Result of line processing
- For UPDATE:
- `transfer`: Object with field-level changes (old and new values)
- `transfer_lines`: Result of line processing (if lines were provided)
Transfer Object Fields:
- `id`: Transfer ID - Unique identifier
- `reference`: Reference Number - Auto-generated reference
- `from_location_id`: Source Location ID - Where stock is being transferred from
- `from_location_name`: Source Location Name - Name of source location
- `to_location_id`: Destination Location ID - Where stock is being transferred to
- `to_location_name`: Destination Location Name - Name of destination location
- `transfer_date`: Transfer Date - When the transfer occurred
- `delivery_date`: Delivery Date - Expected/actual delivery date
- `status`: Status Code - Current transfer status
- `title`: Title - Transfer title
- `note`: Note - General notes
- `admin_note`: Admin Note - Administrative notes
- `delivery_instructions`: Delivery Instructions
- `total`: Total - Total quantity of items transferred
- `discount`: Discount - Applied discount
- `deleted`: Deleted - Whether the transfer is deleted
Transfer Lines Result:
- `success`: Boolean - Whether line processing succeeded (true when at least one line was processed OR skipped as already-confirmed)
- `lines_processed`: Integer - Number of lines written
- `lines_skipped`: Integer - Number of lines skipped due to validation errors
- `lines_skipped_confirmed`: Integer - Number of already-confirmed lines skipped (idempotent re-send, no changes made)
- `errors`: Array of Strings - Per-line validation errors (e.g. attempted ordered-quantity change on a received transfer)
- `notes`: Array of Strings - Per-line informational notes (e.g. confirmed quantity reversed and un-confirmed)
- `message`: String - Description of the result
{
"status": "success",
"message": "Transfer created successfully",
"data": {
"id": 125,
"transfer": {
"id": "125",
"reference": "2024001",
"from_location_id": "1",
"from_location_name": "Main Warehouse",
"to_location_id": "2",
"to_location_name": "Branch Store",
"transfer_date": "2024-01-15 10:30:00",
"delivery_date": "2024-01-20 14:00:00",
"status": "1",
"title": "Monthly Stock Transfer",
"note": "Regular monthly transfer",
"admin_note": "",
"delivery_instructions": "Handle with care",
"total": "100",
"discount": "0",
"deleted": "0"
}
},
"changes": {
"transfer_created": {
"id": 125,
"reference": "2024001",
"from_location_id": 1,
"to_location_id": 2
},
"transfer_lines": {
"success": true,
"lines_processed": 2,
"message": "Transfer lines updated for 2 product(s)"
}
}
}
{
"status": "success",
"message": "Transfer updated successfully",
"data": {
"id": 123,
"transfer": {
"id": "123",
"reference": "2024000",
"from_location_id": "1",
"from_location_name": "Main Warehouse",
"to_location_id": "2",
"to_location_name": "Branch Store",
"transfer_date": "2024-01-14 09:00:00",
"delivery_date": "2024-01-20 14:00:00",
"status": "200",
"title": "Monthly Stock Transfer",
"note": "Transfer completed",
"admin_note": "",
"delivery_instructions": "Handle with care",
"total": "200",
"discount": "0",
"deleted": "0"
}
},
"changes": {
"transfer": {
"status": {
"old": "120",
"new": 200
},
"note": {
"old": "Monthly stock transfer",
"new": "Transfer completed"
}
}
}
}
Stock transfers follow a status flow from creation to completion:
| Status Code | Title | Description |
|---|---|---|
| 1 | draft_transfer | Transfer is in draft state, can be modified |
| 100 | transfer_sent | Transfer has been sent to destination |
| 110 | transfer_preparing | Transfer is being prepared for shipment |
| 120 | transfer_shipped | Transfer has been shipped |
| 200 | transfer_received | Transfer received — stock movement deferred to per-line confirmation (see below) |
| -1 | cancelled | Transfer has been cancelled |
Two-phase receive (v3.08+): When a transfer status changes to 200 (received):
received_quantity is pre-filled with its ordered quantityset_transfer_confirm endpoint, which performs the actual stock movement (deducts from source, adds to destination)set_transfer_confirm without transfer_line_id to confirm all pending lines at onceHere are the possible error messages and their meanings:
UN-AUTHORIZED - _user_name_password_is_set_but_wrong_value!: Incorrect username or password.this_account_does_not_exist_or_your_credentials_do_not_match_this_account: The account doesn't exist or mismatched credentials.UN-AUTHORIZED - header is set but the header value is not correct!: Incorrect authorization header value.METHOD_NOT_ALLOWED: Only POST method is allowed for this endpoint.MISSING_REQUIRED_FIELDS: Missing required fields for creation. Required: from_location_id, to_location_idMISSING_TRANSFER_LINES: Transfer lines are required for creating a stock transfer.TRANSFER_NOT_FOUND: The specified transfer ID was not found (for updates).UPDATE_FAILED: Failed to update the transfer in the database.CREATE_FAILED: Failed to create the transfer in the database.INVALID_LOCATION: The specified location ID was not found.INSUFFICIENT_STOCK: Not enough stock available at source location for transfer.INVALID_STATUS: The status value is not one of the allowed values (1, 100, 110, 120, 200, -1). Returned on UPDATE only.Per-line errors (in changes.transfer_lines.errors, the line is skipped — the request itself still succeeds):
Cannot change ordered quantity for a received (status 200) transfer...: The quantity of an existing line was changed after the transfer was marked received. Ordered quantity is read-only once received; use received_quantity to report actual amounts.from_location_id, to_location_id, and transfer_lines when creating a new transfer.100/110/120 (sent/preparing/shipped) reserves stock at the source location (writes tbl_stockcoming). The stock is now "in transit".200 (received) pre-fills received_quantity but defers the actual stock movement. Call set_transfer_confirm to move stock to the destination.pid and quantity at minimum. Optional line fields: unit_price, unit_price_buy, vat_percent, vat_percent_buy, discount, weight, weight_type, stock_type_id, product_name, product_title, boxcode, line_note, best_before_date (or bbd), barcode, received_quantity (received transfers only).set_transfer_confirm with transfer_line_id to handle partial receipts.received_quantity per line (keeping quantity at the ordered value) when more or fewer items physically arrived than ordered. Already-confirmed lines re-sent unchanged are safely skipped.set_transfer_confirm), stop including it in subsequent transfer_lines arrays. It is ignored (idempotent skip), but sending only pending lines keeps responses clean and avoids ambiguity.