Most ecommerce teams in Latin America run both. Mercado Libre for the marketplace volume — 100+ million buyers in Mexico, Colombia, Argentina, Brazil. Shopify for the branded storefront — direct-to-consumer, higher margins, full control. The problem is that both channels need the same inventory, the same product data, and the same order flow. But their APIs work completely differently.
Build a Shopify integration first and then try to apply the same patterns to MELI, and you'll run into token rotation failures, rate limit cliffs, webhook deduplication issues, and LATAM payment edge cases that Shopify simply doesn't have. This post documents the operational differences — auth, webhooks, rate limits, inventory, orders, and fulfillment — so your integration team builds each one correctly the first time.
Authentication — two completely different models
The auth model is where most teams get surprised by MELI after building Shopify integrations. Shopify's auth is stable and developer-friendly. MELI's is operationally more demanding and has one critical difference that breaks integrations in production.
- OAuth 2.0 — access token + refresh token
- Access tokens expire after 6 hours
- Refresh token rotates on every use — must be persisted immediately or you lose access permanently
- One refresh token per seller account — one per country
- No API key option — OAuth is mandatory for all operations
- App registered at developers.mercadolibre.com — one app covers all LATAM sites
- Admin API Key (static) or OAuth 2.0 for public apps
- Access tokens do not expire unless revoked
- Tokens are stable — store once, use forever
- One token per shop — multi-store requires multiple tokens
- API Key available for private apps — no OAuth flow required
- App registered in Shopify Partner Dashboard — per-store installation
# ── MELI auth — token rotation must be persisted ─────────────────────────── class MELIAuth: def get_token(self, seller_id: str) -> str: refresh = self.db.get_refresh_token(seller_id) # read from DB resp = requests.post("https://api.mercadolibre.com/oauth/token", data={ "grant_type": "refresh_token", "refresh_token": refresh, "client_id": os.environ["MELI_APP_ID"], "client_secret": os.environ["MELI_SECRET"], }) data = resp.json() # CRITICAL: persist the rotated refresh token BEFORE using the access token self.db.set_refresh_token(seller_id, data["refresh_token"]) # must not fail return data["access_token"] # valid for 6 hours # ── Shopify auth — static, no rotation, store once ───────────────────────── class ShopifyAuth: def get_headers(self, shop: str) -> dict: token = os.environ[f"SHOPIFY_TOKEN_{shop.upper()}"] # never expires return { "X-Shopify-Access-Token": token, "Content-Type": "application/json", }
Webhooks — reliability & dedup
Both platforms use webhooks, but their reliability characteristics and security models are fundamentally different. Understanding this determines how you design your event handling architecture for LATAM operations.
- Sends same notification 2–3 times per event — dedup is mandatory
- Payload is minimal: just a resource URL and topic — must re-fetch the full resource
- Validates with
x-signatureHMAC header - Must respond 200 within seconds — MELI retries aggressively on timeout
- Subscription registered per seller via API call
- Topics: orders_v2, items, shipments, claims, stock_locations, questions
- No guaranteed delivery order — events for the same order can arrive out of sequence
- Sends each event once — occasional retries on non-200 response
- Payload is complete — full order/product/inventory object in the body
- Validates with
X-Shopify-Hmac-Sha256header (base64 encoded) - Registered via Admin API or Partner Dashboard
- Topics: orders/create, orders/paid, orders/fulfilled, products/update, inventory_levels/update, etc.
- Events generally arrive in sequence for the same resource
- Shopify confirms delivery — missed events visible in webhook logs
# ── MELI: notification is minimal — must re-fetch the resource ───────────── def handle_meli_notification(payload: dict): # 1. Validate x-signature validate_meli_sig(payload, request.headers.get("x-signature")) # 2. Dedup — MELI sends same notification 2–3 times notif_id = payload["_id"] if redis.get(f"meli:notif:{notif_id}"): return "ok", 200 # already processed redis.setex(f"meli:notif:{notif_id}", 86400, "1") # 24h TTL # 3. Re-fetch the full resource — payload only has the URL resource_url = payload["resource"] # e.g. "/orders/123456789" resource = meli_get(resource_url) dispatch_meli_event.delay(payload["topic"], resource) return "ok", 200 # respond fast — MELI retries on any delay # ── Shopify: full payload arrives — use it directly ───────────────────────── def handle_shopify_webhook(topic: str, payload: dict): # 1. Validate X-Shopify-Hmac-Sha256 validate_shopify_hmac(request.data, request.headers.get("X-Shopify-Hmac-Sha256")) # 2. Use payload directly — full order/product/inventory object already here # Idempotency still recommended but not mandatory for dedup order_id = payload.get("id") dispatch_shopify_event.delay(topic, payload)
Rate limits — where integrations break
Rate limits are where LATAM multi-channel integrations most commonly fail silently. Both APIs have limits, but they're enforced differently — Shopify with a leaky bucket you can burst, MELI with hard per-second limits that cut off without warning.
| API Call | MELI Limit | Shopify Limit | LATAM Impact |
|---|---|---|---|
| List orders | 10 req/s per app | 2 req/s (leaky bucket 40) | Hot Sale/Buen Fin spikes — both limits can be hit simultaneously |
| Get single order | 10 req/s | 2 req/s | MELI requires separate call for order items — doubles effective call count |
| Update inventory | 10 req/s per app | 2 req/s | Multi-location Shopify adds calls; MELI multi-site multiplies them |
| Batch item fetch | 10 req/s (20 items/call) | GraphQL: 1000 cost units/s | MELI batching (20/call) critical for catalog sync — one-by-one burns budget |
| Webhook registration | 10 req/s | 2 req/s | One-time setup — not a runtime concern |
| Rate limit response | 429 — hard cutoff, no burst | 429 — bucket refills at 2/s, Retry-After header | Shopify's Retry-After makes backoff easier; MELI requires your own timer |
import time, requests from functools import wraps def rate_limited_request(max_per_second: float): """Decorator that enforces a per-second call rate with exponential backoff on 429.""" min_interval = 1.0 / max_per_second last_called = [0.0] def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): elapsed = time.time() - last_called[0] wait = min_interval - elapsed if wait > 0: time.sleep(wait) last_called[0] = time.time() # Exponential backoff on 429 for attempt in range(5): resp = fn(*args, **kwargs) if resp.status_code != 429: return resp # Shopify provides Retry-After; MELI doesn't — use exponential fallback retry_after = float(resp.headers.get("Retry-After", 2 ** attempt)) time.sleep(retry_after) resp.raise_for_status() # all retries exhausted return wrapper return decorator # MELI: 10 req/s hard limit — no burst @rate_limited_request(9) # 9/s gives headroom def meli_get(path, headers): return requests.get(f"https://api.mercadolibre.com{path}", headers=headers) # Shopify: leaky bucket — burst to 40, refills at 2/s @rate_limited_request(1.8) # conservative, lets bucket stay healthy def shopify_get(url, headers): return requests.get(url, headers=headers)
Inventory — different models, same stock
Both APIs update inventory, but the models are different enough that a naive implementation will either oversell or hold back excess stock. The critical difference: Shopify tracks inventory levels per location, MELI tracks available quantity per listing. When you sell the same SKU on both, you need a single source of truth and a unified push layer.
- Quantity stored per item listing (and per variation if multi-variant)
- Update via
PUT /items/{id}withavailable_quantity - FULL (Fulfillment by MELI): MELI manages stock at their FC — read-only from your side
- No multi-location concept — MELI sees one number per listing
- Webhook:
itemstopic fires on quantity change - Negative quantity not allowed — MELI pauses listing if you push 0
- Quantity stored per inventory level (variant + location)
- Update via
POST /inventory_levels/set.jsonor GraphQL mutation - Multi-location: different quantities per warehouse, 3PL, retail location
- Available = on_hand − committed (open orders)
- Webhook:
inventory_levels/updatetopic fires on change - "Continue selling when out of stock" can allow negative — dangerous if not disabled
The unified push pattern for LATAM multi-channel operations
When a sale happens on either channel, or when stock changes in your ERP (Odoo), you need to push updated quantities to both MELI and Shopify simultaneously. The safety buffer must account for the combined lag of both channels — a sale on MELI that takes 60 seconds to sync to Shopify, during which another buyer on Shopify purchases the same unit.
SAFETY_BUFFER = 5 # shared across both channels — absorbs cross-channel lag MELI_RESERVED = 2 # extra buffer for MELI specifically (slower sync ack) def push_inventory_both_channels(sku: str, odoo_wh_id: int, meli_item_id: str, meli_site_id: str, shopify_variant_id: int, shopify_location_id: int): """ Reads authoritative stock from Odoo warehouse and pushes to both MELI and Shopify with appropriate buffers. Called on: Odoo stock move, any channel order, scheduled job. """ models, uid = odoo_connect() # Get Odoo available qty (on_hand - reserved) quants = models.execute_kw(ODOO_DB, uid, ODOO_KEY, "stock.quant", "search_read", [[("product_id.default_code", "=", sku), ("location_id.usage", "=", "internal"), ("location_id.warehouse_id", "=", odoo_wh_id)]], {"fields": ["quantity", "reserved_quantity"]} ) available = sum(q["quantity"] - q["reserved_quantity"] for q in quants) base_qty = max(0, int(available) - SAFETY_BUFFER) # Push to MELI (extra buffer — slower confirmation loop) meli_qty = max(0, base_qty - MELI_RESERVED) meli_put( f"/items/{meli_item_id}", {"available_quantity": meli_qty}, site_id=meli_site_id, ) # Push to Shopify (exact base_qty — webhook ack is fast) requests.post( f"https://{SHOPIFY_SHOP}.myshopify.com/admin/api/2024-04/inventory_levels/set.json", headers=shopify_headers(), json={ "location_id": shopify_location_id, "inventory_item_id": shopify_variant_id, "available": base_qty, } )
Orders — payment complexity is the delta
Order creation looks similar on both APIs until you hit LATAM payment methods. Shopify orders in Mexico and Colombia use standard payment gateways — Stripe, PayPal, Mercado Pago as a gateway — with immediate confirmation. MELI orders can use dozens of cash-based and deferred payment methods that require your ERP to hold fulfillment until the payment actually clears.
| Aspect | MELI Orders | Shopify Orders |
|---|---|---|
| Order retrieval | Two API calls: GET /orders/{id} + GET /orders/{id}/order_items | One API call: GET /orders/{id}.json — items included |
| Payment confirmation | payment_status field — must be "approved" before fulfillment for cash methods | financial_status field — "paid" triggers fulfillment in most setups |
| Cash payment clearing time | OXXO: up to 24h (MX) · PSE: 5min–24h (CO) · Boleto: 1–3 biz days (BR) | Store-dependent — usually immediate via gateway |
| Buyer identity | buyer.id (MELI user) + nickname — email is masked for privacy | Full customer object — email, phone, name unmasked |
| Shipping address | Inside shipping.receiver_address — separate from billing | shipping_address at order root level |
| Product reference | seller_sku on each order item — must match your catalog | variant_id + sku on each line_item |
| Order cancellation | Via /orders/{id} status update — MELI charges cancellation fee above threshold | POST /orders/{id}/cancel.json — no marketplace penalty |
| Installments (LATAM) | Meses sin intereses (MX), cuotas (AR/CO), parcelamento (BR) — one payment total in order | Usually single payment — BNPL apps handle installments separately |
a3b9c@users.mercadolibre.com.mx. If your Odoo integration uses email as the customer dedup key (as most Odoo setups do), every MELI customer will appear unique and your customer database will explode with duplicates. Use MELI buyer.id + site_id as your dedup key instead, and store the masked email only for reference.
Fulfillment confirmation — reputation vs convenience
Shopify fulfillment confirmation is a convenience feature. MELI fulfillment confirmation is a reputation-critical operation. The difference changes how you architect the trigger.
- Must confirm within SLA window — late confirmation counts against reputation score
- Late shipment rate > 4% across all MELI sites → account penalty / listing demotion
- Affects MercadoLíder status — your position in search across MX, CO, AR, BR simultaneously
- Carrier codes are country-specific — must map Estafeta/Coordinadora/OCA/Correios correctly
- FULL orders: MELI handles shipping — you still confirm the FBA-style replenishment
- Trigger must be event-driven — a batch job is too slow
- No external SLA — only customer expectation
- No penalty for late confirmation beyond customer satisfaction
- Carrier codes are universal — standard SCAC codes or generic "Other"
- Fulfillment can be created manually, via API, or via third-party fulfillment service
- Tracking number optional at fulfillment creation — can update later
- Batch fulfillment confirmation is acceptable — no tight SLA
| Country | MELI Carrier Code | Carrier Name | Shopify Carrier Code |
|---|---|---|---|
| 🇲🇽 Mexico | estafeta, dhl, redpack, fedex, ups | Estafeta, DHL, Redpack, FedEx, UPS | "Estafeta" / "DHL" / custom |
| 🇨🇴 Colombia | servientrega, coordinadora, deprisa, tcc | Servientrega, Coordinadora, Deprisa, TCC | "Servientrega" / custom |
| 🇦🇷 Argentina | oca, andreani, correoargentino | OCA, Andreani, Correo Argentino | "OCA" / custom |
| 🇧🇷 Brazil | correios, jadlog, total_express | Correios, Jadlog, Total Express | "Correios" / custom |
| 🇺🇸 US → LATAM (CBT) | fedex_international, dhl_express, ups_worldwide | FedEx Int'l, DHL Express, UPS Worldwide | "FedEx" / "DHL" / "UPS" |
Mistakes when running both channels
Applying Shopify token logic to MELI
Shopify tokens don't expire. Teams store them once and move on. Applying the same pattern to MELI — store refresh token in .env, never update — works for 6 hours then silently fails forever.
Processing MELI webhooks like Shopify webhooks
Shopify sends a full payload once. MELI sends a minimal resource URL 2–3 times. Teams that skip the re-fetch get stale data. Teams that skip dedup process each order 2–3 times.
Using MELI buyer email as Odoo customer key
MELI masks buyer emails. Every Mexican or Colombian MELI customer gets a unique @users.mercadolibre.com.mx address. Your Odoo customer DB fills with thousands of meaningless duplicates.
Pushing Shopify total inventory to MELI
If your Shopify store has locations in Mexico and Colombia, the total inventory includes both. Pushing this total to MELI Mexico lists stock you can't ship to Mexican buyers from Colombia.
Fulfilling MELI cash orders before payment clears
OXXO Pay (MX), PSE (CO), Efecty (CO), Boleto (BR) — all have clearing delays. Triggering Odoo fulfillment the moment the MELI order arrives ships product before you're paid.
Using a batch job for MELI fulfillment confirmation
A Shopify fulfillment batch job running every 30 minutes is fine — no penalty. The same pattern on MELI means orders confirmed after the SLA window, late shipment rate climbs, and MercadoLíder status drops across all LATAM sites.
Sharing the same safety buffer for both channels
MELI's inventory sync loop is slower than Shopify's webhook-driven model. Using the same buffer means either MELI oversells during lag, or Shopify holds back more stock than needed, leaving money on the table.
Ignoring MELI carrier codes by country
Shopify accepts free-text carrier names. MELI requires specific coded values that differ by country. "Estafeta" works in Mexico, but is unknown to MELI Colombia — and a wrong carrier code causes the shipment confirmation to fail silently.
Dual-channel readiness checklist
🔐 Auth — Both Channels
- MELI: refresh token persisted to DB and updated synchronously on every exchange
- MELI: token refresh tested — service crash between exchange and persist cannot lose the token
- MELI: separate seller accounts and tokens per country (MLM, MCO, MLA, MLB)
- Shopify: tokens stored in environment variables or secrets manager — not in code
- Shopify: separate tokens per store — no shared credentials across regions
- Both: alert configured for any auth failure — immediate ops notification
📡 Webhooks & Events
- MELI: notification dedup via Redis SET — tested with duplicate notification replay
- MELI: resource re-fetch implemented — payload URL used, not the minimal notification body
- MELI: x-signature validation implemented and tested with tampered payload
- Shopify: X-Shopify-Hmac-Sha256 validation implemented (base64 decoded HMAC)
- Both: webhook endpoint responds 200 within 5 seconds — heavy processing in async task queue
- Both: polling backup job (2-min MELI, 5-min Shopify) as fallback for missed webhooks
📦 Inventory Sync
- ERP (Odoo) is single source of truth — neither MELI nor Shopify writes back to ERP directly
- Shared safety buffer (5 units minimum) applied before pushing to either channel
- Additional MELI buffer (2–3 units) for slower sync acknowledgment
- Shopify push uses location-specific quantity — not total across all locations
- MELI FULL SKUs tracked separately — MELI is master for FULL, Odoo reads only
- Scheduled inventory push: every 15 min to MELI, event-driven to Shopify
🛒 Orders — LATAM Payment Handling
- MELI: order items fetched via separate API call (GET /orders/{id}/order_items)
- MELI: payment_status checked before confirming Odoo sale.order for cash methods
- MELI: OXXO (MX), PSE (CO), Efecty/Baloto (CO), Boleto (BR) hold in draft until approved
- MELI: buyer dedup uses buyer.id + site_id — not masked email
- Shopify: financial_status = paid triggers fulfillment — not orders/create alone
- Both: client_order_ref / order tag checked for idempotency before creating ERP record
🚚 Fulfillment & Reputation
- MELI fulfillment confirmation is event-driven on stock.picking done — not batch
- MELI carrier codes mapped per country — Estafeta/DHL/Redpack (MX), Servientrega/Coordinadora (CO), OCA/Andreani (AR), Correios/Jadlog (BR)
- MELI reputation metrics pulled weekly per site — late shipment, cancellation, claims
- MELI reputation alert at 50% of threshold per country
- Shopify: send_receipt = false for wholesale/B2B orders — no customer-facing email
- Both: fulfillment failure alert — any channel failure triggers immediate ops notification
One team, both channels
The teams that run both Mercado Libre and Shopify well in LATAM are the ones that understand the operational differences rather than treating them as interchangeable. The auth model isn't the same. The webhook contract isn't the same. The inventory model isn't the same. The payment flow isn't the same. And the fulfillment stakes aren't the same.
But once you build each channel correctly — with its own auth handling, its own event dedup logic, its own payment hold rules, its own fulfillment trigger — you can run both from a single source of truth in your ERP. That's the architecture we build at Molten Logistics for operators across Mexico, Colombia, Argentina, and Brazil, and for US brands entering LATAM through both channels simultaneously.
Molten Logistics — Your trusted partner in Logistics & E-commerce
Luis Alba and the Molten team have connected Mercado Libre and Shopify to Odoo, NetSuite, and custom ERPs for sellers operating across Mexico, Colombia, Argentina, Brazil, and the US. We know the OXXO Pay clearing window, the PSE confirmation delay, the MercadoLíder threshold, and the Shopify inventory location model — because we've built for all of them in production.
Schedule a free consultation →