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

URL: https://dmeshcheryakov.ru/blog/ozon-api-integration/
Раздел: Интеграции
Теги: Интеграции, API, E-commerce
Опубликовано: 2026-07-08
Обновлено: 2026-09-07
Автор: Дмитрий Мещеряков (https://dmeshcheryakov.ru)

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

Интеграция с маркетплейсом — не разовая работа «подключить 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-репозитории](https://github.com/dmeshcheryakov/ozon-integration).