Implementing Product Import via Supplier API
API import from supplier is most flexible and current way to get data. Instead of file exports, data taken directly from supplier system on schedule or event. Complexity: each supplier has own API with different auth formats, response structures, pagination models.
Supplier API Types
| Type | Example | Features |
|---|---|---|
| REST JSON | Most modern | Pagination cursor/offset, JWT/API-key |
| REST XML | Old systems (1C) | Need XML parser response |
| SOAP | Corporate ERP | WSDL, SOAPClient |
| GraphQL | Rare | Flexible field selection |
| oData | SAP, Microsoft | $filter, $top, $skip |
Basic Client with Retry and Rate Limiting
class SupplierApiClient
{
private \GuzzleHttp\Client $http;
private RateLimiter $rateLimiter;
public function __construct(
private SupplierApiConfig $config,
) {
$this->http = new \GuzzleHttp\Client([
'base_uri' => $config->baseUrl,
'timeout' => 30,
'handler' => $this->buildHandlerStack(),
]);
}
private function buildHandlerStack(): \GuzzleHttp\HandlerStack
{
$stack = \GuzzleHttp\HandlerStack::create();
$stack->push(\GuzzleHttp\Middleware::retry(
function (int $retries, $request, $response, $exception) {
if ($retries >= 3) return false;
if ($exception instanceof \GuzzleHttp\Exception\ConnectException) return true;
if ($response && $response->getStatusCode() >= 500) return true;
return false;
},
fn(int $retries) => 1000 * (2 ** $retries) // exponential backoff
));
return $stack;
}
public function get(string $path, array $params = []): array
{
// Rate limiting: not more than N requests per second
$this->rateLimiter->throttle($this->config->id, $this->config->rateLimit);
$response = $this->http->get($path, [
'query' => $params,
'headers' => $this->buildHeaders(),
]);
return json_decode($response->getBody(), true);
}
private function buildHeaders(): array
{
return match ($this->config->authType) {
'bearer' => ['Authorization' => 'Bearer ' . $this->config->token],
'api_key' => ['X-API-Key' => $this->config->apiKey],
'basic' => ['Authorization' => 'Basic ' . base64_encode(
$this->config->login . ':' . $this->config->password
)],
default => [],
};
}
}
Pagination Models
Offset Pagination
public function fetchAllProducts(): iterable
{
$page = 1;
$perPage = 100;
do {
$response = $this->client->get('/products', [
'page' => $page,
'per_page' => $perPage,
]);
foreach ($response['data'] as $item) {
yield $item;
}
$hasMore = count($response['data']) === $perPage;
$page++;
} while ($hasMore);
}
Cursor Pagination (efficient for large tables)
public function fetchAllProducts(): iterable
{
$cursor = null;
do {
$params = ['limit' => 200];
if ($cursor) $params['cursor'] = $cursor;
$response = $this->client->get('/v2/products', $params);
foreach ($response['items'] as $item) {
yield $item;
}
$cursor = $response['next_cursor'] ?? null;
} while ($cursor);
}
OAuth 2.0 Authorization
Some suppliers require OAuth 2.0 client credentials:
class OAuth2TokenProvider
{
private ?string $accessToken = null;
private ?int $expiresAt = null;
public function getToken(): string
{
if ($this->accessToken && time() < ($this->expiresAt - 60)) {
return $this->accessToken;
}
$response = Http::asForm()->post($this->tokenUrl, [
'grant_type' => 'client_credentials',
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'scope' => 'products:read stocks:read',
]);
$data = $response->json();
$this->accessToken = $data['access_token'];
$this->expiresAt = time() + $data['expires_in'];
return $this->accessToken;
}
}
Incremental Synchronization
Most valuable mode — get only changes since last sync:
public function fetchUpdatedSince(\DateTimeInterface $since): iterable
{
return $this->fetchAllProducts([
'updated_after' => $since->format(DATE_ATOM),
'fields' => 'sku,price,qty,name,description',
]);
}
Implementation Timeline
- One REST supplier, offset pagination, normalization, import — 2 days
- OAuth 2.0, cursor pagination, incremental sync — +1 day
- Multi-supplier via config, SOAP, rate limiting, retry — +2 days







