# Singapore Port Statistics Scraper: MPA Throughput & Vessel Data (`arman-bd/singapore-port-statistics-scraper`) Actor

Scrape monthly container throughput, vessel arrivals and shipping tonnage for the Port of Singapore from MPA's open data on data.gov.sg, back to 1995.

- **URL**: https://apify.com/arman-bd/singapore-port-statistics-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 port statistic 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

## Singapore Port Statistics Scraper: MPA Throughput & Vessel Data

![Singapore Port Statistics: Monthly container throughput, vessel arrivals and gross tonnage from the official open data API](https://api.apify.com/v2/key-value-stores/ZQOcNAOHrIgTacAmy/records/singapore-port-statistics-scraper.jpg)

Pulls the Maritime and Port Authority of Singapore's monthly port statistics from Singapore's official open data portal: container throughput, vessel arrivals (total and broken down by vessel type), and Singapore Registry of Ships tonnage. The series run monthly back to 1995 for the Port of Singapore, the world's second-largest container port and its largest bunkering port.

This Actor needs no browser, no proxy and no login to run. Point it at one or more of the four built-in series, or at any other data.gov.sg CKAN table by resource id, and it returns clean, typed rows.

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

```
https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/singapore-port-statistics-scraper.md
```

### What you get

| Field | What it holds |
|---|---|
| `dataset` | Which series the row came from: `container_throughput`, `vessel_arrivals_total`, `vessel_arrivals_by_type`, `registered_vessels_tonnage`, or a custom resource id |
| `datasetLabel` | Human-readable name for the series (`Custom` for a caller-supplied resource id) |
| `resourceId` | The underlying data.gov.sg resource id this row came from |
| `id` | The source's own row id, stable across runs |
| `month` | The reporting month, `YYYY-MM` |
| `vesselType` | Vessel category (e.g. `Container`, `Tanker`, `Tug`); only set on `vessel_arrivals_by_type` rows, otherwise `null` |
| `containerThroughputThousandTeu` | Container throughput for the month, in thousand TEUs; only set on `container_throughput` rows |
| `numberOfVessels` | Vessel count for the month; set on every series except `container_throughput` |
| `grossTonnageThousandGt` | Gross tonnage for the month, in thousand GT; set on every series except `container_throughput` |
| `scrapedAt` | Run timestamp |

`RUN_SUMMARY` in the key-value store holds per-dataset row counts, the totals available at the source, the filters you used, and any dataset that failed to fetch.

### Use cases

- **Trade and transhipment analysis.** Container throughput is a standard proxy for Asia-Pacific trade flow and transhipment activity.
- **Port-equity and comparables research.** Feed PSA Singapore volumes into a comparables model against other major container ports.
- **Bunker and commodity desks.** Vessel arrival counts and gross tonnage track activity at the world's largest bunkering port.
- **Macro dashboards.** A monthly, decades-long series with no gaps back to 1995 for trade-cycle and shipping-cycle tracking.
- **Fleet and registry research.** Singapore Registry of Ships counts and tonnage for flag-state and maritime-registry studies.

### Quick start

Every built-in series, full history:

```json
{}
```

Just container throughput, capped to the most recent 24 months:

```json
{
 "datasets": ["container_throughput"],
 "maxRecordsPerDataset": 24
}
```

A specific window across two series:

```json
{
 "datasets": ["container_throughput", "vessel_arrivals_total"],
 "fromMonth": "2015-01",
 "toMonth": "2024-12"
}
```

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `datasets` | array | all four | Which built-in series to pull: `container_throughput`, `vessel_arrivals_total`, `vessel_arrivals_by_type`, `registered_vessels_tonnage`. An empty list means all four. |
| `resourceIds` | array | `[]` | Any other data.gov.sg CKAN resource id (the `d_...` string from a dataset's API panel), read the same way and labelled `Custom`. |
| `fromMonth` | string | none | Earliest month to keep, `YYYY-MM`. Applied after fetching, since the source has no date-range parameter. |
| `toMonth` | string | none | Latest month to keep, `YYYY-MM`. |
| `maxRecordsPerDataset` | integer | `0` (no limit) | Cap rows saved per dataset, keeping the most recent months first when the cap is smaller than the full series. |

Nothing is required. Running with no input returns the full history of all four series, about 4,100 rows in total.

### Output example

A container throughput record:

```json
{
 "dataset": "container_throughput",
 "datasetLabel": "Container throughput",
 "resourceId": "d_da030f7028200d19ffcbe4a2d71af39c",
 "id": 377,
 "month": "2026-05",
 "vesselType": null,
 "containerThroughputThousandTeu": 3942.74,
 "numberOfVessels": null,
 "grossTonnageThousandGt": null,
 "scrapedAt": "2026-08-21T22:47:54.897Z"
}
```

A vessel-arrivals-by-type record, same shape, different fields populated:

```json
{
 "dataset": "vessel_arrivals_by_type",
 "datasetLabel": "Vessel arrivals, by vessel type",
 "resourceId": "d_8f264219109e61fffa87ac64dd5a9a65",
 "id": 3016,
 "month": "2026-05",
 "vesselType": "Miscellaneous",
 "containerThroughputThousandTeu": null,
 "numberOfVessels": 883,
 "grossTonnageThousandGt": 13723.56,
 "scrapedAt": "2026-08-21T22:47:54.897Z"
}
```

### Reading a dataset key

| `datasets` value | Series |
|---|---|
| `container_throughput` | Monthly container throughput, thousand TEUs |
| `vessel_arrivals_total` | Monthly vessel arrivals: count and total gross tonnage |
| `vessel_arrivals_by_type` | The same arrivals, broken down into several rows per month by vessel type |
| `registered_vessels_tonnage` | Singapore Registry of Ships: monthly registered ship count and gross tonnage |

`vessel_arrivals_by_type` carries roughly 8 rows per month (one per vessel category), so its total row count is much larger than the other three series, which are one row per month.

### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/arman-bd~singapore-port-statistics-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "datasets": ["container_throughput"],
 "fromMonth": "2015-01"
 }'
```

### JavaScript example

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

const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('arman-bd/singapore-port-statistics-scraper').call({
 datasets: ['container_throughput', 'vessel_arrivals_total'],
 fromMonth: '2020-01',
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const row of items) console.log(row.month, row.dataset, row.containerThroughputThousandTeu ?? row.numberOfVessels);
```

### Notes

- Numbers arrive from the source as strings and are cast to numbers here; a value the Actor cannot parse comes through as `null` rather than a bad number.
- `container_throughput`, `vessel_arrivals_total` and `registered_vessels_tonnage` are one row per month. `vessel_arrivals_by_type` is several rows per month, one per vessel category, so `maxRecordsPerDataset` caps rows, not months, on that series.
- Datasets are read one at a time with a short delay between them, and a failed fetch is retried automatically before it is recorded as a failure.
- `month` has no timezone; it is the calendar month MPA published the figures for, not a timestamp.
- The latest month available typically lags the calendar by 2-3 months, since MPA needs time to finalise the figures.
- One bad dataset key or resource id is recorded in `RUN_SUMMARY.failures` while the rest of the run continues. The run only fails outright if every requested dataset failed.

### FAQ

**Do I need an API key?** No. The Actor supplies no credentials and needs none.

**Can I get daily or weekly figures?** No. MPA publishes these series monthly; there is no finer granularity at the source.

**Why is `containerThroughputThousandTeu` null on most rows?** Each series has its own metric. Container throughput only appears on `container_throughput` rows; vessel counts and tonnage appear on the other three.

**What does "thousand TEU" and "thousand GT" mean?** Thousand twenty-foot equivalent units (the standard container-volume measure) and thousand gross tons (a vessel-size measure), which is how MPA reports these figures.

**Can I pull a table this Actor doesn't already know about?** Yes, if it is a data.gov.sg CKAN datastore resource: pass its resource id in `resourceIds`. Field names vary by table, so only fields matching the four built-in series' conventions (`month`, `container_throughput`, `number_of_vessels`, `gross_tonnage`, `vessel_type`) are recognised; anything else comes through as `null`.

**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

## `datasets` (type: `array`):

Which built-in MPA series to pull. Leave empty for all four.

## `resourceIds` (type: `array`):

Optional: any other data.gov.sg CKAN datastore resource id (the 'd\_...' string shown in a dataset's API panel). Read the same way as the built-in datasets and labelled 'custom' in the output, with the id itself as the dataset key. Use this to point the Actor at a data.gov.sg table it doesn't already know about, alongside or instead of 'datasets'.

## `fromMonth` (type: `string`):

Earliest month to keep, as YYYY-MM (e.g. '2015-01'). Applied after fetching, since the source has no date-range parameter of its own. Leave empty for the full history, back to 1995.

## `toMonth` (type: `string`):

Latest month to keep, as YYYY-MM. Leave empty for the most recent month MPA has published, which typically runs 2-3 months behind the calendar.

## `maxRecordsPerDataset` (type: `integer`):

Cap rows saved per dataset, keeping the most recent months when the cap is smaller than the full series. 0 for no limit. The full history is small: about 377 rows for each of the three monthly-total series, and roughly 3,000 for the by-vessel-type breakdown, which carries several rows per month.

## Actor input object example

```json
{
  "datasets": [
    "container_throughput"
  ],
  "resourceIds": [
    "d_1a662938ab61d9528c9e2ea1a5a95dc9"
  ],
  "fromMonth": "2015-01",
  "toMonth": "2025-12",
  "maxRecordsPerDataset": 0
}
```

# 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 = {
    "datasets": [
        "container_throughput",
        "vessel_arrivals_total",
        "vessel_arrivals_by_type",
        "registered_vessels_tonnage"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("arman-bd/singapore-port-statistics-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 = { "datasets": [
        "container_throughput",
        "vessel_arrivals_total",
        "vessel_arrivals_by_type",
        "registered_vessels_tonnage",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("arman-bd/singapore-port-statistics-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 '{
  "datasets": [
    "container_throughput",
    "vessel_arrivals_total",
    "vessel_arrivals_by_type",
    "registered_vessels_tonnage"
  ]
}' |
apify call arman-bd/singapore-port-statistics-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arman-bd/singapore-port-statistics-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/1t1FY5L4HvAsJFSz2/builds/KvRAbaXaoDHKho0dw/openapi.json
