Товары на сайте показывают неправильные остатки, менеджеры правят цены в МойСклад, а на сайте они не обновляются. Заказы приходится переносить вручную — это приводит к 20–30% ошибок отгрузки и потерям клиентов. Двусторонняя синхронизация решает проблему: товары, остатки, заказы и статусы обновляются автоматически. Результат — актуальные данные на сайте и в МойСклад без вашего участия. Экономия на ручном вводе данных достигает 200 000 руб. в год.
Какие проблемы решает двусторонняя синхронизация?
Расхождение остатков — самая частая боль. Клиент видит товар в наличии, а на складе его уже нет, или наоборот. Наша синхронизация обновляет остатки каждые 5 минут, снижая количество ошибок отгрузки на 80% по сравнению с ручным управлением. Вторая проблема — ручной перенос заказов. Сотрудники тратят до 2 часов в день на дублирование информации. Двусторонняя синхронизация автоматически создаёт заказ в МойСклад при оформлении на сайте и обновляет статус при отгрузке. Третья — конфликты данных. Если цену изменили одновременно в двух системах, мы используем чёткое правило источника истины: цены и остатки — из МойСклад, SEO и изображения — с сайта.
Как мы реализуем интеграцию
Архитектура и стек
Используем REST API МойСклад версии 1.2, PHP 8.3 с фреймворком Laravel 11 и очередями через Redis. Для аутентификации — Bearer-токен. Все запросы идут через HTTP-клиент с повторными попытками при ошибках. Webhook настроены на события изменения заказов, чтобы статусы обновлялись мгновенно. Архитектура учитывает N+1 запросы и использует чанкованную загрузку данных (пачки по 100 элементов).
Согласно официальной документации API МойСклад, это обеспечивает надёжную интеграцию.
Пример реализации клиента API
class MoiSkladClient
{
private string $baseUrl = 'https://api.moysklad.ru/api/remap/1.2';
private function headers(): array
{
return [
'Authorization' => 'Bearer ' . config('services.moysklad.token'),
'Content-Type' => 'application/json;charset=utf-8',
'Accept-Encoding' => 'gzip',
];
}
public function get(string $path, array $params = []): array
{
return Http::withHeaders($this->headers())
->get("{$this->baseUrl}/{$path}", $params)
->throw()
->json();
}
public function post(string $path, array $data): array
{
return Http::withHeaders($this->headers())
->post("{$this->baseUrl}/{$path}", $data)
->throw()
->json();
}
}
| Сущность | Источник истины | Примечание |
|---|---|---|
| Товары (название, описание, цена) | МойСклад | Менеджеры правят там |
| Остатки | МойСклад | Обновляются при поступлении/продаже |
| Изображения товаров | Сайт | Загружаются через CMS |
| SEO-поля (meta, slug) | Сайт | Не существуют в МС |
| Заказы | Сайт → МС | Создаются на сайте, уходят в МС |
| Статусы заказов | МС → Сайт | Менеджер меняет в МС |
Получение товаров и остатков
class ProductSyncService
{
public function fetchProducts(int $offset = 0, int $limit = 100): array
{
return $this->ms->get('entity/product', [
'offset' => $offset,
'limit' => $limit,
'expand' => 'productFolder,images',
'filter' => 'archived=false',
]);
}
public function fetchStocks(): array
{
return $this->ms->get('report/stock/all/current', [
'stockType' => 'stock',
'includeRelated' => false,
]);
}
public function syncToSite(): void
{
$offset = 0;
$limit = 100;
do {
$response = $this->fetchProducts($offset, $limit);
$products = $response['rows'];
foreach ($products as $msProduct) {
$this->upsertProduct($msProduct);
}
$offset += $limit;
} while ($offset < $response['meta']['size']);
$stocks = $this->fetchStocks();
foreach ($stocks as $stock) {
Product::whereExternalId($stock['assortmentId'])
->update(['stock' => max(0, (int)$stock['stock'])]);
}
}
private function upsertProduct(array $msProduct): void
{
$msId = $msProduct['id'];
$data = [
'external_id' => $msId,
'name' => $msProduct['name'],
'article' => $msProduct['article'] ?? null,
'price' => $this->parsePrice($msProduct['salePrices'][0]['value'] ?? 0),
'description' => $msProduct['description'] ?? '',
'ms_updated_at'=> Carbon::parse($msProduct['updated']),
];
$product = Product::updateOrCreate(['external_id' => $msId], $data);
}
private function parsePrice(int $msPrice): float
{
return $msPrice / 100;
}
}
Передача заказов и webhook
public function pushOrder(Order $order): string
{
$positions = [];
foreach ($order->items as $item) {
$positions[] = [
'assortment' => [
'meta' => [
'href' => "{$this->baseUrl}/entity/product/{$item->product->external_id}",
'type' => 'product',
],
],
'quantity' => $item->quantity,
'price' => $item->price * 100,
];
}
$msOrder = $this->ms->post('entity/customerorder', [
'name' => "Заказ #{$order->id}",
'organization' => [
'meta' => [
'href' => "{$this->baseUrl}/entity/organization/" . config('services.moysklad.org_id'),
'type' => 'organization',
],
],
'agent' => $this->getOrCreateCounterparty($order->customer),
'positions' => $positions,
'description' => "Источник: сайт\nEmail: {$order->customer->email}",
'attributes' => [
[
'meta' => ['href' => $this->orderIdAttributeHref()],
'value' => (string)$order->id,
],
],
]);
$order->update(['ms_order_id' => $msOrder['id']]);
return $msOrder['id'];
}
При изменении заказа в МойСклад webhook уведомляет наш сервер, и статус автоматически обновляется на сайте:
public function handleMsWebhook(Request $request): Response
{
$events = $request->json('events', []);
foreach ($events as $event) {
if ($event['meta']['type'] === 'customerorder') {
$msOrderId = basename($event['meta']['href']);
SyncOrderStatusJob::dispatch($msOrderId);
}
}
return response()->noContent();
}
class SyncOrderStatusJob implements ShouldQueue
{
public function handle(MoiSkladClient $ms): void
{
$msOrder = $ms->get("entity/customerorder/{$this->msOrderId}");
// маппинг статусов и обновление заказа
}
}
Как часто обновляются данные и как разрешаются конфликты?
Остатки синхронизируются каждые 5 минут, товары и заказы — каждые 10 минут. Это обеспечивает актуальность без излишней нагрузки на API. При необходимости частоту можно настроить индивидуально.
При двусторонней синхронизации конфликты неизбежны. Наш подход: цена, остатки, артикул — приоритет МойСклад; SEO-поля и изображения — сайт; название — по дате последнего изменения. Мы гарантируем, что вы не потеряете важные изменения. Ошибки отгрузки обходятся в среднем в 50 000 руб. в месяц — синхронизация сводит их к минимуму.
Сравнение подходов
| Характеристика | Односторонняя | Двусторонняя |
|---|---|---|
| Товары, цены, остатки | ✅ | ✅ |
| Заказы (сайт→МС) | ❌ | ✅ |
| Статусы (МС→сайт) | ❌ | ✅ |
| Разрешение конфликтов | не требуется | ✅ |
| Ручной труд менеджеров | ~1 час/день | автоматически |
| Ошибки отгрузки | 20-30% | <5% |
Двусторонняя синхронизация в 10 раз сокращает время на обработку заказов и практически исключает ошибки отгрузки.
Почему стоит выбрать двустороннюю синхронизацию?
Более 5 лет мы интегрируем интернет-магазины с МойСклад. За это время реализовали 30+ проектов, включая сложные с тысячью SKU. Наши инженеры имеют подтверждённую экспертизу в Laravel и REST API. Мы даём гарантию на работоспособность интеграции и оперативно исправляем возможные нестыковки. Экономия на ручном вводе данных достигает 200 000 руб. в год, а затраты на исправление ошибок отгрузки снижаются до 50 000 руб. в месяц.
Процесс и сроки работы
- Аналитика — изучаем структуру каталога, определяем сущности и правила маппинга.
- Проектирование — проектируем интеграцию: какие поля, как часто, обработка ошибок.
- Реализация — пишем код, настраиваем webhook, очереди.
- Тестирование — запускаем тестовую синхронизацию на копии данных, сверяем результаты.
- Деплой — разворачиваем на боевом сервере, проводим мониторинг.
Ориентировочные сроки: односторонняя синхронизация (МойСклад → сайт) — 2–3 рабочих дня; двусторонняя (с заказами, статусами, webhook) — 6–8 рабочих дней. Срок может увеличиться при сложной структуре каталога (вариации, комплекты). Проведём бесплатный аудит вашей текущей системы и предложим оптимальное решение. Свяжитесь с нами, чтобы навсегда забыть о ручном переносе данных. Получите консультацию уже сегодня.







