We're writing this because we've seen the same problems show up over and over in Trendyol, Hepsiburada, and N11 API integrations. If you're building for the Turkish e-commerce market, this should save you from repeating our mistakes.
Common Problems
1. Rate Limiting
Each marketplace has its own rate-limiting policy, and the documentation is rarely clear about it:
- Trendyol: ~200 requests/minute (based on our experience)
- Hepsiburada: Stricter, and failed requests sometimes time out instead of returning a 429
- N11: The most permissive, but also the least stable
Solution: Build a centralized request queue. Manage rate limits with a token bucket algorithm, a separate bucket per channel.
2. Frequent 5xx Errors
Especially during high-traffic periods (campaign days, sale seasons), the rate of 5xx responses across all marketplaces increases dramatically. Failing to plan for this leads to serious data loss.
// Retry with exponential backoff
async function fetchWithRetry(
url: string,
options: RequestInit,
maxRetries = 3
): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options)
if (response.ok || response.status < 500) return response
if (attempt === maxRetries) return response
} catch (error) {
if (attempt === maxRetries) throw error
}
const delay = Math.min(1000 * 2 ** attempt, 30000)
await new Promise(resolve => setTimeout(resolve, delay))
}
throw new Error('Unreachable')
}3. Webhook Reliability
Marketplaces send webhooks, but:
- They don't retry (fire-and-forget)
- There's no delivery guarantee
- Sometimes they're delayed by minutes
Solution: Accept webhooks, but don't treat them as your source of truth. Sync via polling, and let webhooks serve only as near-real-time notifications.
Architectural Recommendation: Unified Ingestion
Build an adapter layer that normalizes orders from every marketplace into a single format:
interface UnifiedOrder {
externalId: string
marketplace: 'trendyol' | 'hepsiburada' | 'n11'
status: OrderStatus
customer: Customer
items: OrderItem[]
shipping: ShippingInfo
createdAt: Date
}
abstract class MarketplaceAdapter {
abstract fetchOrders(since: Date): Promise<UnifiedOrder[]>
abstract updateOrderStatus(id: string, status: string): Promise<void>
abstract updateStock(sku: string, quantity: number): Promise<void>
}This pattern abstracts away marketplace-specific dependencies. Adding a new marketplace becomes a matter of writing a new adapter.
Stock Synchronization: Avoiding Race Conditions
The biggest risk: overselling when the same product receives simultaneous orders from two different channels.
Solution: optimistic locking or Redis atomic operations
// Atomic stock decrement with Redis
async function decrementStock(sku: string, quantity: number): Promise<boolean> {
const key = `stock:${sku}`
const result = await redis.eval(
`
local current = tonumber(redis.call('GET', KEYS[1])) or 0
if current >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1
end
return 0
`,
[key],
[quantity]
)
return result === 1
}We prefer a combination of Redis atomic operations and database transactions over purely event-driven stock updates. It's simpler and more predictable.
Monitoring
Debugging multi-marketplace integrations without monitoring is essentially impossible. Trace every API call with OpenTelemetry:
- Which marketplace succeeded on which retry attempt?
- How many times did rate limiting trigger?
- What's the average response time?
Having this data in production means you catch issues yourself, instead of finding out from a customer complaint.