# PortSignal API — Full Reference > Maritime lineup data marketplace REST API > Base URL: https://api.port-signal.com ## Authentication All API requests require the `X-API-Key` header. Obtain your key from the Buyer Dashboard at /buyer/dashboard. Key format: `ps_` followed by 32 hexadecimal characters. Example: ``` X-API-Key: ps_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 ``` --- ## GET /v1/lineups Returns lineup records from feeds the authenticated buyer is subscribed to (active or trial). ### Query Parameters | Parameter | Type | Default | Description | |-----------|---------|---------|-------------| | source | string | "all" | Feed UUID to filter by a specific source, or "all" | | port | string | — | UN/LOCODE to filter by port (e.g., FRLEH, BRSSZ) | | country | string | — | Country name to filter by, case-insensitive (e.g., France, Brazil) | | port_name | string | — | Port name to filter by, case-insensitive partial match (e.g., Rouen) | | from | string | — | Start date filter (YYYY-MM-DD), applies to ETA/ETB/ETD | | to | string | — | End date filter (YYYY-MM-DD), applies to ETA/ETB/ETD | | latest | boolean | false | If true, return only the most recent record per vessel+port | | commodity | string | — | Comma-separated list of commodity values. Each value may be a normalized leaf, group, or subgroup name (e.g. "Crude,Containers,VLCC dirty"). Case-insensitive; matched against `commodity_normalized`, `commodity_normalized_group`, and `commodity_normalized_subgroup`. | | limit | integer | 100 | Page size (min: 1, max: 500) | | cursor | string | — | Opaque pagination cursor from previous response | ### Response Format ```json { "data": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "source_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890", "source_name": "Rouen Port Agency", "port_unlocode": "FRLEH", "port_name": "Rouen", "country_name": "France", "berth_terminal": "Terminal 4", "vessel_imo": "9876543", "vessel_name": "TING JIANG", "vessel_type": "Bulk Carrier", "dwt": 82000, "operation": "load", "commodity": "Wheat", "quantity": 45000, "quantity_unit": "MT", "eta": "2026-04-10T08:00:00.000Z", "etb": "2026-04-11T06:00:00.000Z", "etd": "2026-04-13T18:00:00.000Z", "combined_date": "2026-04-10T08:00:00.000Z", "vessel_status": "expected", "shipper": "Cargill SA", "receiver": "COFCO International", "charterer": "Bunge", "operator": "Pacific Basin", "ship_agent": "CMA Ships", "updated_at": "2026-04-09T12:00:00.000Z" } ], "next_cursor": "uuid-of-last-record-or-null" } ``` ### Response Fields | Field | Type | Description | |----------------|--------|-------------| | id | string | Unique record identifier (UUID) | | source_id | string | Source feed identifier (UUID) | | source_name | string | Source display name (or anonymous ID) | | port_unlocode | string | Port UN/LOCODE | | port_name | string | Port name | | country_name | string | Full country name | | berth_terminal | string | Berth or terminal name | | vessel_imo | string | IMO number | | vessel_name | string | Vessel name | | vessel_type | string | Bulk Carrier, Tanker, Container, General Cargo, LNG Carrier, LPG Carrier | | dwt | int | Deadweight tonnage | | operation | string | load, disch, bunker, husbandry | | commodity | string | Commodity name as captured from the source (scraped or AI-classified) | | commodity_normalized | string | Signal normalized leaf commodity (nullable until resolved) | | commodity_normalized_group | string | Signal normalized commodity group / L2 (nullable until resolved) | | commodity_normalized_subgroup | string | Signal normalized commodity subgroup / L1 (nullable until resolved) | | quantity | number | Quantity in specified unit | | quantity_unit | string | Unit of measurement (MT, BBL, Units) | | eta | date | Estimated time of arrival (ISO 8601) | | etb | date | Estimated time of berthing (ISO 8601) | | etd | date | Estimated time of departure (ISO 8601) | | combined_date | date | Earliest date among ETA, ETB, ETD (ISO 8601) | | vessel_status | string | expected, anchored, berthed, departed | | shipper | string | Shipper company name | | receiver | string | Receiver company name | | charterer | string | Charterer company name | | operator | string | Vessel operator company name | | ship_agent | string | Ship agent company name | | updated_at | date | Last record update timestamp (ISO 8601) | --- ## GET /v1/sources Returns the list of feeds the buyer is subscribed to. ### Response Format ```json { "sources": [ { "id": "feed-uuid", "name": "Source Name", "port_name": "Rouen", "port_unlocode": "FRLEH", "frequency": "weekly", "feed_type": "subscription", "coverage_rating": 0.85, "commodity_rating": 0.72, "commercial_rating": 0.60 } ] } ``` --- ## GET /v1/schema/:sourceId Returns the field schema for a specific source feed. ### Path Parameters | Parameter | Type | Description | |-----------|--------|-------------| | sourceId | string | Feed UUID | --- ## Pagination The API uses cursor-based pagination: 1. First request: omit `cursor` parameter 2. Response includes `next_cursor` (string or null) 3. Pass `next_cursor` as `cursor` in the next request 4. When `next_cursor` is null, all records have been retrieved --- ## Rate Limits - 100 requests per minute per API key - Response headers: `X-RateLimit-Remaining` - On limit exceeded: HTTP 429 with `Retry-After: 60` header --- ## Error Codes | Code | HTTP Status | Description | |------------------|-------------|-------------| | UNAUTHORIZED | 401 | Invalid or missing API key | | FORBIDDEN | 403 | API key valid but access denied | | NOT_FOUND | 404 | Resource not found | | VALIDATION_ERROR | 400 | Invalid query parameters | | RATE_LIMITED | 429 | Too many requests | | INTERNAL_ERROR | 500 | Server error | Error response format: ```json { "error": { "code": "UNAUTHORIZED", "message": "Invalid or missing API key", "details": null } } ``` --- ## Code Examples ### cURL ```bash curl -H "X-API-Key: YOUR_API_KEY" \ "https://api.port-signal.com/v1/lineups?limit=10" ``` ### Python ```python import requests headers = {"X-API-Key": "YOUR_API_KEY"} resp = requests.get( "https://api.port-signal.com/v1/lineups", headers=headers, params={"limit": 10}, ) data = resp.json() for record in data["data"]: print(record["vessel_name"], record["commodity"]) ``` ### JavaScript ```javascript const resp = await fetch( "https://api.port-signal.com/v1/lineups?limit=10", { headers: { "X-API-Key": "YOUR_API_KEY" } }, ); const { data } = await resp.json(); console.log(data); ``` ### R ```r library(httr) library(jsonlite) resp <- GET( "https://api.port-signal.com/v1/lineups", add_headers("X-API-Key" = "YOUR_API_KEY"), query = list(limit = 10) ) data <- fromJSON(content(resp, "text")) print(data$data) ```