Testeo real — 5 de junio 2026

¿Se puede scrapear el tracker de MSC?

Sí, se puede. Pero ojo: no con curl, ni con requests, ni con n8n solo, ni con playwright-stealth. El endpoint existe y devuelve JSON limpio, lo que te frena es el filtro de huellas digitales de Akamai. Lo único que pasa ese filtro hoy es un Firefox parcheado a nivel C++ (Camoufox). Te dejo el test, los artifacts y la posta de por qué pasa lo que pasa.

1. El test 2. Por qué curl no anda 3. Por qué Camoufox sí 4. Lo que probé y no anduvo 5. El contrato del worker 6. TL;DR

1. El test que hicimos

Sitio: msc.com/en/track-a-shipment.
Container: MEDU9730548 (BoL MEDUJ0738446, sale de MOIN, Costa Rica, llega a YOKOHAMA, Japón).

❌ curl / requests (HTTP plano)

POST /api/feature/tools/TrackingInfo
Content-Type: application/json

{"trackingType":"Container","trackingNumber":"MEDU9730548"}

→ HTTP 403 — Access Denied
  Reference #18.6b841402.1780679402.2fb6d206
  (Akamai Bot Manager, edgesuite.net)

El endpoint existe, la API existe, el JSON está buenísimo. Pero Akamai te tira 403 al toque. No es un tema de cookies ni de User-Agent: es un tema de fingerprint de TLS, fingerprint de HTTP/2, y ausencia de contexto JS. Tu cliente grita "soy un bot" antes de mandar la primera línea de HTTP.

✅ Camoufox (headless Firefox parcheado)

POST /api/feature/tools/TrackingInfo
( desde una sesión Camoufox con fingerprint real )

→ HTTP 200 — application/json
  5.116 bytes
  BoL: MEDUJ0738446
  Pod ETA: 04/07/2026
  Latest move: COLON, PA
  8 eventos del viaje

Mismo endpoint, mismo body: anda. La diferencia es que el browser tiene la huella digital completa de un Firefox real (TLS, HTTP/2, canvas, fuentes, GPU, locales, timezone), y el motor tiene los parches anti-detect en C++, no en JavaScript.

Lo que devolvió (artifact real)

Screenshot completa de la página de resultados del tracking de MEDU9730548, capturada el 5 de junio 2026 a las 19:07 hora de Europa Central. 1905×3302, full-page render, con el BoL cargado y todos los eventos del viaje.

Screenshot

Resultados del tracking de MEDU9730548 en msc.com

El JSON que devolvió el XHR

El endpoint POST /api/feature/tools/TrackingInfo tira un JSON anidado con toda la info del container. Acá te dejo un fragmento (el JSON completo está en la respuesta, pero es largo de copiar literal):

{
  "IsSuccess": true,
  "Data": {
    "TrackingNumber": "MEDU9730548",
    "BillOfLadings": [{
      "BillOfLadingNumber": "MEDUJ0738446",
      "GeneralTrackingInfo": {
        "ShippedFrom": "MOIN, CR",
        "ShippedTo":   "YOKOHAMA, JP",
        "PortOfLoad":  "MOIN, CR",
        "PortOfDischarge": "YOKOHAMA, JP",
        "Transshipments": ["CRISTOBAL, PA", "RODMAN, PA"],
        "PriceCalculationDate": "24/05/2026"
      },
      "ContainersInfo": [{
        "ContainerNumber": "MEDU9730548",
        "ContainerType":   "40' HIGH CUBE REEFER",
        "LatestMove":      "COLON, PA",
        "PodEtaDate":      "04/07/2026",
        "Delivered":       false,
        "Events": [
          // 8 eventos, del más viejo al más nuevo:
          //   1. 22/05 PUERTO LIMON  → "Empty to Shipper"
          //   2. 22/05 MOIN CR        → "Export received at CY"
          //   3. 24/05 MOIN CR        → "Export Loaded on Vessel" (MSC PASSION III)
          //   4. 25/05 COLON PA       → "Full Transshipment Discharged"
          //   5. 08/06 COLON PA       → "Full Intended Transshipment"
          //   6. 08/06 RODMAN PA      → "Estimated Time of Arrival"
          //   7. 10/06 RODMAN PA      → "Full Intended Transshipment" (RDO ACE)
          //   8. 04/07 YOKOHAMA JP   → "Estimated Time of Arrival" (ETA final)
        ]
      }]
    }]
  }
}

