Development of a Product Synchronization Plugin for Tec.Delivery
Methodology for creating integrations with WMS, ERP, POS, and accounting systems, hereinafter referred to as WMS.
This guide describes the recommended architecture, development workflow, data mapping rules, and integration process with the Merchant Product API for building custom synchronization plugins for categories, products, inventory levels, and images in the Tec.Delivery platform.
Required Information
Before starting development, it is necessary to obtain access credentials and technical information for both the Tec.Delivery platform and the WMS.
Plugin Architecture
It is recommended to separate the plugin into independent modules responsible for scheduling, data retrieval, data transformation, data upload to Tec.Delivery, and logging.
| Module | Purpose | Example |
|---|---|---|
| Scheduler | Controls execution frequency | Cron every 5 minutes |
| Store API Client | Retrieves data from the external system | get_products_store() |
| Tec API Client | Retrieves data from Tec.Delivery | get_products_tec() |
| Mapper | Transforms data | getProducts() |
| Uploader | Sends data to the API | put_products() |
| Logger | Logs plugin activity | integration.log |
Synchronization Types
| Type | Purpose | Recommended Frequency |
|---|---|---|
| Full Synchronization | Validates the entire catalog | 1–2 times per day |
| Change Synchronization | Updates modified products | Every 15–30 minutes |
| Inventory Synchronization | Updates stock balances | Every 5–10 minutes |
Full synchronization runs overnight, product changes are synchronized every 30 minutes, and inventory balances are updated every 5 minutes.
Working with the Merchant Product API
The put-products method of the Merchant Product API is used to upload products and categories.
| Parameter | Description | Example |
|---|---|---|
| api | API group | product |
| method | API method | put-products |
| token | Merchant token | merchant_token |
| language | Source data language | EN |
| translate | Automatic translation | true |
| webhook | URL for processing completion notifications | https://example.com/webhook |
Category Mapping
| External System Field | Tec.Delivery Field | Example |
|---|---|---|
| id | external_id | 1001 |
| parent | external_parent_id | 100 |
| name | name | Beverages |
| modified_at | external_key | 2026-06-01 10:00:00 |
Product Mapping
| External System Field | Tec.Delivery Field | Example |
|---|---|---|
| id | external_id | SKU123 |
| parent | external_parent_id | 1001 |
| name | name | Coca-Cola 500 ml |
| description | description | Carbonated beverage |
| price | price | 199 |
| store_balance | store_balance | 150 |
| modified_at | external_key | 2026-06-01 10:00:00 |
| image_modified_at | external_data | 2026-06-01 10:15:00 |
The external_key field is used to detect product changes, while external_data is used to detect image changes.
Image Synchronization
| Parameter | Description | Example |
|---|---|---|
| url | Image URL | https://site.com/image.jpg |
| resize | Resize image | false |
| bgcolor | Background color | #FFFFFF |
| scale | Scaling factor | 0.8 |
Example of plugin implementation in PHP
$tecApiUrl = "https://api.tec.delivery/common/merchant/1.0/";
$limit = 100;
$settings = [
"tec_token" => "YOUR_TEC_TOKEN",
"api_url" => "https://example.com/api/",
"method_get_product" => "products",
"method_get_balance" => "balance",
"webhook" => "https://example.com/webhook",
"auth" => false, // "login:password"
"language" => "EN",
"translate" => true,
];
echo "Start sync\n";
$put = [];
$put = array_merge($put, sync_categories($settings));
$put = array_merge($put, sync_products($settings));
put_products($settings, $put);
echo "Done\n";
function sync_categories(array $settings): array
{
$tecCat = get_products_tec([
"api" => "product",
"method" => "get-categories",
"fields" => [
"product_id",
"name",
"external_id",
"enable",
"external_parent_id",
"external_key"
],
"filter" => [
"enable" => ["eq" => true]
]
], $settings);
foreach ($tecCat as $key => $cat) {
$tecCat[$key]["exist"] = false;
}
$storeCat = get_products_store([
"type" => "category"
], $settings);
$put = [];
foreach ($storeCat as $cat) {
if (empty($cat["id"])) {
continue;
}
$index = findIndexByField($tecCat, "external_id", $cat["id"]);
if ($index !== false) {
$tecCat[$index]["exist"] = true;
if (($tecCat[$index]["external_key"] ?? null) === ($cat["modified_at"] ?? null)) {
continue;
}
}
$put[] = [
"external_id" => $cat["id"],
"external_parent_id" => $cat["parent"] ?? 0,
"name" => $cat["name"],
"external_key" => $cat["modified_at"] ?? null,
"is_category" => true,
"enable" => true,
];
}
foreach ($tecCat as $cat) {
if ($cat["exist"] === false) {
$put[] = [
"product_id" => $cat["product_id"],
"enable" => false,
];
}
}
return $put;
}
function sync_products(array $settings): array
{
$tecProducts = get_products_tec([
"api" => "product",
"method" => "get-all",
"fields" => [
"product_id",
"name",
"external_id",
"external_key",
"external_data",
"enable"
],
"filter" => [
"enable" => ["eq" => true],
"is_category" => ["eq" => false]
]
], $settings);
foreach ($tecProducts as $key => $product) {
$tecProducts[$key]["exist"] = false;
}
$storeProducts = get_products_store([
"type" => "product"
], $settings);
$put = [];
foreach ($storeProducts as $product) {
if (empty($product["id"])) {
continue;
}
$index = findIndexByField($tecProducts, "external_id", $product["id"]);
if ($index === false) {
$put[] = prepare_product($product, true);
continue;
}
$tecProducts[$index]["exist"] = true;
$changed =
($tecProducts[$index]["external_key"] ?? null) !== ($product["modified_at"] ?? null);
$imageChanged =
($tecProducts[$index]["external_data"] ?? null) !== ($product["image_modified_at"] ?? null);
if ($changed || $imageChanged) {
$put[] = prepare_product($product, $imageChanged);
}
}
foreach ($tecProducts as $product) {
if ($product["exist"] === false) {
$put[] = [
"product_id" => $product["product_id"],
"enable" => false,
"store_balance" => 0,
];
}
}
return $put;
}
function prepare_product(array $product, bool $withImage = false): array
{
$item = [
"external_id" => $product["id"],
"external_parent_id" => $product["parent"] ?? 0,
"external_key" => $product["modified_at"] ?? null,
"external_data" => $product["image_modified_at"] ?? null,
"name" => $product["name"] ?? "",
"description" => $product["description"] ?? "",
"price" => isset($product["price"]) ? (float)$product["price"] : 0,
"store_balance" => isset($product["store_balance"]) ? (int)$product["store_balance"] : 0,
"enable" => true,
"is_category" => false,
];
if ($withImage && !empty($product["image_url"])) {
$item["image"] = [
"url" => $product["image_url"],
"resize" => false,
"bgcolor" => "#FFFFFF",
"scale" => 0.8,
"remove_background" => false,
"hash" => [
"field" => "external_data",
"value" => $product["image_modified_at"] ?? ""
]
];
}
return $item;
}
function put_products(array $settings, array $data): void
{
global $tecApiUrl, $limit;
if (empty($data)) {
echo "Nothing to update\n";
return;
}
$chunks = array_chunk($data, $limit);
$total = count($chunks);
foreach ($chunks as $index => $chunk) {
echo "Put products: " . ($index + 1) . " / {$total}\n";
$request = [
"api" => "product",
"method" => "put-products",
"token" => $settings["tec_token"],
"webhook" => $settings["webhook"],
"language" => $settings["language"] ?? "EN",
"translate" => $settings["translate"] ?? true,
"payload" => $chunk
];
get_curl_response($tecApiUrl, $request);
}
}
function get_products_tec(array $request, array $settings): array
{
global $tecApiUrl, $limit;
$offset = 0;
$result = [];
while ($offset !== false) {
echo "Get Tec products offset: {$offset}\n";
$url = $tecApiUrl . "?token={$settings["tec_token"]}&limit={$limit}&offset={$offset}";
$response = get_curl_response($url, $request);
$data = $response["payload"]["data"] ?? [];
$offset = $response["payload"]["offset"] ?? false;
if (!is_array($data)) {
throw new Exception("Invalid Tec API response");
}
$result = array_merge($result, $data);
}
return $result;
}
function get_products_store(array $params, array $settings, string $method = "method_get_product"): array
{
global $limit;
$offset = 0;
$count = 1;
$result = [];
$query = http_build_query($params);
while ($offset < $count) {
echo "Get store products offset: {$offset}\n";
$url = $settings["api_url"]
. $settings[$method]
. "?limit={$limit}&offset={$offset}";
if ($query !== "") {
$url .= "&" . $query;
}
$response = get_curl_response($url, false, $settings["auth"] ?? false);
$count = (int)($response["count"] ?? 0);
$data = $response["data"] ?? [];
if (!is_array($data)) {
throw new Exception("Invalid store API response");
}
$result = array_merge($result, $data);
$offset += $limit;
}
return $result;
}
function get_curl_response(string $url, array|false $raw = false, string|false $basicAuth = false): array
{
$ch = curl_init($url);
$headers = [
"Accept: application/json;charset=utf-8",
"Accept-Encoding: gzip, deflate"
];
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_ENCODING, "");
if ($raw !== false) {
$json = json_encode($raw, JSON_UNESCAPED_UNICODE);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
$headers[] = "Content-Type: application/json";
$headers[] = "Content-Length: " . strlen($json);
}
if ($basicAuth) {
curl_setopt($ch, CURLOPT_USERPWD, $basicAuth);
}
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CONNECTTIMEOUT => 60,
CURLOPT_TIMEOUT => 60,
]);
$response = curl_exec($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
throw new Exception("cURL error: {$error}");
}
curl_close($ch);
if ($http < 200 || $http >= 300) {
throw new Exception("HTTP error {$http}: {$response}");
}
$decoded = json_decode($response, true);
if (!is_array($decoded)) {
throw new Exception("Invalid JSON response: {$response}");
}
return $decoded;
}
function findIndexByField(array $array, string $field, mixed $value): int|false
{
foreach ($array as $index => $item) {
if (isset($item[$field]) && $item[$field] == $value) {
return $index;
}
}
return false;
}
Synchronization Verification
| Test | Expected Result | Example |
|---|---|---|
| Create Category | Category appears in the catalog | New category imported |
| Create Product | Product appears in the catalog | New SKU imported |
| Modify Product | Changes applied successfully | Price updated |
| Inventory Change | Inventory updated | 150 → 75 |
| Delete Product | Product disabled | enable = false |
Development Recommendations
Final Checklist
- Merchant Token obtained.
- Category retrieval implemented.
- Product retrieval implemented.
- Inventory retrieval implemented.
- Image synchronization configured.
- Incremental synchronization implemented.
- Error logging configured.
- Retry logic configured.
- Result processing webhook configured.
- Testing completed using production data.