You’re trying to quantify how congested Singapore (SGSIN) is right now, but you need a reliable, automatable signal you can plug into planning dashboards and alerts. By the end of this guide you’ll query FreightPulse’s IMF PortWatch–based congestion feed for SGSIN, parse the exact fields you need (congestion, ratio_vs_baseline, as_of), and ship a small integration you can cache and monitor.
What this dataset is (and isn’t)
FreightPulse’s port congestion endpoint is derived from IMF PortWatch and UN Global Platform AIS data. It is lagged by roughly 6–7 days and updates daily; it does not represent live AIS wait/anchorage times. The method compares the last 7 days’ average port calls to a 90-day baseline for the same port and classifies a simple congestion level.
Key points to internalize before you integrate:
- Envelope: responses return {"success": true, "data": {...}} with the values you’ll parse living inside data.
- Fields you will use: congestion (categorical), metrics.ratio_vs_baseline (float), and as_of (ISO date).
- Authentication: header X-API-Key: YOUR_API_KEY. Guests without a key are limited to 100 requests/day per IP.
- Scope: This is congestion relative to historical port call volumes, not a live measurement of vessel waiting times.
- Rates: Do not quote any freight-rates USD fare as today’s price in the same workflow; this endpoint is unrelated to rates.
Quick start: call the SGSIN endpoint
Use the official port code SGSIN. Provide your API key via the X-API-Key header.
cURL (copy/paste):
curl -H "X-API-Key: YOUR_API_KEY" "https://freightpulsehq.com/api/v1/port-congestion?port=SGSIN"
Official example response and field map
Below is the official sample response for SGSIN. Use it to validate your parser and to understand where the fields live in the envelope.
JSON:
{
"success": true,
"data": {
"results": [
{
"port": "Singapore",
"locode": "SG SIN",
"country": "Singapore",
"congestion": "moderate",
"as_of": "2026-10-16",
"metrics": {
"recent_avg_portcalls_per_day": 116.7,
"baseline_avg_portcalls_per_day": 117.4,
"ratio_vs_baseline": 0.99
}
},
{
"port": "Serangoon Harbor",
"locode": "SG SIN",
"country": "Singapore",
"congestion": "moderate",
"as_of": "2026-10-16",
"metrics": {
"recent_avg_portcalls_per_day": 7,
"baseline_avg_portcalls_per_day": 7.1,
"ratio_vs_baseline": 0.98
}
}
],
"global_summary": {
"ports_high_congestion": 0,
"ports_moderate": 2,
"ports_low": 0
},
"meta": {
"source": "IMF PortWatch (portwatch.imf.org) - UN Global Platform AIS data",
"data_lag_days": "~6-7 (daily updates, not real-time)",
"methodology": "congestion level is an approximation: last 7 days' avg port calls vs 90-day baseline for the same port. Does not measure vessel wait/anchorage time."
}
},
"meta": {
"request_id": "85921b91-a184-4edd-85e4-f7c9a668f284"
}
}
What you’ll actually use in code:
- results[].congestion: "low" | "moderate" | "high" (categorical level for the port).
- results[].metrics.ratio_vs_baseline: float; 1.0 means equal to the 90‑day baseline, below/above indicates relative easing/tightening.
- results[].as_of: ISO 8601 date (YYYY-MM-DD) for the last day included in the 7-day average window.
Notes for SGSIN specifically:
- The response may include multiple entries sharing the same UN/LOCODE (e.g., “Singapore” and “Serangoon Harbor”). If you only track the primary port complex, filter by port == "Singapore".
- The meta block reiterates that the signal is lagged ~6–7 days and is not real-time AIS wait data.
Python example: fetch, select Singapore, and read the key fields
The following script shows how to query SGSIN, filter for the primary “Singapore” entry, and extract congestion, ratio_vs_baseline, and as_of. It also demonstrates minimal validation and caching hints.
import os
import sys
import time
import json
import urllib.request
API_KEY = os.getenv("FREIGHTPULSE_API_KEY", "YOUR_API_KEY")
URL = "https://freightpulsehq.com/api/v1/port-congestion?port=SGSIN"
def fetch_sgsin():
req = urllib.request.Request(URL, headers={"X-API-Key": API_KEY})
with urllib.request.urlopen(req, timeout=20) as resp:
raw = resp.read().decode("utf-8")
return json.loads(raw)
def select_primary_singapore(results):
# Prefer the "Singapore" entry when multiple share the same LOCODE
primary = [r for r in results if r.get("port") == "Singapore"]
if primary:
return primary[0]
# Fallback: first result if naming differs
return results[0] if results else None
def main():
data = fetch_sgsin()
if not data.get("success"):
print("API indicated failure", file=sys.stderr)
sys.exit(1)
results = data.get("data", {}).get("results", [])
if not results:
print("No results for SGSIN", file=sys.stderr)
sys.exit(2)
entry = select_primary_singapore(results)
if not entry:
print("Could not identify primary Singapore record", file=sys.stderr)
sys.exit(3)
congestion = entry.get("congestion")
as_of = entry.get("as_of")
ratio = (entry.get("metrics") or {}).get("ratio_vs_baseline")
# Minimal sanity checks
if congestion not in ("low", "moderate", "high"):
print(f"Unexpected congestion level: {congestion}", file=sys.stderr)
if ratio is None:
print("Missing ratio_vs_baseline in metrics", file=sys.stderr)
print(json.dumps({
"port": entry.get("port"),
"locode": entry.get("locode"),
"congestion": congestion,
"ratio_vs_baseline": ratio,
"as_of": as_of
}, ensure_ascii=False))
# Caching hint: dataset is ~6–7 days lagged and updates daily.
# Cache for a day to avoid redundant calls (guest limit is 100/day/IP).
# For example, write to a file with a timestamp.
cache_path = "/tmp/sgsin_congestion.json"
with open(cache_path, "w") as f:
f.write(json.dumps({
"fetched_at_epoch": int(time.time()),
"payload": entry
}))
print(f"Cached to {cache_path}", file=sys.stderr)
if __name__ == "__main__":
main()
SGSIN workflow design: from HTTP to an actionable indicator
Build a single source of truth for “today’s” congestion reading (bearing in mind it’s a lagged, daily dataset). A practical pattern:
- Fetch: Call GET /api/v1/port-congestion?port=SGSIN with X-API-Key.
- Filter: Prefer the entry where port == "Singapore" when multiple records share the LOCODE.
- Persist: Store congestion, ratio_vs_baseline, and as_of in your time-series store with a unique key on as_of to avoid duplicates.
- Display: Show congestion (categorical), ratio_vs_baseline (to two decimals), and as_of as the “data date.”
- Alert: Trigger notifications only when as_of advances or when the categorical congestion changes.
Why ratio_vs_baseline matters: It is the quantitative backbone behind the qualitative label. If you’re tuning thresholds for alerts, use ratio deltas (e.g., 0.04 move w/w) rather than the label alone.
Operational details that save time
- Auth: Send X-API-Key: YOUR_API_KEY in the header. Without a key, guest usage is capped at 100 requests per day per IP.
- Envelope: The root has success and data. Your parser should check success before reading data.results.
- Dates: as_of is an ISO 8601 calendar date (YYYY-MM-DD). Treat it as the end date of the 7-day averaging window. If you aggregate, use the date as a string key and avoid assuming a timezone offset.
- Units: recent_avg_portcalls_per_day and baseline_avg_portcalls_per_day are in port calls per day (floats). ratio_vs_baseline is unitless.
- Updates and lag: Expect new values daily with a ~6–7 day lag to the current calendar date. A missing update on weekends or holidays is generally not a data error; check as_of and retry on the next run.
- Caching: Because data is not real-time, a 24-hour cache per port is appropriate for most dashboards and alerting systems.
- Multiple results per LOCODE: When a LOCODE covers a complex (e.g., “Singapore” and “Serangoon Harbor”), pin the record you care about and be explicit in filters.
- Error handling: If data.results is empty, do not backfill with another port. Surface a health check alert and retry with exponential backoff.
JavaScript (Node) alternative: tiny fetch helper
If you ship Node services, here is a concise fetch that reads the same fields and demonstrates defensive parsing.
import fetch from "node-fetch";
const API_KEY = process.env.FREIGHTPULSE_API_KEY || "YOUR_API_KEY";
const URL = "https://freightpulsehq.com/api/v1/port-congestion?port=SGSIN";
async function fetchSGSIN() {
const res = await fetch(URL, {
headers: { "X-API-Key": API_KEY },
timeout: 20000
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const body = await res.json();
if (!body.success) {
throw new Error("API indicated failure");
}
const results = (body.data && body.data.results) || [];
if (!results.length) {
throw new Error("No results for SGSIN");
}
const primary = results.find(r => r.port === "Singapore") || results[0];
const out = {
port: primary.port,
locode: primary.locode,
congestion: primary.congestion,
ratio_vs_baseline: primary.metrics?.ratio_vs_baseline,
as_of: primary.as_of
};
if (typeof out.ratio_vs_baseline !== "number") {
throw new Error("Missing ratio_vs_baseline");
}
return out;
}
fetchSGSIN()
.then(data => console.log(JSON.stringify(data)))
.catch(err => {
console.error(err.message);
process.exitCode = 1;
});
Testing and validation checklist for SGSIN
- Schema: Verify presence and types of success (bool), data (object), data.results (array), and for the chosen record: congestion (string), as_of (string date), metrics.ratio_vs_baseline (number).
- Deterministic selection: Lock selection to port == "Singapore" and assert locode starts with "SG". Log if the record changes.
- Idempotent storage: Use as_of as the id for upserts so a re-fetch on the same day doesn’t duplicate entries.
- Alert hygiene: Do not alert repeatedly when the API hasn’t advanced as_of. Gate alerts on as_of change or a categorical transition.
- Backoff: On HTTP 429 or temporary network errors, back off and retry later; remember the daily update cadence.
Embedding in planning and ETAs
Because the congestion signal is a 7‑day average vs a 90‑day baseline, pair it with your transit-time models carefully. A practical pattern is to apply small additive or multiplicative adjustments to ETAs when ratio_vs_baseline deviates materially from 1.0, but only for the port legs that are likely to be sensitive to port call throughput (e.g., berth availability proxy). Keep the adjustment bounded; avoid double-counting if you already consume AIS-based waiting signals elsewhere.
In UI components, keep the label and number together. Example: “Singapore: moderate (0.99, as of 2026‑10‑16)”. This prevents confusion about timing and makes audit logs clearer when operations ask “which data date was used?”.
Security, quotas, and change management
- Key handling: Inject X-API-Key from your secrets manager (do not hardcode in source). For local development, use the literal string "YOUR_API_KEY" until you’re ready to provision a real key.
- Guest access: 100 requests/day per IP is available without a key. That’s sufficient for nightly batch jobs during prototyping.
- Rollouts: Since the feed is daily and lagged, stage changes behind a feature flag and verify that as_of and ratio_vs_baseline parse as expected before widening deployment.
- Observability: Log the request_id from the meta block when present to speed up support diagnostics.
Troubleshooting SGSIN integrations
- Empty results: Confirm you’re passing port=SGSIN exactly. Avoid trailing spaces or different cases in the query parameter.
- Multiple entries: If your UI shows two entries for the same LOCODE, decide upfront whether to show both or only “Singapore.” In code, make the choice explicit.
- Stale as_of: This is normal within the 6–7 day lag window. Implement a dashboard badge explaining the data date.
- Unexpected congestion label: Cross-check ratio_vs_baseline to confirm it aligns with your expectations; the label is derived from that ratio per the methodology in data.meta.
FAQ
Q: Is this real-time AIS waiting time for vessels at anchor or berth?
A: No. It’s a statistical signal (7‑day average port calls vs 90‑day baseline) from IMF PortWatch and UN Global Platform AIS, lagged ~6–7 days.
Q: Which fields should I store for alerts?
A: congestion, metrics.ratio_vs_baseline, and as_of. Use as_of as the key for idempotent writes.
Q: How often should I poll?
A: Once per day is sufficient because the dataset updates daily and is lagged. Cache responses for 24 hours per port.
Q: How do I authenticate?
A: Send X-API-Key: YOUR_API_KEY. Guests without a key are limited to 100 requests/day per IP.
Q: Can I combine this with freight rate quotes?
A: Don’t present any USD freight-rate figure as “today’s price” based on this endpoint; it’s not a rates API.
Ready to integrate SGSIN congestion into your ops stack? Start with a free account and the complete endpoint specs in the docs: Register and read the Documentation.