🔼 Upstream API
🚧 Heads‑up! This is our next‑generation API and it's still under active development.
How you can help
- Try it out in non‑production environments first.
- Report issues or request features by mailing [email protected]
Thank you for partnering with us to shape the future of our platform!
Base URL:
https://api.dockflow.com/v3Scope token:upstream_*(production) oruat_upstream_*(sandbox)
1 · Delivery options
| Mode | Workflow | Best for |
|---|---|---|
| Polling | GET /upstream → up to 1000 events, plus cursor for the next page. | Batch jobs, cron. |
| Webhooks | Dockflow POSTs events to your HTTPS URL within seconds (automatic retries). | Real‑time pipelines. |
Polling response envelope
Every polling response is wrapped in this top-level envelope:
{
"data": [ /* array of tradeflow objects, see §2 */ ],
"next_cursor": "eyJjaGFuZ2VkX2F0IjoiMjAyNS0wNS0xMFQxMjoyMjo0OCswMDowMCIsImlkIjoxMjM0NX0="
}
data- the page of tradeflows matching your filters.next_cursor- opaque string. Non-null when another page is available (i.e. the current page is full).nullwhen you have reached the end of the result set.
Polling flow (since + cursor combined)
since and cursor are not alternatives - they are combined in the same query and must be used together when paginating. The correct polling loop is:
- First call of a cycle:
GET /upstream?since=T(whereTis your last saved watermark, see step 4). - If
next_cursoris non-null: call again with bothsince=Tandcursor=<next_cursor>, keepingsince=Tidentical across every call in this cycle. Repeat this step with each newnext_cursoruntilnext_cursorisnull. - When
next_cursorisnull: you have drained the cycle. Stop. - Persist watermark: scan every tradeflow received across all pages of this cycle, take the highest
metadata.last_updated, and save it as your newTfor the next polling cycle.
The since watermark must not change mid-cycle. Only advance it after the cursor loop has completed.
Why the cursor is required (ties on identical timestamps)
Results are ordered by (upstream_changed_at, id). Two or more tradeflows can share the exact same upstream_changed_at down to the microsecond, especially during bulk imports or batch updates. The since filter alone cannot distinguish between tradeflows at the boundary timestamp, so paginating with since only would either skip or duplicate rows at that boundary.
The cursor carries both changed_at and the tradeflow id as a tiebreaker, guaranteeing that every tradeflow is returned exactly once across the full cycle.
Common mistakes
- Dropping the cursor after the first page. If
next_cursoris non-null, you have more data. Treating it as optional skips records. - Advancing
sincebetween pages. This resets the pagination window and causes skips/duplicates. Keepsincefixed for the full cycle. - Using
next_cursoracross polling cycles. Cursors are only valid within a single cycle. When starting a new cycle, go back to step 1 with the new watermark. - Treating the cursor as parseable. It is opaque base64-encoded state. Do not decode or construct it yourself.
2 · Envelope format
{
"version": "3.0.0",
"tradeflow_reference": "PO4564268",
"bill_of_lading": "ANR00001010", // primary bill of lading (null if none)
"booking": "94512540", // primary booking number (null if none)
"additional_references": { // any further references beyond the primary
"bill_of_lading": ["ANR00001011"], // remaining bills of lading (empty if none)
"booking": [] // remaining booking numbers (empty if none)
},
"data_quality": { // information about the Blue Sails
"freshness_min": 12,
"anomalies": 0,
"missed_milestones": 1,
"has_live_data": true,
"is_singular_shipment": false
},
"metadata": {
"last_updated": "2025‑05‑10T12:22:48Z",
"carrier_name": "Maersk",
"carrier_scac": "MAEU",
"is_active": true // whether the tradeflow is currently active
},
"locations": [
{ "reference": "BEANR", "name": "Port of Antwerp", "type": "SEA_PORT",
"timezone": "Europe/Brussels", "country": "Belgium",
"lat": 51.263, "lng": 4.398 }
],
"containers": [
{ "reference": "ABCD1234567", // container number (BIC code)
"iso_type": "45R1", // ISO 6346 size-type code; null if carrier did not provide one
"length_ft": 40, // container length in feet, derived from iso_type
"is_reefer": true, // true if the iso_type identifies a reefer
"free_time": { // detention & demurrage window
"demurrage_free_time_expires": "2025‑06‑08T23:59:00+02:00",
"detention_free_time_expires": "2025‑06‑15T23:59:00+02:00",
"combined_free_time_expires": null, // non-null when carrier bundles D&D into one window (e.g. Evergreen); demurrage_/detention_ then mirror this value
"storage_free_time_expires": null, // terminal storage free-time expiry, independent of carrier D&D
},
"import_release_status": { // terminal release lights (Antwerp only)
"commercial": "RELEASED", // shipping line release
"commercial_color": "GREEN",
"commercial_latest_update": "2025-06-05T10:30:00+02:00",
"customs": "RELEASED", // customs clearance
"customs_color": "GREEN",
"customs_latest_update": "2025-06-05T11:00:00+02:00",
"terminal": "RELEASED", // terminal ready for pickup
"terminal_color": "GREEN",
"terminal_latest_update": "2025-06-05T11:15:00+02:00",
"terminal_code": "BEANR",
"source": "nxtport_cpu"
},
"arrival_at_pod_event_date": "2025‑06‑06T15:30:00‑07:00", // container ETA/ATA at final POD
"arrival_at_pod_event_date_extended": {
"event_date_zulu": "2025-06-06T22:30:00Z",
"event_date_local": "2025-06-06T15:30:00-07:00",
"timezone_geo": "America/Los_Angeles",
"timezone_utc_offset_minutes": -420,
"timezone_abbreviation": "PDT",
"vessel_schedule_event_date_zulu": "2025-06-06T22:30:00Z",
"vessel_schedule_event_date_local": "2025-06-06T15:30:00-07:00"
},
"arrival_at_pod_is_actual": false, // true if actual, false if estimated
"latest_actual_milestone": { // last confirmed milestone for this container
"code": "GN",
"event_date": "2025‑05‑09T10:34:00+02:00",
"event_date_extended": {
"event_date_zulu": "2025-05-09T08:34:00Z",
"event_date_local": "2025-05-09T10:34:00+02:00",
"timezone_geo": "Europe/Brussels",
"timezone_utc_offset_minutes": 120,
"timezone_abbreviation": "CEST"
},
"location_reference": "BEANR",
"is_pol": false, // true if at port of loading
"is_final_pod": false, // true if at final port of discharge
"is_transshipment": false, // true if at a transshipment port
"is_prepol": false, // true if before port of loading
"is_postpod": false // true if after final port of discharge
}}
],
"vessels": [
{ "name": "MAERSK SOPHIE", "imo": "9721234", "mmsi": "219028000" }
],
"shipment": {
"split_shipment_number": 2, // undefined if single leg
"port_of_loading": "BEANR",
"departure_event_date": "2025‑05‑14T10:00:00+02:00",
"departure_event_date_extended": {
"event_date_zulu": "2025-05-14T08:00:00Z",
"event_date_local": "2025-05-14T10:00:00+02:00",
"timezone_geo": "Europe/Brussels",
"timezone_utc_offset_minutes": 120,
"timezone_abbreviation": "CEST",
"vessel_schedule_event_date_zulu": "2025-05-14T08:00:00Z",
"vessel_schedule_event_date_local": "2025-05-14T10:00:00+02:00"
},
"departure_is_actual": true,
"port_of_discharge": "USLAX",
"arrival_event_date": "2025‑06‑06T15:30:00‑07:00",
"arrival_event_date_extended": {
"event_date_zulu": "2025-06-06T22:30:00Z",
"event_date_local": "2025-06-06T15:30:00-07:00",
"timezone_geo": "America/Los_Angeles",
"timezone_utc_offset_minutes": -420,
"timezone_abbreviation": "PDT",
"vessel_schedule_event_date_zulu": "2025-06-06T22:30:00Z",
"vessel_schedule_event_date_local": "2025-06-06T15:30:00-07:00"
},
"arrival_is_actual": false,
"transshipment_ports": ["ESBCN"],
"incoterms": "FOB",
"cargo_opening": "2025‑05‑11T08:00:00+02:00", // earliest moment cargo can be dropped at POL
"shipment_legs": [
{
"sequence_number": 0,
"vessel": "MAERSK SOPHIE",
"departure": "BEANR",
"departure_event_date": "2025-05-14T10:00:00+02:00",
"departure_event_date_extended": {
"event_date_zulu": "2025-05-14T08:00:00Z",
"event_date_local": "2025-05-14T10:00:00+02:00",
"timezone_geo": "Europe/Brussels",
"timezone_utc_offset_minutes": 120,
"timezone_abbreviation": "CEST",
"vessel_schedule_event_date_zulu": "2025-05-14T08:00:00Z",
"vessel_schedule_event_date_local": "2025-05-14T10:00:00+02:00"
},
"departure_is_actual": true,
"departure_location_type": "pol",
"arrival": "ESBCN",
"arrival_event_date": "2025-05-20T08:00:00+02:00",
"arrival_event_date_extended": {
"event_date_zulu": "2025-05-20T06:00:00Z",
"event_date_local": "2025-05-20T08:00:00+02:00",
"timezone_geo": "Europe/Madrid",
"timezone_utc_offset_minutes": 120,
"timezone_abbreviation": "CEST",
"vessel_schedule_event_date_zulu": "2025-05-20T06:00:00Z",
"vessel_schedule_event_date_local": "2025-05-20T08:00:00+02:00"
},
"arrival_is_actual": false,
"arrival_location_type": "transshipment"
},
{
"sequence_number": 1,
"vessel": "MAERSK SOPHIE",
"departure": "ESBCN",
"departure_event_date": "2025-05-21T14:00:00+02:00",
"departure_event_date_extended": {
"event_date_zulu": "2025-05-21T12:00:00Z",
"event_date_local": "2025-05-21T14:00:00+02:00",
"timezone_geo": "Europe/Madrid",
"timezone_utc_offset_minutes": 120,
"timezone_abbreviation": "CEST",
"vessel_schedule_event_date_zulu": "2025-05-21T12:00:00Z",
"vessel_schedule_event_date_local": "2025-05-21T14:00:00+02:00"
},
"departure_is_actual": false,
"departure_location_type": "transshipment",
"arrival": "USLAX",
"arrival_event_date": "2025-06-06T15:30:00-07:00",
"arrival_event_date_extended": {
"event_date_zulu": "2025-06-06T22:30:00Z",
"event_date_local": "2025-06-06T15:30:00-07:00",
"timezone_geo": "America/Los_Angeles",
"timezone_utc_offset_minutes": -420,
"timezone_abbreviation": "PDT",
"vessel_schedule_event_date_zulu": "2025-06-06T22:30:00Z",
"vessel_schedule_event_date_local": "2025-06-06T15:30:00-07:00"
},
"arrival_is_actual": false,
"arrival_location_type": "pod"
}
],
"current_vessel": { // vessel the shipment is currently on
"name": "MAERSK SOPHIE",
"imo": "9721234",
"mmsi": "219028000",
"position": { // live AIS position; null unless in transit (between ATD and ATA)
"lat": 51.2546,
"lng": 4.4025,
"timestamp": "2025-06-04T08:12:00+00:00", // ISO 8601, refreshed at least hourly
"source": "ais"
}
},
"carbon_emissions": { // CO2 emissions (null if not yet calculated)
"distance_km": 12450.5, // route distance in kilometers
"co2_emissions_kg": 2134.8, // total CO2 (Well-to-Wheel)
"ttw_kg": 1850.2, // Tank-to-Wheel (direct combustion)
"wtt_kg": 284.6 // Well-to-Tank (fuel production)
}
},
"events": [
{ "id": "evt_01", "category": "MILESTONE", "code": "GN",
"message": "Gate in",
"event_date": "2025‑05‑09T10:34:00+02:00",
"event_date_extended": {
"event_date_zulu": "2025-05-09T08:34:00Z",
"event_date_local": "2025-05-09T10:34:00+02:00",
"timezone_geo": "Europe/Brussels",
"timezone_utc_offset_minutes": 120,
"timezone_abbreviation": "CEST"
},
"actual": true,
"container_reference": "ABCD1234567",
"location_reference": "BEANR",
"is_pol": false,
"is_final_pod": false,
"is_transshipment": false,
"is_prepol": false,
"is_postpod": false }
]
}
All date‑times (event_date, free‑time expiry, cargo opening) are expressed in the timezone of the location.
Bills of lading & booking numbers
A tradeflow usually has a single bill of lading and booking number, surfaced as the top‑level
bill_of_lading and booking strings (the primary reference). When a tradeflow carries more
than one, the extras are listed in additional_references.bill_of_lading / additional_references.booking.
The primary is the oldest reference (earliest event date) of each type — the same one the
Dockflow UI shows first. These references are tradeflow‑scoped, so on a split shipment they repeat
identically across each shipment entry. Either value is null (and the corresponding additional
array empty) when the tradeflow has no reference of that type yet.
3 · Location type flags
Both the latest_actual_milestone object (in containers) and all events in the events array include boolean flags indicating the type of location where the milestone occurred:
is_pol: Port of Loading — where the container was loaded onto the first vesselis_final_pod: Final Port of Discharge — where the container will be unloaded from the final vesselis_transshipment: Transshipment Port — intermediate port where cargo transfers between vesselsis_prepol: Pre-POL Location — locations before the port of loading (e.g., depot, rail terminal)is_postpod: Post-POD Location — locations after final discharge (e.g., inland destination, final delivery point)
Note: Multiple flags can be true simultaneously. For example, a milestone at a transshipment port that also serves as a POL for the next leg could have both is_transshipment and is_pol set to true.
Usage: These flags are particularly useful for vessel arrival/departure events (VA/VD) to determine whether the vessel is arriving at or departing from a POL, POD, or transshipment port.
4 · Current vessel
The current_vessel field in the shipment object shows which vessel the cargo is currently on:
| Shipment Status | Current Vessel |
|---|---|
| Waiting at POL (no ATD yet) | First vessel |
| In transit on a leg | Vessel of that leg |
| On transshipment quay (arrived at T/S, waiting for next vessel) | null |
| Arrived at final POD | Last vessel |
| Field | Type | Description |
|---|---|---|
name | string | Vessel name |
imo | string | null | IMO number |
mmsi | string | null | MMSI number |
position | object | null | Live AIS position — see below |
Example:
"current_vessel": {
"name": "MAERSK SOPHIE",
"imo": "9721234",
"mmsi": "219028000",
"position": {
"lat": 51.2546,
"lng": 4.4025,
"timestamp": "2025-06-04T08:12:00+00:00",
"source": "ais"
}
}
Returns null (the whole object) when cargo is on a transshipment quay between vessels.
4.1 · Vessel position
current_vessel.position exposes the live AIS-derived position of the current vessel. It is populated only while the shipment is in transit on a sea leg — i.e. between the actual departure (ATD) from the leg's POL and the actual arrival (ATA) at the leg's POD. In all other states (waiting at POL, arrived at final POD), position is null while current_vessel itself stays set.
| Field | Type | Description |
|---|---|---|
lat | number | Latitude in decimal degrees (WGS84) |
lng | number | Longitude in decimal degrees (WGS84) |
timestamp | string | ISO 8601 UTC time of the AIS fix |
source | string | Always "ais" for now; reserved for future provenance values |
Update cadence: the underlying AIS feed is refreshed at least hourly; consumers should treat timestamp as the source of truth for freshness rather than the poll time.
position is null when:
- the shipment is not currently in transit on a sea leg, or
- the vessel has no AIS-linked devices, or
- no AIS reading has been received yet for the vessel.
5 · Carbon emissions
The carbon_emissions field provides CO2 emission data for the shipment route:
| Field | Type | Description |
|---|---|---|
distance_km | float | Total route distance in kilometers (Haversine calculation) |
co2_emissions_kg | float | Total CO2 emissions in kg (Well-to-Wheel = TTW + WTT) |
ttw_kg | float | Tank-to-Wheel emissions (direct fuel combustion) |
wtt_kg | float | Well-to-Tank emissions (upstream fuel production) |
Example:
"carbon_emissions": {
"distance_km": 12450.5,
"co2_emissions_kg": 2134.8,
"ttw_kg": 1850.2,
"wtt_kg": 284.6
}
Note: Returns null if emissions have not yet been calculated (calculation typically occurs after shipment arrival).
6 · Import release status (green lights)
The import_release_status field in each container provides real-time pickup authorization status from terminal systems. This data comes from NxtPort Certified Pickup (CPu) for containers at Antwerp-Bruges terminals.
Fields
| Field | Type | Description |
|---|---|---|
commercial | string | null | Shipping line release state, or null when no reading exists for this light. See Status values below. |
commercial_color | string | null | GREEN, RED, or null when the state has no color mapping. |
commercial_latest_update | string | null | ISO 8601 event date of the current reading, or null when the light is absent or the reading carries no date. Independent of the state value. |
customs | string | null | Customs clearance state, or null when no reading exists for this light. See Status values below. |
customs_color | string | null | GREEN, RED, or null when the state has no color mapping. |
customs_latest_update | string | null | ISO 8601 event date of the current reading, or null when the light is absent or the reading carries no date. Independent of the state value. |
terminal | string | null | Terminal ready state, or null when no reading exists for this light. See Status values below. |
terminal_color | string | null | GREEN, RED, or null when the state has no color mapping. |
terminal_latest_update | string | null | ISO 8601 event date of the current reading, or null when the light is absent or the reading carries no date. Independent of the state value. |
terminal_code | string | null | UNLOCODE of the terminal (e.g., BEANR) — taken from the most recent reading across the three lights. |
source | string | null | Upstream data source (e.g., nxtport_cpu) — taken from the most recent reading across the three lights. |
Status values
The same set of values applies to all three lights:
| Value | *_color | Description |
|---|---|---|
RELEASED | GREEN | Released / cleared / ready |
BLOCKED | RED | Blocked or otherwise not released |
UNKNOWN | null | A reading exists but its upstream state could not be classified as released or blocked |
null | null | No reading has arrived for this light yet (the slot is absent) |
Pickup authorization
Container can be picked up when all three lights are GREEN:
commercial_color = GREEN
customs_color = GREEN
terminal_color = GREEN
─────────────────────────────────────────────
→ Container ready for pickup ✅
If any light is RED or null, the container is not yet ready for pickup.
Example
"import_release_status": {
"commercial": "RELEASED",
"commercial_color": "GREEN",
"commercial_latest_update": "2025-06-05T10:30:00+02:00",
"customs": "RELEASED",
"customs_color": "GREEN",
"customs_latest_update": "2025-06-05T11:00:00+02:00",
"terminal": "RELEASED",
"terminal_color": "GREEN",
"terminal_latest_update": "2025-06-05T11:15:00+02:00",
"terminal_code": "BEANR",
"source": "nxtport_cpu"
}
The three light keys (commercial, customs, terminal) are always present in the object. When a light has not received any reading yet, its value, *_color, and *_latest_update are all null. A light whose reading exists but could not be classified has the value "UNKNOWN" (with *_color null); its *_latest_update may still carry a timestamp. The whole import_release_status block is null only when zero readings exist across all three lights for the container.
Availability
- Supported terminals: Antwerp-Bruges (BEANR, BEZEE) via NxtPort CPu
- Requirements: Your organization must have NxtPort CPu credentials configured in Dockflow
- Returns
null: When import release status is not available for the container (non-Antwerp terminal or no credentials configured)
7 · Vessel Schedule Intelligence
Alongside per-tradeflow ETAs/ETDs, the API includes cross-tradeflow consensus dates derived from all tradeflows sharing the same vessel. When multiple tradeflows track the same vessel at the same port, Dockflow aggregates their readings into a single best-estimate using recency-weighted consensus.
Fields
Vessel schedule data is embedded inside the existing *_extended date objects as two additional fields:
| Field | Type | Description |
|---|---|---|
vessel_schedule_event_date_zulu | string | null | Consensus date in UTC (ISO 8601) |
vessel_schedule_event_date_local | string | null | Consensus date in local time at the location (ISO 8601) |
These fields appear in every _extended object that has vessel schedule context:
- Shipment level:
departure_event_date_extended,arrival_event_date_extended - Leg level: each leg's
departure_event_date_extended,arrival_event_date_extended - Container level:
arrival_at_pod_event_date_extended
Example
"departure_event_date_extended": {
"event_date_zulu": "2025-05-14T08:00:00Z",
"event_date_local": "2025-05-14T10:00:00+02:00",
"timezone_geo": "Europe/Brussels",
"timezone_utc_offset_minutes": 120,
"timezone_abbreviation": "CEST",
"vessel_schedule_event_date_zulu": "2025-05-14T08:00:00Z", // consensus ETD (UTC)
"vessel_schedule_event_date_local": "2025-05-14T10:00:00+02:00" // consensus ETD (local)
}
Interpretation
- Fields are omitted when no vessel schedule data is available (vessel not tracked cross-tradeflow, insufficient data, or low confidence)
- The
vessel_schedule_event_date_localuses the same timezone asevent_date_local(derived from the port location) - When the tradeflow's own
_is_actualistrue, the vessel schedule fields are informational only — the actual reading is authoritative
8 · Extended date format
All date fields (event_date, arrival_at_pod_event_date, departure_event_date, arrival_event_date) include a corresponding *_extended object with full timezone context:
| Field | Type | Description |
|---|---|---|
event_date_zulu | string | UTC/Zulu time in ISO 8601 format |
event_date_local | string | Local time at the location in ISO 8601 format |
timezone_geo | string | IANA timezone identifier (e.g., Europe/Brussels) |
timezone_utc_offset_minutes | integer | UTC offset in minutes (e.g., 120 for UTC+02:00) |
timezone_abbreviation | string | Timezone abbreviation (e.g., CEST, PDT) |
vessel_schedule_event_date_zulu | string | omitted | Vessel schedule consensus date in UTC (see §7) |
vessel_schedule_event_date_local | string | omitted | Vessel schedule consensus date in local time (see §7) |
Example:
"event_date": "2025-05-09T10:34:00+02:00",
"event_date_extended": {
"event_date_zulu": "2025-05-09T08:34:00Z",
"event_date_local": "2025-05-09T10:34:00+02:00",
"timezone_geo": "Europe/Brussels",
"timezone_utc_offset_minutes": 120,
"timezone_abbreviation": "CEST",
"vessel_schedule_event_date_zulu": "2025-05-09T08:30:00Z",
"vessel_schedule_event_date_local": "2025-05-09T10:30:00+02:00"
}
Note: If the location has no timezone configured, timezone_geo, timezone_utc_offset_minutes, and timezone_abbreviation will be null, and event_date_local will match the original event_date. The vessel_schedule_* fields are only present when vessel schedule data is available for that date (see §7).
9 · Status code table
| Code | Category | Description |
|---|---|---|
| GE | MILESTONE | Empty equipment dispatched |
| GN | MILESTONE | Gate in |
| AE | MILESTONE | Loaded on vessel |
| VD | MILESTONE | Vessel departure |
| VA | MILESTONE | Vessel arrival |
| UV | MILESTONE | Unloaded from vessel |
| GT | MILESTONE | Gate out |
| GR | MILESTONE | Empty equipment returned |
10 · Attribute selection
Choose which blocks to receive with the fields parameter (polling and webhooks):
fields=events,locations,containers,shipment,vessels
Default fields (when parameter is omitted): events, locations, containers, shipment
To include the vessels array, explicitly add it to your fields parameter.
11 · Filters
| Param | What it does |
|---|---|
since=ISO‑date | Only events created after this timestamp (UTC). |
tradeflow=REF | Filter by tradeflow reference. |
container=REF | Filter by container reference. |
category=MILESTONE | Event category. |
code=VD | Specific status code. |
active | Filter by status: true (strictly active only), false, or both. Default (no param): active + recently deactivated (7 days). |
per_page=1‑1000 | Page size (default 50, max 1000). |
12 · Error handling
| Status | Meaning | Suggested fix |
|---|---|---|
| 400 | Invalid parameter. | Check fields/filters spelling. |
| 401 | Wrong token scope. | Use an upstream_* token. |
| 413 | Webhook body too large. | Lower per_page or trim blocks via fields. |
| 429 | Rate limit hit. | Wait 60 s / reduce polling. |
| 5xx | Dockflow outage. | Retry with back‑off. |
Questions? Contact [email protected] — engineers respond within one business day.