Главная/Статьи/Интеграция с Ozon API: синхронизация товаров и заказов

Интеграция с Ozon API: синхронизация товаров и заказов

Практическое руководство по интеграции интернет-магазина с маркетплейсом Ozon. Загрузка товаров, обработка заказов, работа с FBS/FBO.

ДМ
Дмитрий Мещеряков
📅 8 июля 2026 г.📖 6 мин чтения

Интеграция с маркетплейсом — не разовая работа «подключить API», а постоянно работающий обмен, у которого есть цена ошибки в деньгах: неверные остатки означают заказы на то, чего нет, и штрафы за отмену.

Разберём основные операции и, отдельно, то, что определяет надёжность обмена: ограничения частоты, идемпотентность и поведение при недоступности API.

Зачем интегрироваться с Ozon

Ozon — один из крупнейших маркетплейсов в России. Интеграция позволяет:

  • Автоматически выгружать товары из вашего каталога
  • Синхронизировать остатки в реальном времени
  • Получать и обрабатывать заказы в своей системе
  • Обновлять статусы и трек-номера
💡 Совет

Ozon предоставляет два типа работы: FBO (склад Ozon) и FBS (ваш склад). Для FBS критична синхронизация остатков — за oversell штрафуют.

Получение доступа к API

  1. Зайдите в личный кабинет продавца Ozon
  2. Перейдите в раздел Настройки → API ключи
  3. Создайте новый ключ с нужными правами
  4. Сохраните Client-Id и Api-Key
bash
# .env
OZON_CLIENT_ID=your_client_id
OZON_API_KEY=your_api_key

Базовый HTTP-клиент

Все запросы к Ozon API требуют заголовков авторизации:

typescript
// lib/ozon-client.ts
const OZON_API_URL = 'https://api-seller.ozon.ru';
interface OzonConfig {
  clientId: string;
  apiKey: string;
}
export class OzonClient {
  private config: OzonConfig;
  constructor(config: OzonConfig) {
    this.config = config;
  }
  async request<T>(endpoint: string, body?: object): Promise<T> {
    const response = await fetch(`${OZON_API_URL}${endpoint}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Client-Id': this.config.clientId,
        'Api-Key': this.config.apiKey,
      },
      body: body ? JSON.stringify(body) : undefined,
    });
    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Ozon API Error: ${error.message}`);
    }
    return response.json();
  }
}
// Использование
const ozon = new OzonClient({
  clientId: process.env.OZON_CLIENT_ID!,
  apiKey: process.env.OZON_API_KEY!,
});

Загрузка товаров

Товары загружаются через endpoint /v3/product/import:

typescript
interface ProductImport {
  offer_id: string;        // Ваш артикул
  name: string;            // Название
  price: string;           // Цена
  old_price?: string;      // Старая цена (зачёркнутая)
  vat: string;             // НДС: "0", "0.1", "0.2"
  weight: number;          // Вес в граммах
  dimension_unit: string;  // "mm"
  weight_unit: string;     // "g"
  height: number;
  width: number;
  depth: number;
  category_id: number;     // ID категории Ozon
  images: string[];        // URL изображений
  attributes: Attribute[]; // Характеристики
}
async function importProducts(products: ProductImport[]) {
  return ozon.request('/v3/product/import', {
    items: products,
  });
}
⚠️ Важно

Ozon модерирует товары 1–3 дня. Не загружайте товары повторно — это создаст дубликаты. Используйте offer_id для обновления существующих.

Синхронизация остатков

⚠️ Важно

Остатки — самая критичная часть интеграции, и ошибки в ней стоят дороже всего. Завышенный остаток означает заказ на отсутствующий товар: отмена, штраф и просевший рейтинг продавца. Заниженный — просто потерянные продажи.

Что из этого следует:

Синхронизируйте разницу, а не весь каталог. Полная выгрузка десяти тысяч позиций каждые пятнадцать минут упрётся в лимиты API и займёт больше времени, чем интервал между запусками. Отправляйте только те позиции, у которых остаток изменился с прошлого раза.

Резервируйте под собственные заказы. Если товар продаётся и на сайте, и на маркетплейсе, отдавать в Ozon полный складской остаток нельзя — иначе одну и ту же последнюю единицу купят дважды.

Не отправляйте остатки при неполных данных. Если выгрузка со склада пришла частично или не пришла вовсе, лучше не обновлять ничего, чем обнулить остатки по половине каталога. Проверка на вменяемость перед отправкой обязательна: резкое падение количества позиций в источнике — повод остановиться и позвать человека.

Для FBS критически важно поддерживать актуальные остатки:

