TEC.DELIVERY
enesilru

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.

Every synchronization plugin must use stable external identifiers and support incremental data updates to minimize API load and reduce synchronization time.
Preparation

Required Information

Before starting development, it is necessary to obtain access credentials and technical information for both the Tec.Delivery platform and the WMS.

Complete Documentation: Product Management API

Plugin Development

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
Example

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

API → Product → put-products

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
Example

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
Images should only be uploaded when the image_modified_at value changes.

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;
}

        
Testing

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
Important Notes

Development Recommendations

Always use stable external_id values. Changing identifiers will result in duplicate products being created.
For large catalogs, always use paginated loading with limit and offset.
Implement retry logic for HTTP 429 and 5xx errors.
Send data in batches of no more than 100 products per request.
Final Checklist

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.