Интеграция с маркетплейсом — не разовая работа «подключить API», а постоянно работающий обмен, у которого есть цена ошибки в деньгах: неверные остатки означают заказы на то, чего нет, и штрафы за отмену.
Разберём основные операции и, отдельно, то, что определяет надёжность обмена: ограничения частоты, идемпотентность и поведение при недоступности API.
Зачем интегрироваться с Ozon
Ozon — один из крупнейших маркетплейсов в России. Интеграция позволяет:
- Автоматически выгружать товары из вашего каталога
- Синхронизировать остатки в реальном времени
- Получать и обрабатывать заказы в своей системе
- Обновлять статусы и трек-номера
Ozon предоставляет два типа работы: FBO (склад Ozon) и FBS (ваш склад). Для FBS критична синхронизация остатков — за oversell штрафуют.
Получение доступа к API
- Зайдите в личный кабинет продавца Ozon
- Перейдите в раздел Настройки → API ключи
- Создайте новый ключ с нужными правами
- Сохраните
Client-IdиApi-Key
# .env
OZON_CLIENT_ID=your_client_id
OZON_API_KEY=your_api_keyБазовый HTTP-клиент
Все запросы к Ozon API требуют заголовков авторизации:
// 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:
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 критически важно поддерживать актуальные остатки:
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:
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);
}
}Обновление статуса отправления
После сборки заказа обновляем статус и добавляем трек:
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 возвращает специфические коды ошибок:
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;
}
}Итоги
- Авторизация — Client-Id и Api-Key в заголовках, оба из конфигурации вне репозитория.
- Товары — загрузка через
/v3/product/import, обновление поoffer_id. Ваш артикул — единственная связь между системами, поэтому его изменение на стороне сайта ломает всю привязку; относитесь к нему как к неизменяемому ключу. - Остатки — только изменившиеся позиции, с резервом под собственные продажи и проверкой источника на полноту.
- Заказы — опрос или уведомления; в обоих случаях обработка обязана быть идемпотентной, потому что один и тот же заказ вы получите повторно.
- Ошибки — учитывайте ограничение частоты запросов и разделяйте временные сбои (повторить) от постоянных (звать человека).
И то, что определяет надёжность больше, чем код самой интеграции:
Обмен работает в фоне и падает молча. Нужен мониторинг: «последняя успешная синхронизация не старше часа», «в очереди не больше N необработанных заданий» — с уведомлением живому человеку. Без этого о сломанном обмене вы узнаете от службы поддержки маркетплейса.
Ведите журнал отправленного и полученного. В спорах о том, какие остатки вы передали и когда, это единственный аргумент.
Обрабатывайте ответы, а не только отправляйте запросы. Ozon принимает загрузку товаров асинхронно: успешный ответ на запрос означает, что задание принято, а не что товары загружены. Результат нужно забирать отдельно — иначе часть позиций тихо не пройдёт модерацию, и вы об этом не узнаете.
Полный код интеграции доступен в GitHub-репозитории.
Полный код интеграции доступен в GitHub-репозитории.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.