--- name: crowley-shipping-advisories-scraper description: Read Crowley Maritime's shipping and logistics advisories as structured records via the Apify Actor arman-bd/crowley-shipping-advisories-scraper. Returns one record per notice with a classified type (SURCHARGE, TERMINAL_HANDLING_CHARGE, VESSEL_SCHEDULE_CHANGE, PORT_OMISSION, RATE_OR_CHARGE_CHANGE, HOLIDAY_SCHEDULE, WEATHER_ADVISORY, REGULATORY_NOTICE, OTHER), the full notice body as plain text, publish and modified timestamps, filterable by date range, full-text search and type. Use for freight audit, rate-tariff tracking, booking-risk alerts on Crowley's Latin America, Caribbean, Central America and Puerto Rico trades, and competitive intelligence on surcharge timing. Not for other carriers, for comment or contact data, or for anything outside Crowley's public advisories feed. --- # Crowley Shipping & Logistics Advisories Scraper Apify Actor `arman-bd/crowley-shipping-advisories-scraper`. One dataset record per advisory, with the notice body fetched in the same request as the listing (no second fetch per notice). It runs without credentials, and with no input at all returns the most recent notices across every classified type. ## When to use it - Freight audit and pay: catch a surcharge or terminal handling charge that changed before it hits an invoice. - Keeping a local-charge tariff table current for Crowley's Latin America, Caribbean, Central America and Puerto Rico lanes without reading PDFs. - Booking-risk alerts: `VESSEL_SCHEDULE_CHANGE` and `PORT_OMISSION` notices affect cargo already booked on an affected sailing. - Competitive intelligence: how often, and by how much, a carrier moves fuel surcharges relative to bunker price changes. - Compliance monitoring: `REGULATORY_NOTICE` surfaces documentation, inspection and tariff-ruling notices that touch customs clearance. ## When not to use it - Any carrier other than Crowley Maritime. This Actor is scoped to one feed. - Comment threads, contact details, or anything not published in the advisories feed itself. - Real-time alerting on a single notice the moment it posts. This is a pull-based Actor; schedule it and diff on `id` or `publishedAt` for near-real-time coverage. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/crowley-shipping-advisories-scraper').call({ noticeTypes: ['SURCHARGE', 'TERMINAL_HANDLING_CHARGE'], after: '2026-01-01', order: 'desc', maxNotices: 100, maxPages: 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~crowley-shipping-advisories-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"noticeTypes":["SURCHARGE","TERMINAL_HANDLING_CHARGE"],"maxNotices":100}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/crowley-shipping-advisories-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `after` | string | no | (none) | Earliest publish date, `YYYY-MM-DD` or a full ISO timestamp. Anchored to the start of that day when a bare date is given. An unparseable value is dropped and named in `RUN_SUMMARY.invalidInputs`, never fatal. | | `before` | string | no | (none) | Latest publish date, same format as `after`. Anchored to the end of that day when a bare date is given. | | `search` | string | no | (none) | Full-text search applied server-side across the notice title and body. | | `noticeTypes` | string[] | no | `[]` | Keep only these classified families: `SURCHARGE`, `TERMINAL_HANDLING_CHARGE`, `VESSEL_SCHEDULE_CHANGE`, `PORT_OMISSION`, `RATE_OR_CHARGE_CHANGE`, `HOLIDAY_SCHEDULE`, `WEATHER_ADVISORY`, `REGULATORY_NOTICE`, `OTHER`. An unrecognized value is dropped and named in `RUN_SUMMARY.invalidInputs` rather than silently narrowing the filter to nothing. Empty means every type. | | `order` | string | no | `desc` | `desc` for newest first, `asc` for oldest first. | | `maxNotices` | integer | no | `100` | Total saved for the run, the most you can be charged for. `0` means no limit, bounded only by `maxPages`. | | `maxPages` | integer | no | `5` | How many 100-notice pages to walk. Clamped to 1 through 30. The category holds roughly 1,900 notices, so 19 pages is a full backfill. | **`maxNotices` and `maxPages` bound the run from opposite ends.** `maxPages` limits what is fetched; `maxNotices` limits what is kept after `noticeTypes` has thrown rows away. A strict `noticeTypes` filter over a quiet window can exhaust `maxPages` before reaching `maxNotices`, and the fix is to raise `maxPages`. ## Output One record per notice. | Field | Type | Notes | |---|---|---| | `id` | number | Crowley's WordPress post ID, stable across runs. | | `noticeType` | string | Classified from the title. See the enum under `noticeTypes` above. `OTHER` when nothing fits, never forced into the nearest guess. | | `title` | string | Notice title, HTML entities decoded. | | `slug` | string | URL slug. | | `link` | string | Canonical URL on crowley.com. | | `publishedAt`, `modifiedAt` | string | ISO 8601 UTC timestamps. | | `excerpt` | string \| null | Auto-generated teaser, plain text. | | `content` | string \| null | The full notice body as plain text: effective dates, charge amounts, ports affected. HTML tables are flattened to ` \| `-joined lines, one per row. | | `categories` | number[] | Crowley's own WordPress category IDs on the post. | | `postType` | string | Always `post` for this feed. | | `scrapedAt` | string | Run timestamp. | A real record: ```json { "id": 74740, "noticeType": "TERMINAL_HANDLING_CHARGE", "title": "Costa Rica Terminal Handling Charge Effective 09-13-2026", "slug": "costa-rica-terminal-handling-charge-effective-09-13-2026", "link": "https://www.crowley.com/uncategorized/costa-rica-terminal-handling-charge-effective-09-13-2026/", "publishedAt": "2026-08-14T20:25:15Z", "modifiedAt": "2026-08-14T20:25:20Z", "excerpt": "Effective September 13, 2026, Crowley will implement an increase to the Costa Rica Terminal Handling Charge on inbound and outbound loads via Costa Rica ports.", "content": "Effective September 13, 2026, Crowley will implement an increase to the Costa Rica Terminal Handling Charge on inbound and outbound loads via Costa Rica ports. This adjustment is due to operational cost increases from APM terminals.\n\nEquipment | Direction | Previous Charge | New Charge |\nAll Containerized Equipment | Inbound | $650 | $675 |\nAll Containerized Equipment | Outbound | $585 | $610 |", "categories": [13, 1], "postType": "post", "scrapedAt": "2026-08-21T22:41:37.000Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** It is where a partial or degraded run says so. ```json { "filters": { "after": "2026-01-01T00:00:00", "before": null, "search": "", "noticeTypes": ["SURCHARGE"], "order": "desc", "maxNotices": 100, "maxPages": 10 }, "invalidInputs": [], "categoryId": 13, "categoryTotalAvailable": 1850, "reportedTotalMatchingQuery": 39, "pagesFetched": 1, "noticesFetched": 39, "noticesFilteredOut": 21, "noticesSaved": 18, "failures": [], "finishedAt": "2026-08-21T22:41:37.000Z" } ``` `categoryId` is resolved fresh every run by slug, not hard-coded, so it is the place to confirm the run found the right category before trusting `noticesSaved`. `reportedTotalMatchingQuery` is the server's own count for the `after`/`before`/ `search` filters before `noticeTypes` narrows further; `noticesFilteredOut` is how much `noticeTypes` discarded from what was fetched. `invalidInputs` names any `after`, `before` or `noticeTypes` value that could not be understood and was dropped rather than failing the run; check it whenever `noticesSaved` looks lower than expected. ## Behaviour to plan around - **`noticeType` is a classification, not a Crowley field.** It is derived from the title with ordered pattern matching built from the titles actually observed in the feed. Most notices land cleanly; `OTHER` covers what does not fit any of the eight named families rather than being forced into the closest one. - **An unparseable date or an unrecognized notice type is dropped, not fatal.** The run continues without that filter and names what was ignored in `RUN_SUMMARY.invalidInputs`. A run that returns fewer notices than expected should be checked there before assuming the filter matched correctly. - **The category is resolved by slug every run**, not a fixed numeric ID, so this keeps working if Crowley's site restructures its taxonomy. - **`content` needs no second fetch.** The full notice body comes back in the same request as the listing. - **A bare date anchors to day boundaries.** `after` starts at 00:00:00 of that day, `before` ends at 23:59:59, so `after: "2026-01-01", before: "2026-01-31"` covers the whole month inclusively. - **A search or type filter matching nothing is a valid, empty result**, not an error and not listed in `failures`. `failures` is reserved for the category lookup or a page request actually failing upstream. - **Transient errors are retried** three times with linear backoff. A rejected query or a not-found response is final and recorded in `failures`. ## Recipes **Invoice-relevant charges, most recent first.** ```json { "noticeTypes": ["SURCHARGE", "TERMINAL_HANDLING_CHARGE", "RATE_OR_CHARGE_CHANGE"], "maxNotices": 100, "maxPages": 5 } ``` Filter further on `content` for the specific port or lane a shipment is booked on. **Incremental pull since the last run.** ```json { "after": "2026-08-01", "order": "asc", "maxNotices": 200, "maxPages": 10 } ``` Set `after` to the `publishedAt` of the newest notice already stored, and walk forward with `order: "asc"` so results arrive in publish order. **Full backfill of one notice family.** ```json { "noticeTypes": ["VESSEL_SCHEDULE_CHANGE", "PORT_OMISSION"], "maxNotices": 0, "maxPages": 20 } ``` `maxNotices: 0` removes the count cap; `maxPages: 20` covers the full ~1,900-notice category at the default page size. A narrow filter over that many pages can still return relatively few rows, since most of the category is other notice types.