2. Por qué curl no anda (lo que Akamai te cocina)

Cuando mandás una request a msc.com con curl, lo que Akamai ve en los primeros 50 ms de la conexión es:

TLS fingerprint (JA3 / JA4)

Antes de mandarse el primer header HTTP, tu cliente hace un handshake TLS. En ese handshake va la lista de cipher suites, extensions, elliptic curves y protocolos ALPN. Cada cliente HTTP (curl, requests, urllib3, Chrome, Firefox) tiene su propio patrón único. Akamai tiene una base con todos los patrones conocidos y los cruza contra el User-Agent que mandaste. Si no matchea, es automático. Por eso cambiar el User-Agent no sirve de nada: el problema está abajo del nivel HTTP, en el handshake, y el header que mandás no influye.

HTTP/2 fingerprint

Casi todos los sitios modernos (MSC incluido) hablan HTTP/2. El frame de SETTINGS, el orden de los headers, los priority hints y el timing del WINDOW_UPDATE también forman un patrón. Cada library de HTTP los manda de forma distinta, y el patrón de curl/requests no corresponde a ningún browser real.

Perfil de ejecución de JavaScript

MSC carga un script de Akamai que corre cientos de micro-checks dentro de la página:

Cada check es una señal. La suma de las señales es lo que decide si te tira captcha, te tira 403, o te deja pasar.

Consistencia interna del fingerprint

Aunque pases los checks anteriores, el fingerprint tiene que ser internamente consistente. Cosas que Akamai crucza:

La forma berreta de spoofear esto es tirar Object.defineProperty(navigator, 'webdriver', {get: () => false}) en la página. No anda. El parche queda en el nivel JavaScript, el engine real sigue tirando los valores viejos, y los detectores se dan cuenta (descriptor del getter mal, valores inconsistentes con el motor, etc.).

3. Por qué Camoufox sí anda

Camoufox es un build parcheado de Firefox. Los parches están en el core C++ del browser (Gecko), no en JavaScript que corre en la página. Esto es la clave: si parcheás el motor, no hay forma de detectarlo desde el nivel JS, porque en el nivel JS los valores ya están bien.

Fingerprint inyectado a nivel C++

Cuando arrancás Camoufox desde Python, el launcher genera un fingerprint (UA, pantalla, GPU, fuentes, locale, timezone, WebRTC, etc.) y se lo pasa al browser por variables de entorno. El Firefox parcheado lee esos valores en los lugares correctos del engine y los devuelve en todos lados: navigator.webdriver, WebGL, canvas, history API, geometría de pantalla, enumeración de fuentes, etc. No solo en el objeto JS, sino en los call sites internos del browser que un detector agresivo revisa.

Y el fingerprint no es una combinación random: sale de BrowserForge, un generador que imita la distribución estadística de dispositivos reales. Tu sesión "Windows 11 Chrome" no parece un recorte de la combinación más popular: parece uno más dentro de la población real de usuarios Windows 11 Chrome.

El protocolo de automatización, escondido de la página

Playwright controla Firefox por Juggler, un protocolo de automatización que corre en su propio módulo XPCOM adentro del browser. Camoufox parchea Juggler: cuando la página llama a document.querySelector, Juggler no inyecta nada de JS que la página pueda detectar. No hay window.__playwright__binding__, no hay Runtime.evaluate, no hay chrome.csi(). Camoufox intercepta la llamada a Juggler, le devuelve a Playwright lo que esperaba, y la página real no se entera. Ni siquiera Object.getOwnPropertyDescriptor te lo pesca, porque el parche está en C++ donde no hay objeto JS que inspeccionar.

Headless disfrazado de ventana

El headless default de Firefox tiene señales obvias (no hay puntero, las dimensiones de pantalla son raras). Camoufox parchea eso, y como fallback corre sobre un X server virtual adentro del contenedor para los casos donde headless filtra.

