Реализация PHP-клиента для Ozon API: от основ до сложных решений

Раздел: Интеграция API -> Интеграция маркетплейсов

Работа с Ozon API: эффективные решения на PHP

Основной подход: использование официального SDK

Как быстро начать взаимодействие с Ozon API через PHP?

Наиболее эффективный способ - воспользоваться пакетом ozon/ozon-api, который предоставляет готовые методы для всех эндпоинтов. Установка выполняется через Composer:

composer require ozon/ozon-api

Ozon api php (ozon api на php)

После установки необходимо создать экземпляр клиента, передав идентификатор клиента (Client ID) и API-ключ (API Key), полученные в личном кабинете Ozon Seller:


require 'vendor/autoload.php';

use Ozon\Api\Client;

$clientId = '1234';
$apiKey = 'your-api-key';

$client = new Client($clientId, $apiKey);
  

Теперь доступны методы для работы с товарами, заказами, отчётами и другими сущностями. Например, получение списка товаров:


$response = $client->product->list([
    'filter' => [
        'visibility' => 'ARCHIVED',
    ],
    'limit' => 100,
    'last_id' => '',
]);

print_r($response);
  

Результат будет содержать массив товаров и информацию для пагинации. SDK автоматически обрабатывает подпись запроса и кодировку.

Типичные проблемы и их решение

  • Ошибка "Invalid signature" - неверная пара Client ID и API Key. Проверьте, что ключи активны и скопированы без лишних пробелов.
  • Ошибка 429 "Too Many Requests" - превышение лимита запросов (10 запросов в секунду). Рекомендуется ввести задержку между запросами (sleep(0.15)) или использовать очередь.
  • Ошибка "Invalid JSON" - неправильный формат данных в теле запроса. Проверьте, что все обязательные поля переданы и типы данных соответствуют документации.

Вариант 1: Как выполнить запрос без SDK, используя cURL?

Если по каким-то причинам использование готового пакета нежелательно, можно отправлять запросы напрямую через cURL. Для этого требуется вручную сформировать заголовки и подпись. Рассмотрим пример получения информации о товаре:


function ozonApiRequest($method, $body, $clientId, $apiKey) {
    $url = "https://api-seller.ozon.ru/v2/$method";
    $headers = [
        'Client-Id: ' . $clientId,
        'Api-Key: ' . $apiKey,
        'Content-Type: application/json',
    ];

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200) {
        throw new Exception("HTTP error $httpCode: $response");
    }

    return json_decode($response, true);
}

$result = ozonApiRequest('product/list', [
    'filter' => ['visibility' => 'VISIBLE'],
    'limit' => 10,
], $clientId, $apiKey);

print_r($result);
  

Распространённая ошибка: неверные заголовки

Часто забывают указать заголовок Content-Type или передают ключи с неправильным именем (например, client-id вместо Client-Id). В документации Ozon заголовки чувствительны к регистру.

Вариант 2: Как обрабатывать пагинацию при получении списка товаров?

Ozon API возвращает постраничные результаты с параметрами last_id и limit. Для получения всех записей необходимо выполнять последовательные запросы, передавая в каждом следующем значение last_id из предыдущего ответа:


function getAllProducts($client) {
    $products = [];
    $lastId = '';
    do {
        $response = $client->product->list([
            'filter' => ['visibility' => 'ALL'],
            'limit' => 1000,
            'last_id' => $lastId,
        ]);
        $items = $response['result']['items'] ?? [];
        $products = array_merge($products, $items);
        $lastId = $response['result']['last_id'] ?? '';
    } while (!empty($lastId));

    return $products;
}
  

Важно учитывать, что лимит за один запрос не превышает 1000 записей. При большом количестве товаров процесс может занять длительное время, поэтому стоит добавить задержку и обработку ошибок.

Проблема: бесконечный цикл при отсутствии пагинации - некоторые методы (например, finance/realization/list) не используют last_id. В таких случаях нужно анализировать наличие поля next_page_token или проверять количество элементов в ответе.

Вариант 3: Как настроить Webhook для получения обновлений от Ozon?

Webhook позволяют получать уведомления о событиях (изменение статуса заказа, возврат и т.д.) без постоянного опроса API. Для подписки на события используется метод webhook/subscribe:


