Mercado Libre
MELI API v2
MLM · MCO · MLA · MLB
both channels
Molten Engine
Unified Layer
single ERP
Shopify
Admin API
REST + GraphQL · Webhooks
MELI API · SHOPIFY API → MOLTEN UNIFIED LAYER → ONE ERP · ONE INVENTORY · ONE TRUTH

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.

GEO scope: This comparison is written for teams running both channels in Mexico (MLM), Colombia (MCO), Argentina (MLA), and Brazil (MLB), and for US brands using Shopify as their storefront while selling into LATAM via Mercado Libre Cross-Border Trade. Every section has country-specific callouts where behavior diverges.

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.

🟡 Mercado Libre
🟢 Shopify
  • 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
The MELI token rotation trap: MELI issues a new refresh token every time you exchange the old one. If your service crashes between receiving the new token and writing it to storage, the old token is already invalidated and the new one is lost. Unlike Shopify where a lost token just means re-installing, a lost MELI refresh token requires the seller to manually re-authorize through MELI's web UI — impossible to automate and requiring seller access. In Mexico and Colombia operations, this has caused multi-day outages when running on serverless infrastructure.
Python · Side-by-side auth clients — MELI vs Shopify
# ── 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.

🟡 Mercado Libre Notifications
🟢 Shopify Webhooks
  • 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-signature HMAC 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-Sha256 header (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
Python · Webhook handlers — MELI notification (re-fetch) vs Shopify (use payload)
# ── 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)
MELI LATAM note: In high-volume periods (Hot Sale in Mexico, Cyber Monday in Colombia, Buen Fin), MELI notification volume spikes 5–10× and duplicate rate increases significantly. Your Redis dedup must handle this load without becoming a bottleneck. Use a Lua script for atomic check-and-set if you're processing 100+ notifications per second.

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 CallMELI LimitShopify LimitLATAM 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
Python · Rate-limited request wrapper — works for both APIs
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.

🟡 MELI Inventory Model
🟢 Shopify Inventory Model
  • Quantity stored per item listing (and per variation if multi-variant)
  • Update via PUT /items/{id} with available_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: items topic 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.json or GraphQL mutation
  • Multi-location: different quantities per warehouse, 3PL, retail location
  • Available = on_hand − committed (open orders)
  • Webhook: inventory_levels/update topic 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.

Python · Unified inventory push — MELI + Shopify from Odoo source
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,
        }
    )
Shopify multi-location note for LATAM: If you have a Mexico warehouse (CDMX) and a Colombia warehouse (BOG) both in Shopify, you must push the correct location-specific quantity. Shopify's total inventory across locations does not map to MELI's single quantity — MELI only knows what you can ship from your fulfillment location in that country.

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.

AspectMELI OrdersShopify 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
MELI buyer email masking: For privacy, MELI does not expose the buyer's real email in the order API — it returns a masked email like 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.
Colombia-specific: In Colombia, PSE (Pago Seguro en Línea) is the dominant bank transfer method used for purchases over ~200,000 COP. PSE transactions show as "pending" in MELI for up to 24 hours while the bank processes the transfer. If your integration confirms Colombian MELI orders to Odoo immediately on receipt — before checking payment_status — you will regularly ship orders that were never paid.

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.

🟡 MELI Fulfillment
🟢 Shopify Fulfillment
  • 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
CountryMELI Carrier CodeCarrier NameShopify Carrier Code
🇲🇽 Mexicoestafeta, dhl, redpack, fedex, upsEstafeta, DHL, Redpack, FedEx, UPS"Estafeta" / "DHL" / custom
🇨🇴 Colombiaservientrega, coordinadora, deprisa, tccServientrega, Coordinadora, Deprisa, TCC"Servientrega" / custom
🇦🇷 Argentinaoca, andreani, correoargentinoOCA, Andreani, Correo Argentino"OCA" / custom
🇧🇷 Brazilcorreios, jadlog, total_expressCorreios, Jadlog, Total Express"Correios" / custom
🇺🇸 US → LATAM (CBT)fedex_international, dhl_express, ups_worldwideFedEx 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.

MELI tokens must be refreshed every 6 hours and the rotated refresh_token persisted to DB synchronously on every exchange.
⚠️

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.

Always re-fetch the MELI resource after receiving the notification. Always dedup by notification _id in Redis before processing.
⚠️

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.

Use MELI buyer.id + site_id as the dedup key. Store masked email as a note field only. Never use it for dedup or marketing.
⚠️

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.

Push only the location-specific quantity for the country matching the MELI site. Mexico MELI gets Mexico warehouse stock. Colombia MELI gets Colombia warehouse stock.
⚠️

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.

Check payment_status = "approved" before confirming the Odoo sale.order. Hold draft for all cash payment types until MELI payment webhook confirms.
⚠️

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.

MELI fulfillment confirmation must be event-driven — triggered immediately on stock.picking done in Odoo, not by a scheduled job.
⚠️

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.

Apply a shared base buffer (5 units) plus an additional MELI-specific buffer (2–3 units) to account for MELI's slower sync acknowledgment cycle.
⚠️

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.

Build and maintain a carrier code lookup table keyed by carrier_name + site_id. Never pass a free-text string to MELI's carrier field.

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 →
Quick recap: MELI auth rotates tokens — persist or lose access. MELI webhooks arrive 2–3× — dedup always. Shopify tokens are stable — store once. Shopify webhooks carry full payloads — use them directly. Inventory: one ERP source of truth, channel-specific buffers. Orders: hold MELI cash payments (OXXO/PSE/Boleto) until approved. Fulfillment: MELI is reputation-critical and event-driven, Shopify is flexible. Carrier codes differ per LATAM country on MELI. That's the complete operational delta.