¿Cuál es la contra?

No es magia. Camoufox pasa ~90% de DataDome y 70–82% de Akamai Bot Manager en modo agresivo (benchmarks de abril/mayo 2026). El resto de los fallos suele ser:

4. Lo que probé y no anduvo

Estrategia Qué pasó Por qué
curl con replay del XHR HTTP 403 (Reference #18.6b841402.1780679402.2fb6d206) JA3 de TLS no matchea ningún browser conocido, fingerprint HTTP/2 es libcurl, sin contexto JS para el script de Akamai
Python requests Igual que curl Mismo stack de TLS y UA, mismo destino
headless Chromium (Playwright default) Muy probablemente 403 (no lo probé, pero es fama) navigator.webdriver=true, artefactos de CDP, Chromium tiene sus propios quilombos de fingerprinting
playwright-stealth (parches JS, no engine) ~60% de acierto en DataDome, menos en Akamai (benchmarks) El spoofing por JS es detectable: el descriptor del getter queda mal, Object.getOwnPropertyDescriptor lo pesca, los internals del engine siguen con los valores reales
nodo HTTP Request de n8n 403 (mismo que requests) n8n usa axios por debajo; mismo fingerprint, sin browser

5. El contrato mínimo del worker (para n8n u otro orquestador)

Si querés meterle un orquestador tipo n8n encima (cron, Google Sheet, Slack, lo que sea), el worker no tiene que ser nada raro. Es un mini servicio HTTP que mantiene una (o un pool chico de) sesiones de Camoufox, y expone un endpoint simple:

POST /track
Content-Type: application/json

{
  "container": "MEDU9730548"
}

→ 200 OK
  {
    "container":         "MEDU9730548",
    "bill_of_lading":    "MEDUJ0738446",
    "shipped_from":      "MOIN, CR",
    "shipped_to":        "YOKOHAMA, JP",
    "port_of_load":      "MOIN, CR",
    "port_of_discharge": "YOKOHAMA, JP",
    "transshipments":    ["CRISTOBAL, PA", "RODMAN, PA"],
    "pod_eta":           "2026-07-04",
    "latest_move":       "COLON, PA",
    "container_type":    "40' HIGH CUBE REEFER",
    "delivered":         false,
    "events": [
      { "date": "2026-05-22", "location": "PUERTO LIMON, CR", "description": "Empty to Shipper" },
      { "date": "2026-05-22", "location": "MOIN, CR",         "description": "Export received at CY" },
      { "date": "2026-05-24", "location": "MOIN, CR",         "description": "Export Loaded on Vessel", "vessel": "MSC PASSION III", "voyage": "PH620R" },
      { "date": "2026-05-25", "location": "COLON, PA",        "description": "Full Transshipment Discharged" },
      { "date": "2026-06-08", "location": "COLON, PA",        "description": "Full Intended Transshipment" },
      { "date": "2026-06-08", "location": "RODMAN, PA",       "description": "Estimated Time of Arrival" },
      { "date": "2026-06-10", "location": "RODMAN, PA",       "description": "Full Intended Transshipment", "vessel": "RDO ACE", "voyage": "2615W" },
      { "date": "2026-07-04", "location": "YOKOHAMA, JP",     "description": "Estimated Time of Arrival", "vessel": "RDO ACE", "voyage": "2615W" }
    ]
  }

→ 404  si el container no existe
→ 502  si MSC está caído
→ 429  si te está rate-limiteando (el worker debería backoffear)

El worker aplana la respuesta anidada de MSC en una estructura plana que se usa re fácil de ahí en adelante. Y guarda el JSON original de MSC adentro (campo raw) por si necesitás algo específico que no esté en la versión plana.

En n8n, el workflow típico es: Cron cada N horas → Read sheet con la lista de containers → Loop uno por uno → HTTP Request al /track del worker → Code node (opcional) para elegir campos → Append row a la historia / base / Slack. Se arma en 10 minutos en el editor visual de n8n.

6. TL;DR

La pregunta que tiene que hacer el orquestador no es "cómo llamo a MSC", es "cómo llamo al worker que tiene la sesión de Camoufox abierta". Ahí ya no hay ningún misterio.