--- name: maersk-port-calls-scraper description: Read Maersk and Sealand port calls and vessel rotations as structured records via the Apify Actor arman-bd/maersk-port-calls-scraper. Returns one record per port call or rotation leg, with vessel identity, terminal, ETA/ETD (estimated vs actual), voyage number and service code, for any port (by UN/LOCODE, Maersk port code, or name) or any vessel (by code, IMO, or name), over a date window. Use for berth-window benchmarking, carrier-of-record ETAs, vessel rotation tracking, service-lane mapping, or supply-chain visibility tooling. Not for live AIS vessel position, container-level tracking, freight rates, or carriers other than Maersk/Sealand. --- # Maersk Port Calls & Vessel Schedules Scraper Apify Actor `arman-bd/maersk-port-calls-scraper`. Name a port, a vessel, or both, and get back Maersk's own published schedule: every vessel calling a port in a date window, or every port a vessel calls in that window. Covers both the Maersk (MAEU) and Sealand (MCPU) brands. It runs without credentials, and with no input at all it returns Rotterdam's next two weeks. ## When to use it - Berth-window benchmarking: how far estimated ETAs move against actuals at a terminal over time. - Carrier-of-record ETAs for a supply-chain visibility tool, sourced straight from the carrier rather than a third-party aggregator. - Vessel rotation tracking: a vessel's full forward port sequence without an AIS feed. - Service-lane mapping: group by `arrivalServiceCode` to see which vessels and ports share a named trade lane. - Cross-referencing a rate sheet's `portCode`/`unLocationCode` against what is actually sailing in a window. ## When not to use it - Live vessel position (AIS/GPS). This returns Maersk's published schedule, not satellite tracking. - Container-level tracking or bill-of-lading status. - Freight rates or booking availability. - Any carrier other than Maersk or Sealand. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/maersk-port-calls-scraper').call({ ports: ['NLRTM'], vessels: [], fromDate: '2026-09-01', toDate: '2026-09-21', carrierCodes: ['MAEU'], maxRecords: 300, }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~maersk-port-calls-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"ports":["Singapore"],"fromDate":"2026-09-01","toDate":"2026-09-15"}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/maersk-port-calls-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `ports` | string[] | no | `["NLRTM"]` | UN/LOCODE (`NLRTM`), Maersk's own port code, or a city/port name (`Rotterdam`), matched against Maersk's live port list. Leave empty together with `vessels` to default to Rotterdam. | | `vessels` | string[] | no | `[]` | Maersk vessel code, IMO number, or vessel name, matched against Maersk's live vessel list. Leave empty to skip vessel rotations. | | `fromDate` | string | no | today | `YYYY-MM-DD`. Start of the schedule window. | | `toDate` | string | no | `fromDate` + 14 days | `YYYY-MM-DD`. End of the schedule window. 2-4 week windows answer reliably; wider windows risk truncation on busy ports. | | `carrierCodes` | string[] | no | `["MAEU"]` | `MAEU` (Maersk) and/or `MCPU` (Sealand). Both are queried together in one request per port/vessel. | | `maxRecords` | integer | no | `500` | Total cap across every port and vessel combined. `0` means no limit. A negative or non-numeric value falls back to `500`, never to unlimited. | **A port lookup and a vessel lookup are independent and additive.** Listing both `ports` and `vessels` runs both flows in the same call and both kinds of record land in the same dataset, distinguished by `source`. ## Output One record per port call (`source: "port-call"`) or per rotation leg (`source: "vessel-rotation"`). Every record carries the same key set regardless of source; fields Maersk's own response does not attach to that flow are filled in from the matched port or vessel instead. | Field | Type | Notes | |---|---|---| | `source` | string | `port-call` or `vessel-rotation`. | | `vesselMaerskCode` | string \| null | Maersk's internal 3-character vessel code. | | `vesselName` | string \| null | Vessel name. | | `vesselIMONumber` | string \| null | IMO number. | | `flagISOCountryCode` | string \| null | Flag state. | | `callSign` | string \| null | Radio call sign. | | `marineContainerTerminalName` | string \| null | Terminal/berth name. | | `marineContainerTerminalRKSTCode` | string \| null | Terminal's RKST code. | | `marineContainerTerminalGeoCode` | string \| null | Terminal's Maersk geo code. | | `arrivalTime` | string \| null | ISO 8601 with local offset. | | `arrivalTimingClassifier` | string \| null | `ESTIMATED` or `ACTUAL`. | | `departureTime` | string \| null | ISO 8601 with local offset. | | `departureTimingClassifier` | string \| null | `ESTIMATED` or `ACTUAL`, independent of the arrival flag. | | `arrivalVoyageNumber` | string \| null | Inbound voyage number for this call. | | `departureVoyageNumber` | string \| null | Outbound voyage number for this call. | | `arrivalServiceName`, `arrivalServiceCode` | string \| null | Named service and code for the inbound leg. | | `departureServiceName`, `departureServiceCode` | string \| null | Named service and code for the outbound leg. | | `isoCountryCode`, `countryName` | string \| null | Port's country. | | `unLocationCode` | string \| null | UN/LOCODE. | | `cityName`, `portName` | string \| null | Port geography. | | `portCode` | string \| null | Maersk's own 13-character port code. | | `regionCode` | string \| null | Sub-country region, when Maersk publishes one. | | `carrierCodesQueried` | string[] | The `carrierCodes` this call was made with. Not a per-record fact: Maersk's response does not split mixed-brand results by brand. | | `queriedPort` | string \| null | The raw `ports` entry that produced this record, on `port-call` rows. | | `queriedVessel` | string \| null | The raw `vessels` entry that produced this record, on `vessel-rotation` rows. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real `port-call` record: ```json { "source": "port-call", "vesselMaerskCode": "W04", "vesselName": "MAERSK SARAT", "vesselIMONumber": "9732591", "marineContainerTerminalName": "Hutchison Ports Delta II", "arrivalTime": "2026-08-22T00:06:00+02:00", "arrivalTimingClassifier": "ACTUAL", "departureTime": "2026-08-23T03:45:00+02:00", "departureTimingClassifier": "ESTIMATED", "arrivalVoyageNumber": "628N", "arrivalServiceName": "SAECS", "arrivalServiceCode": "278", "cityName": "Rotterdam", "portCode": "1JUKNJGWHQBNJ", "carrierCodesQueried": ["MAEU"], "queriedPort": "NLRTM", "queriedVessel": null, "scrapedAt": "2026-08-21T22:50:54.635Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** It is where a partial run admits that it was partial. ```json { "portsRequested": ["NLRTM", "not-a-real-port-xyz"], "vesselsRequested": ["T9P"], "carrierCodes": ["MAEU"], "dateRange": { "fromDate": "2026-08-22", "toDate": "2026-09-05" }, "maxRecords": 500, "recordsSaved": 91, "portCallRecords": 84, "vesselRotationRecords": 7, "requestsMade": 3, "failures": [ { "type": "port", "input": "not-a-real-port-xyz", "error": "no matching port found" } ], "finishedAt": "2026-08-21T22:50:55.161Z" } ``` `failures` names every port or vessel that could not be resolved or fetched, with the raw input you gave. `recordsSaved` equal to `maxRecords` means the cap bound first, so `requestsMade` will then be lower than one request per target, because targets after the cap was hit are never even requested. ## Behaviour to plan around - **A port lookup and a vessel lookup return complementary, not overlapping, data.** A port lookup lists every vessel calling there; a vessel lookup lists every port that vessel calls. The same physical call can appear from both directions in one run if you name both the port and the vessel: they are not deduplicated against each other, because they carry different `source` values and different `queriedPort`/`queriedVessel` provenance. - **`arrivalTimingClassifier`/`departureTimingClassifier` are a snapshot.** They flip from `ESTIMATED` to `ACTUAL` as the call happens. Re-run later in the day to see actuals replace estimates, rather than treating one run as final. - **`carrierCodesQueried` is what you asked for, not a per-record fact.** Querying both `MAEU` and `MCPU` together returns one merged set of calls; Maersk's schedule API does not tag which brand operated a specific one. - **Resolution is case-insensitive and tries exact identifiers before names.** An exact `portCode`/`unLocationCode` or `vesselMaerskCode`/IMO always wins over a name match; a name match tries exact first, then a substring, and takes the first hit. Ambiguous names (a city that exists in two countries) are not guaranteed to resolve to the one you meant. - **A target that resolves but has nothing scheduled in the window is not a failure.** Only a port/vessel that cannot be matched at all, or whose request errors outright, lands in `failures`. - **The run only throws when every single target failed.** One bad name or one failed request never sinks the ones that worked; `failures` names exactly which. - **`maxRecords` bounds the whole run, not each target.** It is checked before each target starts and again before each row is saved, so a low cap can mean a later-listed port or vessel is never even requested. ## Recipes **A terminal's near-term berth window.** ```json { "ports": ["NLRTM"], "fromDate": "2026-09-01", "toDate": "2026-09-15" } ``` Group by `marineContainerTerminalName` to see load across berths at one port. **A vessel's full forward rotation, both brands.** ```json { "vessels": ["Madrid Maersk"], "fromDate": "2026-09-01", "toDate": "2026-11-01", "carrierCodes": ["MAEU", "MCPU"] } ``` Sort by `arrivalTime` for the port sequence in order. **A multi-port sweep for a trade-lane view.** ```json { "ports": ["NLRTM", "USNYC", "SGSIN", "CNSHA"], "fromDate": "2026-09-01", "toDate": "2026-09-15", "maxRecords": 1000 } ``` Filter on `arrivalServiceCode` afterward to isolate one named service across all four ports.