typescript
interface StockUpdate {
  offer_id: string;
  stock: number;
  warehouse_id: number;
}
async function updateStocks(stocks: StockUpdate[]) {
  return ozon.request('/v2/products/stocks', {
    stocks: stocks.map((s) => ({
      offer_id: s.offer_id,
      product_id: 0, // Можно не указывать если есть offer_id
      stock: s.stock,
      warehouse_id: s.warehouse_id,
    })),
  });
}
// Обновляем остатки каждые 5 минут
async function syncAllStocks() {
  const products = await getProductsFromOurDB();
  const warehouseId = await getOzonWarehouseId();
  const stocks = products.map((p) => ({
    offer_id: p.sku,
    stock: p.quantity,
    warehouse_id: warehouseId,
  }));
  await updateStocks(stocks);
  console.log(`Synced ${stocks.length} products`);
}

Получение заказов

Заказы получаем через polling или webhooks:

typescript
interface OrderFilter {
  since: string;  // ISO datetime
  to: string;
  status: string; // awaiting_packaging, awaiting_deliver, etc.
}
async function getOrders(filter: OrderFilter) {
  const result = await ozon.request('/v3/posting/fbs/list', {
    dir: 'ASC',
    filter: {
      since: filter.since,
      to: filter.to,
      status: filter.status,
    },
    limit: 100,
    offset: 0,
    with: {
      analytics_data: true,
      financial_data: true,
    },
  });
  return result.result.postings;
}
// Пример обработки
async function processNewOrders() {
  const orders = await getOrders({
    since: new Date(Date.now() - 3600000).toISOString(), // Последний час
    to: new Date().toISOString(),
    status: 'awaiting_packaging',
  });
  for (const order of orders) {
    await createOrderInOurSystem(order);
    await markOrderAsProcessing(order.posting_number);
  }
}

Обновление статуса отправления

После сборки заказа обновляем статус и добавляем трек:

typescript
async function shipOrder(postingNumber: string, trackNumber: string) {
  // Собираем товары
  await ozon.request('/v3/posting/fbs/ship', {
    posting_number: postingNumber,
    packages: [
      {
        products: [], // Список SKU в посылке
      },
    ],
  });
  // Добавляем трек-номер
  await ozon.request('/v2/posting/fbs/set-tracking-number', {
    tracking_numbers: [
      {
        posting_number: postingNumber,
        tracking_number: trackNumber,
      },
    ],
  });
}

Обработка ошибок

Ozon API возвращает специфические коды ошибок:

typescript
async function safeRequest<T>(fn: () => Promise<T>): Promise<T | null> {
  try {
    return await fn();
  } catch (error) {
    if (error instanceof Error) {
      // Rate limit
      if (error.message.includes('TOO_MANY_REQUESTS')) {
        await sleep(5000);
        return safeRequest(fn);
      }
      // Товар не найден — не критично
      if (error.message.includes('PRODUCT_NOT_FOUND')) {
        console.warn('Product not found, skipping');
        return null;
      }
      // Остальные ошибки логируем
      console.error('Ozon API Error:', error.message);
    }
    throw error;
  }
}

Итоги

  1. Авторизация — Client-Id и Api-Key в заголовках, оба из конфигурации вне репозитория.
  2. Товары — загрузка через /v3/product/import, обновление по offer_id. Ваш артикул — единственная связь между системами, поэтому его изменение на стороне сайта ломает всю привязку; относитесь к нему как к неизменяемому ключу.
  3. Остатки — только изменившиеся позиции, с резервом под собственные продажи и проверкой источника на полноту.
  4. Заказы — опрос или уведомления; в обоих случаях обработка обязана быть идемпотентной, потому что один и тот же заказ вы получите повторно.
  5. Ошибки — учитывайте ограничение частоты запросов и разделяйте временные сбои (повторить) от постоянных (звать человека).

И то, что определяет надёжность больше, чем код самой интеграции:

Обмен работает в фоне и падает молча. Нужен мониторинг: «последняя успешная синхронизация не старше часа», «в очереди не больше N необработанных заданий» — с уведомлением живому человеку. Без этого о сломанном обмене вы узнаете от службы поддержки маркетплейса.

Ведите журнал отправленного и полученного. В спорах о том, какие остатки вы передали и когда, это единственный аргумент.

Обрабатывайте ответы, а не только отправляйте запросы. Ozon принимает загрузку товаров асинхронно: успешный ответ на запрос означает, что задание принято, а не что товары загружены. Результат нужно забирать отдельно — иначе часть позиций тихо не пройдёт модерацию, и вы об этом не узнаете.

Полный код интеграции доступен в GitHub-репозитории.

Полный код интеграции доступен в GitHub-репозитории.

🚀

Хотите такое же решение?

Настрою окружение под ваш проект, учту специфику инфраструктуры и обучу команду.

Обсудить проект →
Бесплатная консультация · Ответ в течение дня

Комментарии

Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.