# Dockflow developer documentation (full text) > The complete Dockflow API documentation, the guides for migrating from ShipsGo, project44, Windward and Cargoes Flow, and the MCP connector pages, as one file. Product documentation is indexed in https://docs.dockflow.com/llms.txt. This file contains all documentation content in a single document following the llmstxt.org standard. ## 🚀 Getting started (Dockflow API v3) :::tip Using Dockflow in the browser? This section is for **API integration**. For how to use the web app (dashboard, automations, terminals), start with the [product introduction](/). ::: > Dockflow offers a **single REST API**. > Use it to **push** purchase orders and containers into the platform and to **pull** milestones, documents and analytics back into any system. --- ## 1 · Create an API token 🔑 1. Sign in to the **API Console** at [console.dockflow.com](https://console.dockflow.com) and open **API Keys**. 2. Create a key, pick **Production** or **UAT** and the **scopes** it is for: `downstream`, `upstream` or `widget`. 3. Copy the key. It is shown only once. Every request sends the key in the `Authorization` header, with or without a `Bearer ` prefix. Key format, environments and error codes: [Authorization](/api/authorization). ### Your first call (UAT, about 2 minutes) Use a **UAT** key with both the `downstream` and `upstream` scopes, on `uat.dockflow.com`. Nothing you send there reaches your production account, and `mock_tracking` gives the tradeflow a predictable journey instead of real carrier data (see [UAT environment](/api/uat)). ```bash export DOCKFLOW_KEY="uat_acme-logistics_Zk3n1c0Q2m9..." # your UAT key from the API Console ``` **1. Send a tradeflow.** ```bash curl -X POST https://uat.dockflow.com/v3/downstream/tradeflows \ -H "Authorization: $DOCKFLOW_KEY" \ -H "Content-Type: application/json" \ -d '{ "tradeflow_reference": "QUICKSTART-001", "port_of_loading": "BEANR", "port_of_discharge": "USLAX", "mock_tracking": { "scenario": "ocean_basic" } }' ``` The answer is `202 Accepted` with `{"status": "queued", ...}`. Processing finishes a few seconds later. **2. Read it back.** A `tradeflow` filter returns that tradeflow regardless of when it changed: ```bash curl "https://uat.dockflow.com/v3/upstream?tradeflow=QUICKSTART-001" \ -H "Authorization: $DOCKFLOW_KEY" ``` If `data` is still empty, processing has not finished yet: wait a few seconds and repeat the call. Once processed, the response holds one record in `data`, with the mock events (gate out empty, gate in full, loaded on vessel at Antwerp, predicted arrival at Los Angeles) and the transport plan. **3. Go to production.** Create a `prod_` key, switch the host to `api.dockflow.com`, drop `mock_tracking`, and send real references (`container_reference`, `bill_of_lading_reference`, `booking_reference`) as described in the [Downstream guide](/api/downstream). For a continuous feed, set up [polling with a cursor or a webhook](/api/upstream). :::tip Building with an AI coding assistant? Point it at [docs.dockflow.com/llms-full.txt](https://docs.dockflow.com/llms-full.txt), the complete Dockflow API documentation as one text file. Every page is also available as markdown by adding `.md` to its URL, for example [/api/downstream.md](https://docs.dockflow.com/api/downstream.md). ::: --- ## 2 · Typical integration flow 1. Use **Downstream API** to create or update tradeflows and upload documents in Dockflow. 2. **Upstream** delivers real‑time status, ETA changes and analytics from Dockflow. 3. Widget & weblink endpoints add lightweight UI options. ```mermaid graph TD %% ---------- Your system ---------- subgraph Your_Systems SYS[💼 ERP / TMS / FMS / Website] end %% ---------- Dockflow ---------- subgraph Dockflow DOWN(🔽 Downstream API) subgraph Upstream UP(🔼 Upstream API) WIDGET[🔎 Widget API] EMBED[🖼️ Widget Embed] end LINK[🔗 Deep link] end %% ---------- Data flow ---------- SYS -->|create tradeflows & documents| DOWN UP -->|webhook or polling| SYS SYS -->|lookup| WIDGET SYS -->|embed Dockflow UI| EMBED SYS -->|open Dockflow UI| LINK %% ---------- Clickable links ---------- click DOWN "/api/downstream" click UP "/api/upstream" click WIDGET "/api/widget-api" click EMBED "/api/widget-embed" click LINK "/api/deep-link" ``` --- ## 3 · Pick the right endpoint | Direction | Path | Authentication | | ---------------------- | ------------------------------------- | ---------------------- | | **Downstream** | `/v3/downstream/tradeflows` | API key (`downstream`) | | **Documents** | `/v3/downstream/documents` | API key (`downstream`) | | **Upstream (polling)** | `GET /v3/upstream` | API key (`upstream`) | | **Upstream (webhook)** | Dockflow POSTs to your HTTPS endpoint | Headers you configure | | **Widget API** | `GET /v3/widget/search` | API key (`widget`), see [Widget API](/api/widget-api) | | **Widget iframe** | `embed.dockflow.com/track` (browser embed) | API key (`widget`), see [Widget Embed](/api/widget-embed) | | **Weblink** | `/redirect` (browser link) | none | *The `/v3` endpoints share one host:* `https://api.dockflow.com` (or `https://uat.dockflow.com` for the sandbox). A `widget`-scope token is limited to the widget endpoints and cannot be combined with other scopes; the older `/api/v2/widget-search` endpoint takes a widget key instead. --- ## 4 · Documentation map | Topic | Go to | | -------------------------------------------- | ------------------------------------------ | | Send tradeflows & documents (**Downstream**) | [Downstream guide](/api/downstream) | | Receive milestones & alerts (**Upstream**) | [Upstream guide](/api/upstream) | | API keys, key format & auth errors | [Authorization](/api/authorization) | | UAT sandbox & mock tracking | [UAT environment](/api/uat) | | On‑the‑spot JSON search (Widget API) | [Widget API](/api/widget-api) | | Embed a Dockflow view (Widget iframe) | [Widget iframe](/api/widget-embed) | | Deep‑link users into Dockflow | [Weblink](/api/deep-link) | --- Need help? Email **[support@dockflow.com](mailto:support@dockflow.com)** – engineers reply within one business day. --- ## 🔑 Authorization ## Authorization via API key Every v3 endpoint is authenticated with an **API key**. A key belongs to your organisation, not to the user who created it, and works for one environment: **Production** or **UAT**. ### Create an API key --- 1. Sign in to the **API Console** at [console.dockflow.com](https://console.dockflow.com) with your Dockflow account. If you do not have an account yet, contact support@dockflow.com. 2. Open **API Keys** and stay on the **API keys** tab. 3. Click **Create Key** and fill in: - **Name**: a label for your own bookkeeping, up to 80 characters (e.g. `Production Integration`). - **Environment**: `Production` or `UAT`. - **Scopes**: what the key is for: `Downstream` (send tradeflows and documents), `Upstream` (read tracking) and/or `Widget`. `Widget` cannot be combined with other scopes. 4. Copy the key. **It is shown only once.** Dockflow stores a hash, so a lost key cannot be recovered. Create a new one instead. :::note You will only see the API key once! Make sure to save it before navigating away. ::: ### Scopes --- | Scope | Use it for | | ----- | ---------- | | `Downstream` | Sending tradeflows and documents: [Downstream](/api/downstream), [Documents](/api/documents) | | `Upstream` | Reading tracking updates: [Upstream](/api/upstream) | | `Widget` | The tracking widget only: [Widget Embed](/api/widget-embed) and [Widget API](/api/widget-api) | A `Widget` key is published in your web page, so it is kept apart from the others: it works only on the widget, and the widget accepts no other key. Create a separate key for each website you embed the widget on, so you can revoke one without touching the others. ### Key format --- ``` {env}_{organisation-slug}_{secret} ``` | Part | Example | Notes | | ---- | ------- | ----- | | `env` | `prod` or `uat` | The environment the key works for. | | `organisation-slug` | `acme-logistics` | Your organisation's name, lower‑cased with punctuation dropped and spaces replaced by `-`. | | `secret` | `f36d0c4e…` (43 characters) | Random, URL‑safe base64. | For example: `prod_acme-logistics_Zk3n1c0Q2m9...` ### Send the key --- Put the key in the `Authorization` header. The bare key and the `Bearer` scheme are both accepted: ```bash curl -H "Authorization: prod_acme-logistics_Zk3n1c0Q2m9..." https://api.dockflow.com/v3/upstream curl -H "Authorization: Bearer prod_acme-logistics_Zk3n1c0Q2m9..." https://api.dockflow.com/v3/upstream ``` ### Environments --- | Key prefix | Host | Data | | ---------- | ---- | ---- | | `prod_…` | `https://api.dockflow.com/v3` | Your organisation | | `uat_…` | `https://uat.dockflow.com/v3` | A separate **UAT organisation**, created automatically the first time a key is created for your organisation | A key only works on its own host. A `uat_` key never reads or writes production data. See [UAT environment](/api/uat). ### Manage keys --- The **API keys** tab lists every key with its name, environment, scopes, creation date and **last used** time. You can rename a key or **revoke** it; scopes are fixed when the key is created. Revocation takes effect immediately and cannot be undone. ### Authentication errors --- Error bodies have the shape `{"error": "", "status": }`. | Status | Message | Cause | | ------ | ------- | ----- | | **401** | `Missing or malformed Authorization header.` | No `Authorization` header, or a value that is not a key. | | **400** | `Token format invalid.` | The value after `Bearer ` is not in the `{env}_{slug}_{secret}` shape. | | **403** | `Token environment (uat) does not match host (prod).` | A `uat_` key sent to `api.dockflow.com`, or a `prod_` key sent to `uat.dockflow.com`. | | **401** | `Token not recognised.` | Unknown key, or the key was mistyped or truncated. | | **403** | `Token has been revoked.` | The key was revoked in the API Console. | | **403** | `Token lacks the "widget" scope.` (or `"downstream"`, `"upstream"`) | A `Widget` key used on a data endpoint, or a data key used on the widget. | --- ## 🧪 UAT / test environment ## Base URL ``` https://uat.dockflow.com/v3 ``` UAT mirrors the production API surface. The only difference is **authentication** and the **optional mock‑tracking features** described below. | Environment | Host | Required key prefix | | ------------- | ------------------ | ------------------- | | Production | `api.dockflow.com` | `prod_…` | | UAT (sandbox) | `uat.dockflow.com` | `uat_…` | > Production keys are rejected on UAT, and vice versa (`403 Token environment … does not match host …`). UAT data lives in a separate **UAT organisation** that Dockflow creates for you the first time an API key is created. A `uat_` key reads and writes only that organisation, so nothing you send in UAT reaches your production account. Create UAT keys in the [API Console](/api/authorization) by choosing the **UAT** environment. --- ## What UAT is UAT is a **separate organisation, not a separate platform**. The first token created for your organisation (production or UAT) automatically creates a companion organisation named ` (UAT)`, and everything a `uat_…` token writes lands there instead of in your live organisation. Your live organisation is granted access to it, so the same login can open it from the organisation switcher in the Dockflow app and in the [API Console](/api/console). That design has two consequences worth planning for: - **You can see your test data in the product.** Switch to the `(UAT)` organisation and the tradeflows you posted appear exactly as production tradeflows do, with the same screens and exports — see [Working in the UAT organisation](#working-in-the-uat-organisation). - **It runs on live infrastructure.** Container tracking, carrier lookups and terminal integrations behave as they do for any organisation: post a real container number and Dockflow will really go and track it. Notifications and upstream delivery for the UAT organisation are live too — anything you subscribe a user to there will actually be sent. Use [mock tracking](#mock-tracking) when you want a predictable journey instead of whatever a real container happens to be doing. ### Differences from production | Behaviour | Production | UAT | | --------------------------------------------- | ---------------------------------------- | -------------------------------------------------- | | Token prefix / host | `prod_…` on `api.dockflow.com` | `uat_…` on `uat.dockflow.com` | | `mock_tracking` | Ignored | Generates a synthetic journey | | `customer_name` and `partners[]` | Create organisations and tradeflow links | **Ignored** — no organisation is created or linked | | Bulk delete `DELETE /downstream/tradeflows/*` | Requires `?confirm=PRODUCTION` | Allowed without confirmation | Everything else — validation, response shapes, upstream payloads, documents — is the same code path as production. --- ## Working in the UAT organisation ### Find the right organisation The sandbox is the organisation whose name **ends in `(UAT)`**. A live organisation may itself be named after a test environment, so the suffix rather than the word is what separates them: `Acme UAT` is a live organisation, `Acme UAT (UAT)` is its sandbox. Anything posted with a `uat_…` token lands in the `(UAT)` one and is visible nowhere else. To open it, go to **Settings ▸ Partners** in the Dockflow app, pick the organisation ending in `(UAT)`, and log in to it. If it is missing from the list or the login is greyed out, mail support@dockflow.com — access is granted per user. ### After a `202` `POST /downstream/tradeflows` answers `{"status": "queued"}` and finishes the work afterwards, so a tradeflow shows up in the app a moment later rather than instantly. `queued` means accepted. If nothing has appeared after a minute, check which organisation you are looking at before re-sending — a payload that was accepted but is "missing" is almost always being looked for in the live organisation instead of the `(UAT)` one. ### Build against a real message Once a tradeflow exists in the UAT organisation, open it and use **Download ▸ Download V3 API** on the tradeflow header. The file is produced by the same renderer as the [Upstream API](/api/upstream), so you can replay it into your own consumer — from Postman or a test — and build your processing without waiting for Dockflow to push anything. One difference to allow for: the download always contains all five blocks, including `vessels`, while a webhook message carries only `events`, `locations`, `containers` and `shipment`. Do not make `vessels` a required field on the strength of the downloaded sample. ## Mock tracking When posting a tradeflow to **`/downstream/tradeflows`** in UAT, include the field `mock_tracking` and Dockflow generates events that simulate real‑world progress. ### Basic format ```jsonc { "tradeflow_reference": "PO4564268", "port_of_loading": "BEANR", "port_of_discharge": "USLAX", "mock_tracking": { "scenario": "ocean_basic" } } ``` | Property | Type | Default | Description | | ---------- | -------- | ------- | ----------------------------------------- | | `scenario` | `string` | – | Selects the event template (table below). | An unknown scenario name is ignored: the tradeflow is created, but no mock events are added. ### Inputs it uses | Field | If you omit it | | ---------------------------------------------------------- | ------------------------------------------------------------- | | `port_of_loading` / `port_of_discharge` (5‑char UN/LOCODE) | The events for that side of the journey are **not** generated | | `container_reference` | A random existing container is picked, so events still appear | | `actual_time_of_departure` / `estimated_time_of_departure` | Departure is set to **yesterday** | | `estimated_time_of_arrival` / `actual_time_of_arrival` | Arrival is set to **30 days from now** | ### Predefined scenarios | Code | Events that will be generated | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ocean_basic` | **Actual, at the port of loading:** gate out empty (departure − 3d), gate in full (− 2d), loaded on vessel (− 1d).**Predicted, at the port of discharge:** vessel arrival (arrival), unloaded from vessel (+ 1d), gate out full (+ 2d), empty equipment returned (+ 7d). | > Additional scenarios may be added in future versions. To see what this scenario looks like on the wire before importing anything, use **Send test** on a webhook destination: it sends an `ocean_basic` record straight to your endpoint (see [Upstream › Webhook delivery](/api/upstream#try-it-before-you-have-shipments)). ### Custom events For full control, send your own `events` array (see [Tradeflows API](/api/downstream)). If both `events` and `mock_tracking.scenario` are present, your own events are kept and the mock sequence is **appended** after them. --- ## Usage example ```bash curl -X POST https://uat.dockflow.com/v3/downstream/tradeflows \ -H "Authorization: uat_acme-logistics_Zk3n1c0Q2m9..." \ -H "Content-Type: application/json" \ -d '{ "tradeflow_reference": "PO4564268", "port_of_loading": "BEANR", "port_of_discharge": "USLAX", "mock_tracking": { "scenario": "ocean_basic" } }' ``` Response (`202 Accepted`): ```json { "status": "queued", "tradeflows": [{ "tradeflow_reference": "PO4564268", "warnings": [] }] } ``` Posting is asynchronous: the tradeflow and its events are processed after the `202`, so poll `GET /v3/upstream?since=…` (or open the `(UAT)` organisation in the app) to see the result. --- ## Cleaning up There is **no automatic wipe** — UAT data stays until you remove it. To start from a clean sheet: ``` DELETE https://uat.dockflow.com/v3/downstream/tradeflows/* ``` That deactivates every tradeflow in your UAT organisation; add `?hard=true` to delete them permanently. Unlike production, no `confirm` parameter is needed. UAT webhooks: configure a destination on your UAT organisation in the API Console to receive UAT updates (see [Upstream › Webhook delivery](/api/upstream#webhook-delivery)). --- Use UAT to validate integrations safely before switching to production. For questions about tokens or mock scenarios, contact Dockflow support. --- ## 🔽 Downstream API - send to 🌐 > **Base URL:** `https://api.dockflow.com/v3` (or `https://uat.dockflow.com/v3` for the > [sandbox](/api/uat)) --- ## Endpoints | Method | Path | Description | | ---------- | ---------------------------------------------- | -------------------------------------------------------------------------------------- | | **POST** | `/downstream/tradeflows` | Create a tradeflow or update an existing one. Accepts a single object **or** an array. | | **DELETE** | `/downstream/tradeflows/{tradeflow_reference}` | Deactivate a tradeflow.`?hard=true` → permanently delete. | --- ## 1 · Create / update tradeflows `POST /downstream/tradeflows` ### Payload format A single JSON object **or** an array of objects. ```jsonc { "tradeflow_reference": "PO4564268", // required // Tracking references — string OR array "container_reference": [ { "reference": "ABCD1234567", "custom_properties": { "requires_inspection": true, "quality_check_date": "2025-05-01" } }, { "reference": "EFGH7654321", "custom_properties": { "requires_inspection": true, "quality_check_date": "2025-05-01" } } ], "container_references_complete": true, "bill_of_lading_reference": "ANR00001010", "booking_reference": ["94512540", "94512888"], // Tradeflow status "active": true, // optional // Carrier name "carrier_name": "Maersk", // optional "carrier_scac": "MAEU", // optional // Partner shortcuts "customer_name": "Fruit Maker EXIM International", "customer_reference": "PO_12345_CUST", // optional "partners": [ { "name": "Best Forwarder BV", "role": "forwarder", "reference": "PO_4564268", "lead_contact": "contact@forwarder.com" // optional - email of lead contact at partner } ], "partners_complete": false, // Team assignment "team_name": "Sales Team EMEA", // optional // Lead contact - individual responsible for this tradeflow "lead_contact": "sophie@company.com", // optional - email or full name // Voyage metadata "port_of_loading": "BEANR", "port_of_discharge": "USLAX", "estimated_time_of_departure": "2025-05-14T12:00:00Z", // or - when vessel has already departed "actual_time_of_departure": "2025-05-14T12:00:00Z", "estimated_time_of_arrival": "2025-06-06T22:00:00Z", // or - when vessel has already arrived "actual_time_of_arrival": "2025-06-06T22:00:00Z", "vessel": "Maersk SOPHIE", "incoterms": "FOB", "references": { "house_bill_of_lading": "hbl_123456789", // optional house bill of lading reference "delivery_location": "Factory yard 12345", // optional, stored as a free-text reference "delivery_event_date": "2021-08-24T15:00:25+00:00" // optional, stored as a free-text reference }, // Optional self-defined events "events": [ { "message": "delivery at client", "container_reference": "ABCD1234567", "location_name": "BEANR", // 5-character UN/LOCODE; any other value leaves the event without a location "event_date": "2025-05-12T08:34:00Z", "actual": true // whether the event already happened – or is scheduled } ], // Per-tradeflow / per-container free-time overrides (PATCH, append-only) "free_time_overrides": [ { "dwell_type": "equipment_free_time_in_port", "free_days": 14 }, { "dwell_type": "equipment_free_time_out_of_port", "free_days": 7, "container_reference": "ABCD1234567" } ], // NEW · Arbitrary key-value pairs defined by you "custom_references": { "invoice_number": "INV-12345", "priority": 2, "quality_check_date": "2025-05-01", "requires_inspection": true } } ``` #### `custom_references` — what & why | Field | Type | Description | | ------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `custom_references` | object | A _free-form dictionary_ whose keys are fully user-defined. Each value should be a string, number, boolean, or ISO-8601 date. Use it for anything that doesn’t fit the prescribed schema (e.g. internal IDs, workflow flags, etc.). | - Values are stored as text. Use flat values only; do not nest objects or arrays. - Keys are case-sensitive. A key you have not used before is created for your organisation. - Sending `null` or an empty string for a key removes that property from the tradeflow; the strings `null` and `#null` are ignored. #### Complete-list flags | Flag | Applies to | Effect when `true` | | ------------------------------------ | ---------------------------- | ---------------------------------- | | `container_references_complete` | `container_reference[]` | Containers not listed are removed. | | `bill_of_lading_references_complete` | `bill_of_lading_reference[]` | Same logic. | | `booking_references_complete` | `booking_reference[]` | Same logic. | | `partners_complete` | `partners[]` | Partners not listed are removed. | If a _\_complete_ flag is omitted, the list is _merged_ instead of _replaced_. #### Partner input - **`customer_name`** — quick way to add one partner with role `customer`. - **`partners`** — full array that supports multiple roles and references. Both inputs may be combined. #### Team assignment - **`team_name`** — assigns an existing team to the tradeflow. - Must be an existing team (minimum 3 characters) - First searches your organization, then partner organizations involved in the tradeflow - Case-insensitive matching (e.g., "Sales Team EMEA" matches "sales team emea") - If no matching team is found in any involved organization, the field is silently ignored #### Lead contact A **lead contact** is the individual user accountable for a tradeflow. They appear in the "My Tradeflows" view and receive assignment notifications. See the [Lead Contact guide](/user-management/lead-contact/) for the full feature overview. - **`lead_contact`** on the tradeflow root — the person responsible for the whole shipment on your side. - **`lead_contact`** on a partner object — the person responsible at that partner organization. **Accepted values** (in order of reliability): | Format | Example | Notes | |--------|---------|-------| | Email | `sophie@company.com` | Recommended — unambiguous exact match | | Full name | `Sophie Dupont` | Matched against `first_name + last_name`, word separators are flexible | **Matching rules:** - Must resolve to an **existing user** within your organization (for the root-level field) or the partner organization (for the partner-level field). - If the user belongs to a different entity than the one being set, the lead contact is also propagated to that entity's tradeflow record. - If no user matches, the field is silently ignored — the tradeflow is still created/updated without a lead contact. #### Free time overrides Per-tradeflow and per-container overrides for free-time monitoring (demurrage, detention, storage, electricity). Useful when a contract grants a one-off extension or grace period that should win over the global free-time rules. | Field | Type | Required | Description | | --------------------- | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `dwell_type` | string (enum) | yes | One of the 5 modern dwell types - see list below. | | `free_days` | integer ≥ 0 | yes\* | Number of free days for this override. Required unless `cancelled: true`. Zero is valid. | | `container_reference` | string | no | ISO 6346 container reference. If present and valid, the override is scoped to that container. If absent, the override is tradeflow-wide. | | `cancelled` | boolean | no | When `true`, cancels the override at this scope. `free_days` is ignored. | **Accepted `dwell_type` values:** - `equipment_free_time_in_port` - equipment free time at the port - `equipment_free_time_out_of_port` - equipment free time after gate-out - `equipment_free_time_combined` - combined pool (carriers like Evergreen, or Maersk on some lanes) - `storage_free_time` - terminal storage above carrier demurrage - `electricity_free_time` - reefer electricity free time **Semantics:** - **PATCH, append-only.** Listed scopes are upserted; unlisted scopes are untouched. Re-posting the same value is a no-op (idempotent). An empty array `[]` is also a no-op. - **Scope precedence.** A per-container override wins over a tradeflow-wide override for that container; tradeflow-wide applies to all other containers. A cancellation at one scope does not affect the override at the other scope. - **Cancellation.** Send `{ "dwell_type": "...", "cancelled": true }` (optionally with a `container_reference`) to clear the override at that exact scope. - **Pre-creation.** A per-container override may be authored before the container is added to the tradeflow; it is picked up automatically once the container appears. - **Effective downstream.** Once accepted, the override is reflected in the upstream API output, in graph-tp transport plans, and in the free-time-exceeded flag generation (typically within a few minutes due to the cron's cooldown). Invalid entries (unknown `dwell_type` — the older names `destination_demurrage` and `destination_detention` are not accepted, non-numeric or negative `free_days`) are silently skipped. A malformed `container_reference` is ignored and the entry is treated as tradeflow-wide. ### Response `202 Accepted` Processing is asynchronous. The response acknowledges each queued item: ```jsonc { "status": "queued", "tradeflows": [ { "tradeflow_reference": "PO4564268", "warnings": [] } ] } ``` `warnings` is reserved for soft-fail feedback and is currently always empty — a value that fails a soft-fail rule is ignored silently. To see what Dockflow did with a payload, open the tradeflow in the app and use **Download ▸ Download V3 API**, or read it back from [the upstream API](/api/upstream). Hard-fail rules are reported synchronously as `400 Bad Request`. --- ## 2 · Remove tradeflows `DELETE /downstream/tradeflows/{tradeflow_reference}` | Action | How | | -------------------------- | ------------------------------------------------------------------------------------------ | | **Deactivate** (preferred) | `DELETE /downstream/tradeflows/PO4564268` — same effect as posting `"active": false`. | | **Permanent delete** | `DELETE /downstream/tradeflows/PO4564268?hard=true` | Returns `204 No Content` on success. ### Bulk delete Using `*` as the `tradeflow_reference` deactivates **every** tradeflow of the organisation the token belongs to. Add `?hard=true` for permanent deletion. ``` DELETE /downstream/tradeflows/* ``` | Environment | Behaviour | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------- | | [UAT](/api/uat) | Allowed as-is. | | Production | Requires `?confirm=PRODUCTION`. Without it the call is refused with `400 Bad Request`; every accepted production bulk delete is logged. When the organisation has no tradeflows, the call returns `404`. | --- ## Validation rules The request is accepted with `202 Accepted` as long as the body is valid JSON and is an object or an array of objects; otherwise it is refused with `400 Bad Request`. Everything else is processed after the response, and an invalid value is skipped silently while the rest of the payload is applied. Check the outcome in the app or via the [upstream API](/api/upstream). `tradeflow_reference` is required. An item without a usable (non-blank) reference is not imported. Values that are skipped silently: - `container_reference` not matching `^[A-Z]{4}[0-9]{7}$` after upper-casing and removing spaces and hyphens. - Empty strings or null values inside array fields. - Dates that cannot be parsed (use ISO-8601, or `YYYYMMDD` for `estimated_time_of_arrival`). - `port_of_loading` / `port_of_discharge` that are not a 2-letter ISO country code or a 5-character UN/LOCODE. - `carrier_name` or `carrier_scac` shorter than 4 characters. - `customer_name` shorter than 2 characters; partner `name` of 2 characters or fewer. - `incoterms` of 2 characters or fewer. - Bill of lading and booking references shorter than 5 characters. - `team_name` shorter than 3 characters or not matching an existing team. - `lead_contact` that matches no existing user. - Unknown top-level properties (outside `custom_references`). - On the [UAT](/api/uat) host, `customer_name` and `partners` are not processed. When `active` is omitted, an inactive tradeflow is reactivated only if its arrival is in the future, or, with no arrival date, if it was created within the last two months. A tradeflow closed because all its containers were returned is the exception: without an arrival date, it is reactivated only when the request names a container it does not have yet. `"active": false` deactivates it. --- ## Error codes | Status | Meaning | | ----------------------------- | ---------------------------------------------------------------------------------------- | | **400 Bad Request** | Body is not valid JSON, or not an object or array. Also `Token format invalid`. | | **401 Unauthorized** | Missing or malformed `Authorization` header, or unknown token. | | **403 Forbidden** | Token environment does not match the host (`prod_` vs `uat_`), or the token is revoked. | | **404 Not Found** | Tradeflow not found on delete. | | **500 Internal Server Error** | Unexpected error during processing. | --- Questions or feedback? Create an issue in the public repository or contact Dockflow support. --- ## 🔼 Upstream API > **Base URL:** `https://api.dockflow.com/v3` > **Authentication:** an API key with the `upstream` scope, `prod_…` on `api.dockflow.com` or `uat_…` on `uat.dockflow.com`. See [Authorization](/api/authorization). --- ## 1 · Delivery options | Mode | Workflow | Best for | | ---- | -------- | -------- | | **Polling** | `GET /upstream` → a page of up to 1000 tradeflows, plus `next_cursor` for the next page. | Batch jobs, cron. | | **Webhooks** | Dockflow POSTs each changed tradeflow to your HTTPS endpoint, about a minute after it changes (see [Webhook delivery](#webhook-delivery)). | Real‑time pipelines. | Both modes return the same record (§2). A tradeflow produces **one record per shipment**: a split shipment yields several records that share a `tradeflow_reference` and differ in `shipment.split_shipment_number`. ### Polling response envelope Every polling response is wrapped in this top-level envelope: ```jsonc { "data": [ /* array of tradeflow objects, see §2 */ ], "next_cursor": "eyJjaGFuZ2VkX2F0IjoiMjAyNS0wNS0xMFQxMjoyMjo0OCswMDowMCIsImlkIjoxMjM0NX0=" } ``` - `data` - the records for the page of tradeflows matching your filters. `per_page` counts **tradeflows**, so a page holding split shipments contains more records than `per_page`. - `next_cursor` - opaque string. **Non-null** when the page holds `per_page` tradeflows and more may follow. **`null`** when you have reached the end of the result set. **Default window.** Without `since`, `cursor`, `tradeflow` or `container`, a call returns only the tradeflows that changed in the **last 15 minutes**. With any of those and no `since`, there is no time window. ### 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: 1. **First call of a cycle:** `GET /upstream?since=T` (where `T` is your last saved watermark, see step 4). 2. **If** `next_cursor` **is non-null:** call again with **both** `since=T` **and** `cursor=`, keeping `since=T` identical across every call in this cycle. Repeat this step with each new `next_cursor` until `next_cursor` is `null`. 3. **When** `next_cursor` **is** `null`**:** you have drained the cycle. Stop. 4. **Persist watermark:** scan every tradeflow received across all pages of this cycle, take the highest `metadata.last_updated`, and save it as your new `T` for 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. `since` is compared with the tradeflow's change timestamp, the value returned as `metadata.last_updated`, and is **exclusive**: a tradeflow whose `last_updated` equals `since` is not returned again. ### Common mistakes - **Dropping the cursor after the first page.** If `next_cursor` is non-null, you have more data. Treating it as optional skips records. - **Advancing `since` between pages.** This resets the pagination window and causes skips/duplicates. Keep `since` fixed for the full cycle. - **Using `next_cursor` across 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. ### Failsafe polling (position kept by Dockflow) Add `failsafe=1` and Dockflow keeps your position instead of you. Send the same request on every poll, and nothing is skipped, also after maintenance or downtime on your side. ``` GET /upstream?failsafe=1&since=2025-05-10T12:00:00Z ``` - **One position per API key and per set of filters.** Every query parameter except `since`, `cursor`, `failsafe` and `per_page` is part of it. Keep the filters identical on every call: a different combination of filters gets a position of its own. - **`since` only seeds the first call.** Without `since`, the first call starts from the default 15-minute window. From the second call on, `since` is ignored. - **Each call continues where the last response ended.** A response never holds more than `per_page` tradeflows. A non-null `next_cursor` means more are waiting: call again with the same request, with or without the cursor. Skipping a page loses nothing. - **A cursor you send is served.** Pass a `next_cursor` you received earlier and that page is sent again, also when your position is already further. - **A slow response is sent again.** When a response takes so long that it may not have reached you, your position does not move, and the next call returns the same tradeflows. To reset a position, contact [support@dockflow.com](mailto:support@dockflow.com). ### Poll history The [API Console](/api/console) lists every call made with your keys to the v3 upstream over the last 30 days: what it asked for, how many tradeflows it got and where the response ended. Each call also shows what your next call said about it: | Next poll | Meaning | | --------- | ------- | | Acked | The next call continued where this response ended. | | Overlap | The next call asked for part of this response again. | | Asked again | The next call sent the same `since` again. | | Gap | The next call started after where this response ended. The tradeflows in between were never asked for, and the console shows how many. | | Concurrent | The next call started before this one had finished. | | No ack | The next call sent neither `since` nor `cursor`. | A gap almost always means a full page whose `next_cursor` was not followed. ### Webhook delivery Instead of polling, Dockflow can POST every changed tradeflow to your endpoint. #### Set up a destination 1. In the [API Console](https://console.dockflow.com), open **API Keys › Webhooks** and add a destination. 2. Enter a **name** and the **URL**. The URL must use `https` and must not contain a username or password. 3. Add the **headers** your endpoint needs to authenticate us, e.g. `Authorization: Bearer …`. Up to 10 headers; `Host`, `Content-Length`, `Content-Type`, `Transfer-Encoding`, `Connection`, `Expect`, `Upgrade`, `X-Dockflow-Signature`, `X-Dockflow-Timestamp`, `X-Shipsgo-Webhook-Id`, `X-Shipsgo-Webhook-Name` and `X-Shipsgo-Webhook-Signature` are set by Dockflow and cannot be overridden. 4. Optionally set **Hold updates for** (0–1440 minutes): only changes at least that old are sent, so a tradeflow that is still being updated settles before it is delivered. 5. Choose the **delivery** mode: | Mode | What happens | | ---- | ------------ | | **Dry run** | Every update is built and recorded in the console, but **nothing is sent**. Use it to inspect what your endpoint would receive. New destinations start here. | | **Live** | Updates are delivered to your endpoint. | | **Off** | Nothing is built, sent or recorded. | You can add several destinations, e.g. a staging and a production endpoint. A UAT destination belongs on your UAT organisation. #### Format A destination delivers one format. The console offers `v3` and `dcsa-tnt-2.2`. For a format of a migrated client (`shipsgo-v2`, `project44-v4`, `windward` or `cargoes-flow`), contact [support@dockflow.com](mailto:support@dockflow.com). See [Output formats](#output-formats). #### Request ```http POST /your/endpoint HTTP/1.1 Content-Type: application/json Authorization: Bearer {"data":[ { /* one record, §2 */ } ]} ``` - Each request carries **exactly one record** in `data`: one shipment of one tradeflow. There is no `next_cursor`. - The record is the same as polling returns with the default `fields` (`events`, `locations`, `containers`, `shipment`). `fields` cannot be changed for webhooks, so `vessels` is not included. - A tradeflow is sent whenever its `metadata.last_updated` moves. The whole record is sent each time, not a diff. - In `shipsgo-v2`, `project44-v4`, `windward` and `cargoes-flow`, the body is that format's own webhook body with no `data` wrapper. Each migration page shows it. #### Responses, retries and ordering | Your endpoint returns | Dockflow does | | --------------------- | ------------- | | `2xx` | Delivered. | | `4xx` (incl. `401`/`403`) | Treated as a refusal. Not retried; delivery moves on to the next update. | | `5xx`, timeout (30 s), connection error | Retried with back‑off, up to 5 attempts in total. | - Respond within **30 seconds**. Redirects are **not** followed: a `3xx` is a failed delivery. - Delivery is **at least once**: you can receive the same record more than once. Records can also arrive out of order. Deduplicate and order on `(tradeflow_reference, shipment.split_shipment_number, metadata.last_updated)` and ignore a record older than the one you already stored. That key applies to `v3` and `dcsa-tnt-2.2` deliveries. The vendor formats have no such key, so use what each body carries to discard an older delivery: `updated_at` in `shipsgo-v2`, `lastModifiedDateTime` in `project44-v4`, `sentAt` in `windward`, which is when the body was rendered. A `cargoes-flow` body has no change time, so treat the latest delivery you received as current, and read the shipment by its number when you need certainty. - Protect your endpoint with the headers you configure (a secret token), and contact support@dockflow.com if you also need to allowlist our IP addresses. #### Signed deliveries A destination in `shipsgo-v2`, `project44-v4`, `windward` or `cargoes-flow` format is signed from the start. Any other destination, `v3` included, is signed once it has a secret: contact [support@dockflow.com](mailto:support@dockflow.com). You receive the secret once and it cannot be read again. Replacing it stops the old secret at once. A signed request carries two extra headers. Test sends and resends are signed too. | Header | Value | | ------ | ----- | | `X-Dockflow-Signature` | `sha256=`: HMAC-SHA256 of the exact request body, with your secret as the key. | | `X-Dockflow-Timestamp` | The time of sending in unix seconds. It is not signed, so it gives no replay protection. | Compute the HMAC over the bytes you received, before any JSON parsing, and compare in constant time: ```python import hashlib, hmac def is_authentic(raw_body: bytes, header: str, secret: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header) ``` ```js const crypto = require("crypto"); function isAuthentic(rawBody, header, secret) { const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); return header.length === expected.length && crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected)); } ``` To check your code: the secret `example-secret` and the body `{"hello":"world"}` give `sha256=9f6d5312d4a9761237003c132a90455cd840401c38b3b17678c16dcf2ca37661`. #### Try it before you have shipments Open the destination in **API Keys › Webhooks** and click **Send test**. Dockflow sends one example record to your endpoint right away, **whatever the delivery mode** (Dry run and Off included), so you can build and debug your handler without real tradeflows. - **Scenario:** `Ocean basic`: one container that left Antwerp (`BEANR`) yesterday and is due in Los Angeles (`USLAX`) in 30 days. Gate‑out empty (`GE`), gate‑in (`GN`) and loading (`AE`) are actual; vessel arrival (`VA`), discharge (`UV`), gate‑out (`GT`) and empty return (`GR`) are estimates. It is the same story as the UAT `ocean_basic` [mock tracking scenario](/api/uat#mock-tracking). Dates are relative to the moment you send. - **Tradeflow reference** (optional): replaces the placeholder `DOCKFLOW-TEST`, so you can test matching an update to an order in your own system. - A test request is identical to a real one except for the extra header **`X-Dockflow-Test: true`**. Filter on it if your endpoint must not act on test data. - A test send is a **single attempt**: it is not retried. The result appears in the delivery history, marked **Test**. - A destination in `shipsgo-v2`, `project44-v4`, `windward` or `cargoes-flow` format is tested with the **Live tradeflow** scenario: the current data of one of your own tradeflows, named by its reference. `Ocean basic` is refused, so you need one real tradeflow first. #### Resend a call Expand any call in the delivery history and click **Resend this body** to send exactly the same bytes again, to the destination's **current** URL and headers, whatever its delivery mode. This is useful after fixing a bug in your handler, or to replay a Dry run body for real. A resend is a single attempt and is marked **Resend**. Bodies are kept for 30 days; an older call can no longer be resent. #### Delivery history Each destination in **API Keys › Webhooks** lists every call Dockflow made: tradeflow, containers, attempt number, result, HTTP status, duration and the start of your response body. You can open the exact body that was sent. Bodies are kept for 30 days. On a destination in `shipsgo-v2`, `project44-v4`, `windward` or `cargoes-flow` format, a call shows no tradeflow reference and no containers. Search by tradeflow ID still works. --- ## 2 · Envelope format ```jsonc { "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": { // indicative; freshness_min, missed_milestones and has_live_data are fixed values today "freshness_min": 12, "anomalies": 0, "missed_milestones": 1, "has_live_data": true, "is_singular_shipment": false }, "metadata": { "last_updated": "2025‑05‑10T12:22:48+00:00", "carrier_name": "Maersk", "carrier_scac": "MAEU", "is_active": true // whether the tradeflow is currently active }, "locations": [ { "reference": "BEANR", "name": "BE Antwerpen", "type": "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 "iso_type_group": "45RT", // ISO 6346 type group for iso_type; groups equivalent size-type codes together (45G1 and 45G0 both report as 45GP). Null when iso_type is unknown "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 windows; null when no free-time rules apply "demurrage_free_time_expires": "2025‑06‑08T23:59:59+02:00", "detention_free_time_expires": "2025‑06‑15T23:59:59+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:00+00:00", "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:00+00:00", "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:00+00:00", "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, // omitted for the first (or only) shipment "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:00+00:00", "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:00+00:00", "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:00+00:00", "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:00+00:00", "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 "cutoffs": [ // carrier deadlines at the port of loading, see below { "type": "VCO", "raw_code": "VGM", "raw_name": "VGM cut-off", "cargo_type": "ALL", "carrier": "MAEU", "operating_carrier": "MAEU", "is_cross_published": false, "at": "2025-05-12T12:00:00+00:00" } ], "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:00+00:00", "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:00+00:00", "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:00+00:00", "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:00+00:00", "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:00+00:00", "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:00+00:00", "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:00+00:00", "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:00+00:00", "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:00+00:00", "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 } ] } ```` **Time zones.** Plain date‑time fields such as `event_date`, `departure_event_date`, `arrival_event_date` and `arrival_at_pod_event_date` are ISO 8601 with an explicit offset, normalised to **UTC** (`+00:00`). For the local time at the location, read `event_date_local` in the matching `*_extended` object (§8). Free‑time expiries are expressed in the timezone of the location where free time starts. Always parse the offset rather than assuming one. ### Free time `free_time` gives the end of each free‑time window for the container, or is `null` when no free‑time rules are configured for it. Each expiry is the last second of the final free day; a field is `null` when that window does not apply or has not started (e.g. no discharge yet). | Field | Window | | ----- | ------ | | `demurrage_free_time_expires` | Free time in port (demurrage), from discharge | | `detention_free_time_expires` | Free time out of port (detention), from gate‑out; before gate‑out it is projected after demurrage/storage | | `combined_free_time_expires` | Combined demurrage + detention. When a combined rule applies, the two fields above carry this same value | | `storage_free_time_expires` | Terminal storage free time, from discharge | ### Cut‑offs `shipment.cutoffs` lists the carrier‑published deadlines for the port of loading, earliest first, or `[]` when none are known. These are the closing times the Dockflow app shows as **Closings**. | Field | Description | | ----- | ----------- | | `type` | Normalised cut‑off type, see below | | `raw_code` / `raw_name` | The code and name as the carrier published them | | `cargo_type` | Cargo type the deadline applies to, when the carrier distinguishes one | | `carrier` | SCAC of the carrier that published the deadline | | `operating_carrier` | SCAC of the vessel operator | | `is_cross_published` | `true` when the deadline comes from another carrier on the same sailing because your carrier published none of that type | | `at` | The deadline (ISO 8601) | Cut‑off types (the first five follow the DCSA Booking 2.0 `cutOffDateTimeCode` codes): | `type` | Deadline | | ------ | -------- | | `DCO` | Documentation cut‑off | | `VCO` | VGM cut‑off | | `FCO` | FCL delivery cut‑off (yard cut‑off) | | `LCO` | LCL delivery cut‑off | | `EFC` | Earliest full‑container delivery (gate opening) | | `SHI` | Shipping instructions cut‑off | | `DGD` | Dangerous goods declaration cut‑off | | `CUSTOMS` | Customs cut‑off | | `OOG` | Out‑of‑gauge cargo cut‑off | | `ECP` | Latest empty container pick‑up | | `ECP_EARLIEST` | Earliest empty container pick‑up | `cargo_opening` is when the terminal opens its gate for the sailing. It can differ from a carrier's earliest‑acceptance cut‑off. ### 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. ### Actual vs estimated departure / arrival `departure_is_actual` and `arrival_is_actual` (at shipment level, and the matching per‑leg keys on each `shipment_leg`) report whether the current transport plan considers the departure, respectively the arrival, **actually to have happened**: - `departure_is_actual` is `true` when the resolved port of loading is backed by a real, dated, actual vessel‑departure (`VD`) or export‑load (`AE`) event. - `arrival_is_actual` is `true` when the final port of discharge is backed by a confirmed arrival reading **and** the departure was itself actual. These fields reflect the **current view of the transport plan**, not a frozen per‑message snapshot. The plan is recomputed as new statuses arrive, so a value may legitimately change between responses (including true → false → true) while the plan is refined around a real departure or arrival — most commonly when an estimated booking stub for the first port of loading is later superseded by the actual departure. Treat them as the latest best answer and re‑read the shipment on updates rather than treating a single response as permanent. --- ## 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 vessel - **`is_final_pod`**: Final Port of Discharge — where the container will be unloaded from the final vessel - **`is_transshipment`**: Transshipment Port — intermediate port where cargo transfers between vessels - **`is_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:** ```jsonc "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. `position` holds the vessel's latest AIS position (`lat`, `lng`, `timestamp`, `source: "ais"`) while the cargo is **under way** on that vessel. It is `null` before departure, after arrival, and when no valid position is known. --- ## 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:** ```jsonc "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`, `BLUE`, `ORANGE`, `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) | The **customs** light can additionally report two non-blocking intermediate states: | Value | `customs_color` | Description | | ----- | --------------- | ----------- | | `PRE_LODGE` | `BLUE` | Customs pre-lodge / partially cleared — indicative | | `PARTIAL` | `ORANGE` | Non-blocking customs control — FAVV (food safety) or transhipment | Neither `PRE_LODGE` nor `PARTIAL` is green, so the container is not yet ready for pickup while either is shown. ### Pickup authorization Container can be picked up when **all three lights are GREEN** (`RELEASED`): ``` commercial = RELEASED (GREEN) customs = RELEASED (GREEN) terminal = RELEASED (GREEN) ───────────────────────────────────────────── → Container ready for pickup ✅ ``` If any light is `RED` or `null` — or the customs light is `BLUE` or `ORANGE` — the container is not yet ready for pickup. ### Example ```jsonc "import_release_status": { "commercial": "RELEASED", "commercial_color": "GREEN", "commercial_latest_update": "2025-06-05T10:30:00+02:00", "customs": "PARTIAL", "customs_color": "ORANGE", "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 Dockflow has no release information for the container (e.g. a non‑Antwerp terminal, or no CPu 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 ```jsonc "departure_event_date_extended": { "event_date_zulu": "2025-05-14T08:00:00+00:00", "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:00+00:00", // 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_local` uses the same timezone as `event_date_local` (derived from the port location) - When the tradeflow's own `_is_actual` is `true`, 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 | The same moment in UTC, ISO 8601 with a `+00:00` offset | | `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](#7--vessel-schedule-intelligence)) | | `vessel_schedule_event_date_local` | string \| _omitted_ | Vessel schedule consensus date in local time (see [§7](#7--vessel-schedule-intelligence)) | **Example:** ```jsonc "event_date": "2025-05-09T10:34:00+02:00", "event_date_extended": { "event_date_zulu": "2025-05-09T08:34:00+00:00", "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:00+00:00", "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](#7--vessel-schedule-intelligence)). --- ## 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 | | **GD** | MILESTONE | Gate out to an inland depot | | **GR** | MILESTONE | Empty equipment returned | --- ## 10 · Attribute selection Choose which blocks to receive with the `fields` parameter (polling only; webhooks always receive the default set): ```bash 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. `version`, `tradeflow_reference`, the bill of lading and booking fields, `data_quality` and `metadata` are always included. --- ## 11 · Filters | Param | What it does | | -------------------- | ----------------------------------------------- | | `since=ISO‑date` | Only tradeflows whose `metadata.last_updated` is **after** this timestamp. Include an offset (`Z` or `+00:00`). Default: the last 15 minutes, unless `cursor`, `tradeflow` or `container` is given. | | `cursor=…` | `next_cursor` from the previous page (§1). | | `tradeflow=REF` | Filter by your tradeflow reference. | | `container=REF` | Filter by container number. | | `category=MILESTONE` | Keep only events of this category: `MILESTONE`, `SHIPMENT` or `LOCATION`. Every event is currently a `MILESTONE`. | | `code=VD` | Keep only events whose code starts with this value. One of `GE`, `GN`, `AE`, `VD`, `VA`, `UV`, `GT`, `GD`, `GR`, `POS`. | | `active` | Filter by status: `true` (strictly active only), `false`, or `both`. Default (no param): active, plus deactivated tradeflows that changed in the last 7 days or after your `since` or cursor, whichever reaches further back. | | `per_page=1‑1000` | Tradeflows per page (default 50, max 1000). | | `fields=…` | Blocks to include (§10). | | `format=…` | Shape of each record: see [Output formats](#output-formats). Default `v3`. | `category` and `code` filter the `events` array inside each record; they do not remove tradeflows from the page. ### Output formats | `format` | Each record in `data` | | -------- | --------------------- | | `v3` | The record of §2. | | `dcsa-tnt-2.2` | The record of §2, with `events` in the DCSA Track & Trace 2.2 shape. | | `shipsgo-v2` | A shipment in the ShipsGo v2 shape. See [Migrate from ShipsGo](/migrate/shipsgo). | | `project44-v4` | A shipment history in the project44 v4 shape. See [Migrate from project44](/migrate/project44). | | `windward` | A tracked shipment in the Windward shape, plus `tradeflow_reference`. See [Migrate from Windward](/migrate/windward). | | `cargoes-flow` | A shipment in the Cargoes Flow shape. See [Migrate from Cargoes Flow](/migrate/cargoes-flow). | The envelope stays `data` and `next_cursor`. In the last four formats, `fields` has no effect. Only `windward` holds `metadata.last_updated`, and `cargoes-flow` has no change timestamp at all. To keep your position in these formats, use [failsafe polling](#failsafe-polling-position-kept-by-dockflow) instead of a `since` watermark. **Only your own tradeflows.** A partner can add your company to a tradeflow they created (for example by naming you as customer when they push it), and by default that tradeflow is in your feed like any other. If your integration only handles tradeflows you created yourself, enable *Only include tradeflows your company created* on the API key in the [API Console](/api/console). The setting belongs to the key, not the request: every call made with that key then skips tradeflows another company created. --- ## 12 · Error handling | Status | Meaning | Suggested fix | | ------- | ----------------------- | --------------------------------------------- | | **400** | Invalid parameter (`since`, `per_page`, `category`, `code` or `format`). The body is a JSON string naming the problem, e.g. `"per_page must be between 1 and 1000"`. | Fix the parameter. | | **401** / **403** | Missing, unknown, revoked or wrong‑environment API key. | See [authentication errors](/api/authorization#authentication-errors). | | **5xx** | Dockflow outage. | Retry with back‑off. | --- Questions? Contact **[support@dockflow.com](mailto:support@dockflow.com)** — engineers respond within one business day. ``` --- ## 📊 API Console ## What is the API Console? The **Dockflow API Console** is where you set up and monitor your integration with Dockflow. Access URL → https://console.dockflow.com Sign in with your Dockflow account. Everything you see belongs to the organisation you are signed in to. To work on your UAT sandbox, switch to your **UAT organisation**. --- ## API Keys The **API Keys** page has these tabs: | Tab | What you can do | | --- | --------------- | | **API keys** | Create, rename and revoke API keys. See each key's environment, scopes and when it was last used. An `Upstream` key can be limited to tradeflows your company created. A new `Widget` key comes with a ready-made iframe snippet to copy. See [Authorization](/api/authorization) and [Widget Embed](/api/widget-embed). | | **Webhooks** | Add webhook destinations, set their headers, delay and delivery mode (Dry run / Live / Off). Open a destination to **send a test** record, see every call Dockflow made to it with the exact body that was sent, and **resend** any call. See [Upstream › Webhook delivery](/api/upstream#webhook-delivery). | | **v2 API messages** | Shown only if your organisation still receives the v2 upstream feed: the delivery history of that feed. | --- ## Other pages | Page | What it shows | | ---- | ------------- | | **Import history** | Tradeflow imports and their outcome. | | **Data quality** | Tracking data quality metrics. See [Data quality](/console/data-quality). | | **Integrations** | Credentials for the data sources Dockflow connects to on your behalf, e.g. terminals and port community systems. See [Terminal integrations](/features/terminal-integrations/). | | **User Activity** | How your people use Dockflow: users active in the last 7 and 30 days, a daily active-users chart, and a per-company table with an Excel export. | --- ## Need help? Email **support@dockflow.com**. Engineers typically respond within one business day. --- ## 🔗 Deep link ## Weblink The weblink is used to link users via the browser to existing tradeflows without knowing the internal Dockflow ID of tradeflows or containers. :::info Users should be logged in to see their tradeflows ::: ### Usage --- Link to a tradeflow by using the redirect-url: ``` https://app.dockflow.com/redirect?tradeflow=xxxxxxxxxx&container=ABCD1234567 ``` - `tradeflow=xxxxxxxxxx `- replace `xxxxxxxxxx` with the name of your tradeflow (as imported) - *[OPTIONAL]* `container=ABCD1234567` - add a container number to redirect to :::info It's also possible to specify only a container number, and no tradeflow number ::: --- ## 📫 Documents API import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; > **Base URL:** `https://api.dockflow.com/v3` --- ## Quick start ```bash # 1. Fetch available document types curl https://api.dockflow.com/v3/downstream/documents/types \ -H "Authorization: Bearer prod_acme-logistics_kJ8sV3nQ..." # 2. Upload a document (replace the `<…>` values) curl -X POST https://api.dockflow.com/v3/downstream/documents \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ ... see JSON example below ...}' ``` --- ## Endpoints | Method | Path | Purpose | | ---------- | --------------------------------------------- | -------------------------------------------------------------------------- | | **GET** | `/downstream/documents/types` | List supported business document types (e.g. `INV`, `POD`, …). | | **POST** | `/downstream/documents` | Upload a new document to a Tradeflow. | | **GET** | `/downstream/documents/{tradeflow_reference}` | List every document already attached to a Tradeflow. | | **PATCH** | `/downstream/documents/{id}` | Update the filename, business type or container link of a single document. | | **DELETE** | `/downstream/documents/{id}` | Permanently remove a document. | --- ## 1 · List business document types `GET /types` Returns an array of objects: ```jsonc [ { "type": "INV", "description": "Commercial invoice" }, { "type": "POD", "description": "Proof of delivery" } ] ``` --- ## 2 · Create a document `POST /downstream/documents` ### Request body | Field | Type | Required | Notes | | ------------------------- | ---------------- | -------- | ---------------------------------------------- | | `tradeflow_reference` | `string` | ✔︎ | Your own reference (PO / SO / BL / …). | | `document_extension` | `string` | ✔︎ | An allowed extension (see "Validation rules"). | | `document_payload_base64` | `string(base64)` | ✔︎ | The **raw** file encoded with Base‑64. | | `document_filename` | `string` | – | Friendly file name shown in Dockflow. | | `document_type` | `string` | – | Must match one of `/types` (`INV`, `POD`, …). | | `container_reference` | `string` | – | ISO 6346 container number to link to. | ```jsonc title="Example request (Invoice PDF)" { "tradeflow_reference": "PO4564268", "document_extension": "pdf", "document_filename": "Invoice 4564268", "document_type": "INV", "container_reference": "ABCD1234567", "document_payload_base64": "JVBERi0xLjQgCjEgMCBvYmog..." } ``` ### Successful response `202 Accepted` ```jsonc { "id": 123456789, "tradeflow_reference": "PO4564268", "document_type": "INV", "filename": "Invoice 4564268.pdf", "filetype": "pdf", "container_reference": "ABCD1234567", "created_at": "2025‑05‑05T14:22:07Z" } ``` _(The payload itself is **not** returned.)_ --- ## 3 · List Tradeflow documents `GET /{tradeflow_reference}` ```bash curl https://api.dockflow.com/v3/downstream/documents/PO4564268 \ -H "Authorization: Bearer " ``` ### Successful response `200 OK` ```jsonc { "data": [ { "id": 123456789, "name": "2025-05-05t14_22_07_00_00_invoice_4564268_inv_tf_789_123456789_v1.pdf", "filetype": "pdf", "input_name": "Invoice 4564268", "version": 0, "document_type": { "name": "INV", "description": "Commercial invoice" }, "metadata": null, "created_at": "2025-05-05T14:22:07+00:00", "updated_at": "2025-05-05T14:22:07+00:00" } ], "tradeflow_reference": "PO4564268", "count": 1 } ``` | Field | Type | Description | | --------------- | --------------- | -------------------------------------------------- | | `data` | `array` | Array of document objects. | | `id` | `integer` | Unique document identifier. | | `name` | `string` | Generated filename (includes timestamp, type, ID). | | `filetype` | `string` | File extension (`pdf`, `doc`, etc.). | | `input_name` | `string\|null` | Original filename provided during upload. | | `version` | `integer` | Document version (starts at 0). | | `document_type` | `object\|null` | Document type with `name` and `description`. | | `metadata` | `object\|null` | Custom metadata (JSON). | | `created_at` | `string` | ISO 8601 timestamp. | | `updated_at` | `string` | ISO 8601 timestamp. | | `count` | `integer` | Total number of documents returned. | --- ## 4 · Update a document `PATCH /{id}` Request body can contain **any** of: ```jsonc { "document_filename": "New name.pdf", "document_type": "POD", "container_reference": "EFGH7654321" } ``` Returns `200 OK` with the updated document, in the same shape as an item of the list in §3. - An unknown `document_type` is ignored and the type is left unchanged. - An unknown `container_reference` unlinks the document from any container. --- ## 5 · Delete a document `DELETE /{id}` Irreversible. ```bash curl -X DELETE https://api.dockflow.com/v3/downstream/documents/123456789 ``` `204 No Content` on success. --- ## Validation rules - `tradeflow_reference` and `document_extension` are required. - `document_extension` **must** be one of `pdf|doc|docx|xls|xlsx|jpg|jpeg|png|gif|bmp|tif|tiff|svg|webp` (lower case). - `document_payload_base64` is required and **must** be valid Base‑64. - `tradeflow_reference` has to exist on **your** Dockflow entity. - `document_type` should be one of the codes from the `/types` endpoint. An unknown or missing value is not rejected; the document is stored without a business type. - An unknown `container_reference` is accepted and the document is stored without a container link. The backend responds with `400 Bad Request` and a message if any of the first four rules fails. --- ## Common errors | Status | When it happens | | ------- | ------------------------------------------------------------------------------------------------- | | **400** | Invalid JSON, missing required fields, bad Base‑64, unsupported `document_extension`, or on `POST` a `tradeflow_reference` that does not exist on your entity; also `Token format invalid`. | | **401** | Missing or malformed `Authorization` header, or unknown token. | | **403** | Token environment does not match the host (`prod_` vs `uat_`), or the token is revoked. | | **404** | `GET`: tradeflow not found. `PATCH` / `DELETE`: document not found, or not on one of your tradeflows. | | **500** | The document could not be created, updated or deleted. | --- ## 🛠️ Upstream API FAQ # Upstream API FAQ The questions that come up most often when building against the v3 Upstream API. For the endpoint reference itself, see [Upstream](./upstream.md). ## Polling and the `since` parameter ### How do I use `since`? Pass it as a query parameter on the GET request: ``` GET /v3/upstream?since=2026-06-06T14:30:00Z ``` Several date formats are accepted, but **ISO 8601 with an explicit timezone is the recommended form**. It removes any ambiguity about how a bare date is interpreted. `since` and `cursor` work together rather than as alternatives. Page through with both until `next_cursor` comes back null, then store the highest `metadata.last_updated` you saw as the starting point for the next run. If you would rather not keep that watermark yourself, send `failsafe=1` and Dockflow keeps your position. See [Failsafe polling](/api/upstream#failsafe-polling-position-kept-by-dockflow). ### Why did a tradeflow come back that had not changed? A tradeflow can be touched by data that is not reflected in the message you receive. The clearest example is data arriving for the container's **next** journey: something changed on the record, so the timestamp moves, but the content of the message for your shipment is the same. This is confusing rather than harmful, and these cases are filtered out where they can be identified. ### I am receiving duplicate messages where only `last_updated` differs Same root cause as above. If you are deduplicating on your side, compare the payload rather than the timestamp. ## Active and deactivated tradeflows ### Do you send `is_active: false`? Yes. The default behaviour without the `active` parameter is to return active tradeflows **plus** recently deactivated ones, within a 7 day window. After that window they drop off entirely. With `active=true` set explicitly, only strictly active tradeflows are returned. :::warning Mind the 7 day window If your polling interval is longer than the deactivation window, you can miss the deactivation entirely. A tradeflow deactivated the day after your last poll will simply be absent from the next one, with no `is_active: false` message ever observed. For an ERP replication that needs to mirror deactivations, poll inside the window. ::: ## Vessel and voyage ### Where is the vessel in the payload? In the `shipment` element, in two places: **`shipment.current_vessel`** - the current vessel with its identifiers: ```json { "current_vessel": { "name": "MAERSK SOPHIE", "imo": "9721234", "mmsi": "219028000" } } ``` **`shipment.shipment_legs`** - the per-leg detail, which is where you look when a journey has more than one vessel. ## Cargo opening and cut-offs ### Where do I find yard opening and closing times? They are separate fields with separate origins. The carrier's own gate-open time (EFC) appears in `cutoffs[]` and can differ from `cargo_opening`: | Field | Contains | Source | |-------|----------|--------| | `cargo_opening` | Yard opening | Terminal data | | `cutoffs[]` | Yard closing, VGM, documentation, shipping instructions, dangerous goods, LCL delivery, customs, out of gauge, earliest full-container delivery (EFC), empty container pickup (ECP) | The carrier | Cut-offs are populated on the export leg at the port of loading. Coverage depends on the carrier and the port publishing them. ## Ingestion ### Why has the feed gone quiet for one tradeflow? The v3 feed does not pause for data anomalies: every change to a tradeflow is delivered, and `data_quality.anomalies` is an indicative count you can use to filter on your side. If a tradeflow has gone quiet, nothing about it has changed since your last poll. See [anomaly classification](/console/data-quality/anomalies/). ## Sending data in ### `customer_name` created a party I did not expect Send the customer once, spelled the same way everywhere. Identical names resolve to the same organisation, but if `customer_name` and a partner entry spell the company differently (for example `ACME` and `ACME NV`), they become two separate parties. Check that `customer_name` holds the company and not the contact person. A contact name in that field creates an entity named after the individual. ### An order was accepted but did not appear A 202 response means the message was accepted for processing, not that the tradeflow exists yet. Processing is asynchronous and normally quick. If it does not appear at all, confirm that `tradeflow_reference` was set on the request. Support can check the inbound logs for your reference and confirm whether the call arrived. ## Errors ### What do the authentication errors mean? | Status | Meaning | | ------ | ------- | | `400` | The token is not in the `{env}_{organisation-slug}_{random}` format. | | `401` | The `Authorization` header is missing or malformed, or the token is not recognised. | | `403` | The token has been revoked, or it is used on the wrong host (`prod_…` on `api.dockflow.com`, `uat_…` on `uat.dockflow.com`). | See [Authorization](./authorization.md). --- ## Related - [Upstream API →](./upstream.md) - [Authorization →](./authorization.md) - [UAT environment →](./uat.md) - [Event timing and delays →](/console/data-quality/event-timing/) - [Why Dockflow differs from the carrier →](https://community.dockflow.com/tag/data-differs-from-carrier) --- ## 🛠️ Client libraries # Client libraries There is no official SDK for the Dockflow API (v3). The API is JSON over HTTPS with a single `Authorization` header, so any HTTP client works and a wrapper adds little. The examples below send a tradeflow and read it back. They need a key with the `downstream` and `upstream` scopes and run against the [UAT sandbox](/api/uat); switch the host to `api.dockflow.com` and use a `prod_` key for production. ## Python ```python import os import time import requests BASE = "https://uat.dockflow.com/v3" HEADERS = {"Authorization": os.environ["DOCKFLOW_KEY"]} resp = requests.post( f"{BASE}/downstream/tradeflows", headers=HEADERS, json={ "tradeflow_reference": "PO4564268", "port_of_loading": "BEANR", "port_of_discharge": "USLAX", "mock_tracking": {"scenario": "ocean_basic"}, }, ) resp.raise_for_status() # 202 Accepted, {"status": "queued", ...} # Processing finishes a few seconds after the 202, so retry until the record appears. for _ in range(10): data = requests.get(f"{BASE}/upstream", headers=HEADERS, params={"tradeflow": "PO4564268"}).json()["data"] if data: break time.sleep(3) print(data) ``` ## TypeScript ```typescript const BASE = "https://uat.dockflow.com/v3"; const headers = { Authorization: process.env.DOCKFLOW_KEY!, "Content-Type": "application/json", }; const sent = await fetch(`${BASE}/downstream/tradeflows`, { method: "POST", headers, body: JSON.stringify({ tradeflow_reference: "PO4564268", port_of_loading: "BEANR", port_of_discharge: "USLAX", mock_tracking: { scenario: "ocean_basic" }, }), }); if (sent.status !== 202) throw new Error(await sent.text()); // Processing finishes a few seconds after the 202, so retry until the record appears. let data: unknown[] = []; for (let i = 0; i < 10 && data.length === 0; i++) { if (i > 0) await new Promise((r) => setTimeout(r, 3000)); const page = await fetch(`${BASE}/upstream?tradeflow=PO4564268`, { headers }); data = (await page.json()).data; } console.log(data); ``` ## Continuous sync For a production feed, poll `GET /v3/upstream` with `failsafe=1` or receive webhooks. The polling loop, cursor handling and webhook setup are described in the [Upstream guide](/api/upstream). ## Working with an AI coding assistant Give the assistant [docs.dockflow.com/llms-full.txt](https://docs.dockflow.com/llms-full.txt), the complete Dockflow API documentation as one text file. Every page is also served as markdown at its URL plus `.md`. --- ## 🔎 Widget API ## Widget API The widget API answers one-off lookups: send a single reference and get back the shipment that best matches it, with its containers, their latest status, the vessel's track and a link to the shipment in your Dockflow portal. It is the endpoint the [Widget Embed](/api/widget-embed) uses. Call it yourself to show the same data in your own interface. :::info Using the v2 widget API? `GET /api/v2/widget-search` with a Web widget key from **Settings → Organisation** keeps working. It is documented under [API v2 › Web-widget API](/api-documentation/web-widget-api). This page describes the v3 endpoint, which takes an API Console key with the `Widget` scope. ::: ### Authentication 🔑 --- Create a key with only the **Widget** scope in the [API Console](https://console.dockflow.com) (see [Authorization](/api/authorization)). Send it in the `Authorization` header. The bare key and the `Bearer` scheme are both accepted: ``` Authorization: Bearer prod_acme-logistics_Zk3n1c0Q2m9... ``` A widget key only ever searches the shipments of your own organisation, and only on this endpoint. Downstream and upstream keys are refused here, and a widget key is refused on every other v3 endpoint. ### Request --- ``` GET https://api.dockflow.com/v3/widget/search?q=YOUR_REFERENCE ``` Use `https://uat.dockflow.com/v3/widget/search` with a `uat_…` key. | Parameter | Description | |---|---| | `q` | **(mandatory)** A container number, booking or bill of lading reference, 4 to 64 characters. | Only the **best match** is returned, never a list. ### Response 📋 --- | Field | Description | |---|---| | `references` | Your own references for the shipment (order, PO, …) | | `carrier` | Carrier name, `null` when unknown | | `booking_reference` / `bill_of_lading_reference` | Carrier references, `null` when unknown | | `departure` / `arrival` | Port of loading and of discharge: `vessel_name`, `event_date`, whether that date is `actual`, and the `location` | | `containers` | Each container with its `type_code`, `type_group`, `length` and its `latest_actual_milestone` | | `tracks` | Position `readings` along the route (`event_date`, `lat`, `lng`), one entry per tracking source | | `details_url` | The shipment in your Dockflow portal, `null` when nothing matches | A `location` has a `name`, `unlocode`, `timezone`, `country` (`name`, `iso_a2`) and a `point` (`lat`, `lng`). `point` is `null` when the coordinates are unknown. Dates are ISO 8601. Show them in the location's `timezone`, not in UTC, or an event can land on the wrong day. A milestone's `status` is a readable label for the main events: | Label | Event | |---|---| | `Gate out empty` | Empty container picked up | | `Gate in full` | Full container delivered at the terminal | | `Loaded on vessel` | Loaded on board | | `Vessel departure` | Vessel left the port | | `Vessel arrival` | Vessel arrived in the port | | `Unloaded from vessel` | Discharged from the vessel | | `Gate out full` | Full container picked up at the terminal | | `Gate out to inland depot` | Moved to an inland depot | | `Empty return` | Empty container returned | Other events are returned with their raw Dockflow status code. `details_url` points to your organisation's own portal domain when you have one (for example `https://yourcompany.dockflow.com/tradeflow/view/…`), and to `app.dockflow.com` otherwise. Opening it requires a Dockflow account with access to the shipment. #### Example ```json { "references": ["PO-2026-0418"], "carrier": "MSC", "booking_reference": "EBKG04821937", "bill_of_lading_reference": "MEDUAN482193", "departure": { "vessel_name": "MSC DEMO", "event_date": "2026-09-25T11:00:00+00:00", "actual": true, "location": { "name": "Antwerpen", "unlocode": "BEANR", "timezone": "Europe/Brussels", "country": { "name": "Belgium", "iso_a2": "BE" }, "point": { "lat": 51.2637, "lng": 4.3987 } } }, "arrival": { "vessel_name": "MSC DEMO", "event_date": "2026-10-05T09:00:00+00:00", "actual": false, "location": { "name": "Istanbul", "unlocode": "TRIST", "timezone": "Europe/Istanbul", "country": { "name": "Türkiye", "iso_a2": "TR" }, "point": { "lat": 40.9667, "lng": 28.8 } } }, "containers": [ { "reference": "DEMU1234565", "type_code": "45G1", "type_group": "GP", "length": "40", "latest_actual_milestone": { "status": "Vessel departure", "event_date": "2026-09-25T11:00:00+00:00", "vessel_name": "MSC DEMO", "location": { "name": "Antwerpen", "unlocode": "BEANR", "timezone": "Europe/Brussels", "country": { "name": "Belgium", "iso_a2": "BE" }, "point": { "lat": 51.2637, "lng": 4.3987 } } } } ], "tracks": [ { "device_reference": "AIS MSC DEMO", "readings": [ { "event_date": "2026-09-25T12:00:00+00:00", "lat": 51.33, "lng": 4.25 }, { "event_date": "2026-09-25T18:00:00+00:00", "lat": 51.45, "lng": 3.1 } ] } ], "details_url": "https://yourcompany.dockflow.com/tradeflow/view/482193" } ``` #### Nothing found When no shipment matches, the API still answers **HTTP 200**, with every field empty: ```json { "references": [], "carrier": null, "booking_reference": null, "bill_of_lading_reference": null, "departure": null, "arrival": null, "containers": [], "tracks": [], "details_url": null } ``` #### Errors Error bodies have the shape `{"error": "", "status": }`. The key errors are listed under [Authorization › Authentication errors](/api/authorization#authentication-errors). Two more are specific to this endpoint: | Status | Message | Cause | | ------ | ------- | ----- | | **403** | `Token lacks the "widget" scope.` | A downstream or upstream key was used. Create a separate key with only the `Widget` scope. | | **422** | `{"message": …, "errors": {"q": [ … ]}}` | `q` is missing, or shorter than 4 or longer than 64 characters. | ### Feedback and Support 📧 --- Questions or issues? Reach out to our support team at support@dockflow.com. --- ## 🖼️ Widget Embed ## Dockflow tracking widget (iframe) The tracking widget is a ready-made page you embed on your own website. Visitors type a container, booking or bill of lading number and see the shipment on a map, with its origin and destination, the vessel's track and the latest status of every container. A **View full details** link opens the shipment in your Dockflow portal, unless you turn it off for the key. ![The Dockflow tracking widget showing a shipment from Antwerpen to Istanbul](./img/widget_v3_example.jpg) :::info Using the v2 web widget? Widgets embedded from `widget.dockflow.com` with a key from **Settings → Organisation → Web widget** keep working. They are documented under [API v2 › Web-widget (iframe)](/api-documentation/web-widget-iframe). The widget on this page runs on `embed.dockflow.com` and needs an API Console key with the `Widget` scope. ::: ### Integration --- 1. In the [API Console](https://console.dockflow.com), open **API Keys** and click **Create Key**. 2. Give it a name, pick the environment, and select only the **Widget** scope. A widget key cannot carry other scopes, because it ends up in your page source. 3. Click **Create Key**. The dialog shows the key and a ready-made embed snippet. Click **Copy Embed Code**. **The key is shown only once.** 4. Paste the snippet into your HTML page: ```html ``` From creating the key to finding a shipment on your own page: ![Creating a Widget key in the API Console, copying the embed code, and searching a container in the embedded widget](./img/widget_v3_flow.gif) _Shown with demo data._ :::caution The key is part of the page source, so anyone visiting your page can use it to search your organisation's shipments. A widget key can only search. It is refused by every other v3 endpoint. You can revoke it in the API Console at any time. ::: ### Arguments --- - `key`: **(mandatory)** Your widget key. Without it, the widget asks for one and the search box stays disabled. - `search`: _(optional)_ A reference to look up as soon as the widget loads, for example when you link to one specific shipment. ``` https://embed.dockflow.com/track?key=YOUR_WIDGET_KEY&search=TCNU1929808 ``` A `uat_…` key searches your [UAT organisation](/api/uat), and a `prod_…` key searches production. The widget picks the matching API host from the key. ### How it behaves --- - The search panel sits on the right. It searches as the visitor types, from 6 characters on. - The result shows the carrier, booking and bill of lading, the departure and (expected) arrival with their dates, and each container with its latest status, place and date. Dates are shown in the port's own time zone. - The map draws the leg from origin to destination and the vessel's track, and zooms to fit the whole journey. - **View full details** opens the shipment in your Dockflow portal, in a new browser tab rather than inside your iframe. It uses your organisation's own portal domain when you have one (for example `yourcompany.dockflow.com`), and `app.dockflow.com` otherwise. The visitor signs in there with their own Dockflow account. - When nothing matches, the panel shows _No shipment found_. :::note If your page sandboxes the iframe (`sandbox="…"`), add `allow-popups` so that **View full details** can open its tab. ::: ### Customization --- You control the size and border of the widget through the iframe attributes (`width`, `height`, `style`). Give it at least **720px of width**, so that the map stays visible next to the results panel. If the visitors of your page have no Dockflow account, you can leave out the **View full details** link. In the [API Console](https://console.dockflow.com), open **API Keys** and uncheck **Show "View full details" link** on the widget key. The change applies to the next search, with no change to your embed code. The content of the widget cannot be styled from your page. It is served from `embed.dockflow.com`, so your CSS does not reach inside the iframe. For branding or colour changes, reach out to support@dockflow.com. ### Build your own UI --- To show the same data in your own interface instead of an iframe, call the [Widget API](/api/widget-api) with the same key. --- ## Using Dockflow in ChatGPT The Dockflow app for ChatGPT lets you ask about your containers in the chat, using your own Dockflow account. ## Adding the app 1. Open [the Dockflow app in ChatGPT](https://chatgpt.com/apps/dockflow/asdk_app_696779b8a08c8191878932b84b3a41fe). 2. Select **Add**. 3. When ChatGPT asks you to connect, sign in with your Dockflow account. 4. Approve the access request. You only do this once. After that the app stays connected to your ChatGPT account. ## Asking a question Once connected, ask about a container by number: - "Where is container MSCU1234567?" - "When does MSCU1234567 arrive?" - "What are the ports for MSCU1234567?" If ChatGPT does not reach for Dockflow on its own, name it in the question: "Ask Dockflow where MSCU1234567 is." ## What you get back For every match, the answer includes: | Field | What it is | | --- | --- | | Container number | The full container number | | Tradeflow | The tradeflow the container belongs to, and its reference | | Port of loading | The load port, with the estimated departure date | | Port of discharge | The final discharge port, with the estimated arrival date | Times are shown in the local time of the place the event happened, with its UTC offset. A load in Gebze reads `09:36 (+03:00)`; an arrival in Antwerp reads `18:00 (+02:00)`. One container can show several offsets, one per port, so read the offset along with the time. If the same container sits on more than one tradeflow, you get one result per tradeflow. That is correct, not a duplicate. ## Limits - **Read-only.** The app can look things up. It cannot create, change or delete anything in Dockflow. - **Your own access only.** You see the tradeflows your Dockflow account can already see. Connecting the app does not change your permissions. - **Container numbers.** The lookup is by container number, not a general search. ## If it does not find your container 1. Check the container number for typos. 2. Open Dockflow and confirm the container is visible to your account. If it is not visible there, the app cannot see it either. 3. If it is visible in Dockflow but the app cannot find it, contact your Dockflow contact person or raise a ticket. ## Privacy and terms The app signs you in with your own Dockflow account. Your Dockflow password is never shared with OpenAI. See the [Privacy Policy](../privacy-policy/index.md) and the [Terms of Service](../terms-of-service/index.md). --- ## Using Dockflow in Claude The Dockflow connector for Claude lets you ask about your containers in the chat, using your own Dockflow account. ## Adding the connector 1. In Claude, open **Settings** and go to **Connectors**. 2. Find **Dockflow** in the connectors directory and select **Connect**. If it is not listed for you yet, select **Add custom connector** instead and enter `https://mcp.dockflow.com/mcp` as the URL. 3. When Claude asks you to connect, sign in with your Dockflow account. 4. Approve the access request. You only do this once. On a Claude Team or Enterprise plan, an owner of your organisation may need to add the connector before you can use it. ## Asking a question Once connected, ask about a container by number: - "Where is container MSCU1234567?" - "When does MSCU1234567 arrive?" - "Has MSCU1234567 been discharged yet?" If Claude does not reach for Dockflow on its own, name it in the question: "Ask Dockflow where MSCU1234567 is." Give the full container number, or at least its first four characters. ## What you get back For every match, the answer can include: | Field | What it is | | --- | --- | | Container number | The full container number, with its size and type | | Tradeflow | The tradeflow the container belongs to, its reference, and a link to it in Dockflow | | Ports | The port of loading and final port of discharge, plus any transshipment ports | | Vessel schedule | Estimated departure from the port of loading and arrival at the port of discharge | | Milestones | Gate in, loaded, departed, arrived, discharged, gate out, delivered and empty return, each marked as happened or expected | Times are shown in the local time of the place the event happened, with its UTC offset. A load in Gebze reads `09:36 (+03:00)`; an arrival in Antwerp reads `18:00 (+02:00)`. One container can show several offsets, one per port, so read the offset along with the time. If the same container sits on more than one tradeflow, you get one result per tradeflow. That is correct, not a duplicate. ## Limits - **Read-only.** The connector can look things up. It cannot create, change or delete anything in Dockflow. - **Your own access only.** You see the tradeflows your Dockflow account can already see. Connecting does not change your permissions. - **Container numbers.** The lookup is by container number, not a general search. ## If it does not find your container 1. Check the container number for typos. 2. Open Dockflow and confirm the container is visible to your account. If it is not visible there, the connector cannot see it either. 3. If it is visible in Dockflow but the connector cannot find it, contact your Dockflow contact person or raise a ticket. ## Privacy and terms The connector signs you in with your own Dockflow account. Your Dockflow password is never shared with Anthropic. See the [Privacy Policy](../privacy-policy/index.md) and the [Terms of Service](../terms-of-service/index.md). --- ## Using Dockflow in Microsoft Copilot Dockflow is available in Microsoft 365 Copilot, so you can ask about your containers without leaving Microsoft 365. It uses your own Dockflow account and returns the same information as the ChatGPT app. ## Adding Dockflow 1. Open Microsoft 365 Copilot. 2. Go to the agent store and search for **Dockflow**. 3. Add it. 4. When prompted to connect, sign in with your Dockflow account and approve the access request. You only do this once. Your Microsoft 365 administrator controls which agents are available in your organisation. If you cannot find or add Dockflow, ask your IT administrator to enable it for your account. ## Asking a question Ask about a container by number: - "Where is container MSCU1234567?" - "When does MSCU1234567 arrive?" - "What are the ports for MSCU1234567?" ## What you get back For every match, the answer includes: | Field | What it is | | --- | --- | | Container number | The full container number | | Tradeflow | The tradeflow the container belongs to, and its reference | | Port of loading | The load port, with the estimated departure date | | Port of discharge | The final discharge port, with the estimated arrival date | Times are shown in the local time of the place the event happened, with its UTC offset. A load in Gebze reads `09:36 (+03:00)`; an arrival in Antwerp reads `18:00 (+02:00)`. One container can show several offsets, one per port, so read the offset along with the time. If the same container sits on more than one tradeflow, you get one result per tradeflow. That is correct, not a duplicate. ## Limits - **Read-only.** Dockflow in Copilot can look things up. It cannot create, change or delete anything in Dockflow. - **Your own access only.** You see the tradeflows your Dockflow account can already see. Adding the agent does not change your permissions. - **Container numbers.** The lookup is by container number, not a general search. ## If it does not find your container 1. Check the container number for typos. 2. Open Dockflow and confirm the container is visible to your account. If it is not visible there, Copilot cannot see it either. 3. If it is visible in Dockflow but Copilot cannot find it, contact your Dockflow contact person or raise a ticket. ## Privacy and terms Dockflow in Copilot signs you in with your own Dockflow account. Your Dockflow password is never shared with Microsoft. See the [Privacy Policy](../privacy-policy/index.md) and the [Terms of Service](../terms-of-service/index.md). --- ## Dockflow in ChatGPT, Claude and Microsoft Copilot You can look up your containers without opening Dockflow. Connect your Dockflow account once, then ask ChatGPT, Claude or Microsoft Copilot about a container in plain language and get the answer in the chat. ## What you can do Ask about any container you can already see in Dockflow. For example: - "Where is container MSCU1234567?" - "When does MSCU1234567 arrive?" - "Show me the ports for MSCU1234567" You get back the container number, the tradeflow it belongs to and its reference, the port of loading with its estimated departure date, and the port of discharge with its estimated arrival date. ## Which one should I use? | | ChatGPT | Claude | Microsoft Copilot | | --- | --- | --- | --- | | Where it runs | ChatGPT, web and mobile | Claude, web, desktop and mobile | Microsoft 365 Copilot | | How to set it up | [Add the Dockflow app](chatgpt/index.md) | [Connect Dockflow in Claude](claude/index.md) | [Add Dockflow in Copilot](copilot/index.md) | | What it can do | Identical | Identical | Identical | All three connect to the same Dockflow account and return the same information. Pick whichever you already have open during the day. ## What it will not do This is a read-only lookup. It is deliberately narrow, so it is worth knowing where the edges are. - **It cannot change anything.** It cannot create, edit or delete a tradeflow, upload a document, or update a date. Everything happens in Dockflow itself. - **It only sees what you see.** Results are limited to the tradeflows your Dockflow account already has access to. Connecting the app does not widen your permissions, and it does not give the assistant access to anyone else's shipments. - **It searches by container number.** It is not a general search across your account. - **One container can appear more than once.** If the same container is linked to several tradeflows, you get one result per tradeflow. That is expected, not a duplicate. ## Your data Each app signs you in with your own Dockflow account and act as you. Your Dockflow password is never shared with OpenAI, Anthropic or Microsoft. See the [Privacy Policy](privacy-policy/index.md) and the [Terms of Service](terms-of-service/index.md). ## Getting help If a container does not come back, first check that it is visible to you in Dockflow itself. If it is, and the assistant still cannot find it, contact your Dockflow contact person or raise a ticket. --- ## Migrate from Cargoes Flow A client built for Cargoes Flow can talk to Dockflow by changing two values: the base URL and the API key. :::note There is no public specification to test this surface against. The first migration runs together with Dockflow and validates the shapes on this page, including the event codes and the webhook body. Contact [support@dockflow.com](mailto:support@dockflow.com) to plan it. ::: ## What to change | Setting | Before | After | | ------- | ------ | ----- | | Base URL | Your Cargoes Flow host | `https://api.dockflow.com/compat/cargoes-flow` | | `X-DPW-ApiKey` | Your Cargoes Flow key | A Dockflow API key | | `X-DPW-Org-Token` | Your organisation token | Accepted and ignored. It can stay. | The path `/flow/api/public_tracking/v1` also works under that base, so a client that only swaps the host needs no path change. ## Get a token 1. Sign in to the [API Console](https://console.dockflow.com) and open **API Keys**. 2. Click **Create Key**, choose the **Production** environment and the **Downstream** scope. 3. Copy the key. It is shown only once. See [Authorization](/api/authorization) for the key format and for revoking a key. ## What is covered | Route | What it does | | ----- | ------------ | | `GET /shipments` | A bare array. Filters: `shipmentType`, `status`, `shipmentNumber`, `mblNumber`, `containerNumber`, `awbNumber`, `search`, `includeUniqueContainers`, `_page`, `_limit`. | | `POST /createShipments` | `formData` rows with `uploadType: FORM_BY_MBL_NUMBER` start tracking. | | `PUT /updateShipments` | Sends shipper, consignee and extra fields to an existing shipment. | | `POST /uploadDocument` | Multipart `files` and `shipmentNumber`. | | `GET /mapRoutes?shipmentNumber=` | The legs as `routes`. Empty until a plan exists. | | `GET /carrierList` | The carriers Dockflow knows. | There is no delete route. `_page` starts at 1. `_limit` defaults to 20, at most 100. The newest shipment comes first. `search` is an exact match on bill of lading, booking, container, reference or shipment number. ### One row per shipment or per container One master bill of lading is one Dockflow tradeflow, with the reference `CARGOES-FLOW-` and the shipment number `DF`. - Without `includeUniqueContainers`: one row per shipment. `containerNumber` is the first box and `containerNumbers` lists all boxes. - With `includeUniqueContainers`, or with a `containerNumber` filter: one row per box. A row has its own events plus the events no box owns, and every row keeps the same `shipmentNumber`. `_limit` counts shipments, so a page can hold more rows than `_limit` when it is expanded per box. ### Answers and errors | Case | Answer | | ---- | ------ | | Create or update succeeds | `200` with `result: SUCCESS` and `errorDetail: []`. A create adds `shipments` with `shipmentNumber`, `mblNumber` and `created`. | | A row is refused | `200` with `result: FAILED` and `errorDetail[].error`. Rows are all or nothing: one bad row means nothing is created or updated. | | A body that is not JSON, or an invalid list parameter | `400` with `result: FAILED`. | | `mapRoutes` for an unknown shipment | `404` with `result: FAILED`. | | A refused key | The Dockflow body `{"error": "...", "status": 401}`. See [Authorization](/api/authorization#authentication-errors). | ## Field mapping ### Create and update | Field | Dockflow | | ----- | -------- | | `mblNumber` | Bill of lading reference. | | `bookingNumber` | Booking reference. | | `incoterm` | Incoterms. | | `oceanLine` | Carrier SCAC when it is four letters, carrier name otherwise. | | `shipper`, `consignee` | Partners with that role. | | `shipmentReference`, `promisedEta`, `promisedEtd`, `shipmentTags`, demurrage fields | Custom references. They never feed the transport plan. `GET /shipments` reads `promisedEta` and `promisedEtd` back. | ### Events | Dockflow code | `code` | | ------------- | ------ | | `GE` | `gateOutEmpty` | | `GN` | `gateInFull` | | `AE` | `loadedOnVessel` | | `VD` | `vesselDeparture`, or `vesselDepartureWithContainer` when the event names a container | | `VA` | `vesselArrival`, or `vesselArrivalWithContainer` when the event names a container | | `UV` | `dischargeFromVessel` | | `GT` | `gateOutFull` | | `GD` | `delivered` | | `GR` | `gateInEmpty` | At a transshipment port the code stays the same and the name adds "at transshipment". Any other Dockflow event keeps its own code and message, with `isCustomEvent: true`. ### Status - `status` is `ACTIVE` for an active shipment and `COMPLETED` otherwise. - `subStatus1` comes from the latest actual event of the table: `AT_ORIGIN`, `IN_TRANSIT`, `AT_DESTINATION`, `DELIVERED` or `EMPTY_RETURNED`. A transshipment event is `IN_TRANSIT`. - `subStatus2` is the code of that event. - `shipmentType` is `AIR_SHIPMENT` for a shipment with air events and `INTERMODAL_SHIPMENT` otherwise. Port fields such as `firstPort`, `lastPort` and an event's `location` carry UN/LOCODEs. `origin` and `destination` on a segment carry names. ### Fields Dockflow adds `shipmentType`, `containerNumbers`, `awbNumber`, `shipmentEvents[].containerNumber`, and on each segment `originPortCode`, `originLatitude`, `originLongitude`, `destinationLatitude` and `destinationLongitude`. ## What stays empty | Field | Value | | ----- | ----- | | `emissions.co2e.value` | `null`. | | `shipmentLegs.road` | `{}`. | | `promisedEta`, `promisedEtd` | `null` in webhook bodies and on the upstream feed. Only `GET /shipments` fills them. | | `loadingPort`, `dischargePort` | `null` when no call carries that port role. | | `segments[].transportMode` | `null` on a leg before the port of loading or after the port of discharge, and on a leg with no known vessel. | | `carrierScac`, `bookingNumber`, event fields | `null` where Dockflow holds no value. | In `carrierList`, `supportsTrackByMbl` and `requiresMbl` are `true` and `supportsTrackByBookingNumber` is `false`, because create accepts only a bill of lading. All rows are `INTERMODAL_SHIPMENT`. ## Webhooks Contact [support@dockflow.com](mailto:support@dockflow.com) to add a destination in the `cargoes-flow` format. You receive its signing secret once. It cannot be read again. Each delivery is one `POST` whose body is the shipment object of `GET /shipments`, without the promised dates. The first migration validates this body. Retries, timeouts and ordering follow [Webhook delivery](/api/upstream#webhook-delivery). | Header | Value | | ------ | ----- | | `X-Dockflow-Signature` | `sha256=`: HMAC-SHA256 of the exact body with your signing secret. | | `X-Dockflow-Timestamp` | Unix seconds. It is not signed, so it gives no replay protection. | ### Verify the signature Compute the HMAC over the exact bytes you received, before any JSON parsing: ```python import hashlib, hmac def is_authentic(raw_body: bytes, header: str, secret: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header) ``` ## How to test 1. Create a key with the **UAT** environment and the **Downstream** scope. It works on `https://uat.dockflow.com/compat/cargoes-flow` and writes to your [UAT organisation](/api/uat). A real number is really tracked there. 2. Create a shipment: ```bash curl -X POST https://uat.dockflow.com/compat/cargoes-flow/createShipments \ -H "X-DPW-ApiKey: uat_acme-logistics_Zk3n1c0Q2m9..." \ -H "Content-Type: application/json" \ -d '{"uploadType":"FORM_BY_MBL_NUMBER","formData":[{"mblNumber":"ANR00001010","oceanLine":"MAEU"}]}' ``` 3. Read it with `GET /shipments?shipmentNumber=DF74123`, using the number from the answer. The import runs after the answer, so the first read can be short. 4. To test a webhook, open the destination in the console and click **Send test** with the **Live tradeflow** scenario, naming the reference `CARGOES-FLOW-` of a tradeflow that has data. The **Ocean basic** scenario is refused for this format. Polling is also possible: `GET /v3/upstream?format=cargoes-flow` with an `Upstream` key returns these records. See [Upstream filters](/api/upstream#11--filters). --- ## Migrate from project44 A client built for project44's v4 ocean tracking API can talk to Dockflow by changing two values. The paths, the request bodies and the response shapes stay as they are. The responses and error bodies on this surface are checked by contract tests against project44's public API document. ## What to change | Setting | Before | After | | ------- | ------ | ----- | | Base URL | Your project44 base URL | `https://api.dockflow.com/compat/project44` | | Client secret | Your project44 client secret | A Dockflow API key | The `client_id` can stay. Dockflow only needs it to be present. ## Get a token 1. Sign in to the [API Console](https://console.dockflow.com) and open **API Keys**. 2. Click **Create Key**, choose the **Production** environment and the **Downstream** scope. 3. Copy the key. It is shown only once. `POST /api/v4/oauth2/token` accepts a form body, a JSON body or Basic authentication, with `grant_type=client_credentials`. It returns your secret as `access_token`, with `token_type: BEARER` and `expires_in: 3600`. The exchange does not check the secret. The first real call does, and the key itself does not expire on that clock. See [Authorization](/api/authorization) for the key format and for revoking a key. ## What is covered All paths are under `/api/v4`. | Route | What it does | | ----- | ------------ | | `POST /oauth2/token` | The token exchange above. | | `POST /shipments/tracking` | Starts tracking. With `id` in the body it adds identifiers to that shipment. Answers `200` with the described shipment. | | `PUT /shipments/tracking` | As `POST`, and the list you send replaces the stored list for each identifier class you name: bills of lading, bookings or containers. | | `GET /shipments/{id}/tracking` | The shipment. | | `GET /shipments/{id}/tracking/history` | Shipment, events, states, positions. This is also the webhook body. | | `GET /shipments/{id}/eventhistory` | Events and states. Add `includeRouteInfo=true` for the route. | | `GET /shipments/{id}/positionhistory` | Positions, which are empty. Add `includeRouteInfo=true` for the route. | | `GET /shipments/{id}/attributes` | An empty list. | | `DELETE /shipments/{id}/tracking` | `202`. A parent id stops tracking and removes the shipment. A container id removes that container. | | Any `/webhooks` path | `403`. Destinations are set outside this API, see [Webhooks](#webhooks). | Not part of this surface: `/api/v4/ocean/shipments`, `/services/unified-search`, loads and carrier push paths. There is no read by identifier: read by `id`. ### Identity One tracked object is one Dockflow tradeflow. The same set of tracking identifiers reaches the same shipment. The shipment `id` is a UUID derived from the tradeflow, and each container has its own child `id` that works on the read and delete routes. ### Errors Errors use the `ApiError` shape: `httpStatusCode`, `httpMessage`, `errorMessage` and `errors`. A token refusal has the same shape, with `401` or `403`. The token route uses `error` and `error_description`. ## Field mapping ### Identifiers you send | Type | Dockflow | | ---- | -------- | | `BILL_OF_LADING` | Bill of lading reference. | | `BOOKING_NUMBER` | Booking reference. | | `CONTAINER_ID`, `relatedShipments` | Container reference. `CONTAINER_NUMBER` is accepted as an alias. | | `CARRIER_SCAC` | Carrier SCAC. No SCAC is required next to a bill or booking. | | `FFW_SCAC` | Accepted. It is only repeated in the answer to the same call. | | `HOUSE_BILL_OF_LADING`, `TRACKING_NUMBER` | Stored and repeated in the answer to the create. Later reads do not show them, and they do not start tracking by themselves. | Other types answer `400`. ### Stops | Dockflow role | `type` | | ------------- | ------ | | Port of loading | `PORT_OF_LOADING` | | Port of discharge | `PORT_OF_DISCHARGE` | | Transshipment | `TRANSSHIPMENT_PORT` | | Before the port of loading | `ORIGIN` | | After the port of discharge | `DESTINATION` | | Anything else | `OTHER` | ### Events | Dockflow code | `type` | | ------------- | ------ | | `GE` | `GATE_OUT_EMPTY` | | `GN` | `GATE_IN_FULL` | | `AE` | `LOAD` | | `VD` | `DEPARTURE_FROM_STOP` | | `VA` | `ARRIVAL_AT_STOP` | | `UV` | `DISCHARGE` | | `GT`, `GD` | `GATE_OUT_FULL` | | `GR` | `GATE_IN_EMPTY` | `details.ocean.code` is filled when the event names its port role, for example `VESSEL_DEPARTURE_FROM_TRANSSHIPMENT_PORT`. When Dockflow does not know the role, the field is absent. `dateTimes` holds `PLANNED`, `ESTIMATE` and `ACTUAL` entries, with `selected` on the one the event shows. A container on an event sits in `dateTimes[].sourceIdentifiers` as `CONTAINER_ID`. Identical events of several containers are one parent event that names them all, and a child read shows only its own. `source` is `CARRIER` or `UNKNOWN`. Other Dockflow events, such as customs events, are left out. ### Segments and states A route segment is `OCEAN` unless the departure or arrival event states `BARGE`, `RAIL` or `TRUCKLOAD`. A state is in transit between an actual departure and arrival, and `AT_STOP` between an actual arrival and the next departure. ## What stays empty | Field | Value | | ----- | ----- | | `positions`, `costs`, `contents`, `exceptions` | `[]`. | | `attributes`, `accessGroups`, `plan`, `shipmentShareLink`, `sharingContext`, `sensorContext`, `emissions` | Absent. | | `receivedDateTime`, `estimateLastCalculatedDateTime`, `plannedEndDateTime` | Absent. | | `location.masterLocationId`, street address | Absent. | | Terminal name | No field carries it. | `routeInfo`, `events`, `plan`, `attributes`, `accessGroups`, `costs`, `contents` and `shipmentShareLink` in a request are not applied. The answer says so in `problems`. A tradeflow with several shipments answers reads from its first shipment. Webhooks deliver one body per shipment, under the same `id`. ## Webhooks The `/webhooks` routes answer `403`. Contact [support@dockflow.com](mailto:support@dockflow.com) to add a destination in the `project44-v4` format. You receive its signing secret once. It cannot be read again. Each delivery is one `POST` whose body has the shape of `GET /shipments/{id}/tracking/history`: `shipment`, `events`, `positions`, `states`, `costs`, `contents` and `exceptions`. Retries, timeouts and ordering follow [Webhook delivery](/api/upstream#webhook-delivery). | Header | Value | | ------ | ----- | | `X-Dockflow-Signature` | `sha256=`: HMAC-SHA256 of the exact body with your signing secret. | | `X-Dockflow-Timestamp` | Unix seconds. It is not signed, so it gives no replay protection. | ### Verify the signature Compute the HMAC over the exact bytes you received, before any JSON parsing: ```python import hashlib, hmac def is_authentic(raw_body: bytes, header: str, secret: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header) ``` ## How to test 1. Create a key with the **UAT** environment and the **Downstream** scope. It works on `https://uat.dockflow.com/compat/project44` and writes to your [UAT organisation](/api/uat). A real number is really tracked there. 2. Exchange and create: ```bash curl -X POST https://uat.dockflow.com/compat/project44/api/v4/oauth2/token \ -d grant_type=client_credentials -d client_id=acme \ -d client_secret=uat_acme-logistics_Zk3n1c0Q2m9... curl -X POST https://uat.dockflow.com/compat/project44/api/v4/shipments/tracking \ -H "Authorization: Bearer uat_acme-logistics_Zk3n1c0Q2m9..." \ -H "Content-Type: application/json" \ -d '{"identifiers":[{"type":"BILL_OF_LADING","value":"ANR00001010"},{"type":"CARRIER_SCAC","value":"MAEU"}]}' ``` 3. Read `GET /api/v4/shipments/{id}/tracking/history` with the `id` from the answer. The import runs after the answer, so the first read can be short. 4. To test a webhook, open the destination in the console and click **Send test** with the **Live tradeflow** scenario, naming the stored reference of a tradeflow that has data. The **Ocean basic** scenario is refused for this format. Polling is also possible: `GET /v3/upstream?format=project44-v4` with an `Upstream` key returns these same history objects. See [Upstream filters](/api/upstream#11--filters). --- ## Migrate from ShipsGo A client built for the ShipsGo v2 ocean API can talk to Dockflow by changing two values. The paths, the request bodies and the response shapes stay as they are. The responses and error bodies on this surface are checked by contract tests against ShipsGo's public OpenAPI document. ## What to change | Setting | Before | After | | ------- | ------ | ----- | | Base URL | Your ShipsGo v2 base URL | `https://api.dockflow.com/compat/shipsgo/v2` | | `X-Shipsgo-User-Token` | Your ShipsGo token | A Dockflow API key | The header name does not change. ## Get a token 1. Sign in to the [API Console](https://console.dockflow.com) and open **API Keys**. 2. Click **Create Key**, choose the **Production** environment and the **Downstream** scope. 3. Copy the key. It is shown only once. See [Authorization](/api/authorization) for the key format and for revoking a key. ## What is covered | Route | What it does | | ----- | ------------ | | `POST /ocean/shipments` | Starts tracking. Answers `200` with `message: SUCCESS` and the short shipment. A shipment that exists already answers `409` with `message: ALREADY_EXISTS` and that shipment. | | `GET /ocean/shipments` | The list, with `skip` and `take` (default 25, at most 100). `meta.more` says if another page follows, and `meta.total` is `null` while it does. | | `GET /ocean/shipments/{id}` | One shipment, with its containers and movements. | | `PATCH /ocean/shipments/{id}` | Changes `reference`. | | `DELETE /ocean/shipments/{id}` | Stops tracking and removes the shipment. | | `GET /ocean/carriers` | The carriers Dockflow knows, with `skip` and `take`. Filters: `filters[status]=eq:` and `filters[name]=contains:`. Every carrier reports `status: ACTIVE`. | `POST` and `DELETE` on followers and tags answer `403` with `message: FORBIDDEN`. `GET /ocean/shipments/{id}/geojson` answers `404`. Air routes are not part of this surface. ### Identity The duplicate rule is the client reference plus the booking number, or plus the container number when there is no booking. Dockflow stores this pair as the tradeflow reference: `~`, or `SHIPSGO-` without a client reference. This stored form is what your colleagues see on the tradeflow in Dockflow. A duplicate `POST` still sends its identifiers to the import, so it can add a container to the existing shipment. ### Errors | Status | `message` | When | | ------ | --------- | ---- | | `401` | `UNAUTHORIZED` | The token is missing, malformed or unknown. | | `403` | `FORBIDDEN` | The token is revoked or lacks the scope, or a `PATCH` or `DELETE` touches a shipment another organisation created. | | `404` | `NOT_FOUND` | No shipment with this id belongs to your organisation. | | `409` | `ALREADY_EXISTS` | See above. | | `422` | `VALIDATION_FAILED` | The body holds `errors`, a map of field to problem. | ## Field mapping ### Create | ShipsGo field | Dockflow | | ------------- | -------- | | `reference` | The first part of the tradeflow reference. 5 to 128 characters. | | `booking_number` | Booking reference. A master bill of lading sent in this field is also stored as a booking reference. | | `container_number` | Container reference. | | `carrier` | Passed on as the carrier name of that SCAC. An unknown SCAC or an `SG_` code is accepted and not passed on. | | `followers`, `tags` | Validated and dropped. No follower mail is sent. | ### Events | Dockflow code | `event` | | ------------- | ------- | | `GE` | `EMSH` | | `GN` | `GTIN` | | `AE` | `LOAD` | | `VD` | `DEPA` | | `VA` | `ARRV` | | `UV` | `DISC` | | `GT`, `GD` | `GTOT` | | `GR` | `EMRT` | A movement has `status: ACT` when it happened and `status: EST` when it is expected. Dockflow events with other codes are left out. ### Shipment status | `status` | When | | -------- | ---- | | `UNTRACKED` | Inactive and no events. | | `NEW` | Active, with no events, no plan, no containers and no update yet. | | `INPROGRESS` | Active with no events, once a plan, a container or an update exists. | | `DISCHARGED` | An actual empty return, or an actual discharge, gate out or delivery at the port of discharge. | | `ARRIVED` | An actual vessel arrival at the port of discharge. | | `SAILING` | An actual departure, or an actual arrival or discharge at another port. | | `LOADED` | An actual load. | | `BOOKED` | Events exist, none of the above. | ### Container status A container takes the status of its latest actual movement: `EMPTY_SHIPPER`, `GATE_IN`, `LOADED`, `SAILING`, `ARRIVED`, `DISCHARGED`, `GATE_OUT` or `EMPTY_RETURN`. An arrival or discharge away from the port of discharge reads `SAILING`. Without an actual movement the status is `UNKNOWN`. `type` is `HC` for an ISO high cube general purpose code, and the ISO code otherwise. ## What stays empty | Field | Value | | ----- | ----- | | `tokens` | `{}`. Dockflow has no map link. | | `creator` | `{}`. | | `followers`, `tags` | Always `[]`. | | `checked_at`, `route.co2_emission` | `null`. | | `route` | `null` until the plan knows a port of loading and a port of discharge. | | `route.port_of_loading.date_of_loading_initial`, `route.port_of_discharge.date_of_discharge_initial` | Absent when no event carries them. | | `route.port_of_discharge.date_of_discharge_predicted` | `null` when no event carries it. | | `event.triggered_by.email` | Absent. | | `X-Shipsgo-Credits-*`, `X-RateLimit-*` response headers | Absent. | `booking_number` and `container_number` show only the identifying number until the import has run. `discarded_at` holds the last change of an inactive shipment, because Dockflow stores no separate deactivation time. ## What behaves differently - List filters other than `filters[id]=eq:` and `filters[container_number]=eq:` answer `422`. Every `order_by` answers `422`. - The list is ordered by last change, oldest first. - Only the `OCEAN.SHIPMENTS.SHIPMENT_UPDATED` webhook exists. ## Webhooks Webhook destinations are set up on the Dockflow side. Contact [support@dockflow.com](mailto:support@dockflow.com) to add a destination in the `shipsgo-v2` format. You receive its signing secret once. It cannot be read again. Each delivery is one `POST` with this body: ```json { "event": { "id": "6f1c2b7a-9d3e-4c10-8a55-1e2f3a4b5c6d", "name": "OCEAN.SHIPMENTS.SHIPMENT_UPDATED", "triggered_by": { "name": "Dockflow" } }, "shipment": { "id": 74123, "status": "SAILING" } } ``` `shipment` is the full object that `GET /ocean/shipments/{id}` returns; the example shortens it. The event `id` is the same for a redelivery of the same body. Retries, timeouts and ordering follow [Webhook delivery](/api/upstream#webhook-delivery). | Header | Value | | ------ | ----- | | `X-Shipsgo-Webhook-Id` | The id of the destination. | | `X-Shipsgo-Webhook-Name` | The slug of the destination, not its display name. | | `X-Shipsgo-Webhook-Signature` | HMAC-SHA256 of the body with your signing secret, as bare hex. | | `X-Dockflow-Signature` | The same value as `sha256=`. | | `X-Dockflow-Timestamp` | Unix seconds. It is not signed, so it gives no replay protection. | ### Verify the signature Compute the HMAC over the exact bytes you received, before any JSON parsing: ```python import hashlib, hmac def is_authentic(raw_body: bytes, header: str, secret: str) -> bool: expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header) ``` Pass the value of `X-Shipsgo-Webhook-Signature` as `header`. For `X-Dockflow-Signature`, compare against `"sha256=" + expected`. An existing ShipsGo verification keeps working when it computes the same bare hex value. ## How to test 1. Create a key with the **UAT** environment and the **Downstream** scope. It works on `https://uat.dockflow.com/compat/shipsgo/v2` and writes to your [UAT organisation](/api/uat). A real number is really tracked there. 2. Create a shipment: ```bash curl -X POST https://uat.dockflow.com/compat/shipsgo/v2/ocean/shipments \ -H "X-Shipsgo-User-Token: uat_acme-logistics_Zk3n1c0Q2m9..." \ -H "Content-Type: application/json" \ -d '{"reference":"PO-4564268","booking_number":"94512540"}' ``` 3. Read it with the `id` from the answer. The import runs after the answer, so `status` starts at `NEW`. ```bash curl https://uat.dockflow.com/compat/shipsgo/v2/ocean/shipments/74123 \ -H "X-Shipsgo-User-Token: uat_acme-logistics_Zk3n1c0Q2m9..." ``` 4. To test a webhook, open the destination in the console and click **Send test** with the **Live tradeflow** scenario, naming the stored reference of a tradeflow that has data. The **Ocean basic** scenario is refused for this format. Polling is also possible: `GET /v3/upstream?format=shipsgo-v2` with an `Upstream` key returns these same shipment objects. See [Upstream filters](/api/upstream#11--filters). --- ## Migrate from Windward A client built for Windward's ocean tracking GraphQL API can talk to Dockflow by changing the endpoint URL and the credential value. Your queries, mutations and variables stay as they are. :::note Windward's endpoint URL and authentication are not public. The endpoint and the authentication below are Dockflow's own, not a copy of Windward's, so expect to change those two settings exactly as described. The first migration runs together with Dockflow and validates this surface. Contact [support@dockflow.com](mailto:support@dockflow.com) to plan it. ::: ## What to change | Setting | After | | ------- | ----- | | Endpoint | `POST https://api.dockflow.com/compat/windward/graphql` | | Credential | A Dockflow API key, in `Authorization: Bearer ` or in `x-api-key: ` | If your client calls `publicAPIToken(clientId, clientSecret)` first, keep that call and put the Dockflow API key in `clientSecret`. Dockflow verifies it and returns it as the token. `clientId` is ignored. ## Get a token 1. Sign in to the [API Console](https://console.dockflow.com) and open **API Keys**. 2. Click **Create Key**, choose the **Production** environment and the **Downstream** scope. 3. Copy the key. It is shown only once. See [Authorization](/api/authorization) for the key format and for revoking a key. ## What is covered | Operation | What it does | | --------- | ------------ | | `publicAPIToken` | The exchange above. | | `upsertTrackedShipments` | Starts tracking, or updates a shipment named by `shipmentId` or by the same identifiers. At most 500 items per call. | | `updateTrackedShipmentsMetadata` | Updates the job number and the metadata. A job number already held by another shipment gives `BAD_USER_INPUT`. | | `deleteTrackedShipments` | Stops tracking and removes the shipments. Returns `false` when an id is not found. | | `trackedShipments` | The list, with `limit` (default 50, at most 500), `offset`, `filterBy` and `searchTerm`. | | `trackedShipmentsByIds` | Reads by id. | | `carriers` | The carriers Dockflow knows, with an optional `aliases` filter. | Windward's vessel screening, company screening, detention and demurrage and early detection operations are not part of this surface. They fail GraphQL validation. ### Errors Input and execution errors are in `errors[]` with HTTP `200`. A filter that cannot be honoured gives `BAD_USER_INPUT`, because ignoring it would return the wrong rows. A refused credential keeps its Dockflow status (`400`, `401` or `403`) with an `errors[]` body and `extensions.code` set to `UNAUTHENTICATED` or `FORBIDDEN`. ### Identity - One tracked object is one Dockflow tradeflow. Ids are 24 hex characters derived from the tradeflow. Ids that Windward issued do not resolve, so upsert your shipments once. - `metadata.jobNumber` is the tradeflow reference. Without one, Dockflow generates a reference from the container, else the bill of lading, else the booking. A job number given later renames that tradeflow. - A job number shared by several containers is one tradeflow with one id. A bill of lading that resolves to several containers is also one record. Such a record has `containerNumber` and `container` set to `null`. The upsert answer repeats what you sent. - A new upsert of a container without its earlier job number opens a second tradeflow. ## Field mapping ### Input | Windward field | Dockflow | | -------------- | -------- | | `containerNumber` | Container reference. | | `bol` | Bill of lading reference. | | `carrierBookingReference` | Booking reference. | | `scac` | Carrier SCAC. | | `metadata.jobNumber` | Tradeflow reference. | | `metadata.pol`, `metadata.pod` | Port of loading, port of discharge. | | `metadata.eta`, `metadata.etd` | Estimated arrival, estimated departure. | | `metadata.businessData` | Custom references. They read back on GraphQL and are empty in webhook bodies. | `customer`, `masterShipment` and `shipmentsUploadId` are accepted and not used. ### Events | Dockflow code | Windward event | | ------------- | -------------- | | `GE` | `EQUIPMENT` `GTOT`, `EMPTY` | | `GN` | `EQUIPMENT` `GTIN`, `LADEN` | | `AE` | `EQUIPMENT` `LOAD`, `LADEN` | | `VD` | `TRANSPORT` `DEPA` | | `VA` | `TRANSPORT` `ARRI` | | `UV` | `EQUIPMENT` `DISC`, `LADEN` | | `GT`, `GD` | `EQUIPMENT` `GTOT`, `LADEN` | | `GR` | `EQUIPMENT` `GTIN`, `EMPTY` | Other Dockflow events are left out. `shipmentLocationTypeCode` and the milestone `type` are `PREPOL`, `POL`, `TSP`, `POD` or `POSTPOD`, from the port roles of the plan. Timestamp codes are `ACT`, `PLN` and `EST`. `DateTime` values are UTC in GraphQL records and local time with offset in webhook bodies. ## What stays empty | Field | Value | | ----- | ----- | | `sharedShipmentLink` | `null`. | | `status.id`, `status.delay.podETA`, `status.delay.reasons` | `null` or `[]`. | | `vessel.flag`, `vessel.class` | `null`. | | `response.latestShipmentDataFetch`, `response.latestSuccessfulDataFetch` | `null`. | | `response.winningIdentifier.type` | `null`. | | `route.geojson` | `null`, in webhook bodies. | | `shipmentEventTypeCode` | `[]`. | | `metadata.eta`, `etd`, `pol`, `pod` | `null` on read. An upsert answer repeats what you sent. | | `carrierLatestStatus.status.type` | `null`, in webhook bodies. | | `port.properties.areaType`, `secondaryLocodes` | `null` or `[]`. | `response.message` only takes `DATA_AVAILABLE` and `NO_DATA_RECEIVED`. ## What behaves differently - `filterBy` supports `trackedShipment_id` and `shipment_container_number` with one value. An empty condition is ignored. Other conditions give `BAD_USER_INPUT`. - `searchTerm` is an exact container number or job number. - `sort` and `orderBy` are accepted and ignored. The list is ordered by last change, oldest first. - A new shipment reads back by id at once. It enters the list when its import has created a shipment. - The `showAISEvents` filter is accepted and has no effect. ## Webhooks Contact [support@dockflow.com](mailto:support@dockflow.com) to add a destination in the `windward` format. You receive its signing secret once. It cannot be read again. Each delivery is one `POST`: ```json { "eventTypes": ["CARRIER_ETA_UPDATE"], "data": { "shipment": { "identifiers": { "trackedShipmentId": "000000000000000000012345" } }, "metadata": { "jobNumber": "PO4564268", "businessData": [] } } } ``` The example shortens `shipment`. It also holds `milestones`, `events`, `vessels`, `ports`, `predicted`, `carrierLatestStatus`, `delay` and `response`. Dockflow delivers once per change and sends the whole record, so the type is chosen from the record and is not a comparison with an earlier state. The first that applies wins: `COMPLETED` (tracking is inactive and has events), `CARRIER_ATA_UPDATE`, `PREDICTED_ETA_UPDATE`, `CARRIER_ETA_UPDATE`, `CARRIER_VESSEL_UPDATE`, `CARRIER_STATUS_UPDATE`, `NEW_SHIPMENT`. Retries, timeouts and ordering follow [Webhook delivery](/api/upstream#webhook-delivery). | Header | Value | | ------ | ----- | | `X-Dockflow-Signature` | `sha256=`: HMAC-SHA256 of the exact body with your signing secret. | | `X-Dockflow-Timestamp` | Unix seconds. It is not signed, so it gives no replay protection. | ### Verify the signature Compute the HMAC over the exact bytes you received, before any JSON parsing: ```python import hashlib, hmac def is_authentic(raw_body: bytes, header: str, secret: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header) ``` ## How to test 1. Create a key with the **UAT** environment and the **Downstream** scope. It works on `https://uat.dockflow.com/compat/windward/graphql` and writes to your [UAT organisation](/api/uat). A real number is really tracked there. 2. Upsert a shipment: ```bash curl -X POST https://uat.dockflow.com/compat/windward/graphql \ -H "x-api-key: uat_acme-logistics_Zk3n1c0Q2m9..." \ -H "Content-Type: application/json" \ -d '{"query":"mutation($s:[TrackedShipmentInput!]!){upsertTrackedShipments(shipments:$s){id}}","variables":{"s":[{"bol":"ANR00001010","scac":"MAEU","metadata":{"jobNumber":"PO4564268"}}]}}' ``` 3. Read it with `trackedShipmentsByIds` and the `id` from the answer. The import runs after the answer, so the first read can be short. 4. To test a webhook, open the destination in the console and click **Send test** with the **Live tradeflow** scenario, naming the job number of a tradeflow that has data. The **Ocean basic** scenario is refused for this format. Polling is also possible: `GET /v3/upstream?format=windward` with an `Upstream` key returns these records. See [Upstream filters](/api/upstream#11--filters).