--- name: panama-canal-advisories-scraper description: Read the Panama Canal Authority's Advisory to Shipping archive as structured records via the Apify Actor arman-bd/panama-canal-advisories-scraper. Returns one record per numbered advisory (ADV-NN-YYYY) with advisory number, year, title, category (Advisory, Monthly Operations Summary, or Notice to Shipping), a link to the source PDF and its upload month, filterable by year range, advisory number and keyword, with optional PDF liveness verification. Use for freight-rate signal on draft/booking/transit changes, canal operations monitoring, or a compliance archive of every advisory issued. Not for the PDF body text itself, real-time vessel positions, or a Spanish-language edition. --- # Panama Canal Advisories to Shipping Scraper Apify Actor `arman-bd/panama-canal-advisories-scraper`. Reads the ACP's public Advisory to Shipping archive and returns one record per numbered advisory, filterable by year, advisory number and keyword. It runs without credentials, and with no input at all it returns the entire archive. ## When to use it - Freight-rate signal: draft restriction, transit slot and booking-system advisories are a leading indicator Panamax rate desks watch, often issued within a day of a Gatun Lake level change. - Canal operations monitoring: the monthly `Monthly Operations Summary` advisory tracks transit counts and tonnage. - Compliance and audit trail: keep a running record of every advisory number issued so a gap in the sequence is visible immediately. - Newsletter or briefing sourcing, where `title`, `category` and `pdfUrl` are enough to link out without re-hosting the document. ## When not to use it - The PDF body text itself (effective dates, feet-TFW draft limits, slot counts as written in the document). This Actor indexes the archive; fetch `pdfUrl` yourself for the document. - Real-time vessel positions, AIS data or actual transit records. This is advisory notices, not traffic data. - A Spanish-language edition. The archive is published in English only. - Historical advisories predating 2020 in bulk. Coverage starts in 2020; a handful of older, differently-named documents exist on the source site but fall outside the numbered `ADV-NN-YYYY` convention this Actor parses. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/panama-canal-advisories-scraper').call({ yearFrom: 2025, keywords: ['draft', 'booking', 'transit'], maxAdvisories: 0, downloadPdf: false, }); 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~panama-canal-advisories-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"yearFrom":2025,"keywords":["draft","booking"]}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/panama-canal-advisories-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `yearFrom` | integer | no | `0` | Keep advisories from this year on, by the advisory's own `ADV-NN-YYYY` year (not upload date). `0` means no lower bound. | | `yearTo` | integer | no | `0` | Keep advisories up to and including this year. `0` means no upper bound. | | `advisoryNumberFrom` | integer | no | `0` | Drop advisories numbered below this. Numbering restarts every year, so this is a within-year filter unless combined with `yearFrom`/`yearTo`. `0` means no filter. | | `keywords` | string[] | no | `[]` | Keep only advisories whose title contains at least one of these, case-insensitive, with hyphens in the title treated as spaces so a multi-word phrase like `"maximum authorized draft"` still matches. Empty keeps everything. | | `downloadPdf` | boolean | no | `false` | HEAD-request each matching PDF to confirm it resolves and record size/content-type. One extra request per advisory, so it roughly doubles run time. | | `maxAdvisories` | integer | no | `0` | Stop after this many advisories, newest (year desc, then advisory number desc) first. `0` returns every match. | **`yearFrom` after `yearTo` is a fatal input error** (an empty range), not a filter that returns nothing, the run exits non-zero with a message naming both values. ## Output One record per numbered advisory found in the archive after filtering. | Field | Type | Notes | |---|---|---| | `advisoryNumber` | number | The `NN` in `ADV-NN-YYYY`. Not globally unique, combine with `year`. | | `year` | number | The `YYYY` in the advisory number. | | `title` | string \| null | Title slug from the filename. `null` on the few advisories whose filename carries no extra text beyond the number. | | `category` | string | `Advisory`, `Monthly Operations Summary` or `Notice to Shipping`, classified from the title. | | `pdfUrl` | string | Direct link to the source PDF. | | `uploadYearMonth` | string | `YYYY-MM` the file was uploaded, read from its storage path. Can differ from `year` when an advisory is uploaded the month after the year it is numbered for. | | `language` | string | Always `"en"`. The archive has no Spanish edition. | | `pdfVerified` | boolean \| null | `null` unless `downloadPdf` is `true`; then whether the PDF resolved. | | `pdfBytes` | number \| null | PDF size from the `Content-Length` header, when `downloadPdf` is on and the check succeeded. | | `pdfContentType` | string \| null | PDF's reported content type, when `downloadPdf` is on and the check succeeded. | | `sourceUrl` | string | The archive page this record was read from. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real record: ```json { "advisoryNumber": 25, "year": 2026, "title": "Adjustment-to-the-Maximum-Allowable-Draft-in-the-Neopanamax-Locks", "category": "Advisory", "pdfUrl": "https://pancanal.com/wp-content/uploads/2026/08/ADV-25-2026-Adjustment-to-the-Maximum-Allowable-Draft-in-the-Neopanamax-Locks.pdf", "uploadYearMonth": "2026-08", "language": "en", "pdfVerified": null, "pdfBytes": null, "pdfContentType": null, "sourceUrl": "https://pancanal.com/en/maritime-services/advisory-to-shipping/", "scrapedAt": "2026-08-21T12:00:00.000Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** ```json { "pageFetched": true, "totalPdfLinks": 1001, "totalAdvisoryLinks": 289, "matchedFilters": 47, "advisoriesSaved": 20, "failures": [], "filters": { "yearFrom": 2025, "yearTo": 0, "advisoryNumberFrom": 0, "keywords": ["draft", "booking"], "downloadPdf": false, "maxAdvisories": 20 }, "finishedAt": "2026-08-21T12:00:03.118Z" } ``` `totalPdfLinks` is every PDF href on the archive page, including the ~1-in-100 that don't follow the numbered convention. `totalAdvisoryLinks` is how many parsed as a numbered advisory before any filter ran, a sharp drop from a previous run's value is the signal the source changed its filename convention. `matchedFilters` is after `yearFrom`/`yearTo`/`advisoryNumberFrom`/`keywords`, before the `maxAdvisories` cap. When `downloadPdf` is on, `failures` lists any PDF that failed its liveness check (`pdfUrl` and `error`), those advisories are still saved and charged, just with `pdfVerified: false`. ## Behaviour to plan around - **The whole archive is read every run.** It is one HTML page; filters and the `maxAdvisories` cap are applied after fetching, not before. `matchedFilters` in `RUN_SUMMARY` tells you what was found before capping. - **Newest first.** The Actor sorts by year then advisory number, both descending, before applying `maxAdvisories`, the opposite of the source page's own oldest-first order, so a capped run gets recent advisories, not 2020's. - **Advisory numbers restart each year.** `ADV-01` exists in every year. Use `year` alongside `advisoryNumber` as the real key, and scope `advisoryNumberFrom` with `yearFrom`/`yearTo` if you mean it within one year. - **A filter matching nothing is a normal, successful run,** not an error: `items` is empty, `advisoriesSaved` is `0`, and `totalAdvisoryLinks` in `RUN_SUMMARY` is still positive, confirming the archive itself was read fine. - **`yearFrom` after `yearTo` is fatal,** because it describes an impossible range rather than a legitimate empty result, the run exits non-zero rather than returning nothing silently. - **A revised advisory can appear as a second PDF for the same number** (a filename ending `Rev1` or similar). Both are returned as separate records sharing `advisoryNumber`/`year`; sort by `scrapedAt` or inspect `title` to find the correction. - **`title` is filename-derived, not OCR'd from the PDF.** Source-side typos (recurring: "Montly" for "Monthly") pass through unchanged; `category` classification tolerates the known ones. - **`downloadPdf` roughly doubles run time** (one HEAD request per advisory) and never fails the run, a dead link is recorded in `RUN_SUMMARY.failures` with `pdfVerified: false` on that record, while every other advisory is unaffected. - **No pagination to worry about.** The whole archive is server-rendered in one page load; there is no "next page" and no JavaScript rendering involved. ## Recipes **Freight-desk draft/booking alert.** Recent, keyword-scoped, no PDF verification needed for a fast poll. ```json { "yearFrom": 2026, "keywords": ["draft", "booking", "maximum authorized draft"] } ``` Run daily, diff on `advisoryNumber`/`year` against the previous run's dataset. **Monthly operations trend line.** Every summary since coverage began, category-filtered client-side. ```json { "keywords": ["Monthly Canal Operations Summary"] } ``` Sort the result by `year` then `uploadYearMonth` to build a time series. **Compliance archive with liveness proof.** Everything, verified. ```json { "downloadPdf": true } ``` Expect this to take noticeably longer than the default (one extra request per advisory, a few hundred advisories); raise the run's timeout if you self-host the call rather than using the Actor's own default. **Latest N, fast.** A quick check for anything new. ```json { "maxAdvisories": 10 } ```