# Hong Kong Port Throughput Scraper: Container & Cargo (`arman-bd/hong-kong-port-throughput-scraper`) Actor

Scrape Hong Kong's official monthly port container throughput, split by Kwai Tsing terminals vs mid-stream, direction and laden/empty, plus the companion seaborne and river cargo tonnage series.

- **URL**: https://apify.com/arman-bd/hong-kong-port-throughput-scraper.md
- **Developed by:** [Arman Hossain](https://apify.com/arman-bd) (community)
- **Categories:** Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 throughput observation scrapeds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Hong Kong Port Throughput Scraper: Container, Seaborne & River Cargo

![Hong Kong Port Throughput: Monthly container, seaborne and river cargo series with the year-on-year change already paired](https://api.apify.com/v2/key-value-stores/ZQOcNAOHrIgTacAmy/records/hong-kong-port-throughput-scraper.jpg)

Pulls Hong Kong's official monthly port statistics: container throughput split by Kwai Tsing terminals vs mid-stream handling, inward vs outward, and laden vs empty, plus the companion seaborne and river cargo tonnage series. The source publishes each level figure alongside its own year-on-year percentage change as two separate rows; this Actor pivots them back together so one dataset row carries both the physical figure and its year-on-year move.

The source is the Census and Statistics Department of the Hong Kong SAR, published as structured JSON. There's no browser, no proxy and no login required to use this Actor.

**Agent skill: [SKILL.md](https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/hong-kong-port-throughput-scraper.md)**

```
https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/hong-kong-port-throughput-scraper.md
```

### What you get

| Field | What it holds |
|---|---|
| `tableId` | The source table id, e.g. `410-55294` |
| `series` | `container-throughput`, `seaborne-cargo`, `river-cargo`, or a title-derived label for any other table id you pass in |
| `tableTitle` | The table's own title, in the language you requested |
| `measureCode` | The source's internal measure code (e.g. `PORT_CONTAINER_TP`) |
| `locationCode`, `location` | Kwai Tsing Container Terminals vs "other than Kwai Tsing" (mid-stream), or `null` for the cargo tables, which don't carry this split |
| `cargoModeCode`, `cargoMode` | `Seaborne` or `River`, or `null` for the container table, which doesn't carry this split |
| `directionCode`, `direction` | Inward, outward, or the aggregate total |
| `containerTypeCode`, `containerType` | Laden, empty, or the aggregate total (container table only, `null` elsewhere) |
| `shipmentTypeCode`, `shipmentType` | Direct shipment, transhipment, or the aggregate total (cargo tables only, `null` elsewhere) |
| `freq` | `M` (monthly) or `Y` (annual) |
| `period` | `YYYYMM` for a monthly row, `YYYY` for an annual row |
| `unit` | The physical unit of `figure`, e.g. `('000 TEUs)` or `('000 tonnes)` |
| `figure` | The level value, in the unit above |
| `figureFlag` | The source's own flag on the level figure: `p` (provisional), `[§3]` (rounds to a negligible change) or `N.A.` |
| `yoyChangePct` | The year-on-year percentage change for the same dimension combination and period |
| `yoyChangeFlag` | The source's flag on the year-on-year figure, same vocabulary as `figureFlag` |
| `provisional` | `true` if either the level or the year-on-year figure is still provisional |
| `scrapedAt` | Run timestamp |

`RUN_SUMMARY` in the key-value store holds per-table counts, the filters you used, each table's own title/source/footnotes, and any table id that failed.

### Use cases

- **Kwai Tsing vs mid-stream tracking.** Watch how much of Hong Kong's container volume is handled at the main Kwai Tsing terminals versus mid-stream and river operations, a split most free trade-statistics sources don't break out.
- **Port competitiveness modelling.** Feed monthly TEU throughput and its year-on-year move into a model tracking Hong Kong against Shenzhen and Guangzhou.
- **Laden/empty balance.** Compare laden vs empty container flows by direction to read import/export imbalance.
- **Cargo mode mix.** Compare the seaborne and river cargo tonnage series to see how much of total throughput moves by river trade.
- **Provisional-to-final tracking.** Re-run monthly and diff on `figureFlag`/`yoyChangeFlag` to see when a provisional figure firms up.

### Quick start

The default run, monthly data across all three built-in tables, capped at 150 rows per table:

```json
{}
```

Just container throughput, a wider monthly window:

```json
{
 "tableIds": ["410-55294"],
 "frequency": "M",
 "maxRecordsPerTable": 500
}
```

Annual container figures for a specific stretch of years:

```json
{
 "tableIds": ["410-55294"],
 "frequency": "Y",
 "periodFrom": "2015",
 "periodTo": "2025"
}
```

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `tableIds` | array | all three built-in ids | `410-55294` (container throughput), `410-55111A` (seaborne cargo) and `410-55112A` (river cargo) are documented by name. Any other table id published in the same `id=NNN-NNNNN` shape is still read, with reduced dimension detail if it uses dimension names this Actor doesn't already recognise. |
| `frequency` | string | `M` | `M` for monthly, `Y` for annual, `ALL` for both. |
| `periodFrom` | string | `""` | Earliest period to keep, as `YYYY-MM`, `YYYYMM` or `YYYY`. Applied after fetching. Empty means the full history. |
| `periodTo` | string | `""` | Latest period to keep, same format. Empty means the most recent published period. |
| `maxRecordsPerTable` | integer | `150` | Cap on saved rows per table, most recent periods first. The source has no server-side pagination or date filter, so every run downloads each table's complete history regardless of this setting; the cap only bounds what gets saved and charged. |
| `lang` | string | `en` | `en`, `tc` (Traditional Chinese) or `sc` (Simplified Chinese). Only affects `tableTitle` and the `location`/`cargoMode`/`direction`/`containerType`/`shipmentType` labels; the `*Code` fields stay the same in every language. |

Nothing is required. Running with no input returns recent monthly data across all three built-in tables.

### Output example

A record from `410-55294`, showing the Kwai Tsing vs mid-stream split:

```json
{
 "tableId": "410-55294",
 "series": "container-throughput",
 "tableTitle": "Port container throughput by main cargo handling location",
 "measureCode": "PORT_CONTAINER_TP",
 "locationCode": "NCT",
 "location": "Other than Kwai Tsing Container Terminals",
 "cargoModeCode": null,
 "cargoMode": null,
 "directionCode": "Out",
 "direction": "Outward",
 "containerTypeCode": "Laden",
 "containerType": "Laden",
 "shipmentTypeCode": null,
 "shipmentType": null,
 "freq": "M",
 "period": "202605",
 "unit": "('000 TEUs)",
 "figure": 98,
 "figureFlag": "p",
 "yoyChangePct": -4.4,
 "yoyChangeFlag": "p",
 "provisional": true,
 "scrapedAt": "2026-08-21T22:49:51.330Z"
}
```

A record from `410-55111A`, showing the cargo-mode dimension the container table doesn't have:

```json
{
 "tableId": "410-55111A",
 "series": "seaborne-cargo",
 "tableTitle": "Seaborne cargo throughput by month",
 "cargoModeCode": "S",
 "cargoMode": "Seaborne",
 "locationCode": null,
 "location": null,
 "shipmentTypeCode": "TS",
 "shipmentType": "Transhipment",
 "freq": "M",
 "period": "202605",
 "unit": "('000 tonnes)"
}
```

### Table ids and what they return

Each id maps to one JSON table on the source, keyed by dimension code plus a description pair, a frequency, a period and two rows per combination: a level figure and a year-on-year percentage change, which this Actor merges into one output row.

| Table id | Series | Dimensions besides direction/period |
|---|---|---|
| `410-55294` | `container-throughput` | Location (Kwai Tsing terminals vs mid-stream), container type (laden/empty) |
| `410-55111A` | `seaborne-cargo` | Shipment type (direct shipment/transhipment) |
| `410-55112A` | `river-cargo` | Shipment type (direct shipment/transhipment) |

### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/arman-bd~hong-kong-port-throughput-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "tableIds": ["410-55294"],
 "frequency": "M",
 "maxRecordsPerTable": 200
 }'
```

### JavaScript example

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('arman-bd/hong-kong-port-throughput-scraper').call({
 tableIds: ['410-55294', '410-55111A', '410-55112A'],
 frequency: 'M',
 maxRecordsPerTable: 300,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const r of items) {
 console.log(`${r.period} ${r.series} ${r.location ?? r.cargoMode ?? 'total'}: ${r.figure} ${r.unit} (${r.yoyChangePct}% YoY)`);
}
```

### Notes

- The source has no date-range or pagination parameter: every run downloads each requested table's complete history (roughly 5-7 MB per table), and `frequency`, `periodFrom`, `periodTo` and `maxRecordsPerTable` all filter after the fact. The download size per table is fixed regardless of how narrow these filters are.
- A level figure and its year-on-year change arrive from the source as two separate rows sharing the same dimensions, frequency and period. This Actor pairs them into one output row. Very early periods can be missing one side (year-on-year needs a prior-year baseline that doesn't exist for the first 12 months of a series), in which case the missing side is `null`.
- `figureFlag`/`yoyChangeFlag` carry the source's own footnote codes verbatim: `p` for provisional, `[§3]` for a change under 0.05%, `N.A.` for not available. `provisional` is `true` whenever either flag contains `p`.
- `location` and `cargoMode` are mutually exclusive across the three built-in tables: the container table has a location split and no cargo-mode split, the cargo tables have a cargo-mode split and no location split. Every record still carries both fields, `null` on whichever doesn't apply, so combining tables in one run keeps a single dataset shape.
- One bad table id is recorded in `RUN_SUMMARY.failures` while the rest of the run continues. The run only errors out if every requested table id failed.
- Requests are retried up to three times with linear backoff on a transient error. A table id or language code the source rejects is a permanent failure for that table and is not retried.

### FAQ

**Do I need an account with the source?** No. You supply no credentials.

**Can I get quarterly figures?** No. The source publishes monthly and annual observations only; `frequency` accepts `M`, `Y` or `ALL`.

**Why is `maxRecordsPerTable` needed if I only want recent months?** The source's API always returns a table's entire history in one response; there is no way to ask it for less. `maxRecordsPerTable` (and `periodFrom`/`periodTo`) filter what gets saved, not what gets fetched.

**Can I read a table id this Actor doesn't already know about?** Yes, pass it in `tableIds`. It's read through the same endpoint and pivoted the same way; you'll get generic `series`/dimension labelling if it uses dimension names outside the ones this Actor maps by name.

**How current is the data?** Monthly figures typically land two to three months after the reference month, and the latest month or two is marked provisional (`figureFlag`/`yoyChangeFlag` of `p`) until the source finalises it.

**Can I plug it into something else?** Yes. Apify API, the client libraries, webhooks, scheduled runs, dataset exports to JSON, CSV or Excel, or MCP. The output is structured JSON.

# Actor input Schema

## `tableIds` (type: `array`):

Which CensTatD table(s) to pull. The three built in are '410-55294' (container throughput by Kwai Tsing vs mid-stream, direction, laden/empty), '410-55111A' (seaborne cargo throughput) and '410-55112A' (river cargo throughput). Any other table id published in the same 'id=NNN-NNNNN' shape can be given too; it is read the same way, with reduced dimension detail if it uses dimension names this Actor does not already recognise. Leave empty for all three built-in tables.

## `frequency` (type: `string`):

Which observation frequency to keep. The source publishes both an annual and a monthly series in the same table.

## `periodFrom` (type: `string`):

Earliest period to keep, as 'YYYY-MM' or 'YYYYMM' for a monthly row, or 'YYYY' for an annual row. Applied after fetching, since the source has no date-range parameter of its own. Leave empty for the full history (monthly data starts in 1999, annual in 1997).

## `periodTo` (type: `string`):

Latest period to keep, in the same format as 'Period from'. Leave empty for the most recent period the source has published.

## `maxRecordsPerTable` (type: `integer`):

Cap on saved rows per table, applied after the period/frequency filters, keeping the most recent periods first. The source has no server-side pagination or date filter, so every run downloads each table's complete history (roughly 6-7 MB) and this cap is what bounds what gets saved and charged.

## `lang` (type: `string`):

Language for the table title and dimension labels (the dimension codes like 'CT' or 'In' stay the same in every language).

## Actor input object example

```json
{
  "tableIds": [
    "410-55294"
  ],
  "frequency": "M",
  "periodFrom": "2024-01",
  "periodTo": "2026-05",
  "maxRecordsPerTable": 150,
  "lang": "en"
}
```

# Actor output Schema

## `items` (type: `string`):

Every record the run produced.

## `runsummary` (type: `string`):

The RUN\_SUMMARY record from the run's key-value store.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "tableIds": [
        "410-55294",
        "410-55111A",
        "410-55112A"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("arman-bd/hong-kong-port-throughput-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "tableIds": [
        "410-55294",
        "410-55111A",
        "410-55112A",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("arman-bd/hong-kong-port-throughput-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "tableIds": [
    "410-55294",
    "410-55111A",
    "410-55112A"
  ]
}' |
apify call arman-bd/hong-kong-port-throughput-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arman-bd/hong-kong-port-throughput-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/KKChbJvuR5nv6UZaK/builds/nnsEmKrnNVqBrhsEU/openapi.json