$response = $client->webhook->subscribe([
    'url' => 'https://your-site.com/ozon-webhook',
    'event_type' => 'ORDER_STATUS_CHANGED',
]);

print_r($response);
  

После подписки Ozon будет отправлять POST-запросы с JSON-телом на указанный URL. На стороне получателя необходимо обрабатывать входящие данные, проверять подпись (если используется секретный ключ) и выполнять нужные действия.

Частая проблема: недоступность URL или неверный формат ответа

Ozon ожидает, что вебхук вернёт HTTP-статус 200 в течение 5 секунд. Если сервер не отвечает или возвращает ошибку, Ozon будет повторять отправку с увеличивающимся интервалом. Рекомендуется отвечать сразу, а бизнес-логику обрабатывать асинхронно (через очередь).

Вариант 4: Как выполнять асинхронные запросы для ускорения массовой обработки?

При необходимости отправить множество запросов (например, обновление остатков по 1000 SKU) последовательные вызовы могут быть слишком медленными. Используя библиотеку Guzzle с асинхронными запросами, можно добиться параллельного выполнения:


use GuzzleHttp\Client;
use GuzzleHttp\Promise;

$client = new Client([
    'base_uri' => 'https://api-seller.ozon.ru/',
    'headers' => [
        'Client-Id' => $clientId,
        'Api-Key' => $apiKey,
        'Content-Type' => 'application/json',
    ],
]);

$promises = [];
foreach ($stocks as $sku => $stock) {
    $promises[] = $client->postAsync('v2/product/import/stocks', [
        'json' => [
            'stocks' => [
                ['sku' => $sku, 'stock' => $stock],
            ],
        ],
    ]);
}

$results = Promise\settle($promises)->wait();
foreach ($results as $result) {
    if ($result['state'] === 'fulfilled') {
        echo 'Успех: ' . $result['value']->getBody();
    } else {
        echo 'Ошибка: ' . $result['reason']->getMessage();
    }
}
  

Потенциальная проблема: превышение лимита запросов при параллельном выполнении

Ozon ограничивает частоту запросов на уровне Client ID. Если одновременно отправить 20 запросов, часть из них может получить ответ 429. Рекомендуется использовать программируемые задержки или группировать запросы пакетами (например, по 10 штук с небольшим интервалом).

Вариант 5: Как обрабатывать ошибки и реализовать повторные попытки?

Стабильная интеграция невозможна без корректной обработки временных сбоев. Классический подход - обёртка с логикой повторных попыток (retry) при получении кодов 429 или 5xx:


function ozonApiWithRetry($method, $body, $clientId, $apiKey, $maxRetries = 3) {
    $retry = 0;
    do {
        try {
            $result = ozonApiRequest($method, $body, $clientId, $apiKey);
            return $result;
        } catch (Exception $e) {
            $retry++;
            if ($retry >= $maxRetries) throw $e;
            $delay = pow(2, $retry) * 100000; // экспоненциальная задержка в микросекундах
            usleep($delay);
        }
    } while (true);
}
  

В данном примере используется экспоненциальная задержка: 0.2 сек, 0.4 сек, 0.8 сек перед следующей попыткой. Такой подход снижает нагрузку на API и улучшает вероятность успешного выполнения.

Подводный камень: не все ошибки стоит повторять

Если API вернуло ошибку 400 (неверные данные) или 403 (доступ запрещён), повторные попытки бесполезны. В таких случаях логировать ошибку и прекращать дальнейшие попытки.

Расширенные примеры программного кода

Полноценный класс для работы с Ozon API

Ниже представлен класс, объединяющий основные методы и обработку ошибок. Такой класс можно использовать в проекте как единую точку взаимодействия с Ozon.

Пример

namespace App\Services;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

class OzonApiService
{
    private $client;
    private $clientId;
    private $apiKey;

    public function __construct(string $clientId, string $apiKey)
    {
        $this->clientId = $clientId;
        $this->apiKey = $apiKey;
        $this->client = new Client([
            'base_uri' => 'https://api-seller.ozon.ru/',
            'headers' => [
                'Client-Id' => $clientId,
                'Api-Key' => $apiKey,
                'Content-Type' => 'application/json',
            ],
            'timeout' => 10.0,
        ]);
    }

