--- name: matson-vessel-schedules-scraper description: Read Matson's O/D vessel schedule as structured records via the Apify Actor arman-bd/matson-vessel-schedules-scraper. Returns one record per sailing with vessel, voyage, transit time, port rotation and the full cargo cut-off matrix (dry, reefer, hazmat, hazmat paperwork, documentation, ITN, AMS, VGM, oversize, China-service), for any origin-destination lane on Matson's network: US mainland/inland, Hawaii, Alaska, Guam, Micronesia, South Pacific and the China/CLX-CCX strings. Use for cargo cut-off tracking, booking deadline alerts, Jones Act capacity monitoring or China-service transit tracking. Not for rates, bookings, container tracking or carriers other than Matson. --- # Matson Vessel Schedules Scraper Apify Actor `arman-bd/matson-vessel-schedules-scraper`. Name the lanes you want as `ORIGIN-DESTINATION`, get one dataset record per sailing, cut-offs included by default. It runs without credentials, and with no input at all it returns Oakland to Honolulu for the next 30 days. ## When to use it - Cargo cut-off tracking: dry, reefer or hazmat cut-off times for sailings already booked or being considered, across as many lanes as one run needs. - Booking deadline alerts: pull a lane on a schedule and fire a reminder ahead of `dryCutoff`/`reeferCutoff`/`hazCutoff` for a known `vvd`. - Jones Act capacity monitoring: Hawaii and Alaska vessel assignments and transit times, where independent schedule data barely exists. - China/CLX-CCX express tracking: transit time and cut-offs specifically for the Transpacific China service lanes. - Comparing transit time or cut-off timing across several lanes in one run. ## When not to use it - Rates or booking. This is schedule and cut-off data only. - Container-level tracking. There is no container or booking number lookup here. - Any carrier other than Matson. - A destination Matson doesn't serve from a given origin, that lane comes back named in `RUN_SUMMARY.failures`, not guessed at. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/matson-vessel-schedules-scraper').call({ lanes: ['OAK-HON', 'TAC-ANK', 'LAX-GUM'], startDate: '2026-09-01', endDate: '2026-10-01', includeCutoffs: true, maxSailingsPerLane: 10, }); 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~matson-vessel-schedules-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"lanes":["OAK-HON"],"maxSailingsPerLane":10}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/matson-vessel-schedules-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `lanes` | string[] | no | `["OAK-HON"]` | One entry per lane, as `ORIGIN-DESTINATION` using Matson's own 3-4 letter location codes. `/`, `>`, `,` and the word `to` also work as separators, case-insensitive. An unrecognised origin, or a destination not served from a given origin, is recorded in `RUN_SUMMARY.failures` and the rest of the run continues. | | `startDate` | string | no | today (UTC) | `YYYY-MM-DD`. Earliest sailing to include. | | `endDate` | string | no | `startDate` + 30 days | `YYYY-MM-DD`. Latest sailing to include. Capped at 180 days after `startDate`, a longer request is clamped, and `RUN_SUMMARY.filters.dateRangeClamped` is `true` when that happened. | | `includeCutoffs` | boolean | no | `true` | Fetch the cut-off matrix and port rotation for every sailing. One extra request per sailing; `false` gets you the sailing list only, faster and cheaper. | | `maxSailingsPerLane` | integer | no | `15` | 1 to 100. Caps sailings saved (and charged) per lane. Out-of-range values (0, negative, non-numeric) fall back to the default rather than being treated as unlimited. | ## Output One record per sailing. | Field | Type | Notes | |---|---|---| | `lane` | string | The requested lane as `ORIGIN-DESTINATION`. | | `originCode`, `originName` | string | Matson's origin code and name. | | `destinationCode`, `destinationName` | string | Matson's destination code and name. | | `destinationRegionCode`, `destinationRegion` | string | Matson's own region grouping for the destination: `HI` Hawaii, `NI` Hawaii neighbor island, `AK` Alaska, `GU` Guam, `FM`/`GO` Micronesia, `SP` South Pacific, `CN` China service, `ML` US mainland, `IR` US inland. | | `vessel`, `voyageNumber`, `direction` | string | Parsed from Matson's own vessel/voyage/direction code. | | `vesselCode`, `vvd` | string | Matson's short vessel code and the vessel-voyage-direction key, which is what `details.php`-equivalent cut-off lookups key on. | | `rowNumber` | number | Position in Matson's own result order. | | `depart`, `arrive` | string | Matson's own strings, in the port's local time. Not normalised to UTC or a single format. | | `transitTime` | string | e.g. `"4 Days"`, from the sailing list. | | `cutoffsIncluded` | boolean | Whether the fields below were fetched for this sailing. | | `totalTransitDays`, `departureDetail`, `arrivalDetail`, `availability` | string \| null | The cut-off lookup's own description of the same sailing, plus cargo availability at destination. `null` when `cutoffsIncluded` is `false` or the lookup failed. | | `dryCutoff`, `reeferCutoff`, `hazCutoff`, `hazPaperCutoff`, `inlandCutoff`, `docCutoff`, `itnCutoff`, `amsCutoff`, `vgmCutoff`, `oversizeCutoff`, `chinaCutoff` | string \| null | Every cut-off Matson publishes. `"N/A"` is Matson's own answer meaning that cut-off type doesn't apply to this sailing; `null` means it wasn't looked up at all. | | `cutoffNote` | string \| null | Free-text note Matson attaches, usually `null`. | | `portRotation` | array | `[{ leg, from, to, vesselVoyage, transitTime, departureDate, arrivalDate }, …]`. One leg for a direct sailing, several for an indirect one. Empty when `cutoffsIncluded` is `false`. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real record: ```json { "lane": "OAK-HON", "originCode": "OAK", "originName": "OAKLAND", "destinationCode": "HON", "destinationName": "HONOLULU", "destinationRegionCode": "HI", "destinationRegion": "Hawaii", "vessel": "MANULANI", "voyageNumber": "269", "direction": "W", "vesselCode": "MLI", "vvd": "MLI269W", "rowNumber": 0, "depart": "Tuesday 08/25/2026 02:00", "arrive": "Saturday 08/29/2026 06:00", "transitTime": "4 Days", "cutoffsIncluded": true, "totalTransitDays": "4 days", "departureDetail": "OAK on Tuesday 08/25 at 02:00", "arrivalDetail": "HON on Saturday 08/29 at 06:00", "availability": "Monday 08/31 at 08:00", "dryCutoff": "Mon 23:00 08/24", "reeferCutoff": "Mon 23:00 08/24", "hazCutoff": "Mon 16:30 08/24", "hazPaperCutoff": "Mon 12:00 08/24", "inlandCutoff": "N/A", "docCutoff": "Mon 12:00 08/24", "itnCutoff": "N/A", "amsCutoff": "N/A", "vgmCutoff": "N/A", "oversizeCutoff": "N/A", "chinaCutoff": "N/A", "cutoffNote": null, "portRotation": [ { "leg": 1, "from": "OAKLAND", "to": "HONOLULU", "vesselVoyage": "MANULANI 269 W", "transitTime": "4 days", "departureDate": "08-25-2026", "arrivalDate": "08-29-2026" } ], "scrapedAt": "2026-08-21T22:51:08.362Z" } ``` ## 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 { "lanesRequested": ["OAK-HON", "TAC-ANK", "LAX-GUM"], "lanesFailed": 1, "failures": [ { "lane": "TAC-ANK", "stage": "search", "error": "retryable HTTP 503" } ], "laneSummaries": [ { "lane": "OAK-HON", "sailingsFound": 8, "sailingsSaved": 8, "skippedByCap": 0 }, { "lane": "LAX-GUM", "sailingsFound": 4, "sailingsSaved": 4, "skippedByCap": 0 } ], "sailingsSaved": 12, "filters": { "lanes": ["OAK-HON", "TAC-ANK", "LAX-GUM"], "startDate": "2026-09-01", "endDate": "2026-10-01", "dateRangeClamped": false, "includeCutoffs": true, "maxSailingsPerLane": 10 }, "finishedAt": "2026-08-21T22:51:12.041Z" } ``` `failures[].stage` is one of `origin` (unrecognised origin code), `destinations` (the destination lookup itself failed), `destination` (a real code Matson doesn't serve from that origin), `search` (the sailing lookup failed) or `details` (a cut-off lookup failed for one sailing, the sailing row is still saved, with `cutoffsIncluded: false`). Only the first four count toward `lanesFailed`; a `details` failure doesn't take the lane down. `laneSummaries[].sailingsFound` with `sailingsFound: 0` and no matching entry in `failures` means the lane is real but nothing is currently scheduled in the window, widen the dates. ## Behaviour to plan around - **The origin code list is read fresh every run,** from Matson's own schedule page, not hard-coded. A code that used to work and now doesn't is a real change on Matson's side, not a stale list in this Actor. - **A destination is validated against what Matson actually serves from that origin** before the sailing lookup runs. A wrong-way pairing (a real code for the wrong origin) is named in `failures` rather than returning a silent empty result. - **`depart`/`arrive`/every cut-off are Matson's own strings**, in the port's local time, in whatever format that particular field uses, the sailing list and the cut-off lookup do not even agree on date format with each other. Parse per-field if you need machine dates; don't assume one shared format. - **`"N/A"` and `null` mean different things.** `"N/A"` is Matson answering that a cut-off type doesn't apply to this sailing. `null` means the cut-off matrix wasn't fetched at all (`cutoffsIncluded: false`), either because `includeCutoffs` was off or the lookup failed. - **A cut-off lookup failing never drops the sailing.** The row is still saved with `cutoffsIncluded: false`, and the failure is named in `RUN_SUMMARY.failures` with the `vvd` it happened on. - **`maxSailingsPerLane` is a per-lane cap, not a run-wide one.** Ten lanes at a cap of 10 can save up to 100 rows, not 10. - **Requests are serialised** across the whole run, lanes, and the details lookups inside a lane, run one at a time, not in parallel. - **The run only throws when every lane failed outright.** One bad lane alongside a good one is a normal exit with the good lane's rows saved and the bad one named in `RUN_SUMMARY.failures`. ## Recipes **Cut-off deadline watch.** A known lane, cut-offs on, run on a schedule a few times a day so a fresh `dryCutoff`/`reeferCutoff` is always in the dataset. ```json { "lanes": ["OAK-HON"], "maxSailingsPerLane": 10, "includeCutoffs": true } ``` Key on `vvd` to track a specific sailing's cut-off across runs rather than treating each run as a fresh list. **Multi-region sweep.** One lane per named region in one run, over a month. ```json { "lanes": ["OAK-HON", "TAC-ANK", "LAX-GUM", "HON-EBY", "LAX-SHA"], "startDate": "2026-09-01", "endDate": "2026-10-01", "maxSailingsPerLane": 10 } ``` South Pacific lanes (`SP` region: Apia, Pago Pago, Suva, Nuku Alofa, Lautoka, Papeete) run far less often than the others, a legitimately empty result there is normal, not a sign of a broken lane; check `RUN_SUMMARY.failures` to tell the two apart. **Fast schedule-only pull.** No cut-off lookups, for when you only need dates and vessels. ```json { "lanes": ["LAX-GUM", "LAX-SHA"], "includeCutoffs": false, "maxSailingsPerLane": 50 } ```