| name | kpler |
| description | Use when fetching oil/gas trade flow data from Kpler. Covers authentication, trade queries, flow aggregations, entity search, vessel positions, and company fleet data. |
Kpler Trade Data API
Access oil and gas trade flows, vessel tracking, and company data from the Kpler terminal.
Prerequisites
- Kpler account with terminal access
- Store credentials in
.env:
KPLER_USERNAME=user@example.com
KPLER_PASSWORD=your_password
Setup
Copy the client module to your project:
cp .claude/skills/kpler/scripts/kpler_client.py scripts/
Install dependencies:
uv add httpx pyjwt python-dotenv
Quick start
import asyncio
from kpler_client import KplerClient
async def main():
async with KplerClient() as client:
# Search for a player
results = await client.search("shell", categories=["PLAYER"])
shell_id = results["players"][0]["entity"]["id"]
# Query their trades
trades = await client.query_trades(players=[shell_id], size=20)
print(f"Found {len(trades['result']['trades'])} trades")
asyncio.run(main())
Authentication
The client handles Auth0 OAuth automatically:
- Auto-login from
.envcredentials - Token storage in
.kpler_token,.kpler_refresh_token - Automatic refresh 5 minutes before expiry
- Retry on 401 with fresh token
Manual auth control:
client = KplerClient()
await client.login("user@example.com", "password") # Manual login
client.is_authenticated() # Check status
await client.logout() # Clear tokens
API methods
Search entities
Find players, vessels, installations, zones, and products by name:
results = await client.search(
text="gazprom",
categories=["PLAYER", "INSTALLATION"], # Optional filter
commodity_types=["lng"], # Optional: lng, oil, lpg, dry
)
# Results grouped by type
for player in results.get("players", []):
print(f"{player['entity']['name']} (ID: {player['entity']['id']})")
Categories: PLAYER, VESSEL, INSTALLATION, ZONE, PRODUCT
Query trades
Get individual trade records:
trades = await client.query_trades(
# Pagination
from_=0,
size=100,
# Filters (use IDs from search - strings work, converted to ints)
locations=[1234], # Zone/installation IDs
products=[5678], # Product IDs
players=[3836], # Company IDs
vessels=[9012], # Vessel IDs
# Trade status
statuses=["completed"], # ongoing, completed, cancelled
trade_types=["import", "export"],
# Options
with_forecasted=False,
)
for trade in trades["data"]:
origin = trade.get("portCallOrigin", {}).get("zone", {}).get("name", "Unknown")
dest = trade.get("portCallDestination", {}).get("zone", {}).get("name", "Unknown")
print(f"{origin} → {dest}")
Query flows (aggregated)
Get aggregated flow data with time series:
flows = await client.query_flows(
# Required
direction="export", # export, import
granularity="months", # years, months, weeks, days
# Date range
start_date="2024-01-01",
end_date="2024-12-31",
# Filters
locations=[1234],
products=[5678],
players=[3836],
# Split results by dimension
split_on="countries", # countries, ports, products, vessels, buyers, sellers
# Options
cumulative=False,
forecasted=False,
intra=False, # Include intra-region flows
)
# Response format: series by date with split values
for entry in flows["series"]:
year = entry["date"]
for dataset in entry.get("datasets", []):
for split in dataset.get("splitValues", []):
vol = split["values"]["volume"]
print(f"{year}: {split['name']} = {vol/1e6:.1f} Mt")
Split options: countries, ports, products, vessels, buyers, sellers, charterers
Get vessel positions
Raw AIS tracking data:
positions = await client.get_vessel_positions(
vessel_id=12345,
start_date="2024-01-01T00:00:00Z", # ISO 8601
end_date="2024-01-31T23:59:59Z",
limit=1000,
)
for pos in positions:
print(f"{pos['timestamp']}: {pos['lat']}, {pos['lon']}")
Get player fleet
Company fleet information:
fleet = await client.get_player_fleet(player_id=3836)
print(f"Company: {fleet['name']}")
print(f"Vessels owned: {len(fleet.get('ownedVessels', []))}")
print(f"Subsidiaries: {len(fleet.get('subsidiaries', []))}")
Query contracts
Long-term agreements and tenders:
contracts = await client.query_contracts(
types=["SPA", "LTA"], # Tender, SPA, LTA, TUA
players=[3836],
from_=0,
size=50,
)
ETL pattern for notebooks
Typical workflow for building a DuckDB database:
# scripts/fetch_kpler.py
import asyncio
import duckdb
from kpler_client import KplerClient
from dotenv import load_dotenv
load_dotenv()
async def fetch_data():
async with KplerClient() as client:
# Search for entities
russia = await client.search("russia", categories=["ZONE"])
russia_id = russia["zones"][0]["entity"]["id"]
# Get export flows
flows = await client.query_flows(
direction="export",
locations=[russia_id],
granularity="months",
start_date="2020-01-01",
end_date="2024-12-31",
split_on="countries",
)
return flows["result"]["series"]
def build_database(data):
con = duckdb.connect("data/data.duckdb")
con.execute("""
CREATE OR REPLACE TABLE flows (
date DATE,
destination VARCHAR,
volume_kt DOUBLE
)
""")
for series in data:
for point in series.get("data", []):
con.execute(
"INSERT INTO flows VALUES (?, ?, ?)",
[point["date"], series["name"], point["value"]]
)
con.close()
if __name__ == "__main__":
data = asyncio.run(fetch_data())
build_database(data)
Add to Makefile:
data:
uv run python scripts/fetch_kpler.py
Rate limits
The Kpler API has rate limits. Add delays for large queries:
import asyncio
for player_id in player_ids:
data = await client.query_trades(players=[player_id])
await asyncio.sleep(0.5) # Rate limit protection
Common entity IDs
Search to find current IDs, as these may change:
| Entity | Example search | Type | ID |
|---|---|---|---|
| Russia | russia |
ZONE | |
| China | china |
ZONE | |
| Shell | shell |
PLAYER | |
| TotalEnergies | total |
PLAYER | |
| Crude oil | crude |
PRODUCT | |
| LNG | lng |
PRODUCT | |
| F-76 Military Diesel | F-76 |
PRODUCT | 1484 |
| Jet JP5 | JP-5 |
PRODUCT | 1650 |
| Jet JP8 | JP-8 |
PRODUCT | 1652 |
| U.S. Navy | navy |
PLAYER | 4224 |
| MSC (Military Sealift Command) | MSC |
PLAYER | 3190 |
Tracking military fuel shipments
Military fuel shipments can be identified through three complementary approaches: by product grade, by destination installation, and by buyer. Combine these for comprehensive coverage, as not all military shipments are labelled with military-specific product grades.
Approach 1: Military-specific product grades
Kpler tracks these military fuel grades directly:
| Product | ID | Type | Parent | NATO code | Use |
|---|---|---|---|---|---|
| F-76 Military Diesel | 1484 | GRADE | Diesel (1394) | F-76 | Naval distillate fuel |
| Jet JP5 | 1650 | GRADE | Jet (1644) | F-44 | Naval aviation fuel (high flash point) |
| Jet JP8 | 1652 | GRADE | Jet (1644) | F-34 | Air force aviation fuel |
These sit within the product hierarchy: Commodities > Liquids > Clean > Clean Products > Middle Distillates > Gasoil/Diesel or Kero/Jet.
Note: Many military shipments are classified under broader categories (Jet A-1, Gasoil, Diesel, Marine Diesel) rather than military-specific grades. Cross-reference with destination and buyer for better identification.
Other relevant product IDs for broader searches:
| Product | ID | Type | Why relevant |
|---|---|---|---|
| Jet | 1644 | PRODUCT | Parent of all jet fuels |
| Jet A-1 | 1646 | GRADE | Standard military jet fuel in practice |
| Diesel | 1394 | PRODUCT | Parent of F-76 |
| Marine Diesel | 1806 | GRADE | Naval vessel fuel |
| Gasoil | 1510 | PRODUCT | Often used for military diesel |
| Kerosene | 1670 | PRODUCT | Heating/aviation overlap |
| Kero/Jet | 1672 | GROUP | Umbrella for all jet/kero |
| Gasoil/Diesel | 1530 | GROUP | Umbrella for all diesel/gasoil |
Approach 2: Military installations
These installations are explicitly military or serve as known military fuel depots. Use their IDs in the locations parameter for query_trades().
US Navy (Pacific):
| Installation | ID | Port (zone ID) | Type |
|---|---|---|---|
| US Navy CFAY Yokosuka | 3025 | Yokosuka (3028) | Clean Products Consumption |
| US Navy Fuel Storage Akasaki | 3036 | Sasebo (3535) | Oil Product Import Terminal |
| US Navy Fuel Storage Akasaki 2 | 3035 | Sasebo (3535) | Oil Product Import Terminal |
| US Navy Yokose Filling Station | 12024 | Sasebo (3535) | Multi-Purpose Import Terminal |
| US Navy Okinawa | 3214 | Okinawa (2530) | Clean Products Import Terminal |
| US Navy White Beach Port Facility | 11006 | Okinawa (2530) | Multi-Purpose Import Terminal |
| US Naval Diego Garcia | 3201 | Diego Garcia (3632) | Oil Products Consumption |
| Naval Air Key West | 9761 | Key West | Multi-Purpose Import Terminal |
| Shell Guam | 2162 | Guam Port (1578) | Clean Products Consumption |
UK MOD:
| Installation | ID | Port (zone ID) | Type |
|---|---|---|---|
| Loch Ewe | 10287 | Loch Ewe (113001) | Multi-Purpose Import Terminal |
| Loch Striven OFD | 3282 | Ardyne Point | Oil Product Import Terminal |
| Thanckes Oil Fuel Depot | 11723 | Thanckes | Multi-Purpose Import Terminal |
| Gosport | 10670 | Portsmouth UK (112940) | Multi-Purpose Import Terminal |
| Faslane Fuel Depot | 10954 | Faslane | Multi-Purpose Import Terminal |
| Devonport | 6193 | Devonport (6241) | Oil Product Import Terminal |
NATO / Allied:
| Installation | ID | Port (zone ID) | Type |
|---|---|---|---|
| NATO Souda | 3090 | Souda Port (3553) | Oil Import Terminal |
| Rota Base | 3236 | Cadiz | Oil Product Import Terminal |
| Brest Base sous-marine | 13189 | Brest | Multi-Purpose Import Terminal |
| Praia da Vitoria | 4368 | Cabo Da Praia | LPG Consumption |
| Nordic Storage Campbeltown | 3259 | Campbeltown Port | Clean Products Consumption |
Approach 3: Military buyers/players
| Player | ID | Notes |
|---|---|---|
| U.S. Navy | 4224 | Primary US military buyer |
| MSC (Military Sealift Command) | 3190 | US Navy logistics arm |
| Defence International | 2312 |
Observed supply patterns
From trade data analysis, these patterns emerge:
US Pacific operations: S-Oil Onsan (Ulsan), SK Ulsan, GS Caltex Yeosu, and KPX Global in South Korea supply F-76 and JP-5 to US Navy bases across the Pacific (Yokosuka, Sasebo, Okinawa, Guam, Diego Garcia, Subic Bay).
UK/NATO European operations: MOH Corinth Refinery (Greece) and Gibraltar-San Roque Refinery supply F-76 and JP-5 to UK depots (Thanckes, Loch Striven, Loch Ewe) and NATO bases (Souda, Rota). Loch Ewe also receives Marine Diesel and Gasoil from NW Europe (Antwerp, Rotterdam, Skagen).
French Navy: Le Havre (Pointe du Hoc, ExxonMobil Port Jerome), Amsterdam (Exolum), Barcelona, and Antwerp supply F-76 to Brest Base sous-marine.
JP-8 to Israel: Valero Bill E (Corpus Christi) and Valero Port Arthur are recurring origins for JP-8 shipments to Ashkelon.
Example: fetch all military fuel trades
MILITARY_PRODUCT_IDS = [1484, 1650, 1652] # F-76, JP-5, JP-8
MILITARY_INSTALLATION_IDS = [
3025, 3036, 3035, 12024, 3214, 11006, 3201, 9761, # US Navy
10287, 3282, 11723, 10670, 10954, 6193, # UK MOD
3090, 3236, 13189, 4368, 3259, # NATO/Allied
]
MILITARY_PLAYER_IDS = [4224, 3190] # U.S. Navy, MSC
async def fetch_military_trades(client):
"""Fetch trades identifiable as military by product, destination, or buyer."""
all_trades = []
# By military product grades
for product_id in MILITARY_PRODUCT_IDS:
trades = await client.query_trades(products=[product_id], size=200)
all_trades.extend(trades.get("data", []))
await asyncio.sleep(0.5)
# By military installations (catches shipments with generic product labels)
for install_id in MILITARY_INSTALLATION_IDS:
trades = await client.query_trades(
to_locations=[{"id": install_id, "resourceType": "installation"}],
size=200,
)
all_trades.extend(trades.get("data", []))
await asyncio.sleep(0.5)
# By military buyer
for player_id in MILITARY_PLAYER_IDS:
trades = await client.query_trades(players=[player_id], size=200)
all_trades.extend(trades.get("data", []))
await asyncio.sleep(0.5)
# Deduplicate by trade ID
seen = set()
unique = []
for t in all_trades:
origin = t.get("forecastPortCallOrigin") or {}
tid = origin.get("id")
if tid and tid not in seen:
seen.add(tid)
unique.append(t)
return unique
Example: aggregate military flows over time
async def military_flow_timeseries(client, start="2020-01-01", end="2025-12-31"):
"""Get monthly military fuel flows split by destination."""
flows = await client.query_flows(
direction="import",
granularity="months",
start_date=start,
end_date=end,
products=[1484, 1650, 1652], # F-76, JP-5, JP-8
split_on="destination ports",
number_of_splits=20,
)
return flows
Limitations
- Many military shipments use generic product labels (Gasoil, Diesel, Jet A-1) so product-based filtering alone will miss a significant share
- Installation-based filtering catches more but may include some commercial activity at dual-use ports
- Ship-to-ship transfers and underway replenishment (UNREP) operations may not appear in trade data
- Faslane installation (10954) returned no trades despite existing in search; may require querying via the port zone instead
Troubleshooting
401 Unauthorized: Credentials invalid or expired. Check .env file.
Empty results: Verify entity IDs are correct. Use search() to find current IDs.
Token errors: Delete .kpler_token* files and re-authenticate.
Rate limited: Add delays between requests. The API may temporarily block rapid queries.
API reference
Base URL: https://terminal.kpler.com/api
| Endpoint | Method | Purpose |
|---|---|---|
/trades |
POST | Query trade records |
/flows |
POST | Aggregate flow data |
/contracts |
GET | Contract data |
/players/{id} |
GET | Company fleet info |
/vessels/{id}/positions |
GET | AIS positions |
/graphql/ |
POST | Entity search |