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.
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 [email protected] 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 <key> or in x-api-key: <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
- Sign in to the API Console and open API Keys.
- Click Create Key, choose the Production environment and the Downstream scope.
- Copy the key. It is shown only once.
See 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.jobNumberis 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
containerNumberandcontainerset tonull. 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
filterBysupportstrackedShipment_idandshipment_container_numberwith one value. An empty condition is ignored. Other conditions giveBAD_USER_INPUT.searchTermis an exact container number or job number.sortandorderByare 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
showAISEventsfilter is accepted and has no effect.
Webhooks
Contact [email protected] to add a destination in the windward format. You receive its signing secret once. It cannot be read again.
Each delivery is one POST:
{
"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.
| Header | Value |
|---|---|
X-Dockflow-Signature | sha256=<hex>: 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:
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
- Create a key with the UAT environment and the Downstream scope. It works on
https://uat.dockflow.com/compat/windward/graphqland writes to your UAT organisation. A real number is really tracked there. - Upsert a shipment:
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"}}]}}'
- Read it with
trackedShipmentsByIdsand theidfrom the answer. The import runs after the answer, so the first read can be short. - 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.