# Cubic API — complete documentation > Read-only REST API for Cubic customers to query their own shipments: status, route and milestones, carrier and bill numbers, containers, packages and parcel tracking. This file is every page of https://developers.gocubic.io concatenated. Index: https://developers.gocubic.io/llms.txt. OpenAPI 3.1 document: https://developers.gocubic.io/openapi.json. --- # Overview Source: https://developers.gocubic.io/index.md > A read-only REST API for Cubic customers to query their own shipments. ## Endpoints Base URL: `https://api.gocubic.io/v1` | Method | Path | Purpose | |---|---|---| | `GET` | `/v1/me` | Verify the key. Call this first. | | `GET` | `/v1/shipments` | List shipments, most recently updated first. | | `GET` | `/v1/shipments/{reference}` | One shipment by its Cubic reference, for example `QB10293`. | | `GET` | `/v1/openapi.json` | The OpenAPI 3.1 document. No authentication. | Both shipment endpoints return the same [Shipment object](https://developers.gocubic.io/shipment-object.md). ## Conventions - **Auth**: `Authorization: Bearer `, with the key in `CUBIC_API_KEY`. One key belongs to one customer account. See [Authentication](https://developers.gocubic.io/authentication.md). - **Format**: JSON. camelCase keys. Enum values are case-sensitive (`OceanFCL`, `EXW`). - **Nulls**: `null` or an empty list means "not known yet" or "does not apply"; never an error. - **Dates**: milestone dates are local `yyyy-MM-dd` with an optional local `HH:mm`, no time zone. `createdAt`, `updatedAt` and `cancelledAt` are ISO 8601 UTC. - **Units**: as recorded, not normalised. Read the `unit` next to every `value`. - **Pagination**: follow `nextCursor` until it is `null`. See [Pagination](https://developers.gocubic.io/pagination.md). - **Errors**: RFC 9457 `application/problem+json` with a `code` and a `hint`. See [Errors](https://developers.gocubic.io/errors.md). - **Rate limit**: 60 requests per minute per key. See [Rate limits](https://developers.gocubic.io/rate-limits.md). ## Machine-readable docs | Resource | URL | |---|---| | Index of these docs | [`https://developers.gocubic.io/llms.txt`](https://developers.gocubic.io/llms.txt) | | All of these docs as one markdown file | [`https://developers.gocubic.io/llms-full.txt`](https://developers.gocubic.io/llms-full.txt) | | OpenAPI 3.1 document | [`https://developers.gocubic.io/openapi.json`](https://developers.gocubic.io/openapi.json) | | Any page as markdown | Append `.md` to its URL: `https://developers.gocubic.io/quickstart.md`. This page is `https://developers.gocubic.io/index.md`. | AI agents: read [For AI agents](https://developers.gocubic.io/for-ai-agents.md) next. Everyone else: [Quickstart](https://developers.gocubic.io/quickstart.md). --- # Quickstart Source: https://developers.gocubic.io/quickstart.md > Get a key, verify it, and list shipments, in curl, TypeScript and Python. ## 1. Get an API key Create a key at [https://app.gocubic.io/settings/api-keys](https://app.gocubic.io/settings/api-keys) (Cubic app → **Settings → API keys**). It is shown once. Every example reads it from an environment variable: ```bash export CUBIC_API_KEY="cubic_live_..." ``` ## 2. Verify the key ```bash curl -s https://api.gocubic.io/v1/me \ -H "Authorization: Bearer $CUBIC_API_KEY" ``` ```json { "account": { "name": "Brightwell Home Ltd" }, "apiKey": { "name": "Inventory sync", "prefix": "cubic_live_9f3k", "createdAt": "2026-09-01T08:15:00Z" } } ``` A `401` means the key is missing or wrong; the body says which. See [Errors](https://developers.gocubic.io/errors.md). ## 3. List shipments ```bash curl -s "https://api.gocubic.io/v1/shipments?status=Active&limit=5" \ -H "Authorization: Bearer $CUBIC_API_KEY" ``` Returns `{ "data": [Shipment, …], "nextCursor": "…" | null }`. Each item is the full [Shipment object](https://developers.gocubic.io/shipment-object.md). For further pages see [Pagination](https://developers.gocubic.io/pagination.md). ## 4. Get one shipment ```bash curl -s https://api.gocubic.io/v1/shipments/QB10293 \ -H "Authorization: Bearer $CUBIC_API_KEY" ``` ## The same in TypeScript No dependencies. Run with `npx tsx quickstart.mts` (Node 18+) or `bun quickstart.mts`. ```ts const BASE_URL = 'https://api.gocubic.io/v1'; const headers = { Authorization: `Bearer ${process.env.CUBIC_API_KEY}` }; async function get(path: string) { const response = await fetch(BASE_URL + path, { headers }); const body = await response.json(); if (!response.ok) throw new Error(`${body.code}: ${body.detail} Hint: ${body.hint}`); return body; } const me = await get('/me'); console.log(`Connected to ${me.account.name} with key ${me.apiKey.prefix}`); const { data, nextCursor } = await get('/shipments?status=Active&limit=5'); for (const shipment of data) { console.log(shipment.reference, shipment.status, shipment.serviceType); } console.log(nextCursor ? 'More pages available' : 'That is everything'); if (data.length > 0) { const shipment = await get(`/shipments/${data[0].reference}`); console.log(shipment.milestones.map((milestone: { type: string }) => milestone.type).join(' → ')); } ``` ## The same in Python `pip install requests`, then `python quickstart.py`. ```python import os import requests BASE_URL = "https://api.gocubic.io/v1" session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['CUBIC_API_KEY']}" def get(path, **params): response = session.get(BASE_URL + path, params=params, timeout=30) body = response.json() if not response.ok: raise RuntimeError(f"{body['code']}: {body['detail']} Hint: {body['hint']}") return body me = get("/me") print(f"Connected to {me['account']['name']} with key {me['apiKey']['prefix']}") page = get("/shipments", status="Active", limit=5) for shipment in page["data"]: print(shipment["reference"], shipment["status"], shipment["serviceType"]) print("More pages available" if page["nextCursor"] else "That is everything") if page["data"]: shipment = get(f"/shipments/{page['data'][0]['reference']}") print(" → ".join(milestone["type"] for milestone in shipment["milestones"])) ``` ## Next - [The Shipment object](https://developers.gocubic.io/shipment-object.md) - [Shipment types](https://developers.gocubic.io/shipment-types.md) - [Recipes](https://developers.gocubic.io/recipes.md) --- # For AI agents Source: https://developers.gocubic.io/for-ai-agents.md > For a coding agent building on the Cubic API — what to read, the order of operations, and the mistakes to avoid. ## What to read | Need | Read | |---|---| | Everything, in one fetch | [`https://developers.gocubic.io/llms-full.txt`](https://developers.gocubic.io/llms-full.txt) | | Exact schemas, enums, parameters, error examples | [`https://developers.gocubic.io/openapi.json`](https://developers.gocubic.io/openapi.json) | | An index of pages | [`https://developers.gocubic.io/llms.txt`](https://developers.gocubic.io/llms.txt) | | One page as markdown | Append `.md` to any page URL: `https://developers.gocubic.io/recipes.md` | ## Order of operations 1. Read the key from `CUBIC_API_KEY`. If it is not set, ask your user for it. Never write it into source code. 2. Call `GET https://api.gocubic.io/v1/me`. `200` confirms the key and names the account. On `401`, stop and report the `hint` to your user. 3. Call `GET /v1/shipments?limit=5` and look at real data before writing parsing code. 4. Build on the [client in Recipes](https://developers.gocubic.io/recipes.md#client): it handles auth, cursors and `429`. 5. On any error, read `code` and `hint` in the response body and follow the hint. The four endpoints on the [Overview](https://developers.gocubic.io/index.md) are the whole API. If your user asks for anything else, say the API does not offer it. ## Common mistakes | Mistake | Instead | |---|---| | Treating `null` as an error. | `null` means "not known yet" or "does not apply". Handle it on every nullable field. | | Reading `milestones[3]`, or assuming a `DestinationPort` milestone exists. | Find milestones by `type` and handle "not found". `portOfDischarge` can be `null`. | | Assuming every milestone has a `place`. | `IntermediaryWarehouse` and `BorderCrossing` milestones always have `place: null`. | | Assuming kilograms, centimetres or cubic metres. | Read `unit` on every measurement and convert explicitly. | | Treating `packages[].weight` as the line total. | It is the weight of one package. Multiply by `quantity`. | | Counting containers on LCL, DDP or DDU shipments as the customer's. | They are shared consolidation containers. Only FCL containers are the customer's own. | | Flagging `incoterms: EXW` on a DDP shipment as inconsistent. | `incoterms` only says who arranges the origin pickup. | | Converting milestone dates to UTC or another time zone. | They are local calendar dates. Compare them as strings. | | Treating an `arrival` with `actual: false` as having happened. | It is an estimate and can change. | | Working out where a shipment is from the `completed` flags. | Read `milestones[currentMilestoneIndex]`. | | Rejecting unknown `parcels.carrier` values. | It is an open set. | | Fetching `GET /v1/shipments/{reference}` for each list item. | List items are already full Shipment objects. | | Reusing a cursor with different filters. | Cursors are tied to the filters that produced them. On `invalid_cursor`, restart without `cursor`. | | Looking up a PO number with `GET /v1/shipments/{reference}`. | That path takes the Cubic reference only. Use `?tag=` (exact) or `?search=` for POs, containers, master bills and FBA IDs. | | Reading `404 shipment_not_found` as "does not exist". | It means "not on this key's account". | | Retrying `400`, `401` or `404`. | Retry only `429` (after `Retry-After`) and `5xx` (with backoff). | | Re-reading every shipment on a schedule. | Poll with `updatedSince` and upsert by `reference`. | --- # Authentication Source: https://developers.gocubic.io/authentication.md > Bearer API keys. One key belongs to one customer account. Send the API key in the `Authorization` header of every request: ```http GET /v1/me HTTP/1.1 Host: api.gocubic.io Authorization: Bearer cubic_live_... ``` `GET /v1/openapi.json` is the only endpoint that needs no key. ## Keys - Keys start with `cubic_live_`. - Create and revoke them at [https://app.gocubic.io/settings/api-keys](https://app.gocubic.io/settings/api-keys) (Cubic app → **Settings → API keys**). - A key is shown once, at creation. A lost key cannot be recovered; create a new one and revoke the old one. - Read the key from the `CUBIC_API_KEY` environment variable. Never commit it, put it in client-side code, or send it in a query string. ## Scope One key belongs to exactly one customer account, and every response contains that account's data only. A shipment that belongs to another account returns `404 shipment_not_found`. ## Verify a key `GET /v1/me` returns the account and key behind the request. ```bash curl -s https://api.gocubic.io/v1/me -H "Authorization: Bearer $CUBIC_API_KEY" ``` ```json { "account": { "name": "Brightwell Home Ltd" }, "apiKey": { "name": "Inventory sync", "prefix": "cubic_live_9f3k", "createdAt": "2026-09-01T08:15:00Z" } } ``` `apiKey.prefix` is the leading characters of the key and is safe to log. A missing key returns `401 missing_api_key`; an unknown or revoked key returns `401 invalid_api_key`. See [Errors](https://developers.gocubic.io/errors.md). --- # Pagination Source: https://developers.gocubic.io/pagination.md > Cursor pagination on GET /v1/shipments. Follow nextCursor until it is null. `GET /v1/shipments` returns one page at a time, most recently updated first: ```json { "data": [{ "reference": "QB51907", "status": "Active", "…": "…" }], "nextCursor": "eyJ1IjoiMjAyNi0wOS0xNVQyMTo0ODowMFoiLCJyIjoiUUI1MTkwNyJ9" } ``` | Parameter | Meaning | |---|---| | `limit` | Page size, 1–100. Default 50. | | `cursor` | The `nextCursor` value of the previous response, unchanged. | ## Rules - Next page: repeat the request with the **same filters** and `cursor` set to the previous `nextCursor`. - `nextCursor: null` means this is the last page. - Cursors are opaque. - An unusable cursor returns `400 invalid_cursor`. Restart without `cursor`. - There is no total count and no page numbers. - A shipment that changes while you paginate moves to the front, so it can appear twice or be skipped within one run. Upsert by `reference` and [poll with `updatedSince`](https://developers.gocubic.io/recipes.md#poll-for-changes). ## Filters All filters are optional and combine with AND. | Parameter | Meaning | |---|---| | `status` | One of `Active`, `Delivered`, `Cancelled`. Repeat it to match any of several: `?status=Active&status=Delivered`. | | `updatedSince` | ISO 8601 timestamp. Shipments whose `updatedAt` is at or after it. | | `createdSince` | ISO 8601 timestamp. Shipments whose `createdAt` is at or after it. | | `tag` | Exact match on one of the customer's own tags (PO numbers). | | `search` | Free text across the reference, tags, master bill / air waybill numbers, container numbers, and Amazon FBA and reference IDs. | An invalid value returns `400 invalid_parameter` naming the `parameter`; the `hint` lists the allowed values. ## Read every page ```ts const BASE_URL = 'https://api.gocubic.io/v1'; const headers = { Authorization: `Bearer ${process.env.CUBIC_API_KEY}` }; const shipments = []; let cursor: string | null = null; do { const url = new URL(`${BASE_URL}/shipments`); url.searchParams.set('limit', '100'); url.searchParams.set('status', 'Active'); if (cursor) url.searchParams.set('cursor', cursor); const response = await fetch(url, { headers }); if (!response.ok) throw new Error(JSON.stringify(await response.json())); const page = await response.json(); shipments.push(...page.data); cursor = page.nextCursor; } while (cursor); console.log(`${shipments.length} active shipments`); ``` ```python import os import requests BASE_URL = "https://api.gocubic.io/v1" headers = {"Authorization": f"Bearer {os.environ['CUBIC_API_KEY']}"} shipments = [] params = {"limit": 100, "status": "Active"} while True: response = requests.get(f"{BASE_URL}/shipments", headers=headers, params=params, timeout=30) response.raise_for_status() page = response.json() shipments.extend(page["data"]) if page["nextCursor"] is None: break params["cursor"] = page["nextCursor"] print(f"{len(shipments)} active shipments") ``` The client in [Recipes](https://developers.gocubic.io/recipes.md#client) wraps this loop and adds 429 backoff. --- # Errors Source: https://developers.gocubic.io/errors.md > RFC 9457 application/problem+json with a machine-readable code and a hint. Every non-2xx response has the content type `application/problem+json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)) and this shape: ```json { "type": "https://developers.gocubic.io/errors/invalid-api-key", "title": "Invalid API key", "status": 401, "code": "invalid_api_key", "detail": "The key in the Authorization header is not recognised or was revoked.", "hint": "Send 'Authorization: Bearer '. Create a key at https://app.gocubic.io/settings/api-keys." } ``` | Field | Meaning | |---|---| | `type` | URI of this error's section on this page. | | `title` | Short summary. | | `status` | The HTTP status code, repeated. | | `code` | Stable, machine-readable. **Branch on this.** | | `detail` | What went wrong with this request. | | `hint` | How to fix the request. | | `parameter` | The offending query parameter. Only on `invalid_parameter`. | ## Error codes | `code` | HTTP status | Cause | Fix | |---|---|---|---| | [`missing_api_key`](#missing-api-key) | 401 | No `Authorization` header, or it is not in the `Bearer ` form. | Send `Authorization: Bearer $CUBIC_API_KEY`. Check the environment variable is set in the process that makes the request. | | [`invalid_api_key`](#invalid-api-key) | 401 | The key is mistyped, truncated, or was revoked. | Do not retry with the same key. Ask the account owner to create a new key at https://app.gocubic.io/settings/api-keys. | | [`shipment_not_found`](#shipment-not-found) | 404 | No shipment has that reference on the account the key belongs to. Shipments of other accounts return the same error. | Check the reference. To find a shipment by any other identifier use `GET /v1/shipments?search=`. | | [`invalid_parameter`](#invalid-parameter) | 400 | A query parameter has a value that is out of range, malformed, or not an allowed enum value. | Read `parameter` to see which one, and `hint` for the allowed values. Correct the request; do not retry unchanged. | | [`invalid_cursor`](#invalid-cursor) | 400 | The `cursor` value was altered, or is reused with different filters than the request that produced it. | Restart from the first page without `cursor`. Never build or modify cursors. | | [`rate_limited`](#rate-limited) | 429 | More than 60 requests in one minute on this key. | Wait `Retry-After` seconds, then retry the same request. | | [`internal_error`](#internal-error) | 500 | A fault on the Cubic side. | Retry with exponential backoff. If it persists, contact Cubic. | ## What to retry | Status | Retry? | |---|---| | 400, 401, 404 | No. Fix the request using `hint`. | | 429 | Yes, after `Retry-After` seconds. See [Rate limits](https://developers.gocubic.io/rate-limits.md). | | 500 | Yes, with exponential backoff. | ## Examples ### `missing_api_key` ```json { "type": "https://developers.gocubic.io/errors/missing-api-key", "title": "Missing API key", "status": 401, "code": "missing_api_key", "detail": "The request has no Authorization header.", "hint": "Send 'Authorization: Bearer '. Create a key at https://app.gocubic.io/settings/api-keys." } ``` ### `invalid_api_key` ```json { "type": "https://developers.gocubic.io/errors/invalid-api-key", "title": "Invalid API key", "status": 401, "code": "invalid_api_key", "detail": "The key in the Authorization header is not recognised or was revoked.", "hint": "Send 'Authorization: Bearer '. Create a key at https://app.gocubic.io/settings/api-keys." } ``` ### `shipment_not_found` ```json { "type": "https://developers.gocubic.io/errors/shipment-not-found", "title": "Shipment not found", "status": 404, "code": "shipment_not_found", "detail": "No shipment with reference 'QB99999' exists on this account.", "hint": "Check the reference (for example 'QB10293'). To look a shipment up by PO number, container, master bill or FBA ID, call GET /v1/shipments?search=." } ``` ### `invalid_parameter` ```json { "type": "https://developers.gocubic.io/errors/invalid-parameter", "title": "Invalid parameter", "status": 400, "code": "invalid_parameter", "detail": "'Shipped' is not a valid value for 'status'.", "hint": "Use one of: Active, Delivered, Cancelled. Repeat the parameter to filter by several statuses.", "parameter": "status" } ``` ### `invalid_cursor` ```json { "type": "https://developers.gocubic.io/errors/invalid-cursor", "title": "Invalid cursor", "status": 400, "code": "invalid_cursor", "detail": "The cursor is malformed or was issued for a different set of filters.", "hint": "Restart pagination: repeat the request without 'cursor', then follow 'nextCursor' from each response." } ``` ### `rate_limited` ```json { "type": "https://developers.gocubic.io/errors/rate-limited", "title": "Rate limit exceeded", "status": 429, "code": "rate_limited", "detail": "This key has made more than 60 requests in the last minute.", "hint": "Wait the number of seconds in the 'Retry-After' header, then retry the same request." } ``` ### `internal_error` ```json { "type": "https://developers.gocubic.io/errors/internal-error", "title": "Internal error", "status": 500, "code": "internal_error", "detail": "An unexpected error occurred while handling the request.", "hint": "Retry with exponential backoff. The request itself is not the problem." } ``` --- # Rate limits Source: https://developers.gocubic.io/rate-limits.md > 60 requests per minute per key, RateLimit headers on every response, and Retry-After on 429. Each API key may make **60 requests per minute**. ## Headers Every response carries: | Header | Meaning | |---|---| | `RateLimit-Limit` | Requests allowed per minute for this key. | | `RateLimit-Remaining` | Requests left in the current window. | | `RateLimit-Reset` | Seconds until the window resets. | A `429` also carries `Retry-After`: seconds to wait before retrying. ```http HTTP/1.1 429 Too Many Requests Content-Type: application/problem+json RateLimit-Limit: 60 RateLimit-Remaining: 0 RateLimit-Reset: 17 Retry-After: 17 ``` ```json { "type": "https://developers.gocubic.io/errors/rate-limited", "title": "Rate limit exceeded", "status": 429, "code": "rate_limited", "detail": "This key has made more than 60 requests in the last minute.", "hint": "Wait the number of seconds in the 'Retry-After' header, then retry the same request." } ``` ## Handling 429 ```ts async function getWithBackoff(url: string, headers: Record) { for (let attempt = 0; ; attempt++) { const response = await fetch(url, { headers }); const retryable = response.status === 429 || response.status >= 500; if (!retryable || attempt === 5) return response; const retryAfter = Number(response.headers.get('Retry-After')); const seconds = retryAfter > 0 ? retryAfter : 2 ** attempt; await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); } } ``` ```python import time import requests def get_with_backoff(url, headers, params=None): for attempt in range(6): response = requests.get(url, headers=headers, params=params, timeout=30) retryable = response.status_code == 429 or response.status_code >= 500 if not retryable or attempt == 5: return response retry_after = response.headers.get("Retry-After") time.sleep(int(retry_after) if retry_after else 2**attempt) ``` ## Staying under the limit - Use `limit=100` when reading many shipments. - List items are full Shipment objects; do not fetch each one again. - Poll with `updatedSince` instead of re-reading everything. - Run requests one after another, not in parallel. --- # The Shipment object Source: https://developers.gocubic.io/shipment-object.md > Every field of the Shipment object, its enums, and the rules for nulls, dates and units. `GET /v1/shipments` and `GET /v1/shipments/{reference}` return the same object. The [OpenAPI document](https://developers.gocubic.io/openapi.json) is the authoritative schema. ## Rules - **Every key is always present.** An unknown value is `null`; an empty list is `[]`. - **`null` means "not known yet" or "does not apply to this shipment"**; it is never an error. A value that is `null` today can be filled in tomorrow. - **Milestone dates are local calendar dates**: `yyyy-MM-dd`, local to the place, with an optional local `HH:mm`. No time zone; do not convert them. They sort correctly as text. - **Timestamps** (`createdAt`, `updatedAt`, `cancelledAt`) are ISO 8601 in UTC. - **Units are as recorded, not normalised.** A weight is `{ "value": 1082, "unit": "KG" }` on one shipment and `{ "value": 7600, "unit": "LB" }` on another, and units can differ within one shipment. - **Enum values are case-sensitive.** Match them exactly. ## Annotated example An ocean DDP shipment, abridged. Complete examples are on [Shipment types](https://developers.gocubic.io/shipment-types.md). ```jsonc { "reference": "QB10293", "status": "Active", // Active | Delivered | Cancelled "serviceType": "OceanDDP", "mode": "Ocean", // Ocean | Air | Land "incoterms": "EXW", // who arranges origin pickup; FOB | EXW | null "tags": ["PO-8841"], // the customer's own PO numbers "descriptionOfGoods": "Yoga mats", "insured": true, "bookingConfirmedByCarrier": true, "origin": { "name": "Shenzhen Lotus Sports Goods Co., Ltd.", "type": "ShippingParty", "city": "Shenzhen", "countryCode": "CN", "code": null, /* … */ }, "portOfLoading": { "name": "Yantian", "type": "Seaport", "code": "CNYTN", "countryCode": "CN", /* … */ }, "portOfDischarge": { "name": "Long Beach", "type": "Seaport", "code": "USLGB", "countryCode": "US", /* … */ }, // can be null "destination": { "name": "ONT8", "type": "FulfillmentCenter", "fulfillmentCenter": "Amazon", "countryCode": "US", /* … */ }, "carrier": { "name": "COSCO", "type": "ShippingLine" }, // null for land "booking": { "bookingNumber": null, "masterBill": "COSU6412345670", // ocean master B/L, or air MAWB "houseBill": "SZX2607001", // ocean house B/L, or air HAWB "cargoCutoff": "2026-07-05", "shippingInstructionCutoff": null, "destinationPortFreeDays": null, "emptyContainerReturnFreeDays": null }, "parcels": { "carrier": "UPS", "trackingNumbers": ["1Z84R2E70312345678"] }, // null when none "amazon": { "fbaIds": ["FBA18K4TQ9ZB"], "referenceIds": ["7KQ2M9XA"] }, // null when none "cargo": { "totalWeight": { "value": 1082, "unit": "KG" }, "totalVolume": { "value": 6.486, "unit": "CubicMeter" }, "chargeableWeight": null, // air and courier only "stackable": null, "containers": [{ "number": "CSNU1234567", "type": "FortyFtHighCube", "sealNumber": "CN8812045" }], // shared container on LCL/DDP/DDU "packages": [{ "type": "Box", "quantity": 62, "hazardous": false, "weight": { "value": 17.45, "unit": "KG" }, // per package, not per line "length": { "value": 48, "unit": "Centimeter" }, "width": { /* … */ }, "height": { /* … */ } }] }, "milestones": [ { "type": "Pickup", "place": { /* … */ }, "warehouseType": null, "cargoReadyDate": "2026-07-03", "arrival": null, "departure": { "date": "2026-07-03", "time": null, "actual": true }, "completed": true, "transportToNext": { "mode": "Truck", "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null } }, { "type": "IntermediaryWarehouse", "warehouseType": "ConsolidationWarehouse", "place": null /* always null */, /* … */ }, { "type": "OriginPort", /* … */ "transportToNext": { "mode": "Voyage", "vessel": "COSCO SHIPPING ANDES", "voyageNumber": "027E", /* … */ } }, { "type": "DestinationPort", "arrival": { "date": "2026-07-28", "time": "08:00", "actual": true }, /* … */ }, { "type": "IntermediaryWarehouse", "warehouseType": "DeconsolidationWarehouse", "place": null, /* … */ }, { "type": "Delivery", "arrival": { "date": "2026-08-04", "time": null, "actual": false } /* estimate */, "transportToNext": null } ], "currentMilestoneIndex": 3, // the DestinationPort milestone: the last one completed "cancelledAt": null, "createdAt": "2026-06-28T10:12:00Z", "updatedAt": "2026-07-29T06:40:00Z" } ``` ## Shipment | Field | Type | Description | |---|---|---| | `reference` | string | Cubic reference. Unique, stable, and the identifier used in `GET /shipments/{reference}`. | | `status` | string | Derived status; the first matching rule wins. `Cancelled`: `cancelledAt` is set. `Delivered`: the `Delivery` milestone has an actual arrival. `Active`: everything else. For where an active shipment is, read `currentMilestoneIndex` and `milestones`. Values: `Active`, `Delivered`, `Cancelled`. | | `serviceType` | string | The service booked. `Express` variants have the same shape as their base type. `DDP`/`DDU` are door-to-door services with duties paid/unpaid. `LandTruck` is a legacy value found only on historical shipments; new land shipments use `LandTruckFTL` or `LandTruckLTL`. Values: `OceanFCL`, `OceanFCLExpress`, `OceanLCL`, `OceanLCLExpress`, `OceanDDU`, `OceanDDP`, `AirFreight`, `AirCourier`, `AirDDU`, `AirDDP`, `LandTruckFTL`, `LandTruckLTL`, `LandRailFCL`, `LandRailLCL`, `LandTruck`. | | `mode` | string | Transport mode of the main leg, implied by `serviceType`. Values: `Ocean`, `Air`, `Land`. | | `incoterms` | string \| null | Who arranges the origin pickup. `EXW`: Cubic collects the cargo from the supplier. `FOB`: the supplier delivers the cargo to the port of loading. It says nothing about duties or door delivery, and it coexists with a DDP/DDU `serviceType`. `null` when not recorded. Values: `FOB`, `EXW`. | | `tags` | string[] | The customer's own tags on the shipment — in practice purchase order numbers. Filter on one with the `tag` query parameter. Empty when none. | | `descriptionOfGoods` | string \| null | Free-text description of the goods. | | `insured` | boolean | Whether the shipment is insured. | | `bookingConfirmedByCarrier` | boolean | Whether the carrier has confirmed the booking. | | `origin` | [Place](#place) \| null | Where the cargo is collected. | | `portOfLoading` | [Place](#place) \| null | Port of loading: the seaport or airport where the main leg starts. `null` when the route has no port (for example land shipments, or courier shipments that list only pickup and delivery). | | `portOfDischarge` | [Place](#place) \| null | Port of discharge: the seaport or airport where the main leg ends. Some routes have no `DestinationPort` milestone; then this is the `Delivery` milestone's place if that is a port (door-to-port shipments), otherwise `null`. Never assume it is set. | | `destination` | [Place](#place) \| null | Where the cargo is delivered. May itself be a port (door-to-port), an Amazon fulfilment centre, or an address. | | `carrier` | [Carrier](#carrier) \| null | The main carrier: the shipping line for ocean, the airline for air, the courier company for `AirCourier`. Always `null` for land shipments, and `null` on other shipments until known. Individual legs carry their own `carrier` in `milestones[].transportToNext`. | | `booking` | [Booking](#booking) | Booking and bill-of-lading details. The object is always present; land and courier shipments usually have every member `null`. `null` means "not known yet" or "does not apply to this shipment"; it is never an error. A value that is not known yet can appear on a later read. | | `parcels` | [Parcels](#parcels) \| null | Last-mile parcel tracking. `null` when there is none. Present on courier shipments and on many DDP/DDU shipments. | | `amazon` | [Amazon](#amazon) \| null | Amazon inbound identifiers. `null` when there are none. | | `cargo` | [Cargo](#cargo) | What is being shipped. The object is always present. Units vary between shipments and even between members of one shipment; convert yourself if you need a single unit. | | `milestones` | [Milestone](#milestone)[] | The route, in order, from `Pickup` to `Delivery`. The milestone types present vary by shipment: never look a milestone up by index, and never assume a given type exists (some routes have no `DestinationPort`, courier shipments may have only `Pickup` and `Delivery`). | | `currentMilestoneIndex` | integer | Index in `milestones` of the last milestone with `completed: true` — where the shipment is, or the milestone it last left. `0` when no milestone is completed yet. The shipment is on its way to the next milestone once this one has an actual `departure`. | | `cancelledAt` | string \| null | When the shipment was cancelled. ISO 8601, UTC. `null` unless cancelled. | | `createdAt` | string | When the shipment was created. ISO 8601, UTC. | | `updatedAt` | string | When anything on the shipment last changed. ISO 8601, UTC. The list is sorted by this, newest first; store the greatest value seen and pass it as `updatedSince` to poll for changes. | ## Place | Field | Type | Description | |---|---|---| | `name` | string \| null | Display name: the company or site name for a `ShippingParty`, the port or airport name for ports, the fulfilment centre code (for example `ONT8`) for a `FulfillmentCenter`. | | `type` | string | Kind of place. `ShippingParty` is a supplier, consignee or warehouse address; `ManualAddress` is a free-typed address. Values: `ShippingParty`, `FulfillmentCenter`, `Seaport`, `Airport`, `InlandTerminal`, `ManualAddress`. | | `city` | string \| null | City. | | `state` | string \| null | State, province or region, where the country uses one. | | `countryCode` | string \| null | ISO 3166-1 alpha-2 country code. | | `code` | string \| null | UN/LOCODE for a `Seaport` or `InlandTerminal` (for example `USLGB`), IATA code for an `Airport` (for example `ORD`). `null` for every other type. | | `fulfillmentCenter` | string \| null | The fulfilment network the place belongs to (for example `Amazon`) when `type` is `FulfillmentCenter`; otherwise `null`. | | `coordinates` | [Coordinates](#coordinates) \| null | Where the place is on the map, or `null` when not known. | ### Coordinates | Field | Type | Description | |---|---|---| | `lat` | number | Latitude. | | `lon` | number | Longitude. | ## Carrier | Field | Type | Description | |---|---|---| | `name` | string | Carrier name. | | `type` | string | Kind of carrier. Values: `ShippingLine`, `Airline`, `Courier`. | ## Booking | Field | Type | Description | |---|---|---| | `bookingNumber` | string \| null | The carrier's booking number. Often `null` even on ocean shipments. | | `masterBill` | string \| null | Master bill number: the ocean master bill of lading (MBL) or the air master air waybill (MAWB). Searchable with `search`. | | `houseBill` | string \| null | House bill number: the ocean house bill of lading (HBL) or the air house air waybill (HAWB). | | `cargoCutoff` | string \| null | Latest date the cargo can be delivered to the port or terminal for the booked departure. Local calendar date at the place it refers to, `yyyy-MM-dd`. It has no time zone; do not convert it. | | `shippingInstructionCutoff` | string \| null | Latest date the shipping instructions can be submitted to the carrier. Local calendar date at the place it refers to, `yyyy-MM-dd`. It has no time zone; do not convert it. | | `destinationPortFreeDays` | integer \| null | Free days at the port of discharge before storage/demurrage charges start. | | `emptyContainerReturnFreeDays` | integer \| null | Free days to return the empty container before detention charges start. | ## Parcels | Field | Type | Description | |---|---|---| | `carrier` | string \| null | The parcel carrier. Known values: `UPS`, `FedEx`, `DHL`, `TNT`, `DPD_UK`, `DPD_DE`. This is an open set — new values can appear without notice, so do not reject unknown ones. | | `trackingNumbers` | string[] | The parcel carrier's tracking numbers. | ## Amazon | Field | Type | Description | |---|---|---| | `fbaIds` | string[] | Amazon FBA shipment IDs. | | `referenceIds` | string[] | Amazon reference IDs. | ## Cargo | Field | Type | Description | |---|---|---| | `totalWeight` | [Weight](#weight) \| null | Total gross weight of the shipment. | | `totalVolume` | [Volume](#volume) \| null | Total volume of the shipment. | | `chargeableWeight` | [Weight](#weight) \| null | Chargeable weight. Air and courier shipments only; always `null` for ocean and land. | | `stackable` | boolean \| null | Whether the cargo can be stacked. `null` when not recorded. | | `containers` | [Container](#container)[] | Ocean (and rail) containers. Empty for air, courier and truck shipments. On FCL shipments these are the customer's own containers. On LCL, DDP and DDU shipments the container is a shared consolidation container: its number is useful for tracking the vessel leg, but the box and its other contents do not belong to the customer — do not count it as "their container". | | `packages` | [Package](#package)[] | Package lines. Each line is `quantity` identical packages. May be empty, notably on FCL shipments. | ### Container | Field | Type | Description | |---|---|---| | `number` | string \| null | Container number (ISO 6346). `null` until a container is assigned, which can be well after booking. Searchable with `search`. | | `type` | string \| null | Container size and kind. Values: `TwentyFt`, `FortyFt`, `FortyFtHighCube`, `FortyFtTempControl`, `FortyFiveFtHighCube`. | | `sealNumber` | string \| null | Seal number. | ### Package | Field | Type | Description | |---|---|---| | `type` | string \| null | Kind of package. Values: `Pallet`, `Box`. | | `quantity` | integer \| null | Number of identical packages on this line. | | `hazardous` | boolean \| null | Whether the packages contain hazardous goods. | | `weight` | [Weight](#weight) \| null | Weight of one package. | | `length` | [Length](#length) \| null | Length of one package. | | `width` | [Length](#length) \| null | Width of one package. | | `height` | [Length](#length) \| null | Height of one package. | ### Weight | Field | Type | Description | |---|---|---| | `value` | number | Magnitude, in `unit`. | | `unit` | string | Kilograms or pounds. Values: `KG`, `LB`. | ### Length | Field | Type | Description | |---|---|---| | `value` | number | Magnitude, in `unit`. | | `unit` | string | Unit of `value`. Values: `Centimeter`, `Inch`, `Millimeter`, `Meter`. | ### Volume | Field | Type | Description | |---|---|---| | `value` | number | Magnitude, in `unit`. | | `unit` | string | Cubic metres or cubic feet. Values: `CubicMeter`, `CubicFeet`. | ## Milestone | Field | Type | Description | |---|---|---| | `type` | string | Role of the milestone. For air shipments `OriginPort`, `TransshipmentPort` and `DestinationPort` are airports. A `Delivery` milestone's place may itself be a port (door-to-port). Values: `Pickup`, `OriginPort`, `TransshipmentPort`, `DestinationPort`, `InlandTerminal`, `BorderCrossing`, `IntermediaryWarehouse`, `Delivery`. | | `place` | [Place](#place) \| null | Where the milestone is. Always `null` for `IntermediaryWarehouse` and `BorderCrossing` milestones — only their type is given. | | `warehouseType` | string \| null | Kind of warehouse, on `IntermediaryWarehouse` milestones. `CFS` is a container freight station. `null` on every other milestone type. Values: `CFS`, `ConsolidationWarehouse`, `DeconsolidationWarehouse`. | | `cargoReadyDate` | string \| null | When the supplier has the cargo ready for collection. Set on the `Pickup` milestone only. Local calendar date at the place it refers to, `yyyy-MM-dd`. It has no time zone; do not convert it. | | `arrival` | [MilestoneDate](#milestonedate) \| null | Arrival at this milestone. `null` on the `Pickup` milestone and whenever no date is known. | | `departure` | [MilestoneDate](#milestonedate) \| null | Departure from this milestone. `null` on the `Delivery` milestone and whenever no date is known. | | `completed` | boolean | Whether the shipment has reached this milestone: picked up for `Pickup`, arrived for every other type. | | `transportToNext` | [TransportToNext](#transporttonext) \| null | The leg leaving this milestone. `null` on the last milestone. | **ETA** is a milestone's `arrival` while `actual` is `false`: the `DestinationPort` milestone for the port of discharge, the `Delivery` milestone for final delivery. ### MilestoneDate | Field | Type | Description | |---|---|---| | `date` | string | Local calendar date, `yyyy-MM-dd`. | | `time` | string \| null | Local time, 24-hour `HH:mm`. `null` when only the date is known. | | `actual` | boolean | `true`: it happened on this date. `false`: this is an estimate (ETA/ETD) and can change on any later read. | ### TransportToNext | Field | Type | Description | |---|---|---| | `mode` | string \| null | How the cargo moves on this leg. `Voyage` is a sea leg. Values: `Truck`, `Voyage`, `Flight`, `Rail`. | | `vessel` | string \| null | Vessel name. `Voyage` legs only. | | `voyageNumber` | string \| null | Voyage number. `Voyage` legs only. | | `flightNumber` | string \| null | Flight number. `Flight` legs only; each flight leg of a transshipped air route has its own. | | `carrier` | [Carrier](#carrier) \| null | The carrier operating this leg. Can differ from the shipment's main `carrier`, for example on an air route flown by two airlines. | --- # Shipment types Source: https://developers.gocubic.io/shipment-types.md > What each service type looks like, with one complete example per family. Every shipment has the same [Shipment object](https://developers.gocubic.io/shipment-object.md) shape; what varies is which parts are filled in. The table shows what is typical, not guaranteed — read what is there rather than assuming a shape from `serviceType`. | Type | Typical milestones | Carrier and bills | Cargo | Parcels | |---|---|---|---|---| | `OceanFCL`, `OceanFCLExpress` | Pickup → OriginPort → 0–2 TransshipmentPort → DestinationPort → Delivery | Shipping line; `masterBill`, `houseBill`, sometimes `bookingNumber` | 1–7 containers with number and seal; packages sometimes | — | | `OceanLCL`, `OceanLCLExpress` | Same, often with `CFS`, `ConsolidationWarehouse` or `DeconsolidationWarehouse` milestones | Same | Packages, plus the shared container's number | — | | `OceanDDP`, `OceanDDU` | As LCL or FCL; sometimes a Rail leg to an InlandTerminal; most deliver to an Amazon fulfilment centre | Same | Containers and packages | Often | | `AirFreight`, `AirDDP`, `AirDDU` | Pickup → OriginPort (airport) → 0–3 TransshipmentPort → DestinationPort → Delivery; a `flightNumber` per leg | Airline; `masterBill` (MAWB), `houseBill` (HAWB) | Packages only; `chargeableWeight` set | Almost always on `AirDDP`/`AirDDU` | | `AirCourier` | The full airport route, or only Pickup → Delivery | Courier company; no bills | Packages, often weight only | Yes | | `LandTruckLTL`, `LandTruckFTL` | Pickup → (BorderCrossing) → Delivery | `carrier` is `null`, `booking` members are `null` | Packages | Rare | | `LandRailFCL`, `LandRailLCL` | Same shape as ocean or land. Very rare. | — | — | — | Example values are invented. ## Ocean FCL Not yet collected: container numbers, seals, bill numbers and the second vessel are still `null`, and every date is an estimate. ```json { "reference": "QB48213", "status": "Active", "serviceType": "OceanFCL", "mode": "Ocean", "incoterms": "FOB", "tags": [ "PO-20931", "PO-20932" ], "descriptionOfGoods": "Stainless steel cookware", "insured": true, "bookingConfirmedByCarrier": true, "origin": { "city": "Ningbo", "state": null, "countryCode": "CN", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 29.8683, "lon": 121.544 }, "name": "Ningbo Hengda Housewares Co., Ltd.", "type": "ShippingParty" }, "portOfLoading": { "city": "Ningbo", "state": null, "countryCode": "CN", "code": "CNNGB", "fulfillmentCenter": null, "coordinates": { "lat": 29.9333, "lon": 121.85 }, "name": "Ningbo", "type": "Seaport" }, "portOfDischarge": { "city": "Felixstowe", "state": null, "countryCode": "GB", "code": "GBFXT", "fulfillmentCenter": null, "coordinates": { "lat": 51.9536, "lon": 1.3511 }, "name": "Felixstowe", "type": "Seaport" }, "destination": { "city": "Daventry", "state": null, "countryCode": "GB", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 52.2578, "lon": -1.1628 }, "name": "Brightwell Home Ltd", "type": "ShippingParty" }, "carrier": { "name": "Maersk", "type": "ShippingLine" }, "booking": { "bookingNumber": "243817265", "masterBill": null, "houseBill": null, "cargoCutoff": "2026-10-01", "shippingInstructionCutoff": "2026-09-29", "destinationPortFreeDays": 7, "emptyContainerReturnFreeDays": 10 }, "parcels": null, "amazon": null, "cargo": { "totalWeight": { "value": 31840, "unit": "KG" }, "totalVolume": { "value": 132.6, "unit": "CubicMeter" }, "chargeableWeight": null, "stackable": null, "containers": [ { "number": null, "type": "FortyFtHighCube", "sealNumber": null }, { "number": null, "type": "FortyFtHighCube", "sealNumber": null } ], "packages": [] }, "milestones": [ { "place": { "city": "Ningbo", "state": null, "countryCode": "CN", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 29.8683, "lon": 121.544 }, "name": "Ningbo Hengda Housewares Co., Ltd.", "type": "ShippingParty" }, "warehouseType": null, "cargoReadyDate": "2026-09-28", "arrival": null, "departure": { "date": "2026-09-29", "time": null, "actual": false }, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null, "mode": "Truck" }, "type": "Pickup", "completed": false }, { "place": { "city": "Ningbo", "state": null, "countryCode": "CN", "code": "CNNGB", "fulfillmentCenter": null, "coordinates": { "lat": 29.9333, "lon": 121.85 }, "name": "Ningbo", "type": "Seaport" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-09-29", "time": null, "actual": false }, "departure": { "date": "2026-10-04", "time": null, "actual": false }, "transportToNext": { "vessel": "MAERSK HERRERA", "voyageNumber": "640W", "flightNumber": null, "carrier": { "name": "Maersk", "type": "ShippingLine" }, "mode": "Voyage" }, "type": "OriginPort", "completed": false }, { "place": { "city": "Gelang Patah", "state": null, "countryCode": "MY", "code": "MYTPP", "fulfillmentCenter": null, "coordinates": { "lat": 1.3631, "lon": 103.5483 }, "name": "Tanjung Pelepas", "type": "Seaport" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-10-11", "time": null, "actual": false }, "departure": { "date": "2026-10-14", "time": null, "actual": false }, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": { "name": "Maersk", "type": "ShippingLine" }, "mode": "Voyage" }, "type": "TransshipmentPort", "completed": false }, { "place": { "city": "Felixstowe", "state": null, "countryCode": "GB", "code": "GBFXT", "fulfillmentCenter": null, "coordinates": { "lat": 51.9536, "lon": 1.3511 }, "name": "Felixstowe", "type": "Seaport" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-11-08", "time": null, "actual": false }, "departure": null, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null, "mode": "Truck" }, "type": "DestinationPort", "completed": false }, { "place": { "city": "Daventry", "state": null, "countryCode": "GB", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 52.2578, "lon": -1.1628 }, "name": "Brightwell Home Ltd", "type": "ShippingParty" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-11-11", "time": null, "actual": false }, "departure": null, "transportToNext": null, "type": "Delivery", "completed": false } ], "currentMilestoneIndex": 0, "cancelledAt": null, "createdAt": "2026-09-08T09:31:00Z", "updatedAt": "2026-09-17T02:15:00Z" } ``` ## Ocean LCL On the water. The `CFS` and `DeconsolidationWarehouse` milestones have `place: null`. The container is shared. `packages[].weight` is per pallet: 6 × 310 KG = 1860 KG. ```json { "reference": "QB51907", "status": "Active", "serviceType": "OceanLCL", "mode": "Ocean", "incoterms": "EXW", "tags": [ "PO-5512" ], "descriptionOfGoods": "Cotton bed linen", "insured": false, "bookingConfirmedByCarrier": true, "origin": { "city": "Mumbai", "state": null, "countryCode": "IN", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 19.076, "lon": 72.8777 }, "name": "Kavya Textiles Pvt. Ltd.", "type": "ShippingParty" }, "portOfLoading": { "city": "Navi Mumbai", "state": null, "countryCode": "IN", "code": "INNSA", "fulfillmentCenter": null, "coordinates": { "lat": 18.95, "lon": 72.95 }, "name": "Nhava Sheva", "type": "Seaport" }, "portOfDischarge": { "city": "Hamburg", "state": null, "countryCode": "DE", "code": "DEHAM", "fulfillmentCenter": null, "coordinates": { "lat": 53.5078, "lon": 9.9667 }, "name": "Hamburg", "type": "Seaport" }, "destination": { "city": "Berlin", "state": null, "countryCode": "DE", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 52.52, "lon": 13.405 }, "name": "Nordlicht Wohnen GmbH", "type": "ShippingParty" }, "carrier": { "name": "Hapag-Lloyd", "type": "ShippingLine" }, "booking": { "bookingNumber": null, "masterBill": "HLCUBO1260845517", "houseBill": "BOM2608377", "cargoCutoff": "2026-09-02", "shippingInstructionCutoff": null, "destinationPortFreeDays": null, "emptyContainerReturnFreeDays": null }, "parcels": null, "amazon": null, "cargo": { "totalWeight": { "value": 1860, "unit": "KG" }, "totalVolume": { "value": 10.08, "unit": "CubicMeter" }, "chargeableWeight": null, "stackable": false, "containers": [ { "number": "HLXU8231457", "type": "FortyFtHighCube", "sealNumber": "HL4471902" } ], "packages": [ { "type": "Pallet", "quantity": 6, "hazardous": false, "weight": { "value": 310, "unit": "KG" }, "length": { "value": 120, "unit": "Centimeter" }, "width": { "value": 100, "unit": "Centimeter" }, "height": { "value": 140, "unit": "Centimeter" } } ] }, "milestones": [ { "place": { "city": "Mumbai", "state": null, "countryCode": "IN", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 19.076, "lon": 72.8777 }, "name": "Kavya Textiles Pvt. Ltd.", "type": "ShippingParty" }, "warehouseType": null, "cargoReadyDate": "2026-08-28", "arrival": null, "departure": { "date": "2026-08-31", "time": null, "actual": true }, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null, "mode": "Truck" }, "type": "Pickup", "completed": true }, { "place": null, "warehouseType": "CFS", "cargoReadyDate": null, "arrival": { "date": "2026-08-31", "time": null, "actual": true }, "departure": { "date": "2026-09-03", "time": null, "actual": true }, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null, "mode": "Truck" }, "type": "IntermediaryWarehouse", "completed": true }, { "place": { "city": "Navi Mumbai", "state": null, "countryCode": "IN", "code": "INNSA", "fulfillmentCenter": null, "coordinates": { "lat": 18.95, "lon": 72.95 }, "name": "Nhava Sheva", "type": "Seaport" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-09-03", "time": null, "actual": true }, "departure": { "date": "2026-09-06", "time": null, "actual": true }, "transportToNext": { "vessel": "BERLIN EXPRESS", "voyageNumber": "2635W", "flightNumber": null, "carrier": { "name": "Hapag-Lloyd", "type": "ShippingLine" }, "mode": "Voyage" }, "type": "OriginPort", "completed": true }, { "place": { "city": "Hamburg", "state": null, "countryCode": "DE", "code": "DEHAM", "fulfillmentCenter": null, "coordinates": { "lat": 53.5078, "lon": 9.9667 }, "name": "Hamburg", "type": "Seaport" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-09-29", "time": null, "actual": false }, "departure": null, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null, "mode": "Truck" }, "type": "DestinationPort", "completed": false }, { "place": null, "warehouseType": "DeconsolidationWarehouse", "cargoReadyDate": null, "arrival": { "date": "2026-10-01", "time": null, "actual": false }, "departure": null, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null, "mode": "Truck" }, "type": "IntermediaryWarehouse", "completed": false }, { "place": { "city": "Berlin", "state": null, "countryCode": "DE", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 52.52, "lon": 13.405 }, "name": "Nordlicht Wohnen GmbH", "type": "ShippingParty" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-10-05", "time": null, "actual": false }, "departure": null, "transportToNext": null, "type": "Delivery", "completed": false } ], "currentMilestoneIndex": 2, "cancelledAt": null, "createdAt": "2026-08-19T13:04:00Z", "updatedAt": "2026-09-15T21:48:00Z" } ``` ## Ocean DDP Delivering to an Amazon fulfilment centre, with `incoterms: EXW`, `amazon` IDs and last-mile `parcels`. Arrived at the port of discharge; delivery is still an estimate. ```json { "reference": "QB10293", "status": "Active", "serviceType": "OceanDDP", "mode": "Ocean", "incoterms": "EXW", "tags": [ "PO-8841" ], "descriptionOfGoods": "Yoga mats", "insured": true, "bookingConfirmedByCarrier": true, "origin": { "city": "Shenzhen", "state": null, "countryCode": "CN", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 22.5, "lon": 114.1 }, "name": "Shenzhen Lotus Sports Goods Co., Ltd.", "type": "ShippingParty" }, "portOfLoading": { "city": "Shenzhen", "state": null, "countryCode": "CN", "code": "CNYTN", "fulfillmentCenter": null, "coordinates": { "lat": 22.5667, "lon": 114.2833 }, "name": "Yantian", "type": "Seaport" }, "portOfDischarge": { "city": "Long Beach", "state": "CA", "countryCode": "US", "code": "USLGB", "fulfillmentCenter": null, "coordinates": { "lat": 33.7542, "lon": -118.2165 }, "name": "Long Beach", "type": "Seaport" }, "destination": { "city": "Moreno Valley", "state": "CA", "countryCode": "US", "code": null, "fulfillmentCenter": "Amazon", "coordinates": { "lat": 33.9, "lon": -117.25 }, "name": "ONT8", "type": "FulfillmentCenter" }, "carrier": { "name": "COSCO", "type": "ShippingLine" }, "booking": { "bookingNumber": null, "masterBill": "COSU6412345670", "houseBill": "SZX2607001", "cargoCutoff": "2026-07-05", "shippingInstructionCutoff": null, "destinationPortFreeDays": null, "emptyContainerReturnFreeDays": null }, "parcels": { "carrier": "UPS", "trackingNumbers": [ "1Z84R2E70312345678", "1Z84R2E70312345687", "1Z84R2E70312345696" ] }, "amazon": { "fbaIds": [ "FBA18K4TQ9ZB" ], "referenceIds": [ "7KQ2M9XA" ] }, "cargo": { "totalWeight": { "value": 1082, "unit": "KG" }, "totalVolume": { "value": 6.486, "unit": "CubicMeter" }, "chargeableWeight": null, "stackable": null, "containers": [ { "number": "CSNU1234567", "type": "FortyFtHighCube", "sealNumber": "CN8812045" } ], "packages": [ { "type": "Box", "quantity": 62, "hazardous": false, "weight": { "value": 17.45, "unit": "KG" }, "length": { "value": 48, "unit": "Centimeter" }, "width": { "value": 48, "unit": "Centimeter" }, "height": { "value": 45.4, "unit": "Centimeter" } } ] }, "milestones": [ { "place": { "city": "Shenzhen", "state": null, "countryCode": "CN", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 22.5, "lon": 114.1 }, "name": "Shenzhen Lotus Sports Goods Co., Ltd.", "type": "ShippingParty" }, "warehouseType": null, "cargoReadyDate": "2026-07-03", "arrival": null, "departure": { "date": "2026-07-03", "time": null, "actual": true }, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null, "mode": "Truck" }, "type": "Pickup", "completed": true }, { "place": null, "warehouseType": "ConsolidationWarehouse", "cargoReadyDate": null, "arrival": { "date": "2026-07-03", "time": null, "actual": true }, "departure": { "date": "2026-07-06", "time": null, "actual": true }, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null, "mode": "Truck" }, "type": "IntermediaryWarehouse", "completed": true }, { "place": { "city": "Shenzhen", "state": null, "countryCode": "CN", "code": "CNYTN", "fulfillmentCenter": null, "coordinates": { "lat": 22.5667, "lon": 114.2833 }, "name": "Yantian", "type": "Seaport" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-07-06", "time": null, "actual": true }, "departure": { "date": "2026-07-09", "time": null, "actual": true }, "transportToNext": { "vessel": "COSCO SHIPPING ANDES", "voyageNumber": "027E", "flightNumber": null, "carrier": { "name": "COSCO", "type": "ShippingLine" }, "mode": "Voyage" }, "type": "OriginPort", "completed": true }, { "place": { "city": "Long Beach", "state": "CA", "countryCode": "US", "code": "USLGB", "fulfillmentCenter": null, "coordinates": { "lat": 33.7542, "lon": -118.2165 }, "name": "Long Beach", "type": "Seaport" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-07-28", "time": "08:00", "actual": true }, "departure": null, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null, "mode": "Truck" }, "type": "DestinationPort", "completed": true }, { "place": null, "warehouseType": "DeconsolidationWarehouse", "cargoReadyDate": null, "arrival": { "date": "2026-07-31", "time": null, "actual": false }, "departure": null, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null, "mode": "Truck" }, "type": "IntermediaryWarehouse", "completed": false }, { "place": { "city": "Moreno Valley", "state": "CA", "countryCode": "US", "code": null, "fulfillmentCenter": "Amazon", "coordinates": { "lat": 33.9, "lon": -117.25 }, "name": "ONT8", "type": "FulfillmentCenter" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-08-04", "time": null, "actual": false }, "departure": null, "transportToNext": null, "type": "Delivery", "completed": false } ], "currentMilestoneIndex": 3, "cancelledAt": null, "createdAt": "2026-06-28T10:12:00Z", "updatedAt": "2026-07-29T06:40:00Z" } ``` ## Air freight One transshipment, so two flight legs, each with its own `flightNumber` and `carrier`. `chargeableWeight` (224 KG) differs from `totalWeight` (168 KG). ```json { "reference": "QB53310", "status": "Active", "serviceType": "AirFreight", "mode": "Air", "incoterms": "EXW", "tags": [ "PO-77104" ], "descriptionOfGoods": "Servo motor assemblies", "insured": true, "bookingConfirmedByCarrier": true, "origin": { "city": "Hanoi", "state": null, "countryCode": "VN", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 21.0285, "lon": 105.8542 }, "name": "Red River Precision Components JSC", "type": "ShippingParty" }, "portOfLoading": { "city": "Hanoi", "state": null, "countryCode": "VN", "code": "HAN", "fulfillmentCenter": null, "coordinates": { "lat": 21.2212, "lon": 105.8072 }, "name": "Noi Bai International Airport", "type": "Airport" }, "portOfDischarge": { "city": "Chicago", "state": "IL", "countryCode": "US", "code": "ORD", "fulfillmentCenter": null, "coordinates": { "lat": 41.9742, "lon": -87.9073 }, "name": "Chicago O'Hare International Airport", "type": "Airport" }, "destination": { "city": "Elk Grove Village", "state": "IL", "countryCode": "US", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 42.0039, "lon": -87.9703 }, "name": "Lakeshore Robotics Inc.", "type": "ShippingParty" }, "carrier": { "name": "Qatar Airways", "type": "Airline" }, "booking": { "bookingNumber": null, "masterBill": "157-48213905", "houseBill": "HAN26091187", "cargoCutoff": null, "shippingInstructionCutoff": null, "destinationPortFreeDays": null, "emptyContainerReturnFreeDays": null }, "parcels": null, "amazon": null, "cargo": { "totalWeight": { "value": 168, "unit": "KG" }, "totalVolume": { "value": 1.344, "unit": "CubicMeter" }, "chargeableWeight": { "value": 224, "unit": "KG" }, "stackable": true, "containers": [], "packages": [ { "type": "Box", "quantity": 14, "hazardous": false, "weight": { "value": 12, "unit": "KG" }, "length": { "value": 60, "unit": "Centimeter" }, "width": { "value": 40, "unit": "Centimeter" }, "height": { "value": 40, "unit": "Centimeter" } } ] }, "milestones": [ { "place": { "city": "Hanoi", "state": null, "countryCode": "VN", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 21.0285, "lon": 105.8542 }, "name": "Red River Precision Components JSC", "type": "ShippingParty" }, "warehouseType": null, "cargoReadyDate": "2026-09-14", "arrival": null, "departure": { "date": "2026-09-14", "time": null, "actual": true }, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null, "mode": "Truck" }, "type": "Pickup", "completed": true }, { "place": { "city": "Hanoi", "state": null, "countryCode": "VN", "code": "HAN", "fulfillmentCenter": null, "coordinates": { "lat": 21.2212, "lon": 105.8072 }, "name": "Noi Bai International Airport", "type": "Airport" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-09-15", "time": null, "actual": true }, "departure": { "date": "2026-09-16", "time": "23:55", "actual": true }, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": "QR8955", "carrier": { "name": "Qatar Airways", "type": "Airline" }, "mode": "Flight" }, "type": "OriginPort", "completed": true }, { "place": { "city": "Doha", "state": null, "countryCode": "QA", "code": "DOH", "fulfillmentCenter": null, "coordinates": { "lat": 25.2731, "lon": 51.6081 }, "name": "Hamad International Airport", "type": "Airport" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-09-17", "time": "04:10", "actual": true }, "departure": { "date": "2026-09-18", "time": "08:30", "actual": true }, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": "QR8141", "carrier": { "name": "Qatar Airways", "type": "Airline" }, "mode": "Flight" }, "type": "TransshipmentPort", "completed": true }, { "place": { "city": "Chicago", "state": "IL", "countryCode": "US", "code": "ORD", "fulfillmentCenter": null, "coordinates": { "lat": 41.9742, "lon": -87.9073 }, "name": "Chicago O'Hare International Airport", "type": "Airport" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-09-18", "time": "15:05", "actual": true }, "departure": null, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null, "mode": "Truck" }, "type": "DestinationPort", "completed": true }, { "place": { "city": "Elk Grove Village", "state": "IL", "countryCode": "US", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 42.0039, "lon": -87.9703 }, "name": "Lakeshore Robotics Inc.", "type": "ShippingParty" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-09-22", "time": null, "actual": false }, "departure": null, "transportToNext": null, "type": "Delivery", "completed": false } ], "currentMilestoneIndex": 3, "cancelledAt": null, "createdAt": "2026-09-09T07:22:00Z", "updatedAt": "2026-09-18T20:41:00Z" } ``` ## Air courier Delivered. Only two milestones, so `portOfLoading` and `portOfDischarge` are `null`. Packages have weight but no dimensions. ```json { "reference": "QB54002", "status": "Delivered", "serviceType": "AirCourier", "mode": "Air", "incoterms": "EXW", "tags": [], "descriptionOfGoods": "Leather handbag samples", "insured": false, "bookingConfirmedByCarrier": true, "origin": { "city": "Istanbul", "state": null, "countryCode": "TR", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 41.0082, "lon": 28.9784 }, "name": "Anatolia Leather Workshop", "type": "ShippingParty" }, "portOfLoading": null, "portOfDischarge": null, "destination": { "city": "Tel Aviv-Yafo", "state": null, "countryCode": "IL", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 32.0853, "lon": 34.7818 }, "name": "Studio Noa Ltd.", "type": "ShippingParty" }, "carrier": { "name": "DHL", "type": "Courier" }, "booking": { "bookingNumber": null, "masterBill": null, "houseBill": null, "cargoCutoff": null, "shippingInstructionCutoff": null, "destinationPortFreeDays": null, "emptyContainerReturnFreeDays": null }, "parcels": { "carrier": "DHL", "trackingNumbers": [ "4839201754", "4839201765" ] }, "amazon": null, "cargo": { "totalWeight": { "value": 16.4, "unit": "KG" }, "totalVolume": null, "chargeableWeight": { "value": 16.5, "unit": "KG" }, "stackable": null, "containers": [], "packages": [ { "type": "Box", "quantity": 2, "hazardous": false, "weight": { "value": 8.2, "unit": "KG" }, "length": null, "width": null, "height": null } ] }, "milestones": [ { "place": { "city": "Istanbul", "state": null, "countryCode": "TR", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 41.0082, "lon": 28.9784 }, "name": "Anatolia Leather Workshop", "type": "ShippingParty" }, "warehouseType": null, "cargoReadyDate": "2026-09-07", "arrival": null, "departure": { "date": "2026-09-08", "time": null, "actual": true }, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": { "name": "DHL", "type": "Courier" }, "mode": "Flight" }, "type": "Pickup", "completed": true }, { "place": { "city": "Tel Aviv-Yafo", "state": null, "countryCode": "IL", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 32.0853, "lon": 34.7818 }, "name": "Studio Noa Ltd.", "type": "ShippingParty" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-09-11", "time": "14:20", "actual": true }, "departure": null, "transportToNext": null, "type": "Delivery", "completed": true } ], "currentMilestoneIndex": 1, "cancelledAt": null, "createdAt": "2026-09-04T11:50:00Z", "updatedAt": "2026-09-11T11:26:00Z" } ``` ## Land truck LTL with a border crossing, before pickup. Cargo is recorded in pounds, inches and cubic feet. ```json { "reference": "QB52764", "status": "Active", "serviceType": "LandTruckLTL", "mode": "Land", "incoterms": null, "tags": [ "PO-3307" ], "descriptionOfGoods": "Injection-moulded cooler lids", "insured": false, "bookingConfirmedByCarrier": false, "origin": { "city": "Apodaca", "state": null, "countryCode": "MX", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 25.7815, "lon": -100.1886 }, "name": "Industrias Regio Plasticos S.A. de C.V.", "type": "ShippingParty" }, "portOfLoading": null, "portOfDischarge": null, "destination": { "city": "Dallas", "state": "TX", "countryCode": "US", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 32.7767, "lon": -96.797 }, "name": "Trinity Outdoor Supply LLC", "type": "ShippingParty" }, "carrier": null, "booking": { "bookingNumber": null, "masterBill": null, "houseBill": null, "cargoCutoff": null, "shippingInstructionCutoff": null, "destinationPortFreeDays": null, "emptyContainerReturnFreeDays": null }, "parcels": null, "amazon": null, "cargo": { "totalWeight": { "value": 7600, "unit": "LB" }, "totalVolume": { "value": 533.3, "unit": "CubicFeet" }, "chargeableWeight": null, "stackable": false, "containers": [], "packages": [ { "type": "Pallet", "quantity": 8, "hazardous": false, "weight": { "value": 950, "unit": "LB" }, "length": { "value": 48, "unit": "Inch" }, "width": { "value": 40, "unit": "Inch" }, "height": { "value": 60, "unit": "Inch" } } ] }, "milestones": [ { "place": { "city": "Apodaca", "state": null, "countryCode": "MX", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 25.7815, "lon": -100.1886 }, "name": "Industrias Regio Plasticos S.A. de C.V.", "type": "ShippingParty" }, "warehouseType": null, "cargoReadyDate": "2026-09-21", "arrival": null, "departure": { "date": "2026-09-22", "time": null, "actual": false }, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null, "mode": "Truck" }, "type": "Pickup", "completed": false }, { "place": null, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-09-23", "time": null, "actual": false }, "departure": null, "transportToNext": { "vessel": null, "voyageNumber": null, "flightNumber": null, "carrier": null, "mode": "Truck" }, "type": "BorderCrossing", "completed": false }, { "place": { "city": "Dallas", "state": "TX", "countryCode": "US", "code": null, "fulfillmentCenter": null, "coordinates": { "lat": 32.7767, "lon": -96.797 }, "name": "Trinity Outdoor Supply LLC", "type": "ShippingParty" }, "warehouseType": null, "cargoReadyDate": null, "arrival": { "date": "2026-09-25", "time": null, "actual": false }, "departure": null, "transportToNext": null, "type": "Delivery", "completed": false } ], "currentMilestoneIndex": 0, "cancelledAt": null, "createdAt": "2026-09-15T16:08:00Z", "updatedAt": "2026-09-18T09:12:00Z" } ``` --- # Recipes Source: https://developers.gocubic.io/recipes.md > A small client with pagination and 429 backoff, and runnable snippets for common jobs. Every recipe imports the client below. Save it as `cubic.mts` or `cubic.py`. Both read the key from `CUBIC_API_KEY`. - TypeScript: no dependencies, Node 18+. Run with `npx tsx .mts` or `bun .mts`. - Python: `pip install requests`, Python 3.9+. Run with `python .py`. TypeScript types: `npx openapi-typescript https://developers.gocubic.io/openapi.json -o cubic-api.d.ts`. ## Client Follows cursors, waits out `429` using `Retry-After`, retries `5xx` with backoff, and raises everything else with the API's `code` and `hint`. ```ts // cubic.mts const BASE_URL = 'https://api.gocubic.io/v1'; const API_KEY = process.env.CUBIC_API_KEY; if (!API_KEY) throw new Error('Set the CUBIC_API_KEY environment variable'); export interface MilestoneDate { date: string; time: string | null; actual: boolean; } export interface Place { name: string | null; code: string | null; countryCode: string | null; } export interface Milestone { type: string; place: Place | null; arrival: MilestoneDate | null; departure: MilestoneDate | null; completed: boolean; } export interface Shipment { reference: string; status: string; serviceType: string; tags: string[]; portOfDischarge: Place | null; milestones: Milestone[]; currentMilestoneIndex: number; updatedAt: string; [field: string]: unknown; } export type Query = Record; export class CubicApiError extends Error { problem: { status: number; code: string; detail: string; hint: string; parameter?: string }; constructor(problem: CubicApiError['problem']) { super(`${problem.status} ${problem.code}: ${problem.detail} Hint: ${problem.hint}`); this.problem = problem; } } const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export async function cubicGet(path: string, query: Query = {}): Promise { const url = new URL(BASE_URL + path); for (const [key, value] of Object.entries(query)) { if (value === undefined) continue; for (const item of Array.isArray(value) ? value : [value]) url.searchParams.append(key, String(item)); } for (let attempt = 0; ; attempt++) { const response = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` } }); if (response.ok) return (await response.json()) as T; const retryable = response.status === 429 || response.status >= 500; if (retryable && attempt < 5) { const retryAfter = Number(response.headers.get('Retry-After')); await sleep((retryAfter > 0 ? retryAfter : 2 ** attempt) * 1000); continue; } const problem = await response .json() .catch(() => ({ status: response.status, code: 'unknown', detail: response.statusText, hint: '' })); throw new CubicApiError(problem); } } export async function* listShipments(query: Query = {}): AsyncGenerator { let cursor: string | undefined; do { const page = await cubicGet<{ data: Shipment[]; nextCursor: string | null }>('/shipments', { limit: 100, ...query, cursor, }); yield* page.data; cursor = page.nextCursor ?? undefined; } while (cursor); } export const findMilestone = (shipment: Shipment, type: string) => shipment.milestones.find((milestone) => milestone.type === type) ?? null; ``` ```python # cubic.py import os import time import requests BASE_URL = "https://api.gocubic.io/v1" session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['CUBIC_API_KEY']}" class CubicApiError(Exception): def __init__(self, problem): super().__init__( f"{problem.get('status')} {problem.get('code')}: {problem.get('detail')} Hint: {problem.get('hint')}" ) self.problem = problem def cubic_get(path, params=None): for attempt in range(6): response = session.get(BASE_URL + path, params=params, timeout=30) if response.ok: return response.json() retryable = response.status_code == 429 or response.status_code >= 500 if retryable and attempt < 5: retry_after = response.headers.get("Retry-After") time.sleep(int(retry_after) if retry_after else 2**attempt) continue try: problem = response.json() except ValueError: problem = {"status": response.status_code, "code": "unknown", "detail": response.reason, "hint": ""} raise CubicApiError(problem) def list_shipments(**filters): """Yield every shipment matching the filters, following cursors. A list value repeats the parameter.""" params = {"limit": 100, **filters} while True: page = cubic_get("/shipments", params) yield from page["data"] if page["nextCursor"] is None: return params["cursor"] = page["nextCursor"] def find_milestone(shipment, milestone_type): return next((milestone for milestone in shipment["milestones"] if milestone["type"] == milestone_type), None) ``` ## Verify the connection ```ts import { cubicGet } from './cubic.mts'; const me = await cubicGet<{ account: { name: string }; apiKey: { name: string; prefix: string } }>('/me'); console.log(`Connected to "${me.account.name}" using key "${me.apiKey.name}" (${me.apiKey.prefix}…)`); ``` ```python from cubic import cubic_get me = cubic_get("/me") print(f"Connected to \"{me['account']['name']}\" using key \"{me['apiKey']['name']}\" ({me['apiKey']['prefix']}…)") ``` ## List active shipments with ETA The ETA is a milestone's `arrival` while `actual` is `false`. Either milestone, or its `arrival`, can be missing. `currentMilestoneIndex` says where the shipment is now. ```ts import { findMilestone, listShipments, type MilestoneDate } from './cubic.mts'; const describe = (arrival: MilestoneDate | null | undefined) => arrival ? `${arrival.date} (${arrival.actual ? 'actual' : 'estimated'})` : 'not known yet'; for await (const shipment of listShipments({ status: 'Active' })) { const port = findMilestone(shipment, 'DestinationPort'); const delivery = findMilestone(shipment, 'Delivery'); console.log( `${shipment.reference} ${shipment.serviceType} ` + `current milestone ${shipment.milestones[shipment.currentMilestoneIndex].type} ` + `port of discharge ${shipment.portOfDischarge?.name ?? '—'}: ${describe(port?.arrival)} ` + `delivery: ${describe(delivery?.arrival)}`, ); } ``` ```python from cubic import find_milestone, list_shipments def describe(milestone): arrival = milestone and milestone["arrival"] if not arrival: return "not known yet" return f"{arrival['date']} ({'actual' if arrival['actual'] else 'estimated'})" for shipment in list_shipments(status="Active"): port = find_milestone(shipment, "DestinationPort") delivery = find_milestone(shipment, "Delivery") port_name = (shipment["portOfDischarge"] or {}).get("name") or "—" print( f"{shipment['reference']} {shipment['serviceType']} " f"current milestone {shipment['milestones'][shipment['currentMilestoneIndex']]['type']} " f"port of discharge {port_name}: {describe(port)} delivery: {describe(delivery)}" ) ``` ## Find a shipment by PO, container, master bill or FBA ID `tag` is an exact match on the customer's own tags (PO numbers). `search` is free text across the reference, tags, master bill / air waybill numbers, container numbers, and Amazon FBA and reference IDs. ```ts import { listShipments } from './cubic.mts'; const [kind, value] = process.argv.slice(2); if (!value || (kind !== 'tag' && kind !== 'search')) { throw new Error('Usage: find.mts tag PO-8841 | find.mts search CSNU1234567'); } let found = 0; for await (const shipment of listShipments({ [kind]: value })) { found++; console.log(shipment.reference, shipment.status, shipment.tags.join(',')); } if (found === 0) console.log(`No shipment matches ${kind}=${value}`); ``` ```python import sys from cubic import list_shipments if len(sys.argv) != 3 or sys.argv[1] not in ("tag", "search"): sys.exit("Usage: find.py tag PO-8841 | find.py search CSNU1234567") kind, value = sys.argv[1:] matches = list(list_shipments(**{kind: value})) for shipment in matches: print(shipment["reference"], shipment["status"], ",".join(shipment["tags"])) if not matches: print(f"No shipment matches {kind}={value}") ``` ## Poll for changes Store the greatest `updatedAt` you have seen and pass it back as `updatedSince`. It is inclusive, so the boundary shipment comes back again: upsert by `reference`. The first run reads everything. ```ts import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { listShipments, type Shipment } from './cubic.mts'; const STATE_FILE = 'cubic-sync-state.json'; const POLL_INTERVAL_MS = 5 * 60 * 1000; const handleChange = (shipment: Shipment) => { console.log(`${shipment.reference} is ${shipment.status} (updated ${shipment.updatedAt})`); }; let updatedSince: string | undefined = existsSync(STATE_FILE) ? JSON.parse(readFileSync(STATE_FILE, 'utf8')).updatedSince : undefined; for (;;) { let newest: string | undefined; for await (const shipment of listShipments({ updatedSince })) { newest ??= shipment.updatedAt; handleChange(shipment); } if (newest) { updatedSince = newest; writeFileSync(STATE_FILE, JSON.stringify({ updatedSince })); } await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); } ``` ```python import json import os import time from cubic import list_shipments STATE_FILE = "cubic-sync-state.json" POLL_INTERVAL_SECONDS = 5 * 60 def handle_change(shipment): print(f"{shipment['reference']} is {shipment['status']} (updated {shipment['updatedAt']})") updated_since = None if os.path.exists(STATE_FILE): with open(STATE_FILE) as state_file: updated_since = json.load(state_file)["updatedSince"] while True: filters = {"updatedSince": updated_since} if updated_since else {} newest = None for shipment in list_shipments(**filters): newest = newest or shipment["updatedAt"] handle_change(shipment) if newest: updated_since = newest with open(STATE_FILE, "w") as state_file: json.dump({"updatedSince": updated_since}, state_file) time.sleep(POLL_INTERVAL_SECONDS) ``` ## What is arriving this week Milestone dates compare correctly as text. This uses the `Delivery` milestone; use `DestinationPort` for arrivals at the port of discharge. ```ts import { findMilestone, listShipments } from './cubic.mts'; const isoDate = (date: Date) => date.toLocaleDateString('en-CA'); const today = new Date(); const from = isoDate(today); const to = isoDate(new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7)); const arriving = []; for await (const shipment of listShipments({ status: 'Active' })) { const arrival = findMilestone(shipment, 'Delivery')?.arrival; if (arrival && arrival.date >= from && arrival.date < to) arriving.push({ shipment, arrival }); } arriving.sort((a, b) => a.arrival.date.localeCompare(b.arrival.date)); for (const { shipment, arrival } of arriving) { console.log(`${arrival.date}${arrival.actual ? '' : ' (estimated)'} ${shipment.reference} ${shipment.tags.join(',')}`); } ``` ```python from datetime import date, timedelta from cubic import find_milestone, list_shipments start = date.today().isoformat() end = (date.today() + timedelta(days=7)).isoformat() arriving = [] for shipment in list_shipments(status="Active"): delivery = find_milestone(shipment, "Delivery") arrival = delivery and delivery["arrival"] if arrival and start <= arrival["date"] < end: arriving.append((arrival, shipment)) for arrival, shipment in sorted(arriving, key=lambda pair: pair[0]["date"]): suffix = "" if arrival["actual"] else " (estimated)" print(f"{arrival['date']}{suffix} {shipment['reference']} {','.join(shipment['tags'])}") ``` ## Export to a spreadsheet One row per shipment in `shipments.csv`. `null` becomes an empty cell. ```ts import { writeFileSync } from 'node:fs'; import { findMilestone, listShipments, type Shipment } from './cubic.mts'; const COLUMNS: [string, (shipment: Shipment) => unknown][] = [ ['reference', (s) => s.reference], ['status', (s) => s.status], ['serviceType', (s) => s.serviceType], ['tags', (s) => s.tags.join(' ')], ['portOfDischarge', (s) => s.portOfDischarge?.name], ['portArrival', (s) => findMilestone(s, 'DestinationPort')?.arrival?.date], ['portArrivalIsActual', (s) => findMilestone(s, 'DestinationPort')?.arrival?.actual], ['deliveryArrival', (s) => findMilestone(s, 'Delivery')?.arrival?.date], ['deliveryArrivalIsActual', (s) => findMilestone(s, 'Delivery')?.arrival?.actual], ['updatedAt', (s) => s.updatedAt], ]; const cell = (value: unknown) => { const text = value === null || value === undefined ? '' : String(value); return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; }; const lines = [COLUMNS.map(([header]) => header).join(',')]; for await (const shipment of listShipments()) { lines.push(COLUMNS.map(([, read]) => cell(read(shipment))).join(',')); } writeFileSync('shipments.csv', lines.join('\n') + '\n'); console.log(`Wrote ${lines.length - 1} shipments to shipments.csv`); ``` ```python import csv from cubic import find_milestone, list_shipments def arrival(shipment, milestone_type, field): milestone = find_milestone(shipment, milestone_type) return milestone["arrival"][field] if milestone and milestone["arrival"] else None COLUMNS = [ ("reference", lambda s: s["reference"]), ("status", lambda s: s["status"]), ("serviceType", lambda s: s["serviceType"]), ("tags", lambda s: " ".join(s["tags"])), ("portOfDischarge", lambda s: (s["portOfDischarge"] or {}).get("name")), ("portArrival", lambda s: arrival(s, "DestinationPort", "date")), ("portArrivalIsActual", lambda s: arrival(s, "DestinationPort", "actual")), ("deliveryArrival", lambda s: arrival(s, "Delivery", "date")), ("deliveryArrivalIsActual", lambda s: arrival(s, "Delivery", "actual")), ("updatedAt", lambda s: s["updatedAt"]), ] count = 0 with open("shipments.csv", "w", newline="") as csv_file: writer = csv.writer(csv_file) writer.writerow([header for header, _ in COLUMNS]) for shipment in list_shipments(): writer.writerow([read(shipment) for _, read in COLUMNS]) count += 1 print(f"Wrote {count} shipments to shipments.csv") ``` ## Sync to a database Incremental sync into SQLite: an upsert keyed on `reference`, resuming from the greatest `updated_at` stored. Run it on a schedule. ```ts // Requires Node 22.13+ for node:sqlite. import { DatabaseSync } from 'node:sqlite'; import { findMilestone, listShipments } from './cubic.mts'; const db = new DatabaseSync('cubic.db'); db.exec(`CREATE TABLE IF NOT EXISTS shipments ( reference TEXT PRIMARY KEY, status TEXT NOT NULL, service_type TEXT NOT NULL, delivery_date TEXT, delivery_is_actual INTEGER, updated_at TEXT NOT NULL, json TEXT NOT NULL )`); const upsert = db.prepare(`INSERT INTO shipments VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(reference) DO UPDATE SET status = excluded.status, service_type = excluded.service_type, delivery_date = excluded.delivery_date, delivery_is_actual = excluded.delivery_is_actual, updated_at = excluded.updated_at, json = excluded.json`); const last = db.prepare('SELECT MAX(updated_at) AS value FROM shipments').get() as { value: string | null }; // One transaction: the list is newest first, so a partial run would move MAX(updated_at) past rows it never stored. let count = 0; db.exec('BEGIN'); try { for await (const shipment of listShipments({ updatedSince: last.value ?? undefined })) { const arrival = findMilestone(shipment, 'Delivery')?.arrival ?? null; upsert.run( shipment.reference, shipment.status, shipment.serviceType, arrival ? arrival.date : null, arrival ? Number(arrival.actual) : null, shipment.updatedAt, JSON.stringify(shipment), ); count++; } db.exec('COMMIT'); } catch (error) { db.exec('ROLLBACK'); throw error; } console.log(`Upserted ${count} shipments`); ``` ```python import json import sqlite3 from cubic import find_milestone, list_shipments db = sqlite3.connect("cubic.db") db.execute( """CREATE TABLE IF NOT EXISTS shipments ( reference TEXT PRIMARY KEY, status TEXT NOT NULL, service_type TEXT NOT NULL, delivery_date TEXT, delivery_is_actual INTEGER, updated_at TEXT NOT NULL, json TEXT NOT NULL )""" ) last = db.execute("SELECT MAX(updated_at) FROM shipments").fetchone()[0] filters = {"updatedSince": last} if last else {} count = 0 for shipment in list_shipments(**filters): delivery = find_milestone(shipment, "Delivery") arrival = delivery and delivery["arrival"] db.execute( """INSERT INTO shipments VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(reference) DO UPDATE SET status = excluded.status, service_type = excluded.service_type, delivery_date = excluded.delivery_date, delivery_is_actual = excluded.delivery_is_actual, updated_at = excluded.updated_at, json = excluded.json""", ( shipment["reference"], shipment["status"], shipment["serviceType"], arrival["date"] if arrival else None, int(arrival["actual"]) if arrival else None, shipment["updatedAt"], json.dumps(shipment), ), ) count += 1 db.commit() print(f"Upserted {count} shipments") ``` ## Handle 429 with backoff The [client](#client) already does this: on `429` it waits `Retry-After` seconds; on `5xx` it backs off 1, 2, 4, 8, 16 seconds; after five retries it raises. A standalone retry loop is on [Rate limits](https://developers.gocubic.io/rate-limits.md#handling-429). --- # Changelog Source: https://developers.gocubic.io/changelog.md > Changes to the Cubic API. ## v1 Initial release: `GET /v1/me`, `GET /v1/shipments`, `GET /v1/shipments/{reference}`, `GET /v1/openapi.json`.