Dropshipping faces a problem: purchase prices change, and manually updating retail prices leads to errors and lost margin. According to our data, manual updates for a store with 500 products take 8 hours per week and result in 5–10% incorrect positions — direct revenue loss. Automating price synchronization solves this: we implemented solutions for 50+ online stores, cutting update time by 10x and eliminating errors entirely. As noted on Wikipedia, dropshipping requires automation to scale. Supplier prices change on a schedule (new price list weekly) or in real time (exchange-traded goods, currency dependency, dynamic pricing). Without automation, the store loses margin when purchase prices rise or loses to competitors when they drop. Our experience helps avoid these problems: over the years we have refined a reliable synchronization scheme that updates prices 10x faster than manual entry and completely eliminates human errors.
Why Automatic Synchronization Matters
Manually updating thousands of products is a path to errors and lost profit. Imagine: the supplier drops the purchase price by 30% and you don't know — competitors are already selling cheaper and you lose sales. Or conversely, a sharp 40% increase eats your margin if you don't raise retail in time. Automation ensures that the retail price always reflects the current purchase price with the desired markup. As a result, margin is preserved and customers see correct prices. We handle integration with any type of supplier.
Price Sources: Approach Comparison
| Source | Update Frequency | Complexity | Reliability |
|---|---|---|---|
| REST API | Real time | Medium | High |
| FTP/CSV price list | 1x per day | Low | High |
| Webhook from supplier | Instant | Medium | High |
| Website scraping | Scheduled | High | Low (requires maintenance) |
Types of Markup Rules
| Rule Type | Application | Example |
|---|---|---|
| Percentage | Universal fixed percentage | 25% on all products |
| Fixed amount | Add a constant | +50 ₽ to purchase price |
| Stepped table | For different price brackets | up to 1000: 40%, 1000–5000: 25%, above: 15% |
Typical Errors with Manual Synchronization
Even experienced managers make mistakes:
- Input errors: swapped digits, extra zeros, wrong currency.
- Missed updates: forgot to update a popular product's price — sales stall.
- Inconsistency: different employees apply different markup on the same product.
- No history: impossible to track when and why a price changed.
Automation eliminates these errors, and all change history is stored in a dedicated table — you always know who updated the price and when.
How We Implement Synchronization
Work stages: analyze current state → design architecture → implement in Laravel (PHP 8.3+) → test on real data → deploy with monitoring. We do everything turnkey.
- Analysis — study price sources and margin requirements.
- Design — choose architecture and markup rules.
- Implementation — write code, integrate with supplier.
- Testing — run on historical data, check anomaly protection.
- Launch — deploy and set up monitoring.
Storing Price History
Schema::create('dropship_price_log', function (Blueprint $table) {
$table->id();
$table->foreignId('dropship_product_id')->constrained();
$table->decimal('prev_supplier_price', 10, 2);
$table->decimal('new_supplier_price', 10, 2);
$table->decimal('prev_retail_price', 10, 2)->nullable();
$table->decimal('new_retail_price', 10, 2)->nullable();
$table->decimal('margin_percent', 5, 2)->nullable();
$table->string('source')->default('sync');
$table->timestamp('recorded_at');
$table->index(['dropship_product_id', 'recorded_at']);
});
Retail Price Calculator
The retail price is calculated from the purchase price using margin rules. Rules can be global, at the supplier level, category level, or specific product.
class PriceCalculator
{
public function calculate(DropshipProduct $dp): float
{
$supplierPrice = $dp->supplier_price;
$marginRule = $this->resolveMarginRule($dp);
return match($marginRule->type) {
'percent' => round($supplierPrice * (1 + $marginRule->value / 100), 2),
'fixed' => round($supplierPrice + $marginRule->value, 2),
'markup_table' => $this->applyMarkupTable($supplierPrice, $marginRule->table),
};
}
private function resolveMarginRule(DropshipProduct $dp): MarginRule
{
// Priority: product > category > supplier > global settings
return $dp->margin_rule
?? $dp->product?->category?->margin_rule
?? $dp->supplier->margin_rule
?? MarginRule::getDefault();
}
/**
* Stepped markup: expensive items — lower %
* [up to 1000 → +40%, 1000–5000 → +25%, 5000+ → +15%]
*/
private function applyMarkupTable(float $price, array $table): float
{
foreach ($table as $tier) {
if ($price <= $tier['max_price']) {
return round($price * (1 + $tier['percent'] / 100), 2);
}
}
// Last tier without max limit
$last = end($table);
return round($price * (1 + $last['percent'] / 100), 2);
}
}
Price Synchronization Job
class SyncSupplierPricesJob implements ShouldQueue
{
public $tries = 3;
public $backoff = [60, 300, 900];
public function handle(
SupplierConnectorFactory $factory,
PriceCalculator $calculator,
): void {
$connector = $factory->make($this->supplier);
$priceList = $connector->getPriceList(); // array [sku => price]
$updatedCount = 0;
foreach ($priceList as $sku => $newSupplierPrice) {
$dp = DropshipProduct::where([
'supplier_id' => $this->supplier->id,
'supplier_sku' => $sku,
])->first();
if (!$dp) continue;
// Skip if price hasn't changed
if (abs($dp->supplier_price - $newSupplierPrice) < 0.01) continue;
$prevSupplierPrice = $dp->supplier_price;
$prevRetailPrice = $dp->product?->price;
$dp->update(['supplier_price' => $newSupplierPrice]);
// Recalculate retail price if not manually locked
$newRetailPrice = null;
if ($dp->product && !$dp->product->price_locked) {
$newRetailPrice = $calculator->calculate($dp);
$dp->product->update(['price' => $newRetailPrice]);
}
DropshipPriceLog::create([
'dropship_product_id' => $dp->id,
'prev_supplier_price' => $prevSupplierPrice,
'new_supplier_price' => $newSupplierPrice,
'prev_retail_price' => $prevRetailPrice,
'new_retail_price' => $newRetailPrice,
'source' => 'sync',
'recorded_at' => now(),
]);
$updatedCount++;
}
Log::info('Price sync completed', [
'supplier' => $this->supplier->slug,
'updated' => $updatedCount,
]);
}
}
How to Prevent Sudden Price Spikes
Sometimes the supplier's price list contains erroneous prices (zero, very high value, typo). Without protection, the store would display an incorrect retail price. We implement anomaly checking: if the purchase price change exceeds a configured threshold (e.g., 50%), the product is flagged for manual verification. For example, the price was 1000 ₽, the supplier sends 1500 (50% change) — at a 50% threshold this is allowed. But if the change is 100% (to 2000 ₽), the product goes to manual review. The manager sees suspicious items in a dedicated admin section and can confirm or reject the update.
class PriceSanityChecker
{
private const MAX_CHANGE_PERCENT = 50; // do not update if change > 50%
public function isSafe(float $prevPrice, float $newPrice): bool
{
if ($newPrice <= 0) return false;
if ($prevPrice <= 0) return true; // first price — accept any positive
$changePercent = abs($newPrice - $prevPrice) / $prevPrice * 100;
if ($changePercent > self::MAX_CHANGE_PERCENT) {
// Log for manual review
Log::warning('Suspicious price change detected', [
'prev' => $prevPrice,
'new' => $newPrice,
'change%' => round($changePercent, 1),
]);
return false;
}
return true;
}
}
Products with suspicious price changes are placed into a manual review queue — the manager sees them in a separate admin section. This prevents setting an incorrect retail price. As a result, sharp spikes do not pass automatically; they require human confirmation.
Currency and Exchange Rate Conversion
If the supplier prices in USD or EUR and the store operates in RUB:
class CurrencyPriceConverter
{
public function convert(float $price, string $fromCurrency, string $toCurrency): float
{
if ($fromCurrency === $toCurrency) return $price;
$rate = Cache::remember(
"exchange_rate_{$fromCurrency}_{$toCurrency}",
3600, // cache for 1 hour
fn() => $this->fetchRate($fromCurrency, $toCurrency)
);
return round($price * $rate, 2);
}
private function fetchRate(string $from, string $to): float
{
// Central Bank of Russia: cbr.ru/scripts/XML_daily.asp
// Or openexchangerates.org, fixer.io
$response = Http::get('https://api.exchangerate-api.com/v4/latest/' . $from);
return $response->json("rates.{$to}");
}
}
What's Included
- Integration with any price source (API, CSV, webhooks)
- Configuration of markup rules (percentage, fixed, stepped)
- Implementation of anomaly spike protection
- Storage and visualization of change history
- Currency conversion with caching
- Operations documentation and team training
- Post-launch support
Contact us for a consultation on your project. We'll help automate pricing and protect your margin.
Estimated Timelines
Price synchronization with one supplier + margin calculator — from a few working days. Stepped markup, currency conversion, spike protection — add up to two more days. Final cost is calculated individually — leave a request and we'll discuss your project. Our engineers guarantee reliable operation of the solution. Get a consultation and project assessment — we'll help automate pricing and protect your margin.







