Integration of Dropshipping Suppliers with E-commerce Store
Supplier integration is a technical task whose complexity is determined not by the number of products, but by the data format and quality on the supplier's side. REST API with documentation is the best case. A price list in Excel without articles and with Cyrillic in headers is the worst. Both happen.
Types of Integrations
REST API — supplier provides endpoints to get catalog, stock, prices, and receive orders. Most convenient format. Requires API key or OAuth authorization.
SOAP/XML-RPC — outdated, but still common format among large distributors and manufacturers. Requires WSDL parsing and client code generation.
FTP/SFTP + CSV/XML — supplier uploads file to server on schedule. Store retrieves and processes it. Cannot check stock in real time.
Email with Price List — extreme case. Parser attachments + OCR for PDF.
EDI (EDIFACT/X12) — used by major FMCG and pharmaceutical distributors.
Connector Factory
class SupplierConnectorFactory
{
public static function make(Supplier $supplier): SupplierConnectorInterface
{
return match($supplier->integration_type) {
'rest_api' => new RestApiConnector($supplier, app(HttpClient::class)),
'soap' => new SoapConnector($supplier),
'ftp_csv' => new FtpCsvConnector($supplier, app(SftpFilesystem::class)),
'ftp_xml' => new FtpXmlConnector($supplier, app(SftpFilesystem::class)),
default => throw new UnsupportedIntegrationTypeException($supplier->integration_type),
};
}
}
FTP + CSV Connector
Some suppliers export price list to FTP once a day. Connector retrieves file, parses and normalizes data:
class FtpCsvConnector implements SupplierConnectorInterface
{
public function getProducts(int $page = 1, int $perPage = 100): array
{
$localPath = $this->downloadFile();
$products = [];
$handle = fopen($localPath, 'r');
$headers = fgetcsv($handle, 0, ';');
$headers = array_map('trim', $headers); // remove BOM and spaces
// Map headers (suppliers call fields differently)
$mapping = $this->resolveHeaderMapping($headers);
while (($row = fgetcsv($handle, 0, ';')) !== false) {
$normalized = $this->normalizeRow(
array_combine($headers, $row),
$mapping
);
if ($normalized) {
$products[] = $normalized;
}
}
fclose($handle);
@unlink($localPath);
return array_slice($products, ($page - 1) * $perPage, $perPage);
}
private function resolveHeaderMapping(array $headers): array
{
// Different suppliers use different names for the same fields
$aliases = [
'sku' => ['артикул', 'sku', 'код', 'article', 'item_no'],
'name' => ['наименование', 'название', 'name', 'title', 'товар'],
'price' => ['цена', 'price', 'стоимость', 'цена_розница'],
'stock' => ['остаток', 'количество', 'stock', 'qty', 'available'],
];
$mapping = [];
foreach ($headers as $header) {
$lower = mb_strtolower(trim($header));
foreach ($aliases as $field => $list) {
if (in_array($lower, $list)) {
$mapping[$field] = $header;
break;
}
}
}
return $mapping;
}
private function downloadFile(): string
{
$remotePath = $this->supplier->credentials['ftp_path'];
$localPath = sys_get_temp_dir() . '/' . uniqid('supplier_') . '.csv';
$this->sftp->download($remotePath, $localPath);
return $localPath;
}
}
SOAP Connector
class SoapConnector implements SupplierConnectorInterface
{
private \SoapClient $client;
public function __construct(private Supplier $supplier)
{
$this->client = new \SoapClient(
$supplier->credentials['wsdl_url'],
['login' => $supplier->credentials['login'],
'password' => $supplier->credentials['password'],
'cache_wsdl' => WSDL_CACHE_DISK,
'trace' => false,
]
);
}
public function getProducts(int $page = 1, int $perPage = 100): array
{
$result = $this->client->GetProductList([
'SessionID' => $this->getSession(),
'PageNum' => $page,
'PageSize' => $perPage,
]);
return collect($result->ProductList->Product ?? [])
->map(fn($item) => new SupplierProductDTO(
sku: $item->Article,
name: $item->Name,
price: (float) $item->Price,
stock: (int) $item->Qty,
))
->toArray();
}
}
Data Normalization from Supplier
Data from different suppliers inevitably diverges in structure. Normalization is performed before saving to dropship_products:
class SupplierProductNormalizer
{
public function normalize(array $raw, Supplier $supplier): ?SupplierProductDTO
{
// Clean SKU from special characters
$sku = preg_replace('/[^\w\-]/', '', $raw['sku'] ?? '');
if (!$sku) return null;
// Normalize price: remove spaces, replace comma with dot
$price = (float) str_replace([' ', ','], ['', '.'], $raw['price'] ?? '0');
if ($price <= 0) return null;
// Normalize stock: "in stock" → 999, "no" → 0
$stock = $this->parseStock($raw['stock'] ?? '0');
return new SupplierProductDTO(
sku: $sku,
name: mb_convert_encoding(trim($raw['name'] ?? ''), 'UTF-8', 'auto'),
price: $price,
stock: $stock,
);
}
private function parseStock(mixed $value): int
{
if (is_numeric($value)) return (int) $value;
$lower = mb_strtolower((string) $value);
return match(true) {
str_contains($lower, 'наличи') => 999,
str_contains($lower, 'нет') => 0,
str_contains($lower, 'ожида') => 0,
default => 0,
};
}
}
Connection Error Handling
Suppliers are unreliable: APIs go down for maintenance, FTP changes directory structure, CSV arrives with different encoding. All connectors are wrapped in Retry policy through Laravel Queue with exponential backoff:
class SyncSupplierJob implements ShouldQueue
{
public $tries = 3;
public $backoff = [60, 300, 900]; // 1 min, 5 min, 15 min
public function failed(Throwable $e): void
{
Notification::route('mail', config('suppliers.admin_email'))
->notify(new SupplierSyncFailedNotification($this->supplier, $e));
}
}
Integration Timeline
| Integration Type | Timeline |
|---|---|
| REST API with documentation | 2–3 days |
| SOAP with WSDL | 3–4 days |
| FTP + CSV (standard format) | 2–3 days |
| FTP + CSV (non-standard format) | 3–5 days |
| Multiple suppliers (each next one) | 1–3 days |