    public function getProducts(array $filter = [], int $limit = 100, string $lastId = ''): array
    {
        $response = $this->request('v2/product/list', [
            'filter' => $filter,
            'limit' => $limit,
            'last_id' => $lastId,
        ]);
        return $response['result'] ?? [];
    }

    public function updateStocks(array $stocks): array
    {
        return $this->request('v2/product/import/stocks', [
            'stocks' => $stocks,
        ]);
    }

    public function getOrders(array $filter = [], string $lastId = ''): array
    {
        return $this->request('v2/postings/list', [
            'filter' => $filter,
            'limit' => 1000,
            'last_id' => $lastId,
        ]);
    }

    private function request(string $method, array $body, int $retries = 3): array
    {
        $attempt = 0;
        while ($attempt < $retries) {
            try {
                $response = $this->client->post($method, [
                    'json' => $body,
                ]);
                $data = json_decode($response->getBody(), true);
                if (json_last_error() !== JSON_ERROR_NONE) {
                    throw new \RuntimeException('Ошибка декодирования JSON: ' . json_last_error_msg());
                }
                return $data;
            } catch (RequestException $e) {
                $attempt++;
                if ($attempt >= $retries) {
                    throw $e;
                }
                $status = $e->getResponse() ? $e->getResponse()->getStatusCode() : 0;
                if (in_array($status, [429, 500, 502, 503, 504])) {
                    $delay = pow(2, $attempt) * 500000;
                    usleep($delay);
                } else {
                    throw $e;
                }
            }
        }
    }
}

Использование:

Пример

$ozon = new OzonApiService('1234', 'your-api-key');
$products = $ozon->getProducts(['visibility' => 'VISIBLE'], 10);
print_r($products);
Array
(
    [items] => Array
        (
            [0] => Array
                (
                    [product_id] => 12345
                    [offer_id] => "SKU-001"
                    [name] => "Товар 1"
                    ...
                )
        )
    [total] => 100
    [last_id] => "abc123"
)

Асинхронное получение информации о нескольких товарах

Пример, когда нужно получить детали сразу для нескольких SKU, используя параллельные запросы через Guzzle и обработку результатов.

Пример

use GuzzleHttp\Client;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\RejectedPromise;

$client = new Client([...]); // базовая конфигурация

$skus = ['SKU-001', 'SKU-002', 'SKU-003'];

$promises = [];
foreach ($skus as $sku) {
    $promises[$sku] = $client->postAsync('v2/product/info', [
        'json' => ['sku' => $sku],
    ]);
}

$results = GuzzleHttp\Promise\settle($promises)->wait();

$productInfo = [];
foreach ($results as $sku => $result) {
    if ($result['state'] === 'fulfilled') {
        $body = $result['value']->getBody();
        $productInfo[$sku] = json_decode($body, true);
    } else {
        $productInfo[$sku] = ['error' => $result['reason']->getMessage()];
    }
}

print_r($productInfo);
Array
(
    [SKU-001] => Array
        (
            [id] => 12345
            [name] => "Товар 1"
            [price] => 100
        )
    [SKU-002] => Array
        (
            [id] => 12346
            [name] => "Товар 2"
            [price] => 200
        )
    [SKU-003] => Array
        (
            [error] => "Client error: 404"
        )
)

Генерация отчёта через Ozon API и скачивание результата

Ozon позволяет запрашивать отчёты (например, по остаткам). Процесс асинхронный: сначала создаётся задача, затем проверяется её статус и скачивается файл.

Пример

// Создание отчёта
$createResponse = $client->post('v2/report/products', [
    'json' => [
        'language' => 'RU',
        'offer_id' => [],
        'visibility' => 'ALL',
    ],
]);
$taskUuid = json_decode($createResponse->getBody(), true)['result']['code'];

// Ожидание завершения
$status = '';
while ($status !== 'OK') {
    sleep(5);
    $statusResponse = $client->post('v2/report/info', [
        'json' => ['code' => $taskUuid],
    ]);
    $status = json_decode($statusResponse->getBody(), true)['result']['status'];
}

// Скачивание
$downloadUrl = json_decode($statusResponse->getBody(), true)['result']['file']['url'];
file_put_contents('report.xlsx', file_get_contents($downloadUrl));
echo 'Отчёт сохранён: report.xlsx';

В результате будет скачан Excel-файл с данными о товарах.

Ozon API на PHP - comments

En
Ozon api php (php)