diff --git a/.github/workflows/publish_typescript_sdk.yml b/.github/workflows/publish_typescript_sdk.yml
new file mode 100644
index 00000000..14f18b45
--- /dev/null
+++ b/.github/workflows/publish_typescript_sdk.yml
@@ -0,0 +1,55 @@
+name: Publish TypeScript SDK
+
+on:
+ release:
+ types: [published]
+
+permissions:
+ contents: read
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 18
+ registry-url: https://registry.npmjs.org/
+
+ - name: Read SDK version
+ id: sdk-version
+ run: |
+ node -e "const { version } = require('./sdks/typescript-sdk/package.json'); const fs = require('fs'); fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\n`);"
+
+ - name: Check release tag matches SDK version
+ id: tag-check
+ run: |
+ tag="${{ github.event.release.tag_name }}"
+ version="${{ steps.sdk-version.outputs.version }}"
+ if [[ "$tag" == "v$version" || "$tag" == "sdk-v$version" || "$tag" == "$version" ]]; then
+ echo "publish=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "publish=false" >> "$GITHUB_OUTPUT"
+ echo "Release tag $tag does not match SDK version $version; skipping publish."
+ fi
+
+ - name: Install dependencies
+ if: steps.tag-check.outputs.publish == 'true'
+ working-directory: sdks/typescript-sdk
+ run: npm ci
+
+ - name: Build
+ if: steps.tag-check.outputs.publish == 'true'
+ working-directory: sdks/typescript-sdk
+ run: npm run build
+
+ - name: Publish
+ if: steps.tag-check.outputs.publish == 'true'
+ working-directory: sdks/typescript-sdk
+ run: npm publish --access public
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 0beb009b..6cc6c874 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.DS_Store
+.beads/
.env.local
.env.development.local
@@ -6,3 +7,5 @@
.tool-versions.local
.pytest_cache
+
+node_modules
diff --git a/docs/api-docs/getting-started/sdk-quickstart.mdx b/docs/api-docs/getting-started/sdk-quickstart.mdx
new file mode 100644
index 00000000..7ec13e95
--- /dev/null
+++ b/docs/api-docs/getting-started/sdk-quickstart.mdx
@@ -0,0 +1,7 @@
+---
+title: SDK Quickstart (TypeScript)
+---
+
+The SDK documentation has moved to the top-level SDK Docs section.
+
+- [Open the SDK quickstart](/sdk/quickstart)
diff --git a/docs/docs.json b/docs/docs.json
index 5ad6030e..c7c2fba3 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -166,6 +166,26 @@
}
]
},
+ {
+ "tab": "SDK Docs",
+ "groups": [
+ {
+ "group": "Getting Started",
+ "pages": [
+ "sdk/introduction",
+ "sdk/quickstart"
+ ]
+ },
+ {
+ "group": "Reference",
+ "pages": [
+ "sdk/methods",
+ "sdk/filtering-pagination",
+ "sdk/error-handling"
+ ]
+ }
+ ]
+ },
{
"tab": "DataSync",
"groups": [
diff --git a/docs/sdk/error-handling.mdx b/docs/sdk/error-handling.mdx
new file mode 100644
index 00000000..9106027b
--- /dev/null
+++ b/docs/sdk/error-handling.mdx
@@ -0,0 +1,70 @@
+---
+title: "Error Handling"
+description: "Catch and handle errors from the Terminal49 SDK"
+---
+
+The SDK throws typed errors you can catch and handle based on the error type.
+
+## Error types
+
+| Error | Cause |
+|-------|-------|
+| `AuthenticationError` | Invalid or missing API token |
+| `AuthorizationError` | Valid token but insufficient permissions |
+| `NotFoundError` | Resource doesn't exist or isn't accessible |
+| `ValidationError` | Invalid request parameters |
+| `RateLimitError` | Too many requests |
+| `FeatureNotEnabledError` | Feature requires a plan upgrade |
+| `UpstreamError` | Carrier or terminal API is unavailable |
+| `Terminal49Error` | Generic error fallback |
+
+## Basic error handling
+
+```typescript
+import {
+ Terminal49Client,
+ AuthenticationError,
+ RateLimitError,
+ NotFoundError,
+} from '@terminal49/sdk';
+
+const client = new Terminal49Client({
+ apiToken: process.env.T49_API_TOKEN!,
+});
+
+try {
+ await client.containers.get('container-uuid');
+} catch (error) {
+ if (error instanceof AuthenticationError) {
+ console.error('Invalid API token');
+ } else if (error instanceof NotFoundError) {
+ console.error('Container not found');
+ } else if (error instanceof RateLimitError) {
+ console.error('Rate limited, retrying in 60s');
+ await new Promise((resolve) => setTimeout(resolve, 60000));
+ } else {
+ throw error;
+ }
+}
+```
+
+## Automatic retries
+
+The SDK automatically retries `429` and `5xx` responses with exponential backoff up to `maxRetries` (default: 2).
+
+```typescript
+const client = new Terminal49Client({
+ apiToken: process.env.T49_API_TOKEN!,
+ maxRetries: 3,
+});
+```
+
+## Error properties
+
+All SDK errors include:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `message` | string | Human-readable error description |
+| `status` | number | HTTP status code |
+| `details` | unknown | Raw error payload from the API |
diff --git a/docs/sdk/filtering-pagination.mdx b/docs/sdk/filtering-pagination.mdx
new file mode 100644
index 00000000..52800232
--- /dev/null
+++ b/docs/sdk/filtering-pagination.mdx
@@ -0,0 +1,94 @@
+---
+title: "Filtering & Pagination"
+description: "Query shipments and containers with filters and handle large result sets"
+---
+
+## Filtering shipments
+
+Pass filter parameters to narrow results:
+
+```typescript
+const shipments = await client.shipments.list({
+ status: 'in_transit',
+ port: 'USLAX',
+ carrier: 'MAEU',
+ updatedAfter: '2025-01-01T00:00:00Z',
+});
+```
+
+Available shipment filters:
+
+| Filter | Type | Description |
+|--------|------|-------------|
+| `status` | string | Shipment status (for example `in_transit` or `delivered`) |
+| `port` | string | UN/LOCODE for port of discharge |
+| `carrier` | string | SCAC code (for example `MAEU`, `HLCU`) |
+| `updatedAfter` | ISO 8601 | Only shipments updated after this timestamp |
+| `includeContainers` | boolean | Set to `false` to omit containers from the included relationships |
+
+## Filtering containers
+
+```typescript
+const containers = await client.containers.list({
+ status: 'discharged',
+ port: 'USLAX',
+ carrier: 'MAEU',
+ updatedAfter: '2025-01-01T00:00:00Z',
+ include: 'shipment,pod_terminal',
+});
+```
+
+Available container filters:
+
+| Filter | Type | Description |
+|--------|------|-------------|
+| `status` | string | Container status |
+| `port` | string | UN/LOCODE for port of discharge |
+| `carrier` | string | SCAC code |
+| `updatedAfter` | ISO 8601 | Only containers updated after this timestamp |
+| `include` | string | Comma-delimited list of related resources to include |
+
+
+For list endpoints, avoid heavy `include` usage for performance. When you need deep relationships, prefer single-resource endpoints like `containers.get` or `shipments.get`.
+
+
+## Pagination
+
+List methods accept pagination options with `page` and `pageSize` (page numbers are 1-based):
+
+```typescript
+const page1 = await client.shipments.list({}, {
+ page: 1,
+ pageSize: 25,
+ format: 'mapped',
+});
+
+const page2 = await client.shipments.list({}, {
+ page: 2,
+ pageSize: 25,
+ format: 'mapped',
+});
+```
+
+When using `format: 'mapped'`, list results include `items`, `links`, and `meta`. When using `format: 'raw'`, these live in the JSON:API response.
+
+## Common patterns
+
+### Recently updated shipments
+
+```typescript
+const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
+
+const updated = await client.shipments.list({
+ updatedAfter: oneDayAgo,
+});
+```
+
+### In-transit containers at a specific port
+
+```typescript
+const containers = await client.containers.list({
+ status: 'in_transit',
+ port: 'USLAX',
+});
+```
diff --git a/docs/sdk/introduction.mdx b/docs/sdk/introduction.mdx
new file mode 100644
index 00000000..da89491a
--- /dev/null
+++ b/docs/sdk/introduction.mdx
@@ -0,0 +1,55 @@
+---
+title: "Introduction"
+description: "TypeScript SDK for the Terminal49 API"
+---
+
+The Terminal49 TypeScript SDK lets you track containers, retrieve shipment data, and receive status updates from your Node.js applications.
+
+## Requirements
+
+- Node.js 18 or later
+- A Terminal49 API key ([get one here](https://app.terminal49.com/developers))
+
+## Install
+
+```bash
+npm install @terminal49/sdk
+```
+
+## Setup
+
+Store your API key as an environment variable:
+
+```bash
+export T49_API_TOKEN=your_api_key
+```
+
+Then initialize the client:
+
+```typescript
+import { Terminal49Client } from '@terminal49/sdk';
+
+const client = new Terminal49Client({
+ apiToken: process.env.T49_API_TOKEN!,
+});
+```
+
+## What you can do
+
+- **Track containers** — Create tracking requests by container number, booking number, or bill of lading
+- **List shipments and containers** — Query with filters by status, port, carrier, or date
+- **Get transport events** — Pull milestones, timestamps, and location updates
+- **Fetch routing details** — See the full journey including vessels and ports
+
+For real-time updates, set up [webhooks](/api-docs/in-depth-guides/webhooks) to receive status changes as they happen.
+
+## Next steps
+
+
+
+ Track your first container in 5 minutes
+
+
+ See all available SDK methods
+
+
diff --git a/docs/sdk/methods.mdx b/docs/sdk/methods.mdx
new file mode 100644
index 00000000..b8bc9cd7
--- /dev/null
+++ b/docs/sdk/methods.mdx
@@ -0,0 +1,124 @@
+---
+title: "Methods Reference"
+description: "All available methods in the Terminal49 SDK"
+---
+
+The SDK exposes a `Terminal49Client` with methods grouped by resource type. Each method corresponds to an [API endpoint](/api-docs/home).
+
+## Resource namespaces (recommended)
+
+### Search
+
+| Method | Description |
+|--------|-------------|
+| `client.search(query)` | Search across shipments and containers by number, reference, or keyword |
+
+### Shipments
+
+| Method | Description |
+|--------|-------------|
+| `client.shipments.get(id, includeContainers?, options?)` | Fetch a shipment by ID. Set `includeContainers: false` to omit container relationships. |
+| `client.shipments.list(filters?, options?)` | List shipments matching filter criteria. |
+| `client.shipments.update(id, attrs, options?)` | Update shipment attributes like reference numbers or tags. |
+| `client.shipments.stopTracking(id, options?)` | Stop tracking a shipment and its containers. |
+| `client.shipments.resumeTracking(id, options?)` | Resume tracking a previously stopped shipment. |
+
+### Containers
+
+| Method | Description |
+|--------|-------------|
+| `client.containers.get(id, include?, options?)` | Fetch a container by ID. `include` is an array of related resources. |
+| `client.containers.list(filters?, options?)` | List containers matching filter criteria. |
+| `client.containers.events(id, options?)` | Get transport events for a container. |
+| `client.containers.route(id, options?)` | Get routing details: vessels, ports, and journey legs. |
+| `client.containers.rawEvents(id, options?)` | Get unprocessed events as received from carriers. |
+| `client.containers.refresh(id, options?)` | Request an immediate data refresh from the carrier. |
+
+### Tracking Requests
+
+| Method | Description |
+|--------|-------------|
+| `client.trackingRequests.list(filters?, options?)` | List tracking requests. |
+| `client.trackingRequests.get(id, options?)` | Fetch a single tracking request. |
+| `client.trackingRequests.update(id, attrs, options?)` | Update tracking request attributes. |
+| `client.trackingRequests.create(params)` | Create a tracking request with an explicit request type and SCAC. |
+| `client.trackingRequests.inferNumber(number)` | Detect whether a number is a container, booking, or bill of lading. |
+| `client.trackingRequests.createFromInfer(number, options?)` | Create a tracking request with automatic number type detection. |
+
+### Shipping Lines
+
+| Method | Description |
+|--------|-------------|
+| `client.shippingLines.list(search?, options?)` | List carriers. Use `search` to filter by name or SCAC. |
+
+## Helpers and aliases
+
+| Method | Description |
+|--------|-------------|
+| `client.trackContainer(params)` | Convenience helper that creates a tracking request using a container or booking number. |
+| `client.listTrackRequests(filters?, options?)` | Alias for `client.trackingRequests.list`. |
+| `client.getDemurrage(containerId)` | Returns a subset of demurrage-related fields for a container. |
+| `client.getRailMilestones(containerId)` | Returns rail milestones derived from transport events. |
+| `client.deserialize(document)` | Deserialize a JSON:API document into plain objects using JSONA. |
+
+## Direct method equivalents
+
+All namespace methods are also available as direct methods on the client:
+
+| Namespace method | Direct method |
+|------------------|--------------|
+| `client.shipments.get` | `client.getShipment` |
+| `client.shipments.list` | `client.listShipments` |
+| `client.shipments.update` | `client.updateShipment` |
+| `client.shipments.stopTracking` | `client.stopTrackingShipment` |
+| `client.shipments.resumeTracking` | `client.resumeTrackingShipment` |
+| `client.containers.get` | `client.getContainer` |
+| `client.containers.list` | `client.listContainers` |
+| `client.containers.events` | `client.getContainerTransportEvents` |
+| `client.containers.route` | `client.getContainerRoute` |
+| `client.containers.rawEvents` | `client.getContainerRawEvents` |
+| `client.containers.refresh` | `client.refreshContainer` |
+| `client.trackingRequests.list` | `client.listTrackingRequests` |
+| `client.trackingRequests.get` | `client.getTrackingRequest` |
+| `client.trackingRequests.update` | `client.updateTrackingRequest` |
+| `client.trackingRequests.create` | `client.createTrackingRequest` |
+| `client.trackingRequests.inferNumber` | `client.inferTrackingNumber` |
+| `client.trackingRequests.createFromInfer` | `client.createTrackingRequestFromInfer` |
+| `client.shippingLines.list` | `client.listShippingLines` |
+
+## Common options
+
+Most methods accept an `options` object with `format`:
+
+```typescript
+const shipment = await client.shipments.get('shipment-id', true, {
+ format: 'mapped',
+});
+```
+
+Supported formats:
+
+- `raw` (default) returns the JSON:API response
+- `mapped` returns simplified objects for methods that support mapping
+- `both` returns `{ raw, mapped }`
+
+You can set a default format when initializing the client:
+
+```typescript
+const client = new Terminal49Client({
+ apiToken: process.env.T49_API_TOKEN!,
+ defaultFormat: 'mapped',
+});
+```
+
+List methods also accept pagination options:
+
+```typescript
+const shipments = await client.shipments.list({}, {
+ page: 1,
+ pageSize: 25,
+ format: 'mapped',
+});
+```
+
+See [Filtering & Pagination](/sdk/filtering-pagination) for details.
diff --git a/docs/sdk/quickstart.mdx b/docs/sdk/quickstart.mdx
new file mode 100644
index 00000000..afefe2f9
--- /dev/null
+++ b/docs/sdk/quickstart.mdx
@@ -0,0 +1,73 @@
+---
+title: "Quickstart"
+description: "Track a container and retrieve shipment data in 5 minutes"
+---
+
+This walkthrough shows the most common SDK operations: creating a tracking request, listing shipments, and fetching container details.
+
+## Prerequisites
+
+Make sure you have [installed the SDK](/sdk/introduction) and set your `T49_API_TOKEN` environment variable.
+
+## Complete example
+
+```typescript
+import { Terminal49Client } from '@terminal49/sdk';
+
+const client = new Terminal49Client({
+ apiToken: process.env.T49_API_TOKEN!,
+});
+
+async function main() {
+ // 1) Track a container (creates a tracking request)
+ // Provide a SCAC for faster, more reliable inference when known.
+ await client.trackingRequests.createFromInfer('MSCU1234567', {
+ scac: 'MSCU',
+ });
+
+ // 2) List your shipments (mapped response)
+ const shipments = await client.shipments.list(
+ { updatedAfter: '2025-01-01T00:00:00Z' },
+ { format: 'mapped' },
+ );
+ console.log(`Found ${shipments.items.length} shipments`);
+
+ // 3) Get a specific container with related data (raw JSON:API)
+ const containerId = 'your-container-uuid';
+ const container = await client.containers.get(containerId, [
+ 'shipment',
+ 'pod_terminal',
+ ]);
+ console.log(container.data?.id);
+
+ // 4) Get transport events (milestones and timeline)
+ const events = await client.containers.events(containerId, {
+ format: 'mapped',
+ });
+ console.log(`Container has ${events.length} events`);
+
+ // 5) Get routing details (vessels, ports, legs)
+ const route = await client.containers.route(containerId, {
+ format: 'mapped',
+ });
+ console.log(`Route has ${route.locations.length} locations`);
+}
+
+main();
+```
+
+## What’s happening
+
+**Tracking requests** tell Terminal49 to start monitoring a container. You can track by container number, booking number, or bill of lading. Once tracked, Terminal49 polls carriers and terminals for updates.
+
+**Shipments** are the parent objects that group related containers. A single bill of lading might have multiple containers.
+
+**Events** are individual milestones: gate out, vessel departure, discharge, and more. Each event has a timestamp, location, and description.
+
+**Routes** show the planned and actual journey, broken into locations with inbound and outbound legs.
+
+## Next steps
+
+- [Methods Reference](/sdk/methods) — See all available operations
+- [Filtering & Pagination](/sdk/filtering-pagination) — Query large datasets efficiently
+- [Webhooks](/api-docs/in-depth-guides/webhooks) — Get notified when shipments update
diff --git a/docs/sdk/typescript/authentication.mdx b/docs/sdk/typescript/authentication.mdx
new file mode 100644
index 00000000..203b64eb
--- /dev/null
+++ b/docs/sdk/typescript/authentication.mdx
@@ -0,0 +1,7 @@
+---
+title: "Authentication"
+---
+
+This page has moved.
+
+- [SDK Introduction](/sdk/introduction)
diff --git a/docs/sdk/typescript/available-methods.mdx b/docs/sdk/typescript/available-methods.mdx
new file mode 100644
index 00000000..1c71ef5d
--- /dev/null
+++ b/docs/sdk/typescript/available-methods.mdx
@@ -0,0 +1,7 @@
+---
+title: "Available Methods"
+---
+
+This page has moved.
+
+- [Methods Reference](/sdk/methods)
diff --git a/docs/sdk/typescript/error-handling.mdx b/docs/sdk/typescript/error-handling.mdx
new file mode 100644
index 00000000..e6f81ad4
--- /dev/null
+++ b/docs/sdk/typescript/error-handling.mdx
@@ -0,0 +1,7 @@
+---
+title: "Error Handling"
+---
+
+This page has moved.
+
+- [Error Handling](/sdk/error-handling)
diff --git a/docs/sdk/typescript/filtering.mdx b/docs/sdk/typescript/filtering.mdx
new file mode 100644
index 00000000..9974198f
--- /dev/null
+++ b/docs/sdk/typescript/filtering.mdx
@@ -0,0 +1,7 @@
+---
+title: "Filtering"
+---
+
+This page has moved.
+
+- [Filtering & Pagination](/sdk/filtering-pagination)
diff --git a/docs/sdk/typescript/installation.mdx b/docs/sdk/typescript/installation.mdx
new file mode 100644
index 00000000..9c7dca5e
--- /dev/null
+++ b/docs/sdk/typescript/installation.mdx
@@ -0,0 +1,7 @@
+---
+title: "TypeScript Installation"
+---
+
+This page has moved.
+
+- [SDK Introduction](/sdk/introduction)
diff --git a/docs/sdk/typescript/pagination.mdx b/docs/sdk/typescript/pagination.mdx
new file mode 100644
index 00000000..d8fc40de
--- /dev/null
+++ b/docs/sdk/typescript/pagination.mdx
@@ -0,0 +1,7 @@
+---
+title: "Pagination"
+---
+
+This page has moved.
+
+- [Filtering & Pagination](/sdk/filtering-pagination)
diff --git a/docs/sdk/typescript/quickstart.mdx b/docs/sdk/typescript/quickstart.mdx
new file mode 100644
index 00000000..b8f3631b
--- /dev/null
+++ b/docs/sdk/typescript/quickstart.mdx
@@ -0,0 +1,7 @@
+---
+title: "Quickstart"
+---
+
+This page has moved.
+
+- [SDK Quickstart](/sdk/quickstart)
diff --git a/sdks/typescript-sdk/.gitignore b/sdks/typescript-sdk/.gitignore
new file mode 100644
index 00000000..ae3897d9
--- /dev/null
+++ b/sdks/typescript-sdk/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+dist/
+.env
+.env.local
+.DS_Store
diff --git a/sdks/typescript-sdk/README.md b/sdks/typescript-sdk/README.md
new file mode 100644
index 00000000..3d17eab2
--- /dev/null
+++ b/sdks/typescript-sdk/README.md
@@ -0,0 +1,131 @@
+# Terminal49 TypeScript SDK
+
+Typed, server-side client for the Terminal49 JSON:API, built with `openapi-fetch`, generated OpenAPI types, and JSONA deserialization. Can be used standalone or inside the MCP server.
+
+## Installation
+
+```bash
+# install from npm (recommended)
+npm install @terminal49/sdk
+
+# or inside this repo
+cd sdks/typescript-sdk
+npm install
+```
+
+## Usage
+
+```ts
+import { Terminal49Client } from '@terminal49/sdk';
+
+const client = new Terminal49Client({ apiToken: process.env.T49_API_TOKEN! });
+const container = await client.getContainer('container-uuid', ['shipment']);
+console.log(container); // raw JSON:API document
+
+// Optional: deserialize JSON:API to plain objects
+const simplified = client.deserialize(container);
+```
+
+## Guide
+
+For a full walkthrough (track a container, list shipments, pull events, and routing),
+see the SDK quickstart in the docs site: `docs/api-docs/getting-started/sdk-quickstart.mdx`.
+
+### Methods
+- `search(query)`
+- `getContainer(id, include?)`
+- `trackContainer({ containerNumber?, bookingNumber?, scac?, refNumbers? })`
+- `createTrackingRequest({ requestType, requestNumber, scac?, refNumbers?, shipmentTags? })`
+- `inferTrackingNumber(number)`
+- `createTrackingRequestFromInfer(number, { scac?, numberType?, refNumbers?, shipmentTags? })`
+- `getShipment(id, includeContainers?)`
+- `listShipments(filters?, options?)`
+- `listContainers(filters?, options?)`
+- `listTrackingRequests(filters?, options?)` / `listTrackRequests(filters?, options?)`
+- `getContainerTransportEvents(id)`
+- `getContainerRoute(id)`
+- `listShippingLines(search?)`
+- `getDemurrage(containerId)` (helper)
+- `getRailMilestones(containerId)` (helper)
+- `deserialize(document)` → JSONA-based plain objects
+
+### Examples
+
+After building, run:
+```bash
+cd sdks/typescript-sdk
+export T49_API_TOKEN=your_token
+export T49_CONTAINER_ID=valid_container_uuid
+npm run build
+npm run example
+```
+
+`example.ts` prints the raw JSON:API response and a simplified view using `deserialize`.
+
+## Development
+
+```bash
+# Generate types from OpenAPI
+npm run generate:types
+
+# Type-check
+npm run type-check
+
+# Tests
+npm test
+
+# Lint (Biome)
+npm run lint
+
+# Build
+npm run build
+```
+
+## Testing
+
+Unit tests:
+```bash
+cd sdks/typescript-sdk
+npm test
+```
+
+Type checks and lint:
+```bash
+cd sdks/typescript-sdk
+npm run type-check
+npm run lint
+```
+
+Smoke tests (optional, require a token):
+```bash
+cd sdks/typescript-sdk
+export T49_API_TOKEN=your_token
+export T49_API_BASE_URL=https://api.terminal49.com/v2
+export T49_INFER_NUMBER=your_tracking_number
+export T49_RUN_SMOKE=1
+npm run smoke
+```
+
+## Fixtures
+
+Generate sanitized, production-based fixtures for tests:
+```bash
+cd sdks/typescript-sdk
+export T49_API_TOKEN=your_token
+export T49_API_BASE_URL=https://api.terminal49.com/v2
+npm run fixtures:generate
+```
+
+This writes JSON:API fixtures to `src/fixtures/` with redacted IDs and numbers.
+Numeric identifiers keep their original prefix while the last few characters are
+obfuscated to preserve shape without exposing real values.
+List endpoints are captured without `include` for performance guidance. Single-resource
+fixtures include both base and `include` variants where supported.
+
+## Publishing (prep)
+- Add a `prepublishOnly` or `prepare` script to run `npm run build` so `dist/` is fresh.
+- Ensure `files`/`exports` only ship built JS/typings (currently `main/types/exports` point to `dist/`).
+
+## Notes
+- Server-only: uses Node fetch (undici types) and targets Node 18+.
+- Returns raw JSON:API documents by default; use `deserialize` for flattened objects or add your own mappers.
diff --git a/sdks/typescript-sdk/biome.json b/sdks/typescript-sdk/biome.json
new file mode 100644
index 00000000..0e53e68b
--- /dev/null
+++ b/sdks/typescript-sdk/biome.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
+ "files": {
+ "ignore": ["dist/**", "node_modules/**", "src/generated/**"]
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space",
+ "indentWidth": 2
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true,
+ "suspicious": {
+ "noExplicitAny": "off"
+ }
+ }
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "single"
+ }
+ }
+}
diff --git a/sdks/typescript-sdk/package-lock.json b/sdks/typescript-sdk/package-lock.json
new file mode 100644
index 00000000..3f228100
--- /dev/null
+++ b/sdks/typescript-sdk/package-lock.json
@@ -0,0 +1,2304 @@
+{
+ "name": "@terminal49/sdk",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@terminal49/sdk",
+ "version": "0.1.0",
+ "dependencies": {
+ "jsona": "^1.12.1",
+ "openapi-fetch": "^0.15.0"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "^1.9.4",
+ "@types/node": "^20.19.25",
+ "@vitest/coverage-v8": "^4.0.18",
+ "dotenv": "^17.2.3",
+ "openapi-typescript": "^7.10.1",
+ "typescript": "^5.6.3",
+ "vitest": "^4.0.13"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@biomejs/biome": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz",
+ "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT OR Apache-2.0",
+ "bin": {
+ "biome": "bin/biome"
+ },
+ "engines": {
+ "node": ">=14.21.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/biome"
+ },
+ "optionalDependencies": {
+ "@biomejs/cli-darwin-arm64": "1.9.4",
+ "@biomejs/cli-darwin-x64": "1.9.4",
+ "@biomejs/cli-linux-arm64": "1.9.4",
+ "@biomejs/cli-linux-arm64-musl": "1.9.4",
+ "@biomejs/cli-linux-x64": "1.9.4",
+ "@biomejs/cli-linux-x64-musl": "1.9.4",
+ "@biomejs/cli-win32-arm64": "1.9.4",
+ "@biomejs/cli-win32-x64": "1.9.4"
+ }
+ },
+ "node_modules/@biomejs/cli-darwin-arm64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz",
+ "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-darwin-x64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz",
+ "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-arm64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz",
+ "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-arm64-musl": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz",
+ "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-x64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
+ "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-x64-musl": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz",
+ "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-win32-arm64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz",
+ "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-win32-x64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz",
+ "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
+ "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
+ "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
+ "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
+ "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
+ "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
+ "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
+ "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
+ "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
+ "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
+ "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
+ "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
+ "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
+ "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
+ "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
+ "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
+ "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
+ "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
+ "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
+ "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
+ "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
+ "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@redocly/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@redocly/config": {
+ "version": "0.22.2",
+ "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz",
+ "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@redocly/openapi-core": {
+ "version": "1.34.5",
+ "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.5.tgz",
+ "integrity": "sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@redocly/ajv": "^8.11.2",
+ "@redocly/config": "^0.22.0",
+ "colorette": "^1.2.0",
+ "https-proxy-agent": "^7.0.5",
+ "js-levenshtein": "^1.1.6",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^5.0.1",
+ "pluralize": "^8.0.0",
+ "yaml-ast-parser": "0.0.43"
+ },
+ "engines": {
+ "node": ">=18.17.0",
+ "npm": ">=9.5.0"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
+ "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
+ "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
+ "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
+ "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
+ "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
+ "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
+ "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
+ "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
+ "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
+ "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
+ "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
+ "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
+ "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
+ "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
+ "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
+ "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
+ "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
+ "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
+ "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
+ "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
+ "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
+ "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.25",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
+ "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/node/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitest/coverage-v8": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz",
+ "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^1.0.2",
+ "@vitest/utils": "4.0.18",
+ "ast-v8-to-istanbul": "^0.3.10",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-reports": "^3.2.0",
+ "magicast": "^0.5.1",
+ "obug": "^2.1.1",
+ "std-env": "^3.10.0",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "4.0.18",
+ "vitest": "4.0.18"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
+ "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.0.18",
+ "@vitest/utils": "4.0.18",
+ "chai": "^6.2.1",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
+ "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.0.18",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
+ "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
+ "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.0.18",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
+ "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.0.18",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
+ "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
+ "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.0.18",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+ "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz",
+ "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^10.0.0"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
+ "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/change-case": {
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
+ "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/colorette": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
+ "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.2.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
+ "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.2",
+ "@esbuild/android-arm": "0.27.2",
+ "@esbuild/android-arm64": "0.27.2",
+ "@esbuild/android-x64": "0.27.2",
+ "@esbuild/darwin-arm64": "0.27.2",
+ "@esbuild/darwin-x64": "0.27.2",
+ "@esbuild/freebsd-arm64": "0.27.2",
+ "@esbuild/freebsd-x64": "0.27.2",
+ "@esbuild/linux-arm": "0.27.2",
+ "@esbuild/linux-arm64": "0.27.2",
+ "@esbuild/linux-ia32": "0.27.2",
+ "@esbuild/linux-loong64": "0.27.2",
+ "@esbuild/linux-mips64el": "0.27.2",
+ "@esbuild/linux-ppc64": "0.27.2",
+ "@esbuild/linux-riscv64": "0.27.2",
+ "@esbuild/linux-s390x": "0.27.2",
+ "@esbuild/linux-x64": "0.27.2",
+ "@esbuild/netbsd-arm64": "0.27.2",
+ "@esbuild/netbsd-x64": "0.27.2",
+ "@esbuild/openbsd-arm64": "0.27.2",
+ "@esbuild/openbsd-x64": "0.27.2",
+ "@esbuild/openharmony-arm64": "0.27.2",
+ "@esbuild/sunos-x64": "0.27.2",
+ "@esbuild/win32-arm64": "0.27.2",
+ "@esbuild/win32-ia32": "0.27.2",
+ "@esbuild/win32-x64": "0.27.2"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/index-to-position": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz",
+ "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/js-levenshtein": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
+ "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsona": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/jsona/-/jsona-1.12.1.tgz",
+ "integrity": "sha512-44WL4ZdsKx//mCDPUFQtbK7mnVdHXcVzbBy7Pzy0LAgXyfpN5+q8Hum7cLUX4wTnRsClHb4eId1hePZYchwczg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.1"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/magicast": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz",
+ "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+ "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/openapi-fetch": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.15.0.tgz",
+ "integrity": "sha512-OjQUdi61WO4HYhr9+byCPMj0+bgste/LtSBEcV6FzDdONTs7x0fWn8/ndoYwzqCsKWIxEZwo4FN/TG1c1rI8IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "openapi-typescript-helpers": "^0.0.15"
+ }
+ },
+ "node_modules/openapi-typescript": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.10.1.tgz",
+ "integrity": "sha512-rBcU8bjKGGZQT4K2ekSTY2Q5veOQbVG/lTKZ49DeCyT9z62hM2Vj/LLHjDHC9W7LJG8YMHcdXpRZDqC1ojB/lw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@redocly/openapi-core": "^1.34.5",
+ "ansi-colors": "^4.1.3",
+ "change-case": "^5.4.4",
+ "parse-json": "^8.3.0",
+ "supports-color": "^10.2.2",
+ "yargs-parser": "^21.1.1"
+ },
+ "bin": {
+ "openapi-typescript": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "typescript": "^5.x"
+ }
+ },
+ "node_modules/openapi-typescript-helpers": {
+ "version": "0.0.15",
+ "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz",
+ "integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==",
+ "license": "MIT"
+ },
+ "node_modules/parse-json": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
+ "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.26.2",
+ "index-to-position": "^1.1.0",
+ "type-fest": "^4.39.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pluralize": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
+ "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
+ "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.57.1",
+ "@rollup/rollup-android-arm64": "4.57.1",
+ "@rollup/rollup-darwin-arm64": "4.57.1",
+ "@rollup/rollup-darwin-x64": "4.57.1",
+ "@rollup/rollup-freebsd-arm64": "4.57.1",
+ "@rollup/rollup-freebsd-x64": "4.57.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.57.1",
+ "@rollup/rollup-linux-arm64-musl": "4.57.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.57.1",
+ "@rollup/rollup-linux-loong64-musl": "4.57.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.57.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.57.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-musl": "4.57.1",
+ "@rollup/rollup-openbsd-x64": "4.57.1",
+ "@rollup/rollup-openharmony-arm64": "4.57.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.57.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.57.1",
+ "@rollup/rollup-win32-x64-gnu": "4.57.1",
+ "@rollup/rollup-win32-x64-msvc": "4.57.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/supports-color": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
+ "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
+ "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
+ "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
+ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.0.18",
+ "@vitest/mocker": "4.0.18",
+ "@vitest/pretty-format": "4.0.18",
+ "@vitest/runner": "4.0.18",
+ "@vitest/snapshot": "4.0.18",
+ "@vitest/spy": "4.0.18",
+ "@vitest/utils": "4.0.18",
+ "es-module-lexer": "^1.7.0",
+ "expect-type": "^1.2.2",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^3.10.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.0.3",
+ "vite": "^6.0.0 || ^7.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.0.18",
+ "@vitest/browser-preview": "4.0.18",
+ "@vitest/browser-webdriverio": "4.0.18",
+ "@vitest/ui": "4.0.18",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yaml-ast-parser": {
+ "version": "0.0.43",
+ "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
+ "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ }
+ }
+}
diff --git a/sdks/typescript-sdk/package.json b/sdks/typescript-sdk/package.json
new file mode 100644
index 00000000..69318589
--- /dev/null
+++ b/sdks/typescript-sdk/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "@terminal49/sdk",
+ "version": "0.1.0",
+ "description": "Terminal49 TypeScript SDK (JSON:API, openapi-fetch)",
+ "type": "module",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": "./dist/index.js",
+ "./client": "./dist/client.js"
+ },
+ "scripts": {
+ "build": "tsc",
+ "type-check": "tsc --noEmit",
+ "test": "vitest",
+ "generate:types": "openapi-typescript ../../docs/openapi.json -o src/generated/terminal49.ts",
+ "example": "node -r dotenv/config dist/scripts/example.js",
+ "fixtures:generate": "node -r dotenv/config scripts/generate-fixtures.mjs",
+ "smoke": "node -r dotenv/config dist/scripts/smoke.js",
+ "smoke:lists": "node -r dotenv/config dist/scripts/list-smoke.js",
+ "prepublishOnly": "npm run build",
+ "lint": "biome check src"
+ },
+ "files": [
+ "dist/**/*"
+ ],
+ "engines": {
+ "node": ">=18"
+ },
+ "dependencies": {
+ "jsona": "^1.12.1",
+ "openapi-fetch": "^0.15.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20.19.25",
+ "@biomejs/biome": "^1.9.4",
+ "@vitest/coverage-v8": "^4.0.18",
+ "dotenv": "^17.2.3",
+ "openapi-typescript": "^7.10.1",
+ "typescript": "^5.6.3",
+ "vitest": "^4.0.13"
+ }
+}
diff --git a/sdks/typescript-sdk/scripts/generate-fixtures.mjs b/sdks/typescript-sdk/scripts/generate-fixtures.mjs
new file mode 100644
index 00000000..0049ad7a
--- /dev/null
+++ b/sdks/typescript-sdk/scripts/generate-fixtures.mjs
@@ -0,0 +1,383 @@
+import crypto from 'node:crypto';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import dotenv from 'dotenv';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const sdkRoot = path.resolve(__dirname, '..');
+
+const envCandidates = [
+ process.env.DOTENV_CONFIG_PATH,
+ path.resolve(sdkRoot, '../.env.local'),
+ path.resolve(sdkRoot, '../.env'),
+ path.resolve(sdkRoot, '.env.local'),
+ path.resolve(sdkRoot, '.env'),
+].filter(Boolean);
+
+for (const candidate of envCandidates) {
+ if (fs.existsSync(candidate)) {
+ dotenv.config({ path: candidate });
+ break;
+ }
+}
+
+const token = process.env.T49_API_TOKEN;
+if (!token) {
+ console.error('Missing T49_API_TOKEN in environment.');
+ process.exit(1);
+}
+
+const baseUrl = (
+ process.env.T49_API_BASE_URL || 'https://api.terminal49.com/v2'
+).replace(/\/+$/, '');
+const authHeader = token.startsWith('Token ') ? token : `Token ${token}`;
+
+const fixturesDir = path.resolve(sdkRoot, 'src/fixtures');
+fs.mkdirSync(fixturesDir, { recursive: true });
+
+const idMaps = new Map();
+const valueMaps = new Map();
+
+function mapId(type, id) {
+ if (!id || typeof id !== 'string') return id;
+ const key = `${type}:${id}`;
+ if (!idMaps.has(key)) {
+ const count =
+ [...idMaps.keys()].filter((k) => k.startsWith(`${type}:`)).length + 1;
+ idMaps.set(key, `${type}-${count}`);
+ }
+ return idMaps.get(key);
+}
+
+function mapValue(key, value, prefix) {
+ if (!value || typeof value !== 'string') return value;
+ const mapKey = `${key}:${prefix}`;
+ if (!valueMaps.has(mapKey)) valueMaps.set(mapKey, new Map());
+ const map = valueMaps.get(mapKey);
+ if (!map.has(value)) {
+ map.set(value, `${prefix}-${String(map.size + 1).padStart(3, '0')}`);
+ }
+ return map.get(value);
+}
+
+function obfuscateTail(value, key, tailLength = 3) {
+ if (!value || typeof value !== 'string') return value;
+ if (value.length <= tailLength) {
+ return mapValue(key, value, key.toUpperCase().slice(0, 3));
+ }
+ const prefix = value.slice(0, -tailLength);
+ const hash = crypto
+ .createHash('sha256')
+ .update(`${key}:${value}`)
+ .digest('hex');
+ const suffix = hash.slice(0, tailLength);
+ return `${prefix}${suffix}`;
+}
+
+function sanitizeAttributes(type, attrs) {
+ if (!attrs || typeof attrs !== 'object') return attrs;
+
+ const sanitized = { ...attrs };
+
+ if (type === 'container') {
+ if (sanitized.number) {
+ sanitized.number = obfuscateTail(sanitized.number, 'container_number');
+ }
+ if (sanitized.container_number) {
+ sanitized.container_number = obfuscateTail(
+ sanitized.container_number,
+ 'container_number',
+ );
+ }
+ }
+
+ if (type === 'shipment') {
+ if (sanitized.bill_of_lading_number) {
+ sanitized.bill_of_lading_number = obfuscateTail(
+ sanitized.bill_of_lading_number,
+ 'bill_of_lading',
+ );
+ }
+ if (sanitized.bill_of_lading) {
+ sanitized.bill_of_lading = obfuscateTail(
+ sanitized.bill_of_lading,
+ 'bill_of_lading',
+ );
+ }
+ if (sanitized.bl_number) {
+ sanitized.bl_number = obfuscateTail(
+ sanitized.bl_number,
+ 'bill_of_lading',
+ );
+ }
+ }
+
+ if (type === 'tracking_request') {
+ if (sanitized.request_number) {
+ sanitized.request_number = obfuscateTail(
+ sanitized.request_number,
+ 'request_number',
+ );
+ }
+ if (Array.isArray(sanitized.ref_numbers)) {
+ sanitized.ref_numbers = sanitized.ref_numbers.map((value) =>
+ obfuscateTail(value, 'ref_number'),
+ );
+ }
+ }
+
+ if (sanitized.booking_number) {
+ sanitized.booking_number = obfuscateTail(
+ sanitized.booking_number,
+ 'booking_number',
+ );
+ }
+
+ if (sanitized.customer_name) {
+ sanitized.customer_name = mapValue(
+ 'customer_name',
+ sanitized.customer_name,
+ 'CUSTOMER',
+ );
+ }
+
+ return sanitized;
+}
+
+function sanitizeRelationships(relationships) {
+ if (!relationships || typeof relationships !== 'object') return relationships;
+ const sanitized = { ...relationships };
+
+ for (const value of Object.values(sanitized)) {
+ if (!value || typeof value !== 'object') continue;
+ if (Array.isArray(value.data)) {
+ value.data = value.data.map((ref) => ({
+ ...ref,
+ id: mapId(ref.type, ref.id),
+ }));
+ } else if (value.data && typeof value.data === 'object') {
+ value.data = { ...value.data, id: mapId(value.data.type, value.data.id) };
+ }
+ }
+
+ return sanitized;
+}
+
+function sanitizeResource(resource) {
+ if (!resource || typeof resource !== 'object') return resource;
+ const type = resource.type;
+ const sanitized = { ...resource };
+
+ if (type && sanitized.id) {
+ sanitized.id = mapId(type, sanitized.id);
+ }
+
+ if (sanitized.attributes) {
+ sanitized.attributes = sanitizeAttributes(type, sanitized.attributes);
+ }
+
+ if (sanitized.relationships) {
+ sanitized.relationships = sanitizeRelationships(sanitized.relationships);
+ }
+
+ return sanitized;
+}
+
+function sanitizeDocument(doc) {
+ if (!doc || typeof doc !== 'object') return doc;
+ const sanitized = JSON.parse(JSON.stringify(doc));
+
+ if (Array.isArray(sanitized.data)) {
+ sanitized.data = sanitized.data.map((item) => sanitizeResource(item));
+ } else if (sanitized.data && typeof sanitized.data === 'object') {
+ sanitized.data = sanitizeResource(sanitized.data);
+ }
+
+ if (Array.isArray(sanitized.included)) {
+ sanitized.included = sanitized.included.map((item) =>
+ sanitizeResource(item),
+ );
+ }
+
+ return sanitized;
+}
+
+async function fetchJson(pathname) {
+ const response = await fetch(`${baseUrl}${pathname}`, {
+ headers: {
+ Authorization: authHeader,
+ Accept: 'application/json',
+ },
+ });
+
+ const text = await response.text();
+ let body;
+ try {
+ body = text ? JSON.parse(text) : null;
+ } catch {
+ body = null;
+ }
+
+ if (!response.ok) {
+ const message = body?.errors
+ ? JSON.stringify(body.errors)
+ : response.statusText;
+ throw new Error(`${response.status} ${message}`);
+ }
+
+ return body;
+}
+
+function writeFixture(name, doc) {
+ const sanitized = sanitizeDocument(doc);
+ const filepath = path.resolve(fixturesDir, `${name}.json`);
+ fs.writeFileSync(filepath, JSON.stringify(sanitized, null, 2));
+ console.log(`Wrote ${path.relative(sdkRoot, filepath)}`);
+}
+
+function firstIdFromList(doc) {
+ if (!doc || !Array.isArray(doc.data) || doc.data.length === 0) return null;
+ return doc.data[0]?.id || null;
+}
+
+function firstIncludedId(doc, type) {
+ if (!doc || !Array.isArray(doc.included)) return null;
+ const match = doc.included.find((item) => item.type === type);
+ return match?.id || null;
+}
+
+function firstRelationshipId(doc, relName) {
+ const rel = doc?.data?.relationships?.[relName]?.data;
+ if (!rel) return null;
+ if (Array.isArray(rel)) return rel[0]?.id || null;
+ return rel.id || null;
+}
+
+async function main() {
+ const fixtures = {};
+
+ const shipmentsList = await fetchJson('/shipments?page[size]=1');
+ fixtures.shipmentsList = shipmentsList;
+ writeFixture('shipments.list', shipmentsList);
+
+ const shipmentId = firstIdFromList(shipmentsList);
+ if (shipmentId) {
+ const shipmentGetBase = await fetchJson(`/shipments/${shipmentId}`);
+ fixtures.shipmentGetBase = shipmentGetBase;
+ writeFixture('shipments.get.base', shipmentGetBase);
+
+ const shipmentGetInclude = await fetchJson(
+ `/shipments/${shipmentId}?include=containers,pod_terminal,port_of_lading,port_of_discharge,destination,destination_terminal`,
+ );
+ fixtures.shipmentGetInclude = shipmentGetInclude;
+ writeFixture('shipments.get.include', shipmentGetInclude);
+ }
+
+ const containersList = await fetchJson('/containers?page[size]=1');
+ fixtures.containersList = containersList;
+ writeFixture('containers.list', containersList);
+
+ const containerId =
+ firstIncludedId(fixtures.shipmentGetInclude, 'container') ||
+ firstRelationshipId(fixtures.shipmentGetInclude, 'containers') ||
+ firstIdFromList(containersList);
+
+ if (containerId) {
+ const containerGetBase = await fetchJson(`/containers/${containerId}`);
+ writeFixture('containers.get.base', containerGetBase);
+
+ const containerGetInclude = await fetchJson(
+ `/containers/${containerId}?include=shipment,pod_terminal`,
+ );
+ writeFixture('containers.get.include', containerGetInclude);
+
+ try {
+ const route = await fetchJson(
+ `/containers/${containerId}/route?include=port,vessel,route_location`,
+ );
+ writeFixture('containers.route', route);
+ } catch (error) {
+ console.warn(`Skipping route fixture: ${error.message}`);
+ }
+
+ try {
+ const events = await fetchJson(
+ `/containers/${containerId}/transport_events?include=location,terminal`,
+ );
+ writeFixture('containers.events', events);
+ } catch (error) {
+ console.warn(`Skipping transport events fixture: ${error.message}`);
+ }
+
+ try {
+ const rawEvents = await fetchJson(
+ `/containers/${containerId}/raw_events`,
+ );
+ writeFixture('containers.raw-events', rawEvents);
+ } catch (error) {
+ console.warn(`Skipping raw events fixture: ${error.message}`);
+ }
+ }
+
+ const trackingList = await fetchJson('/tracking_requests?page[size]=1');
+ fixtures.trackingList = trackingList;
+ writeFixture('tracking-requests.list', trackingList);
+
+ const trackingId = firstIdFromList(trackingList);
+ if (trackingId) {
+ const trackingGetBase = await fetchJson(`/tracking_requests/${trackingId}`);
+ writeFixture('tracking-requests.get.base', trackingGetBase);
+
+ try {
+ const trackingGetInclude = await fetchJson(
+ `/tracking_requests/${trackingId}?include=shipment,container`,
+ );
+ writeFixture('tracking-requests.get.include', trackingGetInclude);
+ } catch (error) {
+ console.warn(
+ `Skipping tracking request include fixture: ${error.message}`,
+ );
+ }
+ }
+
+ const shippingLines = await fetchJson('/shipping_lines');
+ writeFixture('shipping-lines.list', shippingLines);
+
+ const portId =
+ firstIncludedId(fixtures.shipmentGetInclude, 'port') ||
+ firstRelationshipId(fixtures.shipmentGetInclude, 'port_of_lading') ||
+ firstRelationshipId(fixtures.shipmentGetInclude, 'port_of_discharge') ||
+ firstRelationshipId(fixtures.shipmentGetInclude, 'destination');
+
+ if (portId) {
+ try {
+ const port = await fetchJson(`/ports/${portId}`);
+ writeFixture('ports.get', port);
+ } catch (error) {
+ console.warn(`Skipping port fixture: ${error.message}`);
+ }
+ }
+
+ const terminalId =
+ firstIncludedId(fixtures.shipmentGetInclude, 'terminal') ||
+ firstRelationshipId(fixtures.shipmentGetInclude, 'pod_terminal') ||
+ firstRelationshipId(fixtures.shipmentGetInclude, 'destination_terminal');
+
+ if (terminalId) {
+ try {
+ const terminal = await fetchJson(`/terminals/${terminalId}`);
+ writeFixture('terminals.get', terminal);
+ } catch (error) {
+ console.warn(`Skipping terminal fixture: ${error.message}`);
+ }
+ }
+
+ console.log('Fixtures generation complete.');
+}
+
+main().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/sdks/typescript-sdk/src/client.errors.test.ts b/sdks/typescript-sdk/src/client.errors.test.ts
new file mode 100644
index 00000000..f6dfc656
--- /dev/null
+++ b/sdks/typescript-sdk/src/client.errors.test.ts
@@ -0,0 +1,199 @@
+import { describe, expect, it } from 'vitest';
+import {
+ AuthenticationError,
+ AuthorizationError,
+ FeatureNotEnabledError,
+ NotFoundError,
+ RateLimitError,
+ Terminal49Client,
+ Terminal49Error,
+ UpstreamError,
+ ValidationError,
+} from './client.js';
+import { createMockFetch, jsonResponse } from './test/mock-fetch.js';
+
+const baseUrl = 'https://api.test/v2';
+
+describe('Terminal49Client error handling', () => {
+ it('throws AuthenticationError when apiToken is missing', () => {
+ expect(() => new Terminal49Client({ apiToken: '' } as any)).toThrow(
+ AuthenticationError,
+ );
+ });
+
+ it('validates required tracking request fields', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/tracking_requests': () => jsonResponse({ data: {} }, 201),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await expect(
+ client.createTrackingRequest({
+ requestType: 'container',
+ requestNumber: '',
+ }),
+ ).rejects.toBeInstanceOf(ValidationError);
+
+ await expect(
+ client.createTrackingRequest({
+ requestType: '' as any,
+ requestNumber: 'ABC123',
+ }),
+ ).rejects.toBeInstanceOf(ValidationError);
+
+ await expect(client.inferTrackingNumber('')).rejects.toBeInstanceOf(
+ ValidationError,
+ );
+ });
+
+ it('maps 401 to AuthenticationError', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/containers/abc?include=shipment,pod_terminal': () =>
+ jsonResponse({ errors: [{ detail: 'invalid token' }] }, 401),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+ await expect(client.getContainer('abc')).rejects.toBeInstanceOf(
+ AuthenticationError,
+ );
+ });
+
+ it('maps 403 with feature message to FeatureNotEnabledError', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/containers/abc?include=shipment,pod_terminal': () =>
+ jsonResponse({ errors: [{ detail: 'Feature not enabled' }] }, 403),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+ await expect(client.getContainer('abc')).rejects.toBeInstanceOf(
+ FeatureNotEnabledError,
+ );
+ });
+
+ it('maps 403 without feature message to AuthorizationError', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/containers/abc?include=shipment,pod_terminal': () =>
+ jsonResponse({ errors: [{ detail: 'Access forbidden' }] }, 403),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+ await expect(client.getContainer('abc')).rejects.toBeInstanceOf(
+ AuthorizationError,
+ );
+ });
+
+ it('maps 404 to NotFoundError', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/containers/abc?include=shipment,pod_terminal': () =>
+ jsonResponse({ errors: [{ detail: 'missing' }] }, 404),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+ await expect(client.getContainer('abc')).rejects.toBeInstanceOf(
+ NotFoundError,
+ );
+ });
+
+ it('maps 429 to RateLimitError', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/containers/abc?include=shipment,pod_terminal': () =>
+ jsonResponse({ errors: [{ detail: 'too many requests' }] }, 429),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+ await expect(client.getContainer('abc')).rejects.toBeInstanceOf(
+ RateLimitError,
+ );
+ });
+
+ it('maps 5xx to UpstreamError', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/containers/abc?include=shipment,pod_terminal': () =>
+ jsonResponse({ errors: [{ detail: 'server down' }] }, 503),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+ await expect(client.getContainer('abc')).rejects.toBeInstanceOf(
+ UpstreamError,
+ );
+ });
+
+ it('maps unexpected status to Terminal49Error with status in message', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/containers/abc?include=shipment,pod_terminal': () =>
+ jsonResponse({ errors: [{ detail: 'teapot' }] }, 418),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await expect(client.getContainer('abc')).rejects.toThrowError(/418/);
+ await expect(client.getContainer('abc')).rejects.toBeInstanceOf(
+ Terminal49Error,
+ );
+ });
+
+ it('extracts error messages with pointers', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/tracking_requests': () =>
+ jsonResponse(
+ {
+ errors: [
+ {
+ detail: 'request_number is required',
+ source: { pointer: '/data/attributes/request_number' },
+ },
+ ],
+ },
+ 400,
+ ),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await expect(
+ client.createTrackingRequest({
+ requestType: 'container',
+ requestNumber: 'MSCU1234567',
+ }),
+ ).rejects.toThrowError(
+ /request_number is required \(\/data\/attributes\/request_number\)/,
+ );
+ });
+});
diff --git a/sdks/typescript-sdk/src/client.mapping.test.ts b/sdks/typescript-sdk/src/client.mapping.test.ts
new file mode 100644
index 00000000..18f067b3
--- /dev/null
+++ b/sdks/typescript-sdk/src/client.mapping.test.ts
@@ -0,0 +1,617 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { describe, expect, it } from 'vitest';
+import { Terminal49Client } from './client.js';
+import { createMockFetch, jsonResponse } from './test/mock-fetch.js';
+
+const baseUrl = 'https://api.test/v2';
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const fixturesDir = path.resolve(__dirname, 'fixtures');
+
+const itIf = (condition: boolean) => (condition ? it : it.skip);
+
+function fixturePath(name: string) {
+ return path.resolve(fixturesDir, `${name}.json`);
+}
+
+function hasFixture(name: string) {
+ return fs.existsSync(fixturePath(name));
+}
+
+function loadFixture(name: string): T {
+ const raw = fs.readFileSync(fixturePath(name), 'utf-8');
+ return JSON.parse(raw) as T;
+}
+
+function findIncluded(
+ doc: any,
+ type: string,
+ id: string | undefined,
+): any | null {
+ if (!id) return null;
+ const included = Array.isArray(doc?.included) ? doc.included : [];
+ return (
+ included.find((item: any) => item.type === type && item.id === id) || null
+ );
+}
+
+function getShippingLineScac(item: any) {
+ return item?.attributes?.scac || item?.scac;
+}
+
+function expectIfDefined(actual: unknown, expected: unknown) {
+ if (expected !== undefined && expected !== null) {
+ expect(actual).toBe(expected);
+ }
+}
+
+describe('Terminal49Client mapping helpers', () => {
+ it('maps shipping lines with optional fields', async () => {
+ const fixture = loadFixture('shipping-lines.list');
+ const { fetchImpl } = createMockFetch({
+ '/shipping_lines': () => jsonResponse(fixture),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const result = (await client.listShippingLines(undefined, {
+ format: 'mapped',
+ })) as any[];
+
+ const expectedScacs = (fixture?.data || [])
+ .map(getShippingLineScac)
+ .filter(Boolean);
+
+ expect(Array.isArray(result)).toBe(true);
+ expect(result.length).toBe(expectedScacs.length);
+
+ const resultScacs = result.map((item) => item.scac);
+ for (const scac of resultScacs) {
+ expect(expectedScacs).toContain(scac);
+ }
+
+ const first = fixture?.data?.[0];
+ const firstScac = getShippingLineScac(first);
+ const firstName =
+ first?.attributes?.name || first?.attributes?.full_name || firstScac;
+ expectIfDefined(result[0]?.scac, firstScac);
+ expectIfDefined(result[0]?.name, firstName);
+
+ const withShort = fixture?.data?.find(
+ (item: any) => item?.attributes?.short_name || item?.attributes?.nickname,
+ );
+ if (withShort) {
+ const shortScac = getShippingLineScac(withShort);
+ const mapped = result.find((item) => item.scac === shortScac);
+ const expectedShort =
+ withShort.attributes?.short_name || withShort.attributes?.nickname;
+ expect(mapped?.shortName).toBe(expectedShort);
+ }
+
+ const withBolPrefix = fixture?.data?.find(
+ (item: any) => item?.attributes?.bol_prefix,
+ );
+ if (withBolPrefix) {
+ const bolScac = getShippingLineScac(withBolPrefix);
+ const mapped = result.find((item) => item.scac === bolScac);
+ expect(mapped?.bolPrefix).toBe(withBolPrefix.attributes?.bol_prefix);
+ }
+
+ const withNotes = fixture?.data?.find(
+ (item: any) => item?.attributes?.notes,
+ );
+ if (withNotes) {
+ const noteScac = getShippingLineScac(withNotes);
+ const mapped = result.find((item) => item.scac === noteScac);
+ expect(mapped?.notes).toBe(withNotes.attributes?.notes);
+ }
+ });
+
+ itIf(hasFixture('containers.route'))(
+ 'maps container route with ports and vessel legs',
+ async () => {
+ const fixture = loadFixture('containers.route');
+ const { fetchImpl } = createMockFetch({
+ '/containers/cont-1/route?include=port,vessel,route_location': () =>
+ jsonResponse(fixture),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const result = (await client.getContainerRoute('cont-1', {
+ format: 'mapped',
+ })) as any;
+
+ expect(result.totalLegs).toBe(result.locations.length);
+
+ const routeLocationRef =
+ fixture?.data?.relationships?.route_locations?.data?.[0];
+ const routeLocation = routeLocationRef
+ ? findIncluded(fixture, 'route_location', routeLocationRef.id)
+ : null;
+ if (!routeLocation) return;
+
+ const portId = routeLocation?.relationships?.port?.data?.id;
+ const port = portId ? findIncluded(fixture, 'port', portId) : null;
+ if (port) {
+ expectIfDefined(result.locations[0]?.port?.code, port.attributes?.code);
+ expectIfDefined(result.locations[0]?.port?.name, port.attributes?.name);
+ expectIfDefined(
+ result.locations[0]?.port?.countryCode,
+ port.attributes?.country_code,
+ );
+ }
+
+ expectIfDefined(
+ result.locations[0]?.inbound?.mode,
+ routeLocation.attributes?.inbound_mode,
+ );
+ expectIfDefined(
+ result.locations[0]?.inbound?.carrierScac,
+ routeLocation.attributes?.inbound_scac,
+ );
+ expectIfDefined(
+ result.locations[0]?.inbound?.eta,
+ routeLocation.attributes?.inbound_eta_at,
+ );
+ expectIfDefined(
+ result.locations[0]?.inbound?.ata,
+ routeLocation.attributes?.inbound_ata_at,
+ );
+
+ expectIfDefined(
+ result.locations[0]?.outbound?.mode,
+ routeLocation.attributes?.outbound_mode,
+ );
+ expectIfDefined(
+ result.locations[0]?.outbound?.carrierScac,
+ routeLocation.attributes?.outbound_scac,
+ );
+ expectIfDefined(
+ result.locations[0]?.outbound?.etd,
+ routeLocation.attributes?.outbound_etd_at,
+ );
+ expectIfDefined(
+ result.locations[0]?.outbound?.atd,
+ routeLocation.attributes?.outbound_atd_at,
+ );
+
+ const inboundVesselId =
+ routeLocation?.relationships?.inbound_vessel?.data?.id;
+ const inboundVessel = inboundVesselId
+ ? findIncluded(fixture, 'vessel', inboundVesselId)
+ : null;
+ if (inboundVessel) {
+ expectIfDefined(
+ result.locations[0]?.inbound?.vessel?.name,
+ inboundVessel.attributes?.name,
+ );
+ expectIfDefined(
+ result.locations[0]?.inbound?.vessel?.imo,
+ inboundVessel.attributes?.imo,
+ );
+ }
+
+ const outboundVesselId =
+ routeLocation?.relationships?.outbound_vessel?.data?.id;
+ const outboundVessel = outboundVesselId
+ ? findIncluded(fixture, 'vessel', outboundVesselId)
+ : null;
+ if (outboundVessel) {
+ expectIfDefined(
+ result.locations[0]?.outbound?.vessel?.name,
+ outboundVessel.attributes?.name,
+ );
+ expectIfDefined(
+ result.locations[0]?.outbound?.vessel?.imo,
+ outboundVessel.attributes?.imo,
+ );
+ }
+ },
+ );
+
+ it('maps tracking request fields from base response', async () => {
+ const fixture = loadFixture('tracking-requests.get.base');
+ const trackingId = fixture?.data?.id || 'tr-1';
+ const { fetchImpl } = createMockFetch({
+ [`/tracking_requests/${trackingId}`]: () => jsonResponse(fixture),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const result = (await client.getTrackingRequest(trackingId, {
+ format: 'mapped',
+ })) as any;
+
+ const attrs = fixture?.data?.attributes || {};
+ expectIfDefined(result.requestType, attrs.request_type);
+ expectIfDefined(result.requestNumber, attrs.request_number);
+ expectIfDefined(result.status, attrs.status);
+ expectIfDefined(result.scac, attrs.scac);
+
+ if (Array.isArray(attrs.ref_numbers)) {
+ expect(result.refNumbers).toEqual(attrs.ref_numbers);
+ }
+
+ if (!fixture?.included || fixture.included.length === 0) {
+ expect(result.shipment).toBeNull();
+ expect(result.container).toBeNull();
+ }
+ });
+
+ itIf(hasFixture('tracking-requests.get.include'))(
+ 'maps tracking request with shipment and container',
+ async () => {
+ const fixture = loadFixture('tracking-requests.get.include');
+ const trackingId = fixture?.data?.id || 'tr-1';
+ const { fetchImpl } = createMockFetch({
+ [`/tracking_requests/${trackingId}`]: () => jsonResponse(fixture),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const result = (await client.getTrackingRequest(trackingId, {
+ format: 'mapped',
+ })) as any;
+
+ const shipmentRef = fixture?.data?.relationships?.shipment?.data;
+ const containerRef = fixture?.data?.relationships?.container?.data;
+ const shipment = shipmentRef
+ ? findIncluded(fixture, 'shipment', shipmentRef.id)
+ : null;
+ const container = containerRef
+ ? findIncluded(fixture, 'container', containerRef.id)
+ : null;
+
+ if (shipment) {
+ const expectedBill =
+ shipment.attributes?.bill_of_lading_number ||
+ shipment.attributes?.bill_of_lading ||
+ shipment.attributes?.bl_number;
+ expectIfDefined(result.shipment?.billOfLading, expectedBill);
+ expectIfDefined(
+ result.shipment?.shippingLineScac,
+ shipment.attributes?.shipping_line_scac,
+ );
+ }
+
+ if (container) {
+ expectIfDefined(
+ result.container?.number,
+ container.attributes?.number ||
+ container.attributes?.container_number,
+ );
+ expectIfDefined(result.container?.status, container.attributes?.status);
+ }
+ },
+ );
+
+ it('maps shipment ports, terminals, tracking, and container references', async () => {
+ const fixture = loadFixture('shipments.get.include');
+ const shipmentId = fixture?.data?.id || 'ship-1';
+ const { fetchImpl } = createMockFetch({
+ [`/shipments/${shipmentId}?include=containers,pod_terminal,port_of_lading,port_of_discharge,destination,destination_terminal`]:
+ () => jsonResponse(fixture),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const result = (await client.getShipment(shipmentId, true, {
+ format: 'mapped',
+ })) as any;
+
+ const attrs = fixture?.data?.attributes || {};
+ const relationships = fixture?.data?.relationships || {};
+
+ const expectedBill =
+ attrs.bill_of_lading_number || attrs.bill_of_lading || attrs.bl_number;
+ expectIfDefined(result.billOfLading, expectedBill);
+ expectIfDefined(result.shippingLineScac, attrs.shipping_line_scac);
+ expectIfDefined(result.customerName, attrs.customer_name);
+
+ if (Array.isArray(attrs.ref_numbers)) {
+ expect(result.refNumbers).toEqual(attrs.ref_numbers);
+ }
+ if (Array.isArray(attrs.tags)) {
+ expect(result.tags).toEqual(attrs.tags);
+ }
+
+ expectIfDefined(
+ result.tracking?.lineTrackingLastAttemptedAt,
+ attrs.line_tracking_last_attempted_at,
+ );
+ expectIfDefined(
+ result.tracking?.lineTrackingLastSucceededAt,
+ attrs.line_tracking_last_succeeded_at,
+ );
+ expectIfDefined(
+ result.tracking?.lineTrackingStoppedAt,
+ attrs.line_tracking_stopped_at,
+ );
+ expectIfDefined(
+ result.tracking?.lineTrackingStoppedReason,
+ attrs.line_tracking_stopped_reason,
+ );
+
+ expectIfDefined(result.vesselAtPod?.name, attrs.pod_vessel_name);
+ expectIfDefined(result.vesselAtPod?.imo, attrs.pod_vessel_imo);
+ expectIfDefined(result.vesselAtPod?.voyageNumber, attrs.pod_voyage_number);
+
+ const polRef = relationships.port_of_lading?.data;
+ const podRef = relationships.port_of_discharge?.data;
+ const destRef = relationships.destination?.data;
+ const podTerminalRef = relationships.pod_terminal?.data;
+ const destinationTerminalRef = relationships.destination_terminal?.data;
+ const containerRef = relationships.containers?.data?.[0];
+
+ const pol = polRef ? findIncluded(fixture, polRef.type, polRef.id) : null;
+ if (pol) {
+ expectIfDefined(
+ result.ports?.portOfLading?.locode,
+ pol.attributes?.locode,
+ );
+ expectIfDefined(result.ports?.portOfLading?.name, pol.attributes?.name);
+ expectIfDefined(result.ports?.portOfLading?.code, pol.attributes?.code);
+ expectIfDefined(
+ result.ports?.portOfLading?.countryCode,
+ pol.attributes?.country_code,
+ );
+ }
+ expectIfDefined(result.ports?.portOfLading?.etd, attrs.pol_etd_at);
+ expectIfDefined(result.ports?.portOfLading?.atd, attrs.pol_atd_at);
+ expectIfDefined(result.ports?.portOfLading?.timezone, attrs.pol_timezone);
+
+ const podTerminal = podTerminalRef
+ ? findIncluded(fixture, podTerminalRef.type, podTerminalRef.id)
+ : null;
+ if (podTerminal) {
+ expectIfDefined(
+ result.ports?.portOfDischarge?.terminal?.id,
+ podTerminal.id,
+ );
+ expectIfDefined(
+ result.ports?.portOfDischarge?.terminal?.name,
+ podTerminal.attributes?.name,
+ );
+ expectIfDefined(
+ result.ports?.portOfDischarge?.terminal?.nickname,
+ podTerminal.attributes?.nickname,
+ );
+ expectIfDefined(
+ result.ports?.portOfDischarge?.terminal?.firmsCode,
+ podTerminal.attributes?.firms_code,
+ );
+ }
+
+ if (podRef) {
+ const portOfDischarge = findIncluded(fixture, podRef.type, podRef.id);
+ if (portOfDischarge) {
+ expectIfDefined(
+ result.ports?.portOfDischarge?.locode,
+ portOfDischarge.attributes?.locode,
+ );
+ expectIfDefined(
+ result.ports?.portOfDischarge?.name,
+ portOfDischarge.attributes?.name,
+ );
+ expectIfDefined(
+ result.ports?.portOfDischarge?.code,
+ portOfDischarge.attributes?.code,
+ );
+ expectIfDefined(
+ result.ports?.portOfDischarge?.countryCode,
+ portOfDischarge.attributes?.country_code,
+ );
+ }
+ }
+ expectIfDefined(result.ports?.portOfDischarge?.eta, attrs.pod_eta_at);
+ expectIfDefined(result.ports?.portOfDischarge?.ata, attrs.pod_ata_at);
+ expectIfDefined(
+ result.ports?.portOfDischarge?.originalEta,
+ attrs.pod_original_eta_at,
+ );
+ expectIfDefined(
+ result.ports?.portOfDischarge?.timezone,
+ attrs.pod_timezone,
+ );
+
+ if (attrs.destination_locode) {
+ expectIfDefined(
+ result.ports?.destination?.locode,
+ attrs.destination_locode,
+ );
+ expectIfDefined(result.ports?.destination?.name, attrs.destination_name);
+ expectIfDefined(result.ports?.destination?.eta, attrs.destination_eta_at);
+ expectIfDefined(result.ports?.destination?.ata, attrs.destination_ata_at);
+ expectIfDefined(
+ result.ports?.destination?.timezone,
+ attrs.destination_timezone,
+ );
+ }
+
+ const destinationTerminal = destinationTerminalRef
+ ? findIncluded(
+ fixture,
+ destinationTerminalRef.type,
+ destinationTerminalRef.id,
+ )
+ : null;
+ if (destinationTerminal) {
+ expectIfDefined(
+ result.ports?.destination?.terminal?.id,
+ destinationTerminal.id,
+ );
+ expectIfDefined(
+ result.ports?.destination?.terminal?.name,
+ destinationTerminal.attributes?.name,
+ );
+ expectIfDefined(
+ result.ports?.destination?.terminal?.nickname,
+ destinationTerminal.attributes?.nickname,
+ );
+ expectIfDefined(
+ result.ports?.destination?.terminal?.firmsCode,
+ destinationTerminal.attributes?.firms_code,
+ );
+ }
+
+ const container = containerRef
+ ? findIncluded(fixture, containerRef.type, containerRef.id)
+ : null;
+ if (container) {
+ expectIfDefined(
+ result.containers?.[0]?.number,
+ container.attributes?.number || container.attributes?.container_number,
+ );
+ }
+ });
+
+ it('maps container list items from base fixture', async () => {
+ const fixture = loadFixture('containers.list');
+ const { fetchImpl } = createMockFetch({
+ '/containers?include=shipment,pod_terminal': () => jsonResponse(fixture),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const result = (await client.listContainers(
+ {},
+ { format: 'mapped' },
+ )) as any;
+
+ const firstItem = fixture?.data?.[0];
+ expect(result.items.length).toBe(fixture.data.length);
+ expectIfDefined(result.items[0]?.id, firstItem?.id);
+
+ const attrs = firstItem?.attributes || {};
+ expectIfDefined(
+ result.items[0]?.number,
+ attrs.number || attrs.container_number,
+ );
+ expectIfDefined(result.items[0]?.status, attrs.status);
+ expectIfDefined(result.items[0]?.currentStatus, attrs.current_status);
+ expectIfDefined(result.items[0]?.sealNumber, attrs.seal_number);
+
+ expectIfDefined(result.items[0]?.equipment?.type, attrs.equipment_type);
+ expectIfDefined(result.items[0]?.equipment?.length, attrs.equipment_length);
+ expectIfDefined(result.items[0]?.equipment?.height, attrs.equipment_height);
+ expectIfDefined(result.items[0]?.equipment?.weightLbs, attrs.weight_in_lbs);
+
+ expectIfDefined(
+ result.items[0]?.location?.currentLocation,
+ attrs.location_at_pod_terminal,
+ );
+ expectIfDefined(
+ result.items[0]?.location?.availableForPickup,
+ attrs.available_for_pickup,
+ );
+ expectIfDefined(
+ result.items[0]?.location?.podArrivedAt,
+ attrs.pod_arrived_at,
+ );
+ expectIfDefined(
+ result.items[0]?.location?.podDischargedAt,
+ attrs.pod_discharged_at,
+ );
+
+ expectIfDefined(result.items[0]?.demurrage?.pickupLfd, attrs.pickup_lfd);
+ expectIfDefined(
+ result.items[0]?.demurrage?.pickupAppointmentAt,
+ attrs.pickup_appointment_at,
+ );
+ if (Array.isArray(attrs.fees_at_pod_terminal)) {
+ expect(result.items[0]?.demurrage?.fees).toEqual(
+ attrs.fees_at_pod_terminal,
+ );
+ }
+ if (Array.isArray(attrs.holds_at_pod_terminal)) {
+ expect(result.items[0]?.demurrage?.holds).toEqual(
+ attrs.holds_at_pod_terminal,
+ );
+ }
+
+ expectIfDefined(
+ result.items[0]?.rail?.podRailCarrierScac,
+ attrs.pod_rail_carrier_scac,
+ );
+ expectIfDefined(
+ result.items[0]?.rail?.indRailCarrierScac,
+ attrs.ind_rail_carrier_scac,
+ );
+ expectIfDefined(
+ result.items[0]?.rail?.podRailLoadedAt,
+ attrs.pod_rail_loaded_at,
+ );
+ expectIfDefined(
+ result.items[0]?.rail?.podRailDepartedAt,
+ attrs.pod_rail_departed_at,
+ );
+ expectIfDefined(
+ result.items[0]?.rail?.indRailArrivedAt,
+ attrs.ind_rail_arrived_at,
+ );
+ expectIfDefined(
+ result.items[0]?.rail?.indRailUnloadedAt,
+ attrs.ind_rail_unloaded_at,
+ );
+ expectIfDefined(result.items[0]?.rail?.indEtaAt, attrs.ind_eta_at);
+ expectIfDefined(result.items[0]?.rail?.indAtaAt, attrs.ind_ata_at);
+
+ expect(result.items[0]?.shipment).toBeNull();
+ expect(result.items[0]?.terminals?.podTerminal).toBeNull();
+ expect(result.items[0]?.events).toEqual([]);
+ });
+
+ it('returns empty lists when container or shipment list data is not an array', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/containers?include=shipment,pod_terminal': () =>
+ jsonResponse({ data: {} }),
+ '/shipments?include=containers,pod_terminal,port_of_lading,port_of_discharge,destination,destination_terminal':
+ () => jsonResponse({ data: {} }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const containers = (await client.listContainers(
+ {},
+ { format: 'mapped' },
+ )) as any;
+ const shipments = (await client.listShipments(
+ {},
+ { format: 'mapped' },
+ )) as any;
+
+ expect(containers.items).toEqual([]);
+ expect(shipments.items).toEqual([]);
+ });
+});
diff --git a/sdks/typescript-sdk/src/client.request.test.ts b/sdks/typescript-sdk/src/client.request.test.ts
new file mode 100644
index 00000000..9cc97a02
--- /dev/null
+++ b/sdks/typescript-sdk/src/client.request.test.ts
@@ -0,0 +1,509 @@
+import { describe, expect, it } from 'vitest';
+import { Terminal49Client, ValidationError } from './client.js';
+import { createMockFetch, jsonResponse } from './test/mock-fetch.js';
+
+const baseUrl = 'https://api.test/v2';
+
+function buildSearchParams(entries: Array<[string, string]>) {
+ return entries.map(([key, value]) => `${key}=${value}`).join('&');
+}
+
+describe('Terminal49Client request building', () => {
+ it('normalizes base URL without /v2', async () => {
+ const { fetchImpl, calls } = createMockFetch({
+ '/shipping_lines': () => jsonResponse({ data: [] }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: 'https://api.test',
+ fetchImpl,
+ });
+
+ await client.listShippingLines();
+ expect(calls[0].url.toString()).toBe('https://api.test/v2/shipping_lines');
+ });
+
+ it('uses defaultFormat when no format override is provided', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/shipping_lines': () =>
+ jsonResponse({
+ data: [
+ {
+ type: 'shipping_line',
+ id: '1',
+ attributes: { scac: 'MAEU', name: 'Maersk' },
+ },
+ ],
+ }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ defaultFormat: 'mapped',
+ });
+
+ const result = (await client.listShippingLines()) as any[];
+ expect(Array.isArray(result)).toBe(true);
+ expect(result[0].scac).toBe('MAEU');
+ expect((result as any).data).toBeUndefined();
+ });
+
+ it('supports format=both to return raw and mapped data', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/shipping_lines': () =>
+ jsonResponse({
+ data: [
+ {
+ type: 'shipping_line',
+ id: '1',
+ attributes: { scac: 'MAEU', name: 'Maersk' },
+ },
+ ],
+ }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const result = (await client.listShippingLines(undefined, {
+ format: 'both',
+ })) as any;
+ expect(result.raw?.data).toBeDefined();
+ expect(result.mapped?.[0]?.scac).toBe('MAEU');
+ });
+
+ it('builds listShipments filters and pagination', async () => {
+ const search = buildSearchParams([
+ [
+ 'include',
+ 'containers,pod_terminal,port_of_lading,port_of_discharge,destination,destination_terminal',
+ ],
+ ['filter[status]', 'in_transit'],
+ ['filter[pod_locode]', 'USLAX'],
+ ['filter[line_scac]', 'MAEU'],
+ ['filter[updated_at]', '2024-01-01'],
+ ['page[number]', '2'],
+ ['page[size]', '50'],
+ ]);
+
+ const { fetchImpl, calls } = createMockFetch({
+ [`/shipments?${search}`]: () => jsonResponse({ data: [] }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await client.listShipments(
+ {
+ status: 'in_transit',
+ port: 'USLAX',
+ carrier: 'MAEU',
+ updatedAfter: '2024-01-01',
+ },
+ { page: 2, pageSize: 50 },
+ );
+
+ const params = calls[0].url.searchParams;
+ expect(params.get('filter[status]')).toBe('in_transit');
+ expect(params.get('filter[pod_locode]')).toBe('USLAX');
+ expect(params.get('filter[line_scac]')).toBe('MAEU');
+ expect(params.get('filter[updated_at]')).toBe('2024-01-01');
+ expect(params.get('page[number]')).toBe('2');
+ expect(params.get('page[size]')).toBe('50');
+ });
+
+ it('removes containers from include when includeContainers=false', async () => {
+ const search = buildSearchParams([
+ [
+ 'include',
+ 'pod_terminal,port_of_lading,port_of_discharge,destination,destination_terminal',
+ ],
+ ]);
+
+ const { fetchImpl, calls } = createMockFetch({
+ [`/shipments?${search}`]: () => jsonResponse({ data: [] }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await client.listShipments({ includeContainers: false });
+
+ const include = calls[0].url.searchParams.get('include');
+ expect(include).not.toContain('containers');
+ });
+
+ it('builds listContainers filters and pagination with custom include', async () => {
+ const search = buildSearchParams([
+ ['include', 'shipment,pod_terminal,transport_events'],
+ ['filter[status]', 'in_transit'],
+ ['page[number]', '3'],
+ ['page[size]', '10'],
+ ]);
+
+ const { fetchImpl, calls } = createMockFetch({
+ [`/containers?${search}`]: () => jsonResponse({ data: [] }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await client.listContainers(
+ {
+ status: 'in_transit',
+ include: 'shipment,pod_terminal,transport_events',
+ },
+ { page: 3, pageSize: 10 },
+ );
+
+ const params = calls[0].url.searchParams;
+ expect(params.get('include')).toBe(
+ 'shipment,pod_terminal,transport_events',
+ );
+ expect(params.get('filter[status]')).toBe('in_transit');
+ expect(params.get('page[number]')).toBe('3');
+ expect(params.get('page[size]')).toBe('10');
+ });
+
+ it('hits container raw events and refresh endpoints', async () => {
+ const { fetchImpl, calls } = createMockFetch({
+ '/containers/cont-1/raw_events': () => jsonResponse({ data: [] }),
+ '/containers/cont-1/refresh': () =>
+ jsonResponse({ data: { id: 'cont-1' } }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await client.getContainerRawEvents('cont-1');
+ await client.refreshContainer('cont-1');
+
+ expect(
+ calls[0].url.pathname.endsWith('/containers/cont-1/raw_events'),
+ ).toBe(true);
+ expect(calls[1].url.pathname.endsWith('/containers/cont-1/refresh')).toBe(
+ true,
+ );
+ expect(calls[1].init?.method).toBe('PATCH');
+ });
+
+ it('lists tracking requests with pagination and supports alias', async () => {
+ const search = buildSearchParams([
+ ['page[number]', '2'],
+ ['page[size]', '25'],
+ ]);
+
+ const { fetchImpl, calls } = createMockFetch({
+ [`/tracking_requests?${search}`]: () => jsonResponse({ data: [] }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await client.listTrackingRequests({}, { page: 2, pageSize: 25 });
+ await client.listTrackRequests({}, { page: 2, pageSize: 25 });
+
+ expect(calls.length).toBe(2);
+ });
+
+ it('sends JSON:API payload for updateTrackingRequest', async () => {
+ let captured: any = null;
+ const { fetchImpl } = createMockFetch({
+ '/tracking_requests/tr-1': (init) => {
+ captured = JSON.parse(String(init?.body));
+ return jsonResponse({ data: { id: 'tr-1' } });
+ },
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await client.updateTrackingRequest('tr-1', { status: 'paused' });
+
+ expect(captured).toEqual({
+ data: {
+ type: 'tracking_request',
+ id: 'tr-1',
+ attributes: { status: 'paused' },
+ },
+ });
+ });
+
+ it('uses inferred selected scac when creating tracking request from infer', async () => {
+ let captured: any = null;
+ const { fetchImpl } = createMockFetch({
+ '/tracking_requests/infer_number': () =>
+ jsonResponse({
+ data: {
+ attributes: {
+ number_type: 'container',
+ shipping_line: { selected: { scac: 'MAEU' }, candidates: [] },
+ },
+ },
+ }),
+ '/tracking_requests': (init) => {
+ captured = JSON.parse(String(init?.body));
+ return jsonResponse({ data: { id: 'tr-1' } }, 201);
+ },
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await client.createTrackingRequestFromInfer('MSCU1234567');
+ expect(captured.data.attributes.scac).toBe('MAEU');
+ });
+
+ it('uses inferred candidate scac when only one is present', async () => {
+ let captured: any = null;
+ const { fetchImpl } = createMockFetch({
+ '/tracking_requests/infer_number': () =>
+ jsonResponse({
+ data: {
+ attributes: {
+ number_type: 'container',
+ shipping_line: { candidates: [{ scac: 'CAND' }] },
+ },
+ },
+ }),
+ '/tracking_requests': (init) => {
+ captured = JSON.parse(String(init?.body));
+ return jsonResponse({ data: { id: 'tr-2' } }, 201);
+ },
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await client.createTrackingRequestFromInfer('MSCU1234567');
+ expect(captured.data.attributes.scac).toBe('CAND');
+ });
+
+ it('throws when infer does not provide number type', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/tracking_requests/infer_number': () =>
+ jsonResponse({
+ data: {
+ attributes: {
+ shipping_line: { selected: { scac: 'MAEU' } },
+ },
+ },
+ }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await expect(
+ client.createTrackingRequestFromInfer('MSCU1234567'),
+ ).rejects.toBeInstanceOf(ValidationError);
+ });
+
+ it('throws when infer does not provide scac', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/tracking_requests/infer_number': () =>
+ jsonResponse({
+ data: {
+ attributes: {
+ number_type: 'container',
+ shipping_line: { candidates: [] },
+ },
+ },
+ }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await expect(
+ client.createTrackingRequestFromInfer('MSCU1234567'),
+ ).rejects.toBeInstanceOf(ValidationError);
+ });
+
+ it('sends JSON:API payloads for shipment updates and tracking controls', async () => {
+ let updatePayload: any = null;
+ let stopPayload: any = null;
+ let resumePayload: any = null;
+
+ const { fetchImpl, calls } = createMockFetch({
+ '/shipments/ship-1': (init) => {
+ updatePayload = JSON.parse(String(init?.body));
+ return jsonResponse({ data: { id: 'ship-1' } });
+ },
+ '/shipments/ship-1/stop_tracking': (init) => {
+ stopPayload = JSON.parse(String(init?.body));
+ return jsonResponse({ data: { id: 'ship-1' } });
+ },
+ '/shipments/ship-1/resume_tracking': (init) => {
+ resumePayload = JSON.parse(String(init?.body));
+ return jsonResponse({ data: { id: 'ship-1' } });
+ },
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await client.updateShipment('ship-1', { status: 'paused' });
+ await client.stopTrackingShipment('ship-1');
+ await client.resumeTrackingShipment('ship-1');
+
+ expect(updatePayload).toEqual({
+ data: {
+ type: 'shipment',
+ id: 'ship-1',
+ attributes: { status: 'paused' },
+ },
+ });
+ expect(stopPayload).toEqual({
+ data: { type: 'shipment', id: 'ship-1' },
+ });
+ expect(resumePayload).toEqual({
+ data: { type: 'shipment', id: 'ship-1' },
+ });
+
+ expect(calls[0].init?.method).toBe('PATCH');
+ expect(calls[1].init?.method).toBe('PATCH');
+ expect(calls[2].init?.method).toBe('PATCH');
+ });
+
+ it('derives demurrage fields from container attributes', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/containers/cont-1?include=pod_terminal': () =>
+ jsonResponse({
+ data: {
+ id: 'cont-1',
+ attributes: {
+ pickup_lfd: '2024-02-01',
+ pickup_appointment_at: '2024-02-02T00:00:00Z',
+ available_for_pickup: true,
+ fees_at_pod_terminal: [{ amount: 10 }],
+ holds_at_pod_terminal: [{ type: 'customs' }],
+ pod_arrived_at: '2024-01-30T00:00:00Z',
+ pod_discharged_at: '2024-01-31T00:00:00Z',
+ },
+ },
+ }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const demurrage = await client.getDemurrage('cont-1');
+ expect(demurrage).toEqual({
+ container_id: 'cont-1',
+ pickup_lfd: '2024-02-01',
+ pickup_appointment_at: '2024-02-02T00:00:00Z',
+ available_for_pickup: true,
+ fees_at_pod_terminal: [{ amount: 10 }],
+ holds_at_pod_terminal: [{ type: 'customs' }],
+ pod_arrived_at: '2024-01-30T00:00:00Z',
+ pod_discharged_at: '2024-01-31T00:00:00Z',
+ });
+ });
+
+ it('filters rail milestones to rail events only', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/containers/cont-1?include=transport_events': () =>
+ jsonResponse({
+ data: {
+ id: 'cont-1',
+ attributes: {
+ pod_rail_carrier_scac: 'BNSF',
+ ind_rail_carrier_scac: 'NS',
+ pod_rail_loaded_at: '2024-01-01T00:00:00Z',
+ pod_rail_departed_at: '2024-01-02T00:00:00Z',
+ ind_rail_arrived_at: '2024-01-05T00:00:00Z',
+ ind_rail_unloaded_at: '2024-01-06T00:00:00Z',
+ ind_eta_at: '2024-01-04T00:00:00Z',
+ ind_ata_at: '2024-01-05T00:00:00Z',
+ },
+ },
+ included: [
+ {
+ id: 'rail-1',
+ type: 'transport_event',
+ attributes: {
+ event: 'rail.loaded',
+ event_time: '2024-01-02T00:00:00Z',
+ },
+ },
+ {
+ id: 'truck-1',
+ type: 'transport_event',
+ attributes: {
+ event: 'truck.arrived',
+ event_time: '2024-01-03T00:00:00Z',
+ },
+ },
+ ],
+ }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const result = await client.getRailMilestones('cont-1');
+ expect(result.rail_events.length).toBe(1);
+ expect(result.rail_events[0].event).toBe('rail.loaded');
+ });
+
+ it('supports manual search endpoint', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/search?query=ABC123': () => jsonResponse({ hits: 2 }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const result = await client.search('ABC123');
+ expect(result).toEqual({ hits: 2 });
+ });
+});
diff --git a/sdks/typescript-sdk/src/client.test.ts b/sdks/typescript-sdk/src/client.test.ts
new file mode 100644
index 00000000..5ac12b9b
--- /dev/null
+++ b/sdks/typescript-sdk/src/client.test.ts
@@ -0,0 +1,397 @@
+import { describe, expect, it } from 'vitest';
+import {
+ FeatureNotEnabledError,
+ NotFoundError,
+ Terminal49Client,
+ ValidationError,
+} from './client.js';
+import { createMockFetch, jsonResponse } from './test/mock-fetch.js';
+
+const baseUrl = 'https://api.test/v2';
+
+describe('Terminal49Client', () => {
+ it('retries on 500 and succeeds on second attempt', async () => {
+ let attempt = 0;
+ const { fetchImpl, calls } = createMockFetch({
+ '/containers/abc/route?include=port,vessel,route_location': () => {
+ attempt += 1;
+ if (attempt === 1) {
+ return jsonResponse({ errors: [{ detail: 'server error' }] }, 500);
+ }
+ return jsonResponse({ data: { id: 'route-1' } });
+ },
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ maxRetries: 1,
+ } as any);
+
+ const result = await client.getContainerRoute('abc');
+ expect(result.data.id).toBe('route-1');
+ expect(calls.length).toBe(2);
+ });
+
+ it('maps 404 responses to NotFoundError', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/containers/missing?include=shipment,pod_terminal': () =>
+ jsonResponse({ errors: [{ detail: 'not found' }] }, 404),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await expect(client.getContainer('missing')).rejects.toBeInstanceOf(
+ NotFoundError,
+ );
+ });
+
+ it('adds auth header and include params when fetching container', async () => {
+ const { fetchImpl, calls } = createMockFetch({
+ '/containers/abc?include=shipment,pod_terminal': () =>
+ jsonResponse({ data: { id: 'abc', attributes: {} } }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const result = await client.getContainer('abc');
+
+ expect(result.data.id).toBe('abc');
+ expect(calls.length).toBe(1);
+
+ const headers = new Headers(calls[0].init?.headers);
+ expect(headers.get('Authorization')).toBe('Token token-123');
+ expect(calls[0].url.searchParams.get('include')).toBe(
+ 'shipment,pod_terminal',
+ );
+ });
+
+ it('sets include params on shipment and lists shipping lines with search', async () => {
+ const { fetchImpl, calls } = createMockFetch({
+ '/shipments/ship-1?include=containers,pod_terminal,port_of_lading,port_of_discharge,destination,destination_terminal':
+ () => jsonResponse({ data: { id: 'ship-1' } }),
+ '/shipping_lines?search=MAEU': () =>
+ jsonResponse({
+ data: [{ attributes: { scac: 'MAEU', name: 'Maersk' } }],
+ }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await client.getShipment('ship-1');
+ await client.listShippingLines('MAEU');
+
+ const includeCall = calls.find((c) =>
+ c.url.pathname.endsWith('/shipments/ship-1'),
+ );
+ expect(includeCall).toBeDefined();
+ expect(includeCall?.url.searchParams.get('include')).toContain(
+ 'containers',
+ );
+
+ const shippingCall = calls.find((c) =>
+ c.url.pathname.endsWith('/shipping_lines'),
+ );
+ expect(shippingCall).toBeDefined();
+ expect(shippingCall?.url.searchParams.get('search')).toBe('MAEU');
+ });
+
+ it('sends JSON:API payload when tracking container', async () => {
+ let capturedBody: any = null;
+ const { fetchImpl } = createMockFetch({
+ '/tracking_requests': (init) => {
+ capturedBody = JSON.parse(String(init?.body));
+ return jsonResponse({ data: { id: 'tr-1', attributes: {} } }, 201);
+ },
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await client.trackContainer({
+ containerNumber: 'MSCU1234567',
+ scac: 'MSCU',
+ });
+
+ expect(capturedBody).toEqual({
+ data: {
+ type: 'tracking_request',
+ attributes: {
+ request_type: 'container',
+ request_number: 'MSCU1234567',
+ scac: 'MSCU',
+ ref_numbers: undefined,
+ },
+ },
+ });
+ });
+
+ it('uses deserialize helper to flatten JSON:API with included', async () => {
+ const doc = {
+ data: {
+ id: 'cont-1',
+ type: 'container',
+ attributes: { number: 'MSCU1234567' },
+ relationships: {
+ shipment: { data: { type: 'shipment', id: 'ship-1' } },
+ },
+ },
+ included: [
+ {
+ id: 'ship-1',
+ type: 'shipment',
+ attributes: { bill_of_lading_number: 'BL123' },
+ },
+ ],
+ };
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl: async () => jsonResponse(doc),
+ });
+
+ const result = await client.getContainer('cont-1');
+ const simplified = client.deserialize(result);
+
+ expect(simplified.id).toBe('cont-1');
+ expect(simplified.shipment?.id).toBe('ship-1');
+ expect(simplified.shipment?.bill_of_lading_number).toBe('BL123');
+ });
+
+ it('maps tracking request with linked shipment/container', async () => {
+ const doc = {
+ data: {
+ id: 'tr-1',
+ type: 'tracking_request',
+ attributes: {
+ request_type: 'container',
+ request_number: 'MSCU1234567',
+ status: 'created',
+ scac: 'MSCU',
+ },
+ relationships: {
+ shipment: { data: { type: 'shipment', id: 'ship-1' } },
+ container: { data: { type: 'container', id: 'cont-1' } },
+ },
+ },
+ included: [
+ {
+ id: 'ship-1',
+ type: 'shipment',
+ attributes: {
+ bill_of_lading_number: 'BL123',
+ shipping_line_scac: 'MSCU',
+ },
+ },
+ {
+ id: 'cont-1',
+ type: 'container',
+ attributes: { number: 'MSCU1234567', status: 'in_transit' },
+ },
+ ],
+ };
+
+ const { fetchImpl } = createMockFetch({
+ '/tracking_requests/tr-1': () => jsonResponse(doc),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const result = await client.getTrackingRequest('tr-1', {
+ format: 'mapped',
+ });
+ expect((result as any).shipment?.id).toBe('ship-1');
+ expect((result as any).container?.number).toBe('MSCU1234567');
+ });
+
+ it('maps container list with equipment and terminals when included', async () => {
+ const doc = {
+ data: [
+ {
+ id: 'cont-1',
+ type: 'container',
+ attributes: {
+ number: 'MSCU1234567',
+ status: 'in_transit',
+ equipment_type: 'dry',
+ equipment_length: 40,
+ equipment_height: 9,
+ weight_in_lbs: 22000,
+ location_at_pod_terminal: 'LAX',
+ available_for_pickup: true,
+ pod_arrived_at: '2024-01-01T00:00:00Z',
+ pod_discharged_at: '2024-01-02T00:00:00Z',
+ pickup_lfd: '2024-01-05',
+ pickup_appointment_at: '2024-01-04T00:00:00Z',
+ },
+ relationships: {
+ pod_terminal: { data: { type: 'terminal', id: 'term-1' } },
+ },
+ },
+ ],
+ included: [
+ {
+ id: 'term-1',
+ type: 'terminal',
+ attributes: {
+ name: 'Terminal 1',
+ nickname: 'T1',
+ firms_code: 'F123',
+ },
+ },
+ ],
+ };
+
+ const { fetchImpl } = createMockFetch({
+ '/containers?include=shipment,pod_terminal': () => jsonResponse(doc),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const result = (await client.listContainers(
+ {},
+ { format: 'mapped' },
+ )) as any;
+ expect(result.items[0].equipment?.type).toBe('dry');
+ expect(result.items[0].terminals?.podTerminal?.name).toBe('Terminal 1');
+ });
+
+ it('maps transport events with location/terminal', async () => {
+ const doc = {
+ data: [
+ {
+ id: 'ev-1',
+ type: 'transport_event',
+ attributes: {
+ event: 'container.transport.vessel_loaded',
+ event_time: '2024-01-01T00:00:00Z',
+ },
+ relationships: {
+ location: { data: { id: 'loc-1', type: 'location' } },
+ terminal: { data: { id: 'term-1', type: 'terminal' } },
+ },
+ },
+ ],
+ included: [
+ {
+ id: 'loc-1',
+ type: 'location',
+ attributes: { name: 'Los Angeles', locode: 'USLAX' },
+ },
+ {
+ id: 'term-1',
+ type: 'terminal',
+ attributes: { name: 'Yusen', nickname: 'YUS', firms_code: 'Y790' },
+ },
+ ],
+ };
+
+ const { fetchImpl } = createMockFetch({
+ '/containers/abc/transport_events?include=location,terminal': () =>
+ jsonResponse(doc),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const events = (await client.getContainerTransportEvents('abc', {
+ format: 'mapped',
+ })) as any[];
+ expect(events[0].event).toBe('container.transport.vessel_loaded');
+ expect(events[0].location?.locode).toBe('USLAX');
+ expect(events[0].terminal?.firmsCode).toBe('Y790');
+ });
+
+ it('maps 403 feature gating to FeatureNotEnabledError', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/containers/abc/route?include=port,vessel,route_location': () =>
+ jsonResponse({ errors: [{ detail: 'Feature not enabled' }] }, 403),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await expect(client.getContainerRoute('abc')).rejects.toBeInstanceOf(
+ FeatureNotEnabledError,
+ );
+ });
+
+ it('handles validation errors with proper message extraction', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/tracking_requests': () =>
+ jsonResponse(
+ {
+ errors: [
+ {
+ detail: 'request_number is required',
+ source: { pointer: '/data/attributes/request_number' },
+ },
+ ],
+ },
+ 400,
+ ),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ await expect(
+ client.trackContainer({ bookingNumber: '', refNumbers: ['a'] }),
+ ).rejects.toThrowError(
+ /request_number is required \(\/data\/attributes\/request_number\)/,
+ );
+
+ await expect(
+ client.trackContainer({ bookingNumber: '', refNumbers: ['a'] }),
+ ).rejects.toBeInstanceOf(ValidationError);
+ });
+
+ it('supports manual search endpoint', async () => {
+ const { fetchImpl } = createMockFetch({
+ '/search?query=ABC123': () => jsonResponse({ hits: 1 }),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const result = await client.search('ABC123');
+ expect(result).toEqual({ hits: 1 });
+ });
+});
diff --git a/sdks/typescript-sdk/src/client.ts b/sdks/typescript-sdk/src/client.ts
new file mode 100644
index 00000000..97633acf
--- /dev/null
+++ b/sdks/typescript-sdk/src/client.ts
@@ -0,0 +1,769 @@
+import { Jsona } from 'jsona';
+import createClient, { type FetchResponse } from 'openapi-fetch';
+import {
+ AuthenticationError,
+ AuthorizationError,
+ FeatureNotEnabledError,
+ NotFoundError,
+ RateLimitError,
+ Terminal49Error,
+ UpstreamError,
+ ValidationError,
+ extractErrorMessage,
+ toTerminal49Error,
+} from './client/errors.js';
+import {
+ mapContainerList,
+ mapRoute,
+ mapShipment,
+ mapShipmentList,
+ mapShippingLines,
+ mapTrackingRequest,
+ mapTrackingRequestList,
+ mapTransportEvents,
+} from './client/mappers.js';
+import type { paths } from './generated/terminal49.js';
+import type {
+ Container,
+ PaginatedResult,
+ Shipment,
+ TrackingRequest,
+} from './types/models.js';
+import type {
+ CallOptions,
+ ListOptions,
+ ResponseFormat,
+} from './types/options.js';
+
+/**
+ * Terminal49 API Client
+ * Typed wrapper around Terminal49's JSON:API using openapi-fetch + openapi-typescript.
+ * Can be used standalone or plugged into the MCP tools.
+ */
+
+export {
+ AuthenticationError,
+ AuthorizationError,
+ FeatureNotEnabledError,
+ NotFoundError,
+ RateLimitError,
+ Terminal49Error,
+ UpstreamError,
+ ValidationError,
+};
+
+export interface Terminal49ClientConfig {
+ apiToken: string;
+ apiBaseUrl?: string;
+ maxRetries?: number;
+ fetchImpl?: typeof fetch;
+ defaultFormat?: ResponseFormat;
+}
+
+export type TrackingRequestType =
+ | 'container'
+ | 'bill_of_lading'
+ | 'booking_number';
+
+type Client = ReturnType>;
+
+type FormattedResult =
+ | TDoc
+ | TMapped
+ | { raw: TDoc; mapped: TMapped };
+
+export interface CreateTrackingRequestFromInferOptions {
+ scac?: string;
+ numberType?: string;
+ refNumbers?: string[];
+ shipmentTags?: string[];
+}
+
+function normalizeBaseUrl(input?: string): string {
+ const defaultBase = 'https://api.terminal49.com/v2';
+ if (!input) return defaultBase;
+ try {
+ const url = new URL(input);
+ const path = url.pathname.replace(/\/+$/, '');
+ if (path === '' || path === '/') {
+ url.pathname = '/v2';
+ }
+ return url.toString().replace(/\/+$/, '');
+ } catch {
+ return input;
+ }
+}
+
+export class Terminal49Client {
+ private apiToken: string;
+ private apiBaseUrl: string;
+ private maxRetries: number;
+ private client: Client;
+ private jsona: Jsona;
+ private defaultFormat: ResponseFormat;
+ private authedFetch: typeof fetch;
+
+ constructor(config: Terminal49ClientConfig) {
+ if (!config.apiToken) {
+ throw new AuthenticationError('API token is required');
+ }
+
+ this.apiToken = config.apiToken;
+ this.apiBaseUrl = normalizeBaseUrl(config.apiBaseUrl);
+ this.maxRetries = config.maxRetries ?? 2;
+ this.defaultFormat = config.defaultFormat ?? 'raw';
+ this.authedFetch = this.buildFetch(config.fetchImpl ?? fetch);
+ this.client = createClient({
+ baseUrl: this.apiBaseUrl,
+ fetch: this.authedFetch,
+ });
+ this.jsona = new Jsona();
+ }
+
+ /**
+ * Deserialize a JSON:API document into plain objects.
+ * Useful when you want a simplified shape instead of JSON:API.
+ */
+ deserialize(document: unknown): T {
+ return this.jsona.deserialize(document as any) as T;
+ }
+
+ // ========= Resource namespaces =========
+
+ public shipments = {
+ get: (id: string, includeContainers = true, options?: CallOptions) =>
+ this.getShipment(id, includeContainers, options),
+ list: (
+ filters: {
+ status?: string;
+ port?: string;
+ carrier?: string;
+ updatedAfter?: string;
+ includeContainers?: boolean;
+ } = {},
+ options?: ListOptions,
+ ) => this.listShipments(filters, options),
+ update: (id: string, attrs: Record, options?: CallOptions) =>
+ this.updateShipment(id, attrs, options),
+ stopTracking: (id: string, options?: CallOptions) =>
+ this.stopTrackingShipment(id, options),
+ resumeTracking: (id: string, options?: CallOptions) =>
+ this.resumeTrackingShipment(id, options),
+ };
+
+ public containers = {
+ get: (id: string, include?: string[], options?: CallOptions) =>
+ this.getContainer(id, include, options),
+ list: (
+ filters: {
+ status?: string;
+ port?: string;
+ carrier?: string;
+ updatedAfter?: string;
+ include?: string;
+ } = {},
+ options?: ListOptions,
+ ) => this.listContainers(filters, options),
+ events: (id: string, options?: CallOptions) =>
+ this.getContainerTransportEvents(id, options),
+ route: (id: string, options?: CallOptions) =>
+ this.getContainerRoute(id, options),
+ rawEvents: (id: string, options?: CallOptions) =>
+ this.getContainerRawEvents(id, options),
+ refresh: (id: string, options?: CallOptions) =>
+ this.refreshContainer(id, options),
+ };
+
+ public shippingLines = {
+ list: (search?: string, options?: CallOptions) =>
+ this.listShippingLines(search, options),
+ };
+
+ public trackingRequests = {
+ list: (filters: Record = {}, options?: ListOptions) =>
+ this.listTrackingRequests(filters, options),
+ get: (id: string, options?: CallOptions) =>
+ this.getTrackingRequest(id, options),
+ update: (id: string, attrs: Record, options?: CallOptions) =>
+ this.updateTrackingRequest(id, attrs, options),
+ create: (params: {
+ requestType: TrackingRequestType;
+ requestNumber: string;
+ scac?: string;
+ refNumbers?: string[];
+ shipmentTags?: string[];
+ }) => this.createTrackingRequest(params),
+ inferNumber: (number: string) => this.inferTrackingNumber(number),
+ createFromInfer: (
+ number: string,
+ options?: CreateTrackingRequestFromInferOptions,
+ ) => this.createTrackingRequestFromInfer(number, options),
+ };
+
+ // ========= API methods =========
+
+ async search(query: string): Promise {
+ const params = new URLSearchParams({ query });
+ return this.executeManual(`${this.apiBaseUrl}/search?${params.toString()}`);
+ }
+
+ async getContainer(
+ id: string,
+ include: string[] = ['shipment', 'pod_terminal'],
+ options?: CallOptions,
+ ): Promise {
+ const includeParam = include.length > 0 ? include.join(',') : undefined;
+ const raw = await this.execute(() =>
+ this.client.GET('/containers/{id}', {
+ params: {
+ path: { id },
+ query: includeParam ? ({ include: includeParam } as any) : undefined,
+ },
+ }),
+ );
+ return this.formatResult(raw, options?.format);
+ }
+
+ async trackContainer(params: {
+ containerNumber?: string;
+ bookingNumber?: string;
+ scac?: string;
+ refNumbers?: string[];
+ }): Promise {
+ const requestType: TrackingRequestType = params.containerNumber
+ ? 'container'
+ : 'bill_of_lading';
+ const requestNumber = params.containerNumber || params.bookingNumber;
+
+ const missingRequestMessage =
+ 'request_number is required (/data/attributes/request_number)';
+ if (!requestNumber) {
+ throw new ValidationError(missingRequestMessage);
+ }
+
+ return this.createTrackingRequest({
+ requestType,
+ requestNumber,
+ scac: params.scac,
+ refNumbers: params.refNumbers,
+ });
+ }
+
+ async createTrackingRequest(params: {
+ requestType: TrackingRequestType;
+ requestNumber: string;
+ scac?: string;
+ refNumbers?: string[];
+ shipmentTags?: string[];
+ }): Promise {
+ if (!params.requestNumber) {
+ throw new ValidationError(
+ 'request_number is required (/data/attributes/request_number)',
+ );
+ }
+ if (!params.requestType) {
+ throw new ValidationError(
+ 'request_type is required (/data/attributes/request_type)',
+ );
+ }
+
+ const payload = {
+ data: {
+ type: 'tracking_request' as const,
+ attributes: {
+ request_type: params.requestType,
+ request_number: params.requestNumber,
+ scac: params.scac ?? '',
+ ref_numbers: params.refNumbers,
+ shipment_tags: params.shipmentTags,
+ },
+ },
+ };
+
+ return this.execute(() =>
+ this.client.POST('/tracking_requests', {
+ body: payload as any,
+ }),
+ );
+ }
+
+ async inferTrackingNumber(number: string): Promise {
+ if (!number || number.trim() === '') {
+ throw new ValidationError('number is required (/data/attributes/number)');
+ }
+
+ return this.execute(() =>
+ this.client.POST('/tracking_requests/infer_number', {
+ body: { number } as any,
+ }),
+ );
+ }
+
+ async createTrackingRequestFromInfer(
+ number: string,
+ options: CreateTrackingRequestFromInferOptions = {},
+ ): Promise<{ infer: any; trackingRequest: any }> {
+ const infer = await this.inferTrackingNumber(number);
+ const attrs = infer?.data?.attributes || {};
+ const numberType = this.normalizeInferNumberType(
+ attrs.number_type || options.numberType,
+ );
+ const shippingLine = attrs.shipping_line || {};
+ const selected = shippingLine.selected || null;
+ const candidates = Array.isArray(shippingLine.candidates)
+ ? shippingLine.candidates
+ : [];
+
+ const scac =
+ options.scac ||
+ selected?.scac ||
+ (candidates.length === 1 ? candidates[0]?.scac : undefined);
+
+ if (!numberType) {
+ throw new ValidationError(
+ 'Unable to infer tracking number type. Provide numberType to override.',
+ );
+ }
+
+ if (!scac) {
+ throw new ValidationError(
+ 'Unable to infer carrier SCAC. Provide scac or use infer candidates to select a carrier.',
+ );
+ }
+
+ const trackingRequest = await this.createTrackingRequest({
+ requestType: numberType,
+ requestNumber: number,
+ scac,
+ refNumbers: options.refNumbers,
+ shipmentTags: options.shipmentTags,
+ });
+
+ return { infer, trackingRequest };
+ }
+
+ async getShipment(
+ id: string,
+ includeContainers = true,
+ options?: CallOptions,
+ ): Promise {
+ const includes = includeContainers
+ ? 'containers,pod_terminal,port_of_lading,port_of_discharge,destination,destination_terminal'
+ : 'pod_terminal,port_of_lading,port_of_discharge,destination,destination_terminal';
+
+ const raw = await this.execute(() =>
+ this.client.GET('/shipments/{id}', {
+ params: {
+ path: { id },
+ query: { include: includes } as any,
+ },
+ }),
+ );
+ return this.formatResult(raw, options?.format, mapShipment);
+ }
+
+ async listShipments(
+ filters: {
+ status?: string;
+ port?: string;
+ carrier?: string;
+ updatedAfter?: string;
+ includeContainers?: boolean;
+ } = {},
+ options?: ListOptions,
+ ): Promise>> {
+ const params: Record = {
+ include:
+ 'containers,pod_terminal,port_of_lading,port_of_discharge,destination,destination_terminal',
+ };
+
+ if (filters.status) params['filter[status]'] = filters.status;
+ if (filters.port) params['filter[pod_locode]'] = filters.port;
+ if (filters.carrier) params['filter[line_scac]'] = filters.carrier;
+ if (filters.updatedAfter)
+ params['filter[updated_at]'] = filters.updatedAfter;
+
+ if (filters.includeContainers === false) {
+ params.include =
+ 'pod_terminal,port_of_lading,port_of_discharge,destination,destination_terminal';
+ }
+
+ this.applyPagination(params, options);
+
+ const raw = await this.execute(() =>
+ this.client.GET('/shipments', {
+ params: { query: params as any },
+ }),
+ );
+ return this.formatResult(raw, options?.format, (doc) =>
+ this.mapListResult(doc, mapShipmentList),
+ );
+ }
+
+ async updateShipment(
+ id: string,
+ attrs: Record,
+ options?: CallOptions,
+ ): Promise {
+ const payload = {
+ data: {
+ type: 'shipment' as const,
+ id,
+ attributes: attrs,
+ },
+ };
+
+ const raw = await this.execute(() =>
+ this.client.PATCH('/shipments/{id}', {
+ params: { path: { id } },
+ body: payload as any,
+ }),
+ );
+
+ return this.formatResult(raw, options?.format, mapShipment);
+ }
+
+ async stopTrackingShipment(id: string, options?: CallOptions): Promise {
+ const payload = { data: { type: 'shipment' as const, id } };
+ const raw = await this.execute(() =>
+ this.client.PATCH('/shipments/{id}/stop_tracking', {
+ params: { path: { id } },
+ body: payload as any,
+ }),
+ );
+ return this.formatResult(raw, options?.format, mapShipment);
+ }
+
+ async resumeTrackingShipment(
+ id: string,
+ options?: CallOptions,
+ ): Promise {
+ const payload = { data: { type: 'shipment' as const, id } };
+ const raw = await this.execute(() =>
+ this.client.PATCH('/shipments/{id}/resume_tracking', {
+ params: { path: { id } },
+ body: payload as any,
+ }),
+ );
+ return this.formatResult(raw, options?.format, mapShipment);
+ }
+
+ async getDemurrage(containerId: string): Promise {
+ const data = await this.getContainer(containerId, ['pod_terminal']);
+ const container = data.data?.attributes || {};
+ return {
+ container_id: containerId,
+ pickup_lfd: container.pickup_lfd,
+ pickup_appointment_at: container.pickup_appointment_at,
+ available_for_pickup: container.available_for_pickup,
+ fees_at_pod_terminal: container.fees_at_pod_terminal,
+ holds_at_pod_terminal: container.holds_at_pod_terminal,
+ pod_arrived_at: container.pod_arrived_at,
+ pod_discharged_at: container.pod_discharged_at,
+ };
+ }
+
+ async getContainerTransportEvents(
+ id: string,
+ options?: CallOptions,
+ ): Promise {
+ const raw = await this.execute(() =>
+ this.client.GET('/containers/{id}/transport_events', {
+ params: {
+ path: { id },
+ query: { include: 'location,terminal' },
+ },
+ }),
+ );
+ return this.formatResult(raw, options?.format, mapTransportEvents);
+ }
+
+ async getContainerRoute(id: string, options?: CallOptions): Promise {
+ const raw = await this.execute(() =>
+ this.client.GET('/containers/{id}/route', {
+ params: {
+ path: { id },
+ query: { include: 'port,vessel,route_location' } as any,
+ },
+ }),
+ );
+ return this.formatResult(raw, options?.format, mapRoute);
+ }
+
+ async listShippingLines(
+ search?: string,
+ options?: CallOptions,
+ ): Promise {
+ const query = search ? { search } : undefined;
+ const raw = await this.execute(() =>
+ this.client.GET('/shipping_lines', {
+ params: { query: query as any },
+ }),
+ );
+ return this.formatResult(raw, options?.format, mapShippingLines);
+ }
+
+ async getRailMilestones(containerId: string): Promise {
+ const data = await this.getContainer(containerId, ['transport_events']);
+ const container = data.data?.attributes || {};
+ const included = data.included || [];
+
+ const railEvents = included
+ .filter((item: any) => item.type === 'transport_event')
+ .filter((item: any) => item.attributes?.event?.startsWith('rail.'))
+ .map((item: any) => item.attributes);
+
+ return {
+ container_id: containerId,
+ pod_rail_carrier_scac: container.pod_rail_carrier_scac,
+ ind_rail_carrier_scac: container.ind_rail_carrier_scac,
+ pod_rail_loaded_at: container.pod_rail_loaded_at,
+ pod_rail_departed_at: container.pod_rail_departed_at,
+ ind_rail_arrived_at: container.ind_rail_arrived_at,
+ ind_rail_unloaded_at: container.ind_rail_unloaded_at,
+ ind_eta_at: container.ind_eta_at,
+ ind_ata_at: container.ind_ata_at,
+ rail_events: railEvents,
+ };
+ }
+
+ async listContainers(
+ filters: {
+ status?: string;
+ port?: string;
+ carrier?: string;
+ updatedAfter?: string;
+ include?: string;
+ } = {},
+ options?: ListOptions,
+ ): Promise>> {
+ const params: Record = {
+ include: filters.include || 'shipment,pod_terminal',
+ };
+ if (filters.status) params['filter[status]'] = filters.status;
+ if (filters.port) params['filter[pod_locode]'] = filters.port;
+ if (filters.carrier) params['filter[line_scac]'] = filters.carrier;
+ if (filters.updatedAfter)
+ params['filter[updated_at]'] = filters.updatedAfter;
+
+ this.applyPagination(params, options);
+
+ const raw = await this.execute(() =>
+ this.client.GET('/containers', {
+ params: { query: params as any },
+ }),
+ );
+ return this.formatResult(raw, options?.format, (doc) =>
+ this.mapListResult(doc, mapContainerList),
+ );
+ }
+
+ async getContainerRawEvents(id: string, options?: CallOptions): Promise {
+ const raw = await this.execute(() =>
+ this.client.GET('/containers/{id}/raw_events', {
+ params: { path: { id } },
+ }),
+ );
+ return this.formatResult(raw, options?.format);
+ }
+
+ async refreshContainer(id: string, options?: CallOptions): Promise {
+ const raw = await this.execute(() =>
+ this.client.PATCH('/containers/{id}/refresh', {
+ params: { path: { id } },
+ }),
+ );
+ return this.formatResult(raw, options?.format);
+ }
+
+ async listTrackingRequests(
+ filters: Record = {},
+ options?: ListOptions,
+ ): Promise>> {
+ const params: Record = { ...filters };
+ this.applyPagination(params, options);
+
+ const raw = await this.execute(() =>
+ this.client.GET('/tracking_requests', {
+ params: { query: params as any },
+ }),
+ );
+ return this.formatResult(raw, options?.format, (doc) =>
+ this.mapListResult(doc, mapTrackingRequestList),
+ );
+ }
+
+ async listTrackRequests(
+ filters: Record = {},
+ options?: ListOptions,
+ ): Promise>> {
+ return this.listTrackingRequests(filters, options);
+ }
+
+ async getTrackingRequest(id: string, options?: CallOptions): Promise {
+ const raw = await this.execute(() =>
+ this.client.GET('/tracking_requests/{id}', {
+ params: { path: { id } },
+ }),
+ );
+ return this.formatResult(raw, options?.format, mapTrackingRequest);
+ }
+
+ async updateTrackingRequest(
+ id: string,
+ attrs: Record,
+ options?: CallOptions,
+ ): Promise {
+ const payload = {
+ data: {
+ type: 'tracking_request' as const,
+ id,
+ attributes: attrs,
+ },
+ };
+
+ const raw = await this.execute(() =>
+ this.client.PATCH('/tracking_requests/{id}', {
+ params: { path: { id } },
+ body: payload as any,
+ }),
+ );
+
+ return this.formatResult(raw, options?.format, mapTrackingRequest);
+ }
+
+ // ========= internal helpers =========
+
+ private buildFetch(fetchImpl: typeof fetch) {
+ return async (
+ input: Request | URL | string,
+ init?: RequestInit,
+ ): Promise => {
+ const headers = new Headers(init?.headers);
+ const authHeader = this.apiToken.startsWith('Token ')
+ ? this.apiToken
+ : `Token ${this.apiToken}`;
+ headers.set('Authorization', authHeader);
+ headers.set('Accept', 'application/json');
+ if (init?.body !== undefined && !headers.has('Content-Type')) {
+ headers.set('Content-Type', 'application/json');
+ }
+
+ return fetchImpl(input, { ...init, headers });
+ };
+ }
+
+ private async execute(
+ fn: () => Promise>,
+ ): Promise {
+ return this.executeWithRetry(fn, 0);
+ }
+
+ private async executeWithRetry(
+ fn: () => Promise>,
+ attempt: number,
+ ): Promise {
+ const { data, error, response } = await fn();
+
+ if (data !== undefined && response?.ok !== false) {
+ return data as T;
+ }
+
+ const status = response?.status ?? 500;
+
+ if ((status === 429 || status >= 500) && attempt < this.maxRetries) {
+ const delay = 2 ** attempt * 500;
+ await this.sleep(delay);
+ return this.executeWithRetry(fn, attempt + 1);
+ }
+
+ const errorBody = error ?? (await this.safeParse(response));
+ throw toTerminal49Error(status, extractErrorMessage(errorBody), errorBody);
+ }
+
+ private async executeManual(
+ input: Request | URL | string,
+ init?: RequestInit,
+ ): Promise {
+ return this.executeWithRetry(
+ async (): Promise> => {
+ const response = await this.authedFetch(input, init);
+ let body: any = undefined;
+ try {
+ body = await response.clone().json();
+ } catch {
+ body = undefined;
+ }
+ return {
+ data: response.ok ? (body as T) : undefined,
+ error: response.ok ? undefined : body,
+ response,
+ };
+ },
+ 0,
+ );
+ }
+
+ private async safeParse(response?: Response | null): Promise {
+ if (!response) return null;
+ try {
+ return await response.clone().json();
+ } catch {
+ return null;
+ }
+ }
+
+ private sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+ }
+
+ private applyPagination(
+ params: Record,
+ options?: ListOptions,
+ ) {
+ if (!options) return;
+ if (options.page !== undefined)
+ params['page[number]'] = String(options.page);
+ if (options.pageSize !== undefined)
+ params['page[size]'] = String(options.pageSize);
+ }
+
+ private normalizeInferNumberType(
+ numberType?: string,
+ ): TrackingRequestType | null {
+ if (!numberType) return null;
+ if (numberType === 'booking') return 'booking_number';
+ if (numberType === 'booking_number') return 'booking_number';
+ if (numberType === 'bill_of_lading' || numberType === 'container')
+ return numberType;
+ return null;
+ }
+
+ // ========= mapping helpers =========
+
+ private formatResult(
+ raw: TDoc,
+ format: ResponseFormat | undefined,
+ mapper?: (doc: TDoc) => TMap,
+ ): TDoc | TMap | { raw: TDoc; mapped: TMap } {
+ const effective = format || this.defaultFormat || 'raw';
+ if (effective === 'raw') return raw;
+ if (effective === 'mapped') return mapper ? mapper(raw) : (raw as any);
+ if (effective === 'both')
+ return mapper
+ ? { raw, mapped: mapper(raw) }
+ : { raw, mapped: raw as any };
+ return raw;
+ }
+
+ private mapListResult(
+ doc: any,
+ mapper: (doc: any) => T[],
+ ): PaginatedResult {
+ return {
+ items: mapper(doc),
+ links: doc?.links,
+ meta: doc?.meta,
+ };
+ }
+}
diff --git a/sdks/typescript-sdk/src/client/errors.ts b/sdks/typescript-sdk/src/client/errors.ts
new file mode 100644
index 00000000..3279df83
--- /dev/null
+++ b/sdks/typescript-sdk/src/client/errors.ts
@@ -0,0 +1,151 @@
+export class Terminal49Error extends Error {
+ status?: number;
+ details?: unknown;
+
+ constructor(message: string, status?: number, details?: unknown) {
+ super(message);
+ this.name = 'Terminal49Error';
+ this.status = status;
+ this.details = details;
+ }
+}
+
+export class AuthenticationError extends Terminal49Error {
+ constructor(message: string, status = 401, details?: unknown) {
+ super(message, status, details);
+ this.name = 'AuthenticationError';
+ }
+}
+
+export class AuthorizationError extends Terminal49Error {
+ constructor(message: string, status = 403, details?: unknown) {
+ super(message, status, details);
+ this.name = 'AuthorizationError';
+ }
+}
+
+export class FeatureNotEnabledError extends AuthorizationError {
+ constructor(message: string, status = 403, details?: unknown) {
+ super(message, status, details);
+ this.name = 'FeatureNotEnabledError';
+ }
+}
+
+export class NotFoundError extends Terminal49Error {
+ constructor(message: string, status = 404, details?: unknown) {
+ super(message, status, details);
+ this.name = 'NotFoundError';
+ }
+}
+
+export class ValidationError extends Terminal49Error {
+ constructor(message: string, status = 400, details?: unknown) {
+ super(message, status, details);
+ this.name = 'ValidationError';
+ }
+}
+
+export class RateLimitError extends Terminal49Error {
+ constructor(message: string, status = 429, details?: unknown) {
+ super(message, status, details);
+ this.name = 'RateLimitError';
+ }
+}
+
+export class UpstreamError extends Terminal49Error {
+ constructor(message: string, status = 500, details?: unknown) {
+ super(message, status, details);
+ this.name = 'UpstreamError';
+ }
+}
+
+export function extractErrorMessage(body: any): string {
+ if (typeof body === 'string') {
+ return body;
+ }
+
+ if (body?.error && typeof body.error === 'string') {
+ return body.error;
+ }
+
+ if (typeof body?.errors === 'string') {
+ return body.errors;
+ }
+
+ if (body?.errors && Array.isArray(body.errors) && body.errors.length > 0) {
+ return body.errors
+ .map((error: any) => {
+ const detail = error.detail;
+ const title = error.title;
+ const code = error.code;
+ const pointer = error.source?.pointer;
+ let msg = detail || title || code || 'Unknown error';
+ if (pointer) msg += ` (${pointer})`;
+ return msg;
+ })
+ .join('; ');
+ }
+
+ if (body?.message) {
+ return body.message;
+ }
+
+ if (body?.detail && typeof body.detail === 'string') {
+ return body.detail;
+ }
+
+ return 'Unknown error';
+}
+
+export function toTerminal49Error(
+ status: number,
+ message: string,
+ details?: unknown,
+): Terminal49Error {
+ switch (status) {
+ case 400:
+ return new ValidationError(message, status, details);
+ case 401:
+ return new AuthenticationError(
+ 'Invalid or missing API token',
+ status,
+ details,
+ );
+ case 403: {
+ const normalized = message || 'Access forbidden';
+ const featureNotEnabled = /not enabled|feature/i.test(normalized);
+ return featureNotEnabled
+ ? new FeatureNotEnabledError(normalized, status, details)
+ : new AuthorizationError(normalized, status, details);
+ }
+ case 404:
+ return new NotFoundError(
+ message || 'Resource not found',
+ status,
+ details,
+ );
+ case 422:
+ return new ValidationError(message, status, details);
+ case 429:
+ return new RateLimitError(
+ message || 'Rate limit exceeded',
+ status,
+ details,
+ );
+ case 500:
+ case 502:
+ case 503:
+ case 504:
+ return new UpstreamError(
+ message || `Upstream server error (${status})`,
+ status,
+ details,
+ );
+ default:
+ return new Terminal49Error(
+ `Unexpected response status: ${status}${message ? ` - ${message}` : ''}`,
+ status,
+ details,
+ );
+ }
+}
diff --git a/sdks/typescript-sdk/src/client/mappers.ts b/sdks/typescript-sdk/src/client/mappers.ts
new file mode 100644
index 00000000..db90481e
--- /dev/null
+++ b/sdks/typescript-sdk/src/client/mappers.ts
@@ -0,0 +1,450 @@
+import type {
+ Container,
+ Route,
+ Shipment,
+ ShippingLine,
+ TrackingRequest,
+} from '../types/models.js';
+
+function toCamelCase(obj: Record): Record {
+ const result: Record = {};
+ for (const [key, value] of Object.entries(obj || {})) {
+ const camelKey = key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
+ result[camelKey] = value;
+ }
+ return result;
+}
+
+function createIncludedFinder(included: any[]) {
+ return (id: string, type: string) =>
+ included.find((item: any) => item.id === id && item.type === type);
+}
+
+export function mapTransportEvents(doc: any) {
+ const events = doc?.data || [];
+ const included = doc?.included || [];
+ const findIncluded = createIncludedFinder(included);
+
+ return events.map((item: any) => {
+ const evAttrs = item.attributes || {};
+ const locRef = item.relationships?.location?.data;
+ const termRef = item.relationships?.terminal?.data;
+ const location = locRef ? findIncluded(locRef.id, 'location') : null;
+ const terminal = termRef ? findIncluded(termRef.id, 'terminal') : null;
+ return {
+ id: item.id,
+ ...toCamelCase(evAttrs),
+ location: location
+ ? {
+ id: location.id,
+ name: location.attributes?.name,
+ locode: location.attributes?.locode,
+ }
+ : undefined,
+ terminal: terminal
+ ? {
+ id: terminal.id,
+ name: terminal.attributes?.name,
+ nickname: terminal.attributes?.nickname,
+ firmsCode: terminal.attributes?.firms_code,
+ }
+ : undefined,
+ };
+ });
+}
+
+export function mapRoute(doc: any): Route {
+ const route = doc.data?.attributes || {};
+ const relationships = doc.data?.relationships || {};
+ const included = doc.included || [];
+
+ const routeLocationRefs = relationships.route_locations?.data || [];
+ const routeLocations = routeLocationRefs
+ .map((ref: any) => {
+ const location = included.find(
+ (item: any) => item.id === ref.id && item.type === 'route_location',
+ );
+ if (!location) return null;
+
+ const attrs = location.attributes || {};
+ const rels = location.relationships || {};
+
+ const portId = rels.port?.data?.id;
+ const port = included.find(
+ (item: any) => item.id === portId && item.type === 'port',
+ );
+
+ const inboundVesselId = rels.inbound_vessel?.data?.id;
+ const outboundVesselId = rels.outbound_vessel?.data?.id;
+ const inboundVessel = included.find(
+ (item: any) => item.id === inboundVesselId && item.type === 'vessel',
+ );
+ const outboundVessel = included.find(
+ (item: any) => item.id === outboundVesselId && item.type === 'vessel',
+ );
+
+ return {
+ port: port
+ ? {
+ code: port.attributes?.code,
+ name: port.attributes?.name,
+ city: port.attributes?.city,
+ countryCode: port.attributes?.country_code,
+ }
+ : null,
+ inbound: {
+ mode: attrs.inbound_mode,
+ carrierScac: attrs.inbound_scac,
+ eta: attrs.inbound_eta_at,
+ ata: attrs.inbound_ata_at,
+ vessel: inboundVessel
+ ? {
+ name: inboundVessel.attributes?.name,
+ imo: inboundVessel.attributes?.imo,
+ }
+ : null,
+ },
+ outbound: {
+ mode: attrs.outbound_mode,
+ carrierScac: attrs.outbound_scac,
+ etd: attrs.outbound_etd_at,
+ atd: attrs.outbound_atd_at,
+ vessel: outboundVessel
+ ? {
+ name: outboundVessel.attributes?.name,
+ imo: outboundVessel.attributes?.imo,
+ }
+ : null,
+ },
+ };
+ })
+ .filter((loc: any) => loc !== null);
+
+ return {
+ id: doc.data?.id,
+ totalLegs: routeLocations.length,
+ locations: routeLocations,
+ createdAt: route.created_at,
+ updatedAt: route.updated_at,
+ };
+}
+
+export function mapShippingLines(doc: any): ShippingLine[] {
+ const data = Array.isArray(doc?.data) ? doc.data : [];
+ return data
+ .map((item: any) => {
+ const attrs = item?.attributes || {};
+ const scac = attrs.scac || item?.scac;
+ if (!scac) return null;
+ return {
+ scac,
+ name: attrs.name || attrs.full_name || scac,
+ shortName: attrs.short_name || attrs.nickname || undefined,
+ bolPrefix: attrs.bol_prefix || undefined,
+ notes: attrs.notes || undefined,
+ } as ShippingLine;
+ })
+ .filter(Boolean) as ShippingLine[];
+}
+
+export function mapContainer(doc: any): Container {
+ const attrs = doc?.data?.attributes || {};
+ const attrCamel = toCamelCase(attrs);
+ const relationships = doc?.data?.relationships || {};
+ const included = doc?.included || [];
+
+ const findIncluded = createIncludedFinder(included);
+
+ const shipmentRef = relationships.shipment?.data;
+ const shipmentIncluded = shipmentRef
+ ? findIncluded(shipmentRef.id, 'shipment')
+ : null;
+
+ const podTerminalRef = relationships.pod_terminal?.data;
+ const destinationTerminalRef = relationships.destination_terminal?.data;
+ const podTerminal = podTerminalRef
+ ? findIncluded(podTerminalRef.id, 'terminal')
+ : null;
+ const destTerminal = destinationTerminalRef
+ ? findIncluded(destinationTerminalRef.id, 'terminal')
+ : null;
+
+ const transportEvents = included
+ .filter((item: any) => item.type === 'transport_event')
+ .map((item: any) => {
+ const evAttrs = item.attributes || {};
+ const locRef = item.relationships?.location?.data;
+ const termRef = item.relationships?.terminal?.data;
+ const location = locRef ? findIncluded(locRef.id, 'location') : null;
+ const terminal = termRef ? findIncluded(termRef.id, 'terminal') : null;
+ return {
+ id: item.id,
+ ...toCamelCase(evAttrs),
+ location: location
+ ? {
+ id: location.id,
+ name: location.attributes?.name,
+ locode: location.attributes?.locode,
+ }
+ : undefined,
+ terminal: terminal
+ ? {
+ id: terminal.id,
+ name: terminal.attributes?.name,
+ nickname: terminal.attributes?.nickname,
+ firmsCode: terminal.attributes?.firms_code,
+ }
+ : undefined,
+ };
+ });
+
+ return {
+ id: doc?.data?.id,
+ ...attrCamel,
+ number: attrs.number || attrs.container_number,
+ status: attrs.status,
+ equipment: {
+ type: attrs.equipment_type,
+ length: attrs.equipment_length,
+ height: attrs.equipment_height,
+ weightLbs: attrs.weight_in_lbs,
+ },
+ location: {
+ currentLocation: attrs.location_at_pod_terminal,
+ availableForPickup: attrs.available_for_pickup,
+ podArrivedAt: attrs.pod_arrived_at,
+ podDischargedAt: attrs.pod_discharged_at,
+ },
+ demurrage: {
+ pickupLfd: attrs.pickup_lfd,
+ pickupAppointmentAt: attrs.pickup_appointment_at,
+ fees: attrs.fees_at_pod_terminal,
+ holds: attrs.holds_at_pod_terminal,
+ },
+ terminals: {
+ podTerminal: podTerminal
+ ? {
+ id: podTerminal.id,
+ name: podTerminal.attributes?.name,
+ nickname: podTerminal.attributes?.nickname,
+ firmsCode: podTerminal.attributes?.firms_code,
+ }
+ : null,
+ destinationTerminal: destTerminal
+ ? {
+ id: destTerminal.id,
+ name: destTerminal.attributes?.name,
+ nickname: destTerminal.attributes?.nickname,
+ firmsCode: destTerminal.attributes?.firms_code,
+ }
+ : null,
+ },
+ rail: {
+ podRailCarrierScac: attrs.pod_rail_carrier_scac,
+ indRailCarrierScac: attrs.ind_rail_carrier_scac,
+ podRailLoadedAt: attrs.pod_rail_loaded_at,
+ podRailDepartedAt: attrs.pod_rail_departed_at,
+ indRailArrivedAt: attrs.ind_rail_arrived_at,
+ indRailUnloadedAt: attrs.ind_rail_unloaded_at,
+ indEtaAt: attrs.ind_eta_at,
+ indAtaAt: attrs.ind_ata_at,
+ },
+ events: transportEvents,
+ shipment: shipmentIncluded
+ ? {
+ id: shipmentIncluded.id,
+ billOfLading:
+ shipmentIncluded.attributes?.bill_of_lading_number ||
+ shipmentIncluded.attributes?.bill_of_lading ||
+ shipmentIncluded.attributes?.bl_number,
+ shippingLineScac: shipmentIncluded.attributes?.shipping_line_scac,
+ }
+ : null,
+ };
+}
+
+export function mapContainerList(doc: any): Container[] {
+ if (!Array.isArray(doc?.data)) return [];
+ return doc.data.map((item: any) =>
+ mapContainer({ data: item, included: doc.included || [] }),
+ );
+}
+
+export function mapShipment(doc: any): Shipment {
+ const attrs = doc?.data?.attributes || {};
+ const attrCamel = toCamelCase(attrs);
+ const relationships = doc?.data?.relationships || {};
+ const included = doc?.included || [];
+
+ const findIncluded = createIncludedFinder(included);
+
+ const shipment: Shipment = {
+ id: doc?.data?.id,
+ billOfLading:
+ attrs.bill_of_lading_number ||
+ attrs.bill_of_lading ||
+ attrs.bl_number ||
+ attrs.bill_of_lading_number,
+ shippingLineScac: attrs.shipping_line_scac,
+ customerName: attrs.customer_name,
+ containers: [],
+ ...attrCamel,
+ };
+
+ const containerRefs = relationships.containers?.data || [];
+ shipment.containers = containerRefs
+ .map((ref: any) => {
+ const c = findIncluded(ref.id, 'container');
+ if (!c) return null;
+ return {
+ id: c.id,
+ number: c.attributes?.number || c.attributes?.container_number,
+ };
+ })
+ .filter(Boolean) as Array<{ id: string; number?: string }>;
+
+ shipment.refNumbers = attrs.ref_numbers;
+ shipment.tags = attrs.tags;
+ shipment.vesselAtPod = {
+ name: attrs.pod_vessel_name,
+ imo: attrs.pod_vessel_imo,
+ voyageNumber: attrs.pod_voyage_number,
+ };
+
+ const portOfLadingRef = relationships.port_of_lading?.data;
+ const portOfDischargeRef = relationships.port_of_discharge?.data;
+ const destinationTerminalRef = relationships.destination_terminal?.data;
+ const podTerminalRef = relationships.pod_terminal?.data;
+
+ const pol = portOfLadingRef ? findIncluded(portOfLadingRef.id, 'port') : null;
+ const pod = portOfDischargeRef
+ ? findIncluded(portOfDischargeRef.id, 'port')
+ : null;
+ const destTerminal = destinationTerminalRef
+ ? findIncluded(destinationTerminalRef.id, 'terminal')
+ : null;
+ const podTerminal = podTerminalRef
+ ? findIncluded(podTerminalRef.id, 'terminal')
+ : null;
+
+ shipment.ports = {
+ portOfLading: pol
+ ? {
+ locode: pol.attributes?.locode,
+ name: pol.attributes?.name,
+ code: pol.attributes?.code,
+ countryCode: pol.attributes?.country_code,
+ etd: attrs.pol_etd_at,
+ atd: attrs.pol_atd_at,
+ timezone: attrs.pol_timezone,
+ }
+ : null,
+ portOfDischarge: pod
+ ? {
+ locode: pod.attributes?.locode,
+ name: pod.attributes?.name,
+ code: pod.attributes?.code,
+ countryCode: pod.attributes?.country_code,
+ eta: attrs.pod_eta_at,
+ ata: attrs.pod_ata_at,
+ originalEta: attrs.pod_original_eta_at,
+ timezone: attrs.pod_timezone,
+ terminal: podTerminal
+ ? {
+ id: podTerminal.id,
+ name: podTerminal.attributes?.name,
+ nickname: podTerminal.attributes?.nickname,
+ firmsCode: podTerminal.attributes?.firms_code,
+ }
+ : null,
+ }
+ : null,
+ destination: attrs.destination_locode
+ ? {
+ locode: attrs.destination_locode,
+ name: attrs.destination_name,
+ eta: attrs.destination_eta_at,
+ ata: attrs.destination_ata_at,
+ timezone: attrs.destination_timezone,
+ terminal: destTerminal
+ ? {
+ id: destTerminal.id,
+ name: destTerminal.attributes?.name,
+ nickname: destTerminal.attributes?.nickname,
+ firmsCode: destTerminal.attributes?.firms_code,
+ }
+ : null,
+ }
+ : null,
+ };
+
+ shipment.tracking = {
+ lineTrackingLastAttemptedAt: attrs.line_tracking_last_attempted_at,
+ lineTrackingLastSucceededAt: attrs.line_tracking_last_succeeded_at,
+ lineTrackingStoppedAt: attrs.line_tracking_stopped_at,
+ lineTrackingStoppedReason: attrs.line_tracking_stopped_reason,
+ };
+
+ return shipment;
+}
+
+export function mapShipmentList(doc: any): Shipment[] {
+ if (!Array.isArray(doc?.data)) return [];
+ return doc.data.map((item: any) =>
+ mapShipment({ data: item, included: doc.included || [] }),
+ );
+}
+
+export function mapTrackingRequest(doc: any): TrackingRequest {
+ const attrs = doc?.data?.attributes || {};
+ const relationships = doc?.data?.relationships || {};
+ const included = doc?.included || [];
+
+ const findIncluded = createIncludedFinder(included);
+
+ const shipmentRef = relationships.shipment?.data;
+ const containerRef = relationships.container?.data;
+
+ const shipmentIncluded = shipmentRef
+ ? findIncluded(shipmentRef.id, 'shipment')
+ : null;
+ const containerIncluded = containerRef
+ ? findIncluded(containerRef.id, 'container')
+ : null;
+
+ return {
+ id: doc?.data?.id,
+ requestType: attrs.request_type,
+ requestNumber: attrs.request_number,
+ status: attrs.status,
+ scac: attrs.scac,
+ refNumbers: attrs.ref_numbers,
+ shipment: shipmentIncluded
+ ? {
+ id: shipmentIncluded.id,
+ billOfLading:
+ shipmentIncluded.attributes?.bill_of_lading_number ||
+ shipmentIncluded.attributes?.bill_of_lading ||
+ shipmentIncluded.attributes?.bl_number,
+ shippingLineScac: shipmentIncluded.attributes?.shipping_line_scac,
+ }
+ : null,
+ container: containerIncluded
+ ? {
+ id: containerIncluded.id,
+ number:
+ containerIncluded.attributes?.number ||
+ containerIncluded.attributes?.container_number,
+ status: containerIncluded.attributes?.status,
+ }
+ : null,
+ ...toCamelCase(attrs),
+ };
+}
+
+export function mapTrackingRequestList(doc: any): TrackingRequest[] {
+ if (!Array.isArray(doc?.data)) return [];
+ return doc.data.map((item: any) =>
+ mapTrackingRequest({ data: item, included: doc.included || [] }),
+ );
+}
diff --git a/sdks/typescript-sdk/src/fixtures/.gitkeep b/sdks/typescript-sdk/src/fixtures/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/sdks/typescript-sdk/src/fixtures/containers.events.json b/sdks/typescript-sdk/src/fixtures/containers.events.json
new file mode 100644
index 00000000..9cd48deb
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/containers.events.json
@@ -0,0 +1,777 @@
+{
+ "data": [
+ {
+ "id": "transport_event-1",
+ "type": "transport_event",
+ "attributes": {
+ "event": "container.transport.transshipment_departed",
+ "created_at": "2026-02-03T04:53:33Z",
+ "voyage_number": "546E",
+ "timestamp": "2025-11-25T00:30:00Z",
+ "data_source": "shipping_line",
+ "invalidated_at": null,
+ "location_locode": "CNSHG",
+ "timezone": "Asia/Shanghai",
+ "value": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-1",
+ "type": "vessel"
+ }
+ },
+ "source_event": {
+ "data": {
+ "id": "raw_event-7",
+ "type": "raw_event"
+ }
+ },
+ "previous_version": {
+ "data": null
+ },
+ "location": {
+ "data": {
+ "id": "port-3",
+ "type": "port"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "transport_event-2",
+ "type": "transport_event",
+ "attributes": {
+ "event": "container.transport.transshipment_loaded",
+ "created_at": "2026-02-03T04:53:33Z",
+ "voyage_number": "546E",
+ "timestamp": "2025-11-24T15:57:00Z",
+ "data_source": "shipping_line",
+ "invalidated_at": null,
+ "location_locode": "CNSHG",
+ "timezone": "Asia/Shanghai",
+ "value": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-1",
+ "type": "vessel"
+ }
+ },
+ "source_event": {
+ "data": {
+ "id": "raw_event-6",
+ "type": "raw_event"
+ }
+ },
+ "previous_version": {
+ "data": null
+ },
+ "location": {
+ "data": {
+ "id": "port-3",
+ "type": "port"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "transport_event-3",
+ "type": "transport_event",
+ "attributes": {
+ "event": "container.transport.transshipment_discharged",
+ "created_at": "2026-02-03T04:53:33Z",
+ "voyage_number": "545E",
+ "timestamp": "2025-11-20T07:14:00Z",
+ "data_source": "shipping_line",
+ "invalidated_at": null,
+ "location_locode": "CNSHG",
+ "timezone": "Asia/Shanghai",
+ "value": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-2",
+ "type": "vessel"
+ }
+ },
+ "source_event": {
+ "data": {
+ "id": "raw_event-5",
+ "type": "raw_event"
+ }
+ },
+ "previous_version": {
+ "data": null
+ },
+ "location": {
+ "data": {
+ "id": "port-3",
+ "type": "port"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "transport_event-4",
+ "type": "transport_event",
+ "attributes": {
+ "event": "container.transport.transshipment_arrived",
+ "created_at": "2026-02-03T04:53:33Z",
+ "voyage_number": "545E",
+ "timestamp": "2025-11-19T23:06:00Z",
+ "data_source": "shipping_line",
+ "invalidated_at": null,
+ "location_locode": "CNSHG",
+ "timezone": "Asia/Shanghai",
+ "value": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-2",
+ "type": "vessel"
+ }
+ },
+ "source_event": {
+ "data": {
+ "id": "raw_event-4",
+ "type": "raw_event"
+ }
+ },
+ "previous_version": {
+ "data": null
+ },
+ "location": {
+ "data": {
+ "id": "port-3",
+ "type": "port"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "transport_event-5",
+ "type": "transport_event",
+ "attributes": {
+ "event": "container.transport.full_out",
+ "created_at": "2026-02-03T04:53:33Z",
+ "voyage_number": null,
+ "timestamp": "2026-01-06T21:25:00Z",
+ "data_source": "shipping_line",
+ "invalidated_at": null,
+ "location_locode": "USCHS",
+ "timezone": "America/New_York",
+ "value": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "vessel": {
+ "data": null
+ },
+ "source_event": {
+ "data": {
+ "id": "raw_event-10",
+ "type": "raw_event"
+ }
+ },
+ "previous_version": {
+ "data": null
+ },
+ "location": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "transport_event-6",
+ "type": "transport_event",
+ "attributes": {
+ "event": "container.transport.vessel_loaded",
+ "created_at": "2026-02-03T04:53:33Z",
+ "voyage_number": "545E",
+ "timestamp": "2025-11-18T13:57:00Z",
+ "data_source": "shipping_line",
+ "invalidated_at": null,
+ "location_locode": "KRPUS",
+ "timezone": "Asia/Seoul",
+ "value": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-2",
+ "type": "vessel"
+ }
+ },
+ "source_event": {
+ "data": {
+ "id": "raw_event-2",
+ "type": "raw_event"
+ }
+ },
+ "previous_version": {
+ "data": null
+ },
+ "location": {
+ "data": {
+ "id": "port-1",
+ "type": "port"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "transport_event-7",
+ "type": "transport_event",
+ "attributes": {
+ "event": "container.transport.vessel_discharged",
+ "created_at": "2026-02-03T04:53:33Z",
+ "voyage_number": "546E",
+ "timestamp": "2025-12-29T14:24:00Z",
+ "data_source": "shipping_line",
+ "invalidated_at": null,
+ "location_locode": "USCHS",
+ "timezone": "America/New_York",
+ "value": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-1",
+ "type": "vessel"
+ }
+ },
+ "source_event": {
+ "data": {
+ "id": "raw_event-9",
+ "type": "raw_event"
+ }
+ },
+ "previous_version": {
+ "data": null
+ },
+ "location": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "transport_event-8",
+ "type": "transport_event",
+ "attributes": {
+ "event": "container.transport.vessel_departed",
+ "created_at": "2026-02-03T04:53:33Z",
+ "voyage_number": "546E",
+ "timestamp": "2025-11-18T15:26:00Z",
+ "data_source": "shipping_line",
+ "invalidated_at": null,
+ "location_locode": "KRPUS",
+ "timezone": "Asia/Seoul",
+ "value": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-2",
+ "type": "vessel"
+ }
+ },
+ "source_event": {
+ "data": {
+ "id": "raw_event-3",
+ "type": "raw_event"
+ }
+ },
+ "previous_version": {
+ "data": null
+ },
+ "location": {
+ "data": {
+ "id": "port-1",
+ "type": "port"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "transport_event-9",
+ "type": "transport_event",
+ "attributes": {
+ "event": "container.transport.vessel_berthed",
+ "created_at": "2026-02-03T04:53:33Z",
+ "voyage_number": "546E",
+ "timestamp": "2025-12-29T10:26:32Z",
+ "data_source": "ais",
+ "invalidated_at": null,
+ "location_locode": "USCHS",
+ "timezone": "America/New_York",
+ "value": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-1",
+ "type": "vessel"
+ }
+ },
+ "source_event": {
+ "data": {
+ "id": "raw_event-39",
+ "type": "raw_event"
+ }
+ },
+ "previous_version": {
+ "data": null
+ },
+ "location": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ },
+ "terminal": {
+ "data": {
+ "id": "terminal-1",
+ "type": "terminal"
+ }
+ }
+ }
+ },
+ {
+ "id": "transport_event-10",
+ "type": "transport_event",
+ "attributes": {
+ "event": "container.transport.vessel_arrived",
+ "created_at": "2026-02-03T04:53:33Z",
+ "voyage_number": "546E",
+ "timestamp": "2025-12-29T09:28:23Z",
+ "data_source": "ais",
+ "invalidated_at": null,
+ "location_locode": "USCHS",
+ "timezone": "America/New_York",
+ "value": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-1",
+ "type": "vessel"
+ }
+ },
+ "source_event": {
+ "data": {
+ "id": "raw_event-40",
+ "type": "raw_event"
+ }
+ },
+ "previous_version": {
+ "data": null
+ },
+ "location": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "transport_event-11",
+ "type": "transport_event",
+ "attributes": {
+ "event": "container.transport.full_in",
+ "created_at": "2026-02-03T04:53:33Z",
+ "voyage_number": null,
+ "timestamp": "2025-11-12T03:23:00Z",
+ "data_source": "shipping_line",
+ "invalidated_at": null,
+ "location_locode": "KRPUS",
+ "timezone": "Asia/Seoul",
+ "value": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "vessel": {
+ "data": null
+ },
+ "source_event": {
+ "data": {
+ "id": "raw_event-1",
+ "type": "raw_event"
+ }
+ },
+ "previous_version": {
+ "data": null
+ },
+ "location": {
+ "data": {
+ "id": "port-1",
+ "type": "port"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ }
+ ],
+ "included": [
+ {
+ "id": "port-3",
+ "type": "port",
+ "attributes": {
+ "id": "c8db7333-7912-4092-a370-9823965d1394",
+ "name": "Shanghai",
+ "code": "CNSHG",
+ "state_abbr": "SH",
+ "city": "Shanghai",
+ "country_code": "CN",
+ "latitude": "31.36636",
+ "longitude": "121.6147",
+ "time_zone": "Asia/Shanghai"
+ },
+ "relationships": {
+ "terminals": {
+ "data": [
+ {
+ "id": "terminal-16",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-17",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-18",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-19",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-20",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-21",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-22",
+ "type": "terminal"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "id": "port-2",
+ "type": "port",
+ "attributes": {
+ "id": "d8138df6-f542-4f69-a3e3-e5fd6b16b63a",
+ "name": "Charleston",
+ "code": "USCHS",
+ "state_abbr": "SC",
+ "city": "Charleston",
+ "country_code": "US",
+ "latitude": "32.831492181",
+ "longitude": "-79.89124957",
+ "time_zone": "America/New_York"
+ },
+ "relationships": {
+ "terminals": {
+ "data": [
+ {
+ "id": "terminal-11",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-12",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-13",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-1",
+ "type": "terminal"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "id": "port-1",
+ "type": "port",
+ "attributes": {
+ "id": "f11d479d-5501-4b24-8703-4133f0b202b7",
+ "name": "Busan",
+ "code": "KRPUS",
+ "state_abbr": "26",
+ "city": "Busan",
+ "country_code": "KR",
+ "latitude": "35.10162",
+ "longitude": "129.036",
+ "time_zone": "Asia/Seoul"
+ },
+ "relationships": {
+ "terminals": {
+ "data": [
+ {
+ "id": "terminal-9",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-8",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-7",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-6",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-5",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-4",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-3",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-2",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-10",
+ "type": "terminal"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "id": "terminal-1",
+ "type": "terminal",
+ "attributes": {
+ "id": "a59b3bd1-d497-4a48-bfb6-5cf2c4824a60",
+ "nickname": "WWT",
+ "name": "Wando Welch Terminal",
+ "firms_code": "N598",
+ "smdg_code": "WWMT",
+ "bic_facility_code": null,
+ "provided_data": {
+ "pickup_lfd": false,
+ "pod_full_out_at": true,
+ "pickup_lfd_notes": "",
+ "available_for_pickup": true,
+ "fees_at_pod_terminal": false,
+ "holds_at_pod_terminal": true,
+ "pickup_appointment_at": false,
+ "location_at_pod_terminal": false,
+ "available_for_pickup_notes": "",
+ "fees_at_pod_terminal_notes": "",
+ "holds_at_pod_terminal_notes": "",
+ "pickup_appointment_at_notes": "",
+ "pod_full_out_chassis_number": true,
+ "location_at_pod_terminal_notes": "",
+ "pod_full_out_chassis_number_notes": ""
+ },
+ "street": "400 Long Point Rd. ",
+ "city": "Mt. Pleasant",
+ "state": "South Carolina",
+ "state_abbr": "SC",
+ "zip": "29464",
+ "country": "United States",
+ "facility_type": "ocean_terminal"
+ },
+ "relationships": {
+ "port": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ }
+ }
+ }
+ ],
+ "links": {
+ "self": "https://api.terminal49.com/v2/containers/cbc36c41-3b62-4bdc-8449-c446faf4aa3a/transport_events?include=location,terminal",
+ "current": "https://api.terminal49.com/v2/containers/cbc36c41-3b62-4bdc-8449-c446faf4aa3a/transport_events?include=location,terminal&page[number]=1"
+ }
+}
diff --git a/sdks/typescript-sdk/src/fixtures/containers.get.base.json b/sdks/typescript-sdk/src/fixtures/containers.get.base.json
new file mode 100644
index 00000000..245a0290
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/containers.get.base.json
@@ -0,0 +1,170 @@
+{
+ "data": {
+ "id": "container-1",
+ "type": "container",
+ "attributes": {
+ "number": "RMCU74210c6",
+ "seal_number": null,
+ "created_at": "2026-02-03T04:53:33Z",
+ "ref_numbers": [],
+ "pod_arrived_at": "2025-12-29T09:28:23Z",
+ "pod_discharged_at": "2025-12-29T14:24:00Z",
+ "final_destination_full_out_at": null,
+ "holds_at_pod_terminal": [],
+ "available_for_pickup": false,
+ "delivered_at": null,
+ "current_status": "picked_up",
+ "empty_out_at": null,
+ "pol_full_in_at": "2025-11-12T03:23:00Z",
+ "pol_vessel_loaded_at": "2025-11-18T13:57:00Z",
+ "pol_vessel_departed_at": "2025-11-18T15:26:00Z",
+ "equipment_type": "tank",
+ "equipment_length": 20,
+ "equipment_height": "standard",
+ "pod_full_out_at": "2026-01-06T21:25:00Z",
+ "empty_terminated_at": null,
+ "terminal_checked_at": null,
+ "fees_at_pod_terminal": [],
+ "pickup_lfd": null,
+ "pickup_appointment_at": null,
+ "pod_full_out_chassis_number": null,
+ "location_at_pod_terminal": null,
+ "pod_last_tracking_request_at": null,
+ "shipment_last_tracking_request_at": "2026-02-03T18:02:04Z",
+ "rail_last_tracking_request_at": null,
+ "availability_known": false,
+ "pod_timezone": "America/New_York",
+ "final_destination_timezone": null,
+ "weight_in_lbs": 0,
+ "empty_terminated_timezone": "America/New_York",
+ "pod_rail_carrier_scac": null,
+ "ind_rail_carrier_scac": null,
+ "pod_rail_loaded_at": null,
+ "pod_rail_departed_at": null,
+ "ind_eta_at": null,
+ "ind_ata_at": null,
+ "ind_rail_unloaded_at": null,
+ "ind_facility_holds": null,
+ "ind_facility_fees": null,
+ "ind_facility_lfd_on": null,
+ "import_deadlines": {
+ "pickup_lfd_terminal": null,
+ "pickup_lfd_rail": null,
+ "pickup_lfd_line": null
+ },
+ "ssl_lfd": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "pickup_facility": {
+ "data": null
+ },
+ "pod_terminal": {
+ "data": {
+ "id": "terminal-1",
+ "type": "terminal"
+ }
+ },
+ "transport_events": {
+ "data": [
+ {
+ "id": "transport_event-1",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-2",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-3",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-4",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-5",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-6",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-7",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-8",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-9",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-10",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-11",
+ "type": "transport_event"
+ }
+ ]
+ },
+ "raw_events": {
+ "data": [
+ {
+ "id": "raw_event-1",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-2",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-3",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-4",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-5",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-6",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-7",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-8",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-9",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-10",
+ "type": "raw_event"
+ }
+ ]
+ }
+ }
+ },
+ "links": {
+ "self": "https://api.terminal49.com/v2/containers/cbc36c41-3b62-4bdc-8449-c446faf4aa3a"
+ }
+}
diff --git a/sdks/typescript-sdk/src/fixtures/containers.get.include.json b/sdks/typescript-sdk/src/fixtures/containers.get.include.json
new file mode 100644
index 00000000..a13624bb
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/containers.get.include.json
@@ -0,0 +1,296 @@
+{
+ "data": {
+ "id": "container-1",
+ "type": "container",
+ "attributes": {
+ "number": "RMCU74210c6",
+ "seal_number": null,
+ "created_at": "2026-02-03T04:53:33Z",
+ "ref_numbers": [],
+ "pod_arrived_at": "2025-12-29T09:28:23Z",
+ "pod_discharged_at": "2025-12-29T14:24:00Z",
+ "final_destination_full_out_at": null,
+ "holds_at_pod_terminal": [],
+ "available_for_pickup": false,
+ "delivered_at": null,
+ "current_status": "picked_up",
+ "empty_out_at": null,
+ "pol_full_in_at": "2025-11-12T03:23:00Z",
+ "pol_vessel_loaded_at": "2025-11-18T13:57:00Z",
+ "pol_vessel_departed_at": "2025-11-18T15:26:00Z",
+ "equipment_type": "tank",
+ "equipment_length": 20,
+ "equipment_height": "standard",
+ "pod_full_out_at": "2026-01-06T21:25:00Z",
+ "empty_terminated_at": null,
+ "terminal_checked_at": null,
+ "fees_at_pod_terminal": [],
+ "pickup_lfd": null,
+ "pickup_appointment_at": null,
+ "pod_full_out_chassis_number": null,
+ "location_at_pod_terminal": null,
+ "pod_last_tracking_request_at": null,
+ "shipment_last_tracking_request_at": "2026-02-03T18:02:04Z",
+ "rail_last_tracking_request_at": null,
+ "availability_known": false,
+ "pod_timezone": "America/New_York",
+ "final_destination_timezone": null,
+ "weight_in_lbs": 0,
+ "empty_terminated_timezone": "America/New_York",
+ "pod_rail_carrier_scac": null,
+ "ind_rail_carrier_scac": null,
+ "pod_rail_loaded_at": null,
+ "pod_rail_departed_at": null,
+ "ind_eta_at": null,
+ "ind_ata_at": null,
+ "ind_rail_unloaded_at": null,
+ "ind_facility_holds": null,
+ "ind_facility_fees": null,
+ "ind_facility_lfd_on": null,
+ "import_deadlines": {
+ "pickup_lfd_terminal": null,
+ "pickup_lfd_rail": null,
+ "pickup_lfd_line": null
+ },
+ "ssl_lfd": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "pickup_facility": {
+ "data": null
+ },
+ "pod_terminal": {
+ "data": {
+ "id": "terminal-1",
+ "type": "terminal"
+ }
+ },
+ "transport_events": {
+ "data": [
+ {
+ "id": "transport_event-1",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-2",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-3",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-4",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-5",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-6",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-7",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-8",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-9",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-10",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-11",
+ "type": "transport_event"
+ }
+ ]
+ },
+ "raw_events": {
+ "data": [
+ {
+ "id": "raw_event-1",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-2",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-3",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-4",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-5",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-6",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-7",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-8",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-9",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-10",
+ "type": "raw_event"
+ }
+ ]
+ }
+ }
+ },
+ "links": {
+ "self": "https://api.terminal49.com/v2/containers/cbc36c41-3b62-4bdc-8449-c446faf4aa3a?include=shipment,pod_terminal"
+ },
+ "included": [
+ {
+ "id": "shipment-1",
+ "type": "shipment",
+ "attributes": {
+ "created_at": "2026-02-03T04:53:33Z",
+ "ref_numbers": [],
+ "tags": [],
+ "bill_of_lading_number": "HLCUSEL251063ad5",
+ "normalized_number": "HLCUSEL251063257",
+ "shipping_line_scac": "HLCU",
+ "shipping_line_name": "Hapag-Lloyd",
+ "shipping_line_short_name": "Hapag-Lloyd",
+ "customer_name": "CUSTOMER-001",
+ "port_of_lading_locode": "KRPUS",
+ "port_of_lading_name": "Busan",
+ "port_of_discharge_locode": "USCHS",
+ "port_of_discharge_name": "Charleston",
+ "pod_vessel_name": "MAERSK SHIVLING",
+ "pod_vessel_imo": "9728253",
+ "pod_voyage_number": "546E",
+ "destination_locode": null,
+ "destination_name": null,
+ "destination_timezone": null,
+ "destination_ata_at": null,
+ "destination_eta_at": null,
+ "pol_etd_at": null,
+ "pol_atd_at": "2025-11-18T15:26:00Z",
+ "pol_timezone": "Asia/Seoul",
+ "pod_eta_at": null,
+ "pod_original_eta_at": null,
+ "pol_original_etd_at": null,
+ "destination_original_eta_at": null,
+ "pod_ata_at": "2025-12-29T09:28:23Z",
+ "pod_timezone": "America/New_York",
+ "line_tracking_last_attempted_at": "2026-02-03T18:02:03Z",
+ "line_tracking_last_succeeded_at": "2026-02-03T18:02:04Z",
+ "line_tracking_stopped_at": null,
+ "line_tracking_stopped_reason": null
+ },
+ "links": {
+ "self": "/v2/shipments/a99e23fe-84ca-4dac-ab15-c4996c7fe3d6"
+ },
+ "relationships": {
+ "port_of_lading": {
+ "data": {
+ "id": "port-1",
+ "type": "port"
+ }
+ },
+ "port_of_discharge": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ },
+ "pod_terminal": {
+ "data": {
+ "id": "terminal-1",
+ "type": "terminal"
+ }
+ },
+ "destination": {
+ "data": null
+ },
+ "destination_terminal": {
+ "data": null
+ },
+ "line_tracking_stopped_by_user": {
+ "data": null
+ },
+ "containers": {
+ "data": [
+ {
+ "id": "container-1",
+ "type": "container"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "id": "terminal-1",
+ "type": "terminal",
+ "attributes": {
+ "id": "a59b3bd1-d497-4a48-bfb6-5cf2c4824a60",
+ "nickname": "WWT",
+ "name": "Wando Welch Terminal",
+ "firms_code": "N598",
+ "smdg_code": "WWMT",
+ "bic_facility_code": null,
+ "provided_data": {
+ "pickup_lfd": false,
+ "pod_full_out_at": true,
+ "pickup_lfd_notes": "",
+ "available_for_pickup": true,
+ "fees_at_pod_terminal": false,
+ "holds_at_pod_terminal": true,
+ "pickup_appointment_at": false,
+ "location_at_pod_terminal": false,
+ "available_for_pickup_notes": "",
+ "fees_at_pod_terminal_notes": "",
+ "holds_at_pod_terminal_notes": "",
+ "pickup_appointment_at_notes": "",
+ "pod_full_out_chassis_number": true,
+ "location_at_pod_terminal_notes": "",
+ "pod_full_out_chassis_number_notes": ""
+ },
+ "street": "400 Long Point Rd. ",
+ "city": "Mt. Pleasant",
+ "state": "South Carolina",
+ "state_abbr": "SC",
+ "zip": "29464",
+ "country": "United States",
+ "facility_type": "ocean_terminal"
+ },
+ "relationships": {
+ "port": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/sdks/typescript-sdk/src/fixtures/containers.list.json b/sdks/typescript-sdk/src/fixtures/containers.list.json
new file mode 100644
index 00000000..f7b0782a
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/containers.list.json
@@ -0,0 +1,300 @@
+{
+ "data": [
+ {
+ "id": "container-2",
+ "type": "container",
+ "attributes": {
+ "number": "EISU8332dc1",
+ "seal_number": "EMCRDE4724",
+ "created_at": "2025-11-05T15:10:29Z",
+ "ref_numbers": [],
+ "pod_arrived_at": "2025-10-31T04:00:00Z",
+ "pod_discharged_at": "2025-10-31T04:00:00Z",
+ "final_destination_full_out_at": null,
+ "holds_at_pod_terminal": [
+ {
+ "status": "hold",
+ "name": "freight",
+ "description": ""
+ },
+ {
+ "status": "hold",
+ "name": "customs",
+ "description": ""
+ }
+ ],
+ "available_for_pickup": true,
+ "delivered_at": null,
+ "current_status": "available",
+ "empty_out_at": null,
+ "pol_full_in_at": "2025-09-09T16:00:00Z",
+ "pol_vessel_loaded_at": null,
+ "pol_vessel_departed_at": null,
+ "equipment_type": "dry",
+ "equipment_length": 40,
+ "equipment_height": "high_cube",
+ "pod_full_out_at": "2025-11-01T04:00:00Z",
+ "empty_terminated_at": null,
+ "terminal_checked_at": "2026-02-04T05:04:40Z",
+ "fees_at_pod_terminal": [],
+ "pickup_lfd": "2025-11-05T05:00:00Z",
+ "pickup_appointment_at": null,
+ "pod_full_out_chassis_number": null,
+ "location_at_pod_terminal": null,
+ "pod_last_tracking_request_at": "2026-02-04T05:04:40Z",
+ "shipment_last_tracking_request_at": "2026-02-04T03:14:40Z",
+ "rail_last_tracking_request_at": "2026-02-04T04:12:55Z",
+ "availability_known": true,
+ "pod_timezone": "America/New_York",
+ "final_destination_timezone": "America/New_York",
+ "weight_in_lbs": 14242,
+ "empty_terminated_timezone": "America/New_York",
+ "pod_rail_carrier_scac": "CSXT",
+ "ind_rail_carrier_scac": "CSXT",
+ "pod_rail_loaded_at": "2025-11-01T19:33:00Z",
+ "pod_rail_departed_at": "2025-11-01T04:00:00Z",
+ "ind_eta_at": null,
+ "ind_ata_at": "2025-11-03T05:00:00Z",
+ "ind_rail_unloaded_at": "2025-11-03T14:56:00Z",
+ "ind_facility_holds": null,
+ "ind_facility_fees": null,
+ "ind_facility_lfd_on": "2025-11-05T05:00:00Z",
+ "import_deadlines": {
+ "pickup_lfd_terminal": null,
+ "pickup_lfd_rail": "2025-11-05T05:00:00Z",
+ "pickup_lfd_line": null
+ },
+ "ssl_lfd": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-2",
+ "type": "shipment"
+ }
+ },
+ "pickup_facility": {
+ "data": {
+ "id": "terminal-14",
+ "type": "terminal"
+ }
+ },
+ "pod_terminal": {
+ "data": {
+ "id": "terminal-15",
+ "type": "terminal"
+ }
+ },
+ "transport_events": {
+ "data": [
+ {
+ "id": "transport_event-12",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-13",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-14",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-15",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-16",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-17",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-18",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-19",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-20",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-21",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-22",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-23",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-24",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-25",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-26",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-27",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-28",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-29",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-30",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-31",
+ "type": "transport_event"
+ }
+ ]
+ },
+ "raw_events": {
+ "data": [
+ {
+ "id": "raw_event-11",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-12",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-13",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-14",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-15",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-16",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-17",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-18",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-19",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-20",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-21",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-22",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-23",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-24",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-25",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-26",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-27",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-28",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-29",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-30",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-31",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-32",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-33",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-34",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-35",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-36",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-37",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-38",
+ "type": "raw_event"
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "meta": {
+ "total": 292863
+ },
+ "links": {
+ "self": "https://api.terminal49.com/v2/containers?page[size]=1",
+ "current": "https://api.terminal49.com/v2/containers?page[number]=1&page[size]=1",
+ "next": "https://api.terminal49.com/v2/containers?page[number]=2&page[size]=1",
+ "last": "https://api.terminal49.com/v2/containers?page[number]=292863&page[size]=1"
+ }
+}
diff --git a/sdks/typescript-sdk/src/fixtures/containers.raw-events.json b/sdks/typescript-sdk/src/fixtures/containers.raw-events.json
new file mode 100644
index 00000000..d61f89ec
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/containers.raw-events.json
@@ -0,0 +1,661 @@
+{
+ "data": [
+ {
+ "id": "raw_event-1",
+ "type": "raw_event",
+ "attributes": {
+ "actual_on": null,
+ "estimated_at": null,
+ "actual_at": "2025-11-12T03:23:00Z",
+ "will_occur_at": null,
+ "event": "full_in",
+ "timestamp": "2025-11-12T03:23:00Z",
+ "estimated": false,
+ "invalidated_at": null,
+ "index": 0,
+ "original_event": "Truck Arrival in",
+ "created_at": "2026-02-03T04:53:33Z",
+ "data_source": "shipping_line",
+ "data_provider_name": "Hapag-Lloyd",
+ "data_provider_code": "HLCU",
+ "voyage_number": "",
+ "location_name": "Busan",
+ "location_locode": "KRPUS",
+ "vessel_name": null,
+ "vessel_imo": null,
+ "timezone": "Asia/Seoul"
+ },
+ "relationships": {
+ "location": {
+ "data": {
+ "id": "port-1",
+ "type": "port"
+ }
+ },
+ "vessel": {
+ "data": null
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "raw_event-2",
+ "type": "raw_event",
+ "attributes": {
+ "actual_on": null,
+ "estimated_at": null,
+ "actual_at": "2025-11-18T13:57:00Z",
+ "will_occur_at": null,
+ "event": "vessel_loaded",
+ "timestamp": "2025-11-18T13:57:00Z",
+ "estimated": false,
+ "invalidated_at": null,
+ "index": 1,
+ "original_event": "Vessel Loaded",
+ "created_at": "2026-02-03T04:53:33Z",
+ "data_source": "shipping_line",
+ "data_provider_name": "Hapag-Lloyd",
+ "data_provider_code": "HLCU",
+ "voyage_number": "545E",
+ "location_name": "Busan",
+ "location_locode": "KRPUS",
+ "vessel_name": "GUNDE MAERSK",
+ "vessel_imo": "9359014",
+ "timezone": "Asia/Seoul"
+ },
+ "relationships": {
+ "location": {
+ "data": {
+ "id": "port-1",
+ "type": "port"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-2",
+ "type": "vessel"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "raw_event-3",
+ "type": "raw_event",
+ "attributes": {
+ "actual_on": null,
+ "estimated_at": null,
+ "actual_at": "2025-11-18T15:26:00Z",
+ "will_occur_at": null,
+ "event": "vessel_departed",
+ "timestamp": "2025-11-18T15:26:00Z",
+ "estimated": false,
+ "invalidated_at": null,
+ "index": 2,
+ "original_event": "Vessel departed",
+ "created_at": "2026-02-03T04:53:33Z",
+ "data_source": "shipping_line",
+ "data_provider_name": "Hapag-Lloyd",
+ "data_provider_code": "HLCU",
+ "voyage_number": "545E",
+ "location_name": "Busan",
+ "location_locode": "KRPUS",
+ "vessel_name": "GUNDE MAERSK",
+ "vessel_imo": "9359014",
+ "timezone": "Asia/Seoul"
+ },
+ "relationships": {
+ "location": {
+ "data": {
+ "id": "port-1",
+ "type": "port"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-2",
+ "type": "vessel"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "raw_event-4",
+ "type": "raw_event",
+ "attributes": {
+ "actual_on": null,
+ "estimated_at": null,
+ "actual_at": "2025-11-19T23:06:00Z",
+ "will_occur_at": null,
+ "event": "transshipment_arrived",
+ "timestamp": "2025-11-19T23:06:00Z",
+ "estimated": false,
+ "invalidated_at": null,
+ "index": 3,
+ "original_event": "Vessel arrived",
+ "created_at": "2026-02-03T04:53:33Z",
+ "data_source": "shipping_line",
+ "data_provider_name": "Hapag-Lloyd",
+ "data_provider_code": "HLCU",
+ "voyage_number": "545E",
+ "location_name": "Shanghai",
+ "location_locode": "CNSHG",
+ "vessel_name": "GUNDE MAERSK",
+ "vessel_imo": "9359014",
+ "timezone": "Asia/Shanghai"
+ },
+ "relationships": {
+ "location": {
+ "data": {
+ "id": "port-3",
+ "type": "port"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-2",
+ "type": "vessel"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "raw_event-5",
+ "type": "raw_event",
+ "attributes": {
+ "actual_on": null,
+ "estimated_at": null,
+ "actual_at": "2025-11-20T07:14:00Z",
+ "will_occur_at": null,
+ "event": "transshipment_discharged",
+ "timestamp": "2025-11-20T07:14:00Z",
+ "estimated": false,
+ "invalidated_at": null,
+ "index": 4,
+ "original_event": "Vessel Discharged",
+ "created_at": "2026-02-03T04:53:33Z",
+ "data_source": "shipping_line",
+ "data_provider_name": "Hapag-Lloyd",
+ "data_provider_code": "HLCU",
+ "voyage_number": "545E",
+ "location_name": "Shanghai",
+ "location_locode": "CNSHG",
+ "vessel_name": "GUNDE MAERSK",
+ "vessel_imo": "9359014",
+ "timezone": "Asia/Shanghai"
+ },
+ "relationships": {
+ "location": {
+ "data": {
+ "id": "port-3",
+ "type": "port"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-2",
+ "type": "vessel"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "raw_event-6",
+ "type": "raw_event",
+ "attributes": {
+ "actual_on": null,
+ "estimated_at": null,
+ "actual_at": "2025-11-24T15:57:00Z",
+ "will_occur_at": null,
+ "event": "transshipment_loaded",
+ "timestamp": "2025-11-24T15:57:00Z",
+ "estimated": false,
+ "invalidated_at": null,
+ "index": 5,
+ "original_event": "Vessel Loaded",
+ "created_at": "2026-02-03T04:53:33Z",
+ "data_source": "shipping_line",
+ "data_provider_name": "Hapag-Lloyd",
+ "data_provider_code": "HLCU",
+ "voyage_number": "546E",
+ "location_name": "Shanghai",
+ "location_locode": "CNSHG",
+ "vessel_name": "MAERSK SHIVLING",
+ "vessel_imo": "9728253",
+ "timezone": "Asia/Shanghai"
+ },
+ "relationships": {
+ "location": {
+ "data": {
+ "id": "port-3",
+ "type": "port"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-1",
+ "type": "vessel"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "raw_event-7",
+ "type": "raw_event",
+ "attributes": {
+ "actual_on": null,
+ "estimated_at": null,
+ "actual_at": "2025-11-25T00:30:00Z",
+ "will_occur_at": null,
+ "event": "transshipment_departed",
+ "timestamp": "2025-11-25T00:30:00Z",
+ "estimated": false,
+ "invalidated_at": null,
+ "index": 6,
+ "original_event": "Vessel departed",
+ "created_at": "2026-02-03T04:53:33Z",
+ "data_source": "shipping_line",
+ "data_provider_name": "Hapag-Lloyd",
+ "data_provider_code": "HLCU",
+ "voyage_number": "546E",
+ "location_name": "Shanghai",
+ "location_locode": "CNSHG",
+ "vessel_name": "MAERSK SHIVLING",
+ "vessel_imo": "9728253",
+ "timezone": "Asia/Shanghai"
+ },
+ "relationships": {
+ "location": {
+ "data": {
+ "id": "port-3",
+ "type": "port"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-1",
+ "type": "vessel"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "raw_event-8",
+ "type": "raw_event",
+ "attributes": {
+ "actual_on": null,
+ "estimated_at": null,
+ "actual_at": "2025-12-29T10:27:00Z",
+ "will_occur_at": null,
+ "event": "vessel_arrived",
+ "timestamp": "2025-12-29T10:27:00Z",
+ "estimated": false,
+ "invalidated_at": null,
+ "index": 7,
+ "original_event": "Vessel arrived",
+ "created_at": "2026-02-03T04:53:33Z",
+ "data_source": "shipping_line",
+ "data_provider_name": "Hapag-Lloyd",
+ "data_provider_code": "HLCU",
+ "voyage_number": "546E",
+ "location_name": "Charleston",
+ "location_locode": "USCHS",
+ "vessel_name": "MAERSK SHIVLING",
+ "vessel_imo": "9728253",
+ "timezone": "America/New_York"
+ },
+ "relationships": {
+ "location": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-1",
+ "type": "vessel"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "raw_event-9",
+ "type": "raw_event",
+ "attributes": {
+ "actual_on": null,
+ "estimated_at": null,
+ "actual_at": "2025-12-29T14:24:00Z",
+ "will_occur_at": null,
+ "event": "vessel_discharged",
+ "timestamp": "2025-12-29T14:24:00Z",
+ "estimated": false,
+ "invalidated_at": null,
+ "index": 8,
+ "original_event": "Vessel Discharged",
+ "created_at": "2026-02-03T04:53:33Z",
+ "data_source": "shipping_line",
+ "data_provider_name": "Hapag-Lloyd",
+ "data_provider_code": "HLCU",
+ "voyage_number": "546E",
+ "location_name": "Charleston",
+ "location_locode": "USCHS",
+ "vessel_name": "MAERSK SHIVLING",
+ "vessel_imo": "9728253",
+ "timezone": "America/New_York"
+ },
+ "relationships": {
+ "location": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ },
+ "vessel": {
+ "data": {
+ "id": "vessel-1",
+ "type": "vessel"
+ }
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ },
+ {
+ "id": "raw_event-10",
+ "type": "raw_event",
+ "attributes": {
+ "actual_on": null,
+ "estimated_at": null,
+ "actual_at": "2026-01-06T21:25:00Z",
+ "will_occur_at": null,
+ "event": "full_out",
+ "timestamp": "2026-01-06T21:25:00Z",
+ "estimated": false,
+ "invalidated_at": null,
+ "index": 9,
+ "original_event": "Truck Departure from",
+ "created_at": "2026-02-03T04:53:33Z",
+ "data_source": "shipping_line",
+ "data_provider_name": "Hapag-Lloyd",
+ "data_provider_code": "HLCU",
+ "voyage_number": "",
+ "location_name": "Charleston",
+ "location_locode": "USCHS",
+ "vessel_name": null,
+ "vessel_imo": null,
+ "timezone": "America/New_York"
+ },
+ "relationships": {
+ "location": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ },
+ "vessel": {
+ "data": null
+ },
+ "container": {
+ "data": {
+ "id": "container-1",
+ "type": "container"
+ }
+ },
+ "terminal": {
+ "data": null
+ }
+ }
+ }
+ ],
+ "included": [
+ {
+ "id": "port-1",
+ "type": "port",
+ "attributes": {
+ "id": "f11d479d-5501-4b24-8703-4133f0b202b7",
+ "name": "Busan",
+ "code": "KRPUS",
+ "state_abbr": "26",
+ "city": "Busan",
+ "country_code": "KR",
+ "latitude": "35.10162",
+ "longitude": "129.036",
+ "time_zone": "Asia/Seoul"
+ },
+ "relationships": {
+ "terminals": {
+ "data": [
+ {
+ "id": "terminal-9",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-8",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-7",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-6",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-5",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-4",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-3",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-2",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-10",
+ "type": "terminal"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "id": "vessel-2",
+ "type": "vessel",
+ "attributes": {
+ "name": "GUNDE MAERSK",
+ "imo": "9359014",
+ "mmsi": "477893700",
+ "latitude": 34.485483333,
+ "longitude": -121.061733333,
+ "nautical_speed_knots": 9,
+ "navigational_heading_degrees": 284,
+ "position_timestamp": "2026-02-04T05:01:22Z"
+ }
+ },
+ {
+ "id": "port-3",
+ "type": "port",
+ "attributes": {
+ "id": "c8db7333-7912-4092-a370-9823965d1394",
+ "name": "Shanghai",
+ "code": "CNSHG",
+ "state_abbr": "SH",
+ "city": "Shanghai",
+ "country_code": "CN",
+ "latitude": "31.36636",
+ "longitude": "121.6147",
+ "time_zone": "Asia/Shanghai"
+ },
+ "relationships": {
+ "terminals": {
+ "data": [
+ {
+ "id": "terminal-16",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-17",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-18",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-19",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-20",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-21",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-22",
+ "type": "terminal"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "id": "vessel-1",
+ "type": "vessel",
+ "attributes": {
+ "name": "MAERSK SHIVLING",
+ "imo": "9728253",
+ "mmsi": "636017104",
+ "latitude": -5.226666667,
+ "longitude": 80.95,
+ "nautical_speed_knots": 16,
+ "navigational_heading_degrees": 51,
+ "position_timestamp": "2026-02-04T04:40:43Z"
+ }
+ },
+ {
+ "id": "port-2",
+ "type": "port",
+ "attributes": {
+ "id": "d8138df6-f542-4f69-a3e3-e5fd6b16b63a",
+ "name": "Charleston",
+ "code": "USCHS",
+ "state_abbr": "SC",
+ "city": "Charleston",
+ "country_code": "US",
+ "latitude": "32.831492181",
+ "longitude": "-79.89124957",
+ "time_zone": "America/New_York"
+ },
+ "relationships": {
+ "terminals": {
+ "data": [
+ {
+ "id": "terminal-11",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-12",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-13",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-1",
+ "type": "terminal"
+ }
+ ]
+ }
+ }
+ }
+ ]
+}
diff --git a/sdks/typescript-sdk/src/fixtures/ports.get.json b/sdks/typescript-sdk/src/fixtures/ports.get.json
new file mode 100644
index 00000000..04ce490f
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/ports.get.json
@@ -0,0 +1,62 @@
+{
+ "data": {
+ "id": "port-1",
+ "type": "port",
+ "attributes": {
+ "id": "f11d479d-5501-4b24-8703-4133f0b202b7",
+ "name": "Busan",
+ "code": "KRPUS",
+ "state_abbr": "26",
+ "city": "Busan",
+ "country_code": "KR",
+ "latitude": "35.10162",
+ "longitude": "129.036",
+ "time_zone": "Asia/Seoul"
+ },
+ "relationships": {
+ "terminals": {
+ "data": [
+ {
+ "id": "terminal-9",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-8",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-7",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-6",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-5",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-4",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-3",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-2",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-10",
+ "type": "terminal"
+ }
+ ]
+ }
+ }
+ },
+ "links": {
+ "self": "https://api.terminal49.com/v2/ports/f11d479d-5501-4b24-8703-4133f0b202b7"
+ }
+}
diff --git a/sdks/typescript-sdk/src/fixtures/shipments.get.base.json b/sdks/typescript-sdk/src/fixtures/shipments.get.base.json
new file mode 100644
index 00000000..3e90de7d
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/shipments.get.base.json
@@ -0,0 +1,85 @@
+{
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment",
+ "attributes": {
+ "created_at": "2026-02-03T04:53:33Z",
+ "ref_numbers": [],
+ "tags": [],
+ "bill_of_lading_number": "HLCUSEL251063ad5",
+ "normalized_number": "HLCUSEL251063257",
+ "shipping_line_scac": "HLCU",
+ "shipping_line_name": "Hapag-Lloyd",
+ "shipping_line_short_name": "Hapag-Lloyd",
+ "customer_name": "CUSTOMER-001",
+ "port_of_lading_locode": "KRPUS",
+ "port_of_lading_name": "Busan",
+ "port_of_discharge_locode": "USCHS",
+ "port_of_discharge_name": "Charleston",
+ "pod_vessel_name": "MAERSK SHIVLING",
+ "pod_vessel_imo": "9728253",
+ "pod_voyage_number": "546E",
+ "destination_locode": null,
+ "destination_name": null,
+ "destination_timezone": null,
+ "destination_ata_at": null,
+ "destination_eta_at": null,
+ "pol_etd_at": null,
+ "pol_atd_at": "2025-11-18T15:26:00Z",
+ "pol_timezone": "Asia/Seoul",
+ "pod_eta_at": null,
+ "pod_original_eta_at": null,
+ "pol_original_etd_at": null,
+ "destination_original_eta_at": null,
+ "pod_ata_at": "2025-12-29T09:28:23Z",
+ "pod_timezone": "America/New_York",
+ "line_tracking_last_attempted_at": "2026-02-03T18:02:03Z",
+ "line_tracking_last_succeeded_at": "2026-02-03T18:02:04Z",
+ "line_tracking_stopped_at": null,
+ "line_tracking_stopped_reason": null
+ },
+ "links": {
+ "self": "/v2/shipments/a99e23fe-84ca-4dac-ab15-c4996c7fe3d6"
+ },
+ "relationships": {
+ "port_of_lading": {
+ "data": {
+ "id": "port-1",
+ "type": "port"
+ }
+ },
+ "port_of_discharge": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ },
+ "pod_terminal": {
+ "data": {
+ "id": "terminal-1",
+ "type": "terminal"
+ }
+ },
+ "destination": {
+ "data": null
+ },
+ "destination_terminal": {
+ "data": null
+ },
+ "line_tracking_stopped_by_user": {
+ "data": null
+ },
+ "containers": {
+ "data": [
+ {
+ "id": "container-1",
+ "type": "container"
+ }
+ ]
+ }
+ }
+ },
+ "links": {
+ "self": "https://api.terminal49.com/v2/shipments/a99e23fe-84ca-4dac-ab15-c4996c7fe3d6"
+ }
+}
diff --git a/sdks/typescript-sdk/src/fixtures/shipments.get.include.json b/sdks/typescript-sdk/src/fixtures/shipments.get.include.json
new file mode 100644
index 00000000..0b578d3f
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/shipments.get.include.json
@@ -0,0 +1,390 @@
+{
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment",
+ "attributes": {
+ "created_at": "2026-02-03T04:53:33Z",
+ "ref_numbers": [],
+ "tags": [],
+ "bill_of_lading_number": "HLCUSEL251063ad5",
+ "normalized_number": "HLCUSEL251063257",
+ "shipping_line_scac": "HLCU",
+ "shipping_line_name": "Hapag-Lloyd",
+ "shipping_line_short_name": "Hapag-Lloyd",
+ "customer_name": "CUSTOMER-001",
+ "port_of_lading_locode": "KRPUS",
+ "port_of_lading_name": "Busan",
+ "port_of_discharge_locode": "USCHS",
+ "port_of_discharge_name": "Charleston",
+ "pod_vessel_name": "MAERSK SHIVLING",
+ "pod_vessel_imo": "9728253",
+ "pod_voyage_number": "546E",
+ "destination_locode": null,
+ "destination_name": null,
+ "destination_timezone": null,
+ "destination_ata_at": null,
+ "destination_eta_at": null,
+ "pol_etd_at": null,
+ "pol_atd_at": "2025-11-18T15:26:00Z",
+ "pol_timezone": "Asia/Seoul",
+ "pod_eta_at": null,
+ "pod_original_eta_at": null,
+ "pol_original_etd_at": null,
+ "destination_original_eta_at": null,
+ "pod_ata_at": "2025-12-29T09:28:23Z",
+ "pod_timezone": "America/New_York",
+ "line_tracking_last_attempted_at": "2026-02-03T18:02:03Z",
+ "line_tracking_last_succeeded_at": "2026-02-03T18:02:04Z",
+ "line_tracking_stopped_at": null,
+ "line_tracking_stopped_reason": null
+ },
+ "links": {
+ "self": "/v2/shipments/a99e23fe-84ca-4dac-ab15-c4996c7fe3d6"
+ },
+ "relationships": {
+ "port_of_lading": {
+ "data": {
+ "id": "port-1",
+ "type": "port"
+ }
+ },
+ "port_of_discharge": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ },
+ "pod_terminal": {
+ "data": {
+ "id": "terminal-1",
+ "type": "terminal"
+ }
+ },
+ "destination": {
+ "data": null
+ },
+ "destination_terminal": {
+ "data": null
+ },
+ "line_tracking_stopped_by_user": {
+ "data": null
+ },
+ "containers": {
+ "data": [
+ {
+ "id": "container-1",
+ "type": "container"
+ }
+ ]
+ }
+ }
+ },
+ "links": {
+ "self": "https://api.terminal49.com/v2/shipments/a99e23fe-84ca-4dac-ab15-c4996c7fe3d6?include=containers,pod_terminal,port_of_lading,port_of_discharge,destination,destination_terminal"
+ },
+ "included": [
+ {
+ "id": "container-1",
+ "type": "container",
+ "attributes": {
+ "number": "RMCU74210c6",
+ "seal_number": null,
+ "created_at": "2026-02-03T04:53:33Z",
+ "ref_numbers": [],
+ "pod_arrived_at": "2025-12-29T09:28:23Z",
+ "pod_discharged_at": "2025-12-29T14:24:00Z",
+ "final_destination_full_out_at": null,
+ "holds_at_pod_terminal": [],
+ "available_for_pickup": false,
+ "delivered_at": null,
+ "current_status": "picked_up",
+ "empty_out_at": null,
+ "pol_full_in_at": "2025-11-12T03:23:00Z",
+ "pol_vessel_loaded_at": "2025-11-18T13:57:00Z",
+ "pol_vessel_departed_at": "2025-11-18T15:26:00Z",
+ "equipment_type": "tank",
+ "equipment_length": 20,
+ "equipment_height": "standard",
+ "pod_full_out_at": "2026-01-06T21:25:00Z",
+ "empty_terminated_at": null,
+ "terminal_checked_at": null,
+ "fees_at_pod_terminal": [],
+ "pickup_lfd": null,
+ "pickup_appointment_at": null,
+ "pod_full_out_chassis_number": null,
+ "location_at_pod_terminal": null,
+ "pod_last_tracking_request_at": null,
+ "shipment_last_tracking_request_at": "2026-02-03T18:02:04Z",
+ "rail_last_tracking_request_at": null,
+ "availability_known": false,
+ "pod_timezone": "America/New_York",
+ "final_destination_timezone": null,
+ "weight_in_lbs": 0,
+ "empty_terminated_timezone": "America/New_York",
+ "pod_rail_carrier_scac": null,
+ "ind_rail_carrier_scac": null,
+ "pod_rail_loaded_at": null,
+ "pod_rail_departed_at": null,
+ "ind_eta_at": null,
+ "ind_ata_at": null,
+ "ind_rail_unloaded_at": null,
+ "ind_facility_holds": null,
+ "ind_facility_fees": null,
+ "ind_facility_lfd_on": null,
+ "import_deadlines": {
+ "pickup_lfd_terminal": null,
+ "pickup_lfd_rail": null,
+ "pickup_lfd_line": null
+ },
+ "ssl_lfd": null
+ },
+ "relationships": {
+ "shipment": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "pickup_facility": {
+ "data": null
+ },
+ "pod_terminal": {
+ "data": {
+ "id": "terminal-1",
+ "type": "terminal"
+ }
+ },
+ "transport_events": {
+ "data": [
+ {
+ "id": "transport_event-1",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-2",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-3",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-4",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-5",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-6",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-7",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-8",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-9",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-10",
+ "type": "transport_event"
+ },
+ {
+ "id": "transport_event-11",
+ "type": "transport_event"
+ }
+ ]
+ },
+ "raw_events": {
+ "data": [
+ {
+ "id": "raw_event-1",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-2",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-3",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-4",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-5",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-6",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-7",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-8",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-9",
+ "type": "raw_event"
+ },
+ {
+ "id": "raw_event-10",
+ "type": "raw_event"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "id": "terminal-1",
+ "type": "terminal",
+ "attributes": {
+ "id": "a59b3bd1-d497-4a48-bfb6-5cf2c4824a60",
+ "nickname": "WWT",
+ "name": "Wando Welch Terminal",
+ "firms_code": "N598",
+ "smdg_code": "WWMT",
+ "bic_facility_code": null,
+ "provided_data": {
+ "pickup_lfd": false,
+ "pod_full_out_at": true,
+ "pickup_lfd_notes": "",
+ "available_for_pickup": true,
+ "fees_at_pod_terminal": false,
+ "holds_at_pod_terminal": true,
+ "pickup_appointment_at": false,
+ "location_at_pod_terminal": false,
+ "available_for_pickup_notes": "",
+ "fees_at_pod_terminal_notes": "",
+ "holds_at_pod_terminal_notes": "",
+ "pickup_appointment_at_notes": "",
+ "pod_full_out_chassis_number": true,
+ "location_at_pod_terminal_notes": "",
+ "pod_full_out_chassis_number_notes": ""
+ },
+ "street": "400 Long Point Rd. ",
+ "city": "Mt. Pleasant",
+ "state": "South Carolina",
+ "state_abbr": "SC",
+ "zip": "29464",
+ "country": "United States",
+ "facility_type": "ocean_terminal"
+ },
+ "relationships": {
+ "port": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ }
+ }
+ },
+ {
+ "id": "port-1",
+ "type": "port",
+ "attributes": {
+ "id": "f11d479d-5501-4b24-8703-4133f0b202b7",
+ "name": "Busan",
+ "code": "KRPUS",
+ "state_abbr": "26",
+ "city": "Busan",
+ "country_code": "KR",
+ "latitude": "35.10162",
+ "longitude": "129.036",
+ "time_zone": "Asia/Seoul"
+ },
+ "relationships": {
+ "terminals": {
+ "data": [
+ {
+ "id": "terminal-2",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-3",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-4",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-5",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-6",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-7",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-8",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-9",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-10",
+ "type": "terminal"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "id": "port-2",
+ "type": "port",
+ "attributes": {
+ "id": "d8138df6-f542-4f69-a3e3-e5fd6b16b63a",
+ "name": "Charleston",
+ "code": "USCHS",
+ "state_abbr": "SC",
+ "city": "Charleston",
+ "country_code": "US",
+ "latitude": "32.831492181",
+ "longitude": "-79.89124957",
+ "time_zone": "America/New_York"
+ },
+ "relationships": {
+ "terminals": {
+ "data": [
+ {
+ "id": "terminal-11",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-12",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-13",
+ "type": "terminal"
+ },
+ {
+ "id": "terminal-1",
+ "type": "terminal"
+ }
+ ]
+ }
+ }
+ }
+ ]
+}
diff --git a/sdks/typescript-sdk/src/fixtures/shipments.list.json b/sdks/typescript-sdk/src/fixtures/shipments.list.json
new file mode 100644
index 00000000..f058acc4
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/shipments.list.json
@@ -0,0 +1,94 @@
+{
+ "data": [
+ {
+ "id": "shipment-1",
+ "type": "shipment",
+ "attributes": {
+ "created_at": "2026-02-03T04:53:33Z",
+ "ref_numbers": [],
+ "tags": [],
+ "bill_of_lading_number": "HLCUSEL251063ad5",
+ "normalized_number": "HLCUSEL251063257",
+ "shipping_line_scac": "HLCU",
+ "shipping_line_name": "Hapag-Lloyd",
+ "shipping_line_short_name": "Hapag-Lloyd",
+ "customer_name": "CUSTOMER-001",
+ "port_of_lading_locode": "KRPUS",
+ "port_of_lading_name": "Busan",
+ "port_of_discharge_locode": "USCHS",
+ "port_of_discharge_name": "Charleston",
+ "pod_vessel_name": "MAERSK SHIVLING",
+ "pod_vessel_imo": "9728253",
+ "pod_voyage_number": "546E",
+ "destination_locode": null,
+ "destination_name": null,
+ "destination_timezone": null,
+ "destination_ata_at": null,
+ "destination_eta_at": null,
+ "pol_etd_at": null,
+ "pol_atd_at": "2025-11-18T15:26:00Z",
+ "pol_timezone": "Asia/Seoul",
+ "pod_eta_at": null,
+ "pod_original_eta_at": null,
+ "pol_original_etd_at": null,
+ "destination_original_eta_at": null,
+ "pod_ata_at": "2025-12-29T09:28:23Z",
+ "pod_timezone": "America/New_York",
+ "line_tracking_last_attempted_at": "2026-02-03T18:02:03Z",
+ "line_tracking_last_succeeded_at": "2026-02-03T18:02:04Z",
+ "line_tracking_stopped_at": null,
+ "line_tracking_stopped_reason": null
+ },
+ "links": {
+ "self": "/v2/shipments/a99e23fe-84ca-4dac-ab15-c4996c7fe3d6"
+ },
+ "relationships": {
+ "port_of_lading": {
+ "data": {
+ "id": "port-1",
+ "type": "port"
+ }
+ },
+ "port_of_discharge": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ },
+ "pod_terminal": {
+ "data": {
+ "id": "terminal-1",
+ "type": "terminal"
+ }
+ },
+ "destination": {
+ "data": null
+ },
+ "destination_terminal": {
+ "data": null
+ },
+ "line_tracking_stopped_by_user": {
+ "data": null
+ },
+ "containers": {
+ "data": [
+ {
+ "id": "container-1",
+ "type": "container"
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "meta": {
+ "total": 170470,
+ "size": 1
+ },
+ "links": {
+ "self": "https://api.terminal49.com/v2/shipments?page[size]=1",
+ "current": "https://api.terminal49.com/v2/shipments?page[number]=1&page[size]=1",
+ "next": "https://api.terminal49.com/v2/shipments?page[number]=2&page[size]=1",
+ "last": "https://api.terminal49.com/v2/shipments?page[number]=170470&page[size]=1"
+ }
+}
diff --git a/sdks/typescript-sdk/src/fixtures/shipping-lines.list.json b/sdks/typescript-sdk/src/fixtures/shipping-lines.list.json
new file mode 100644
index 00000000..79c0f85c
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/shipping-lines.list.json
@@ -0,0 +1,1454 @@
+{
+ "data": [
+ {
+ "id": "shipping_line-1",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "ACLU",
+ "name": "Atlantic Container Line",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://www.aclcargo.com/trackCargo.php?search_for=${BLNUM}",
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "ACL",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-2",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "ANNU",
+ "name": "Australia National Line",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://www.anl.com.au/ebusiness/tracking/search?SearchBy=BL&Reference=${BLNUM}",
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "ANL",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-3",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "APLU",
+ "name": "American President Lines",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://www.apl.com/ebusiness/tracking/search?SearchBy=BL&Reference=${BLNUM}",
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "APL",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-4",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "ARKU",
+ "name": "Arkas Container Transport",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": false,
+ "pod_arrived_at": false,
+ "pod_full_out_at": false,
+ "equipment_height": false,
+ "equipment_length": false,
+ "pod_discharged_at": false,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": "http://www.arkasline.com.tr/en/contact_us.html",
+ "alternative_scacs": [],
+ "short_name": "Arkas",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-5",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "CHVW",
+ "name": "Swire Shipping",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": ["QWJA", "PLLU"],
+ "short_name": "CHVW",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": false
+ }
+ },
+ {
+ "id": "shipping_line-6",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "CLAM",
+ "name": "Crowley",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": false,
+ "pod_arrived_at": false,
+ "pod_full_out_at": false,
+ "equipment_height": true,
+ "equipment_length": false,
+ "pod_discharged_at": false,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": ["CWLQ"],
+ "short_name": "Crowley",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": false
+ }
+ },
+ {
+ "id": "shipping_line-7",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "CMDU",
+ "name": "CMA CGM",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://www.cma-cgm.com/ebusiness/tracking/search?SearchBy=BL&Reference=${BLNUM}&search=Search",
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "CMA CGM",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-8",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "COSU",
+ "name": "COSCO",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": true,
+ "weight_in_lbs": true,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "http://elines.coscoshipping.com/ebusiness/cargoTracking?trackingType=BILLOFLADING&number=${BLNUM}",
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "COSCO",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-9",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "CULU",
+ "name": "China United Lines",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": true,
+ "weight_in_lbs": true,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "CULines",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-10",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "EGLV",
+ "name": "Evergreen",
+ "provided_data": {
+ "ssl_lfd": true,
+ "seal_number": true,
+ "weight_in_lbs": true,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://www.shipmentlink.com/servlet/TDB1_CargoTracking.do",
+ "contact_url": "https://www.evergreen-line.com",
+ "alternative_scacs": [],
+ "short_name": "Evergreen",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-11",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "GOSU",
+ "name": "Gold Star Line",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "GSL",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": false
+ }
+ },
+ {
+ "id": "shipping_line-12",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "HDMU",
+ "name": "Hyundai Merchant Marine",
+ "provided_data": {
+ "ssl_lfd": true,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "Hyundai",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-13",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "HDUJ",
+ "name": "HEDE SHipping",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": false,
+ "pod_arrived_at": false,
+ "pod_full_out_at": false,
+ "equipment_height": false,
+ "equipment_length": false,
+ "pod_discharged_at": false,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": "http://elines.hedehk.com/cargoDynamicEN",
+ "alternative_scacs": [],
+ "short_name": "HEDE",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-14",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "HLCU",
+ "name": "Hapag-Lloyd",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://www.hapag-lloyd.com/en/online-business/tracing/tracing-by-booking.html?blno=${BLNUM}",
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "Hapag-Lloyd",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-15",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "IILU",
+ "name": "Independent Container Line",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": false,
+ "pod_arrived_at": false,
+ "pod_full_out_at": false,
+ "equipment_height": false,
+ "equipment_length": false,
+ "pod_discharged_at": false,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "ICL",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": false
+ }
+ },
+ {
+ "id": "shipping_line-16",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "MAEU",
+ "name": "Maersk",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": true,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://www.maersk.com/tracking/#tracking?${BLNUM}",
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "Maersk",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-17",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "MATS",
+ "name": "Matson Navigation Company",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": true,
+ "weight_in_lbs": true,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://www.matson.com/shipment-tracking.html",
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "Matson",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": false
+ }
+ },
+ {
+ "id": "shipping_line-18",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "MSCU",
+ "name": "Mediterranean Shipping Company",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://www.msc.com/track-a-shipment",
+ "contact_url": null,
+ "alternative_scacs": ["MEDU"],
+ "short_name": "MSC",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-19",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "ONEY",
+ "name": "Ocean Network Express",
+ "provided_data": {
+ "ssl_lfd": true,
+ "seal_number": true,
+ "weight_in_lbs": true,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://ecomm.one-line.com/ecom/CUP_HOM_3301.do",
+ "contact_url": "https://us.one-line.com/standard-page/customer-service-contacts",
+ "alternative_scacs": [],
+ "short_name": "ONE",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-20",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "OOLU",
+ "name": "Orient Overseas Container Line",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": true,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://www.oocl.com/eng/ourservices/eservices/cargotracking/Pages/cargotracking.aspx",
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "OOCL",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-21",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "PCIU",
+ "name": "Pacific International Lines",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": false,
+ "pod_arrived_at": false,
+ "pod_full_out_at": false,
+ "equipment_height": false,
+ "equipment_length": false,
+ "pod_discharged_at": false,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://www.pilship.com/--/120.html?refnumbers=${BLNUM}&search_type=bl",
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "PIL",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-22",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "SAFM",
+ "name": "Safmarine Container Line",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": true,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "Safmarine Container Line",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": false
+ }
+ },
+ {
+ "id": "shipping_line-23",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "SEAU",
+ "name": "Sealand Americas",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": true,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://my.sealand.com/tracking/#tracking/${BLNUM}",
+ "contact_url": "https://www.sealandmaersk.com/contact",
+ "alternative_scacs": ["SEAL"],
+ "short_name": "SeaLand Americas",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-24",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "SEJJ",
+ "name": "SeaLand Europe",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": true,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://my.sealand.com/tracking/#tracking/${BLNUM}",
+ "contact_url": "https://www.sealandmaersk.com/contact",
+ "alternative_scacs": [],
+ "short_name": "SeaLand Europe",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-25",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "SMLM",
+ "name": "SM Line",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": true,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://esvc.smlines.com/smline/CUP_HOM_3301.do",
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "SM Line",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-26",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "SMLU",
+ "name": "Seaboard Marine",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": false,
+ "pod_arrived_at": false,
+ "pod_full_out_at": false,
+ "equipment_height": false,
+ "equipment_length": false,
+ "pod_discharged_at": false,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "Seaboard Marine",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-27",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "SQQY",
+ "name": "SeaLead",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": true,
+ "weight_in_lbs": true,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": ["SJHH"],
+ "short_name": "SeaLead",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-28",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "SSBF",
+ "name": "Swire",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": true,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": ["WWSU"],
+ "short_name": "Swire",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": false
+ }
+ },
+ {
+ "id": "shipping_line-29",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "SSPH",
+ "name": "Seth Shipping",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "Seth",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": false
+ }
+ },
+ {
+ "id": "shipping_line-30",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "TEST",
+ "name": "T49 Test Carrier",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": false,
+ "pod_arrived_at": false,
+ "pod_full_out_at": false,
+ "equipment_height": false,
+ "equipment_length": false,
+ "pod_discharged_at": false,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "T49 line",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": false
+ }
+ },
+ {
+ "id": "shipping_line-31",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "TJFH",
+ "name": "Transfar",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "Transfar",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": false
+ }
+ },
+ {
+ "id": "shipping_line-32",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "TRBR",
+ "name": "Trailer Bridge",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": false,
+ "pod_arrived_at": false,
+ "pod_full_out_at": false,
+ "equipment_height": false,
+ "equipment_length": false,
+ "pod_discharged_at": false,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": ["TBIE", "PYRR"],
+ "short_name": "Trailer Bridge",
+ "bill_of_lading_tracking_support": false,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": false
+ }
+ },
+ {
+ "id": "shipping_line-33",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "TRKU",
+ "name": "Turkon Line",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "Turkon",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-34",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "TXZJ",
+ "name": "T.S. Lines",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": false,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": false,
+ "equipment_length": false,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "TS Lines",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-35",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "WDSB",
+ "name": "World Direct Shipping",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": false,
+ "pod_arrived_at": false,
+ "pod_full_out_at": false,
+ "equipment_height": false,
+ "equipment_length": false,
+ "pod_discharged_at": false,
+ "seal_number_notes": "",
+ "empty_terminated_at": false,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "WDS",
+ "bill_of_lading_tracking_support": false,
+ "booking_number_tracking_support": false,
+ "container_number_tracking_support": false
+ }
+ },
+ {
+ "id": "shipping_line-36",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "WHLC",
+ "name": "Wan Hai Lines",
+ "provided_data": {
+ "ssl_lfd": false,
+ "seal_number": true,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": false,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "Wan Hai Lines",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-37",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "YMLU",
+ "name": "Yangming Marine Transport",
+ "provided_data": {
+ "ssl_lfd": true,
+ "seal_number": true,
+ "weight_in_lbs": true,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": "https://www.yangming.com/e-service/Track_Trace/blconnect.aspx?BLADG=${BLNUM}&rdolType=BL",
+ "contact_url": null,
+ "alternative_scacs": ["YMPR", "YMSG", "YMJA"],
+ "short_name": "Yangming",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ },
+ {
+ "id": "shipping_line-38",
+ "type": "shipping_line",
+ "attributes": {
+ "scac": "ZIMU",
+ "name": "Zim American Integrated Shipping Services",
+ "provided_data": {
+ "ssl_lfd": true,
+ "seal_number": false,
+ "weight_in_lbs": false,
+ "equipment_type": true,
+ "pod_arrived_at": true,
+ "pod_full_out_at": true,
+ "equipment_height": true,
+ "equipment_length": true,
+ "pod_discharged_at": true,
+ "seal_number_notes": "",
+ "empty_terminated_at": true,
+ "weight_in_lbs_notes": "",
+ "equipment_type_notes": "",
+ "pod_arrived_at_notes": "",
+ "pod_full_out_at_notes": "",
+ "equipment_height_notes": "",
+ "equipment_length_notes": "",
+ "pod_discharged_at_notes": "",
+ "empty_terminated_at_notes": "",
+ "final_destination_full_out_at": true,
+ "final_destination_full_out_at_notes": ""
+ },
+ "tracking_url": null,
+ "contact_url": null,
+ "alternative_scacs": [],
+ "short_name": "Zim Line",
+ "bill_of_lading_tracking_support": true,
+ "booking_number_tracking_support": true,
+ "container_number_tracking_support": true
+ }
+ }
+ ],
+ "links": {
+ "self": "https://api.terminal49.com/v2/shipping_lines",
+ "current": "https://api.terminal49.com/v2/shipping_lines?page[number]=1",
+ "next": "https://api.terminal49.com/v2/shipping_lines?page[number]=2",
+ "last": "https://api.terminal49.com/v2/shipping_lines?page[number]=2"
+ }
+}
diff --git a/sdks/typescript-sdk/src/fixtures/terminals.get.json b/sdks/typescript-sdk/src/fixtures/terminals.get.json
new file mode 100644
index 00000000..48656fb9
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/terminals.get.json
@@ -0,0 +1,49 @@
+{
+ "data": {
+ "id": "terminal-1",
+ "type": "terminal",
+ "attributes": {
+ "id": "a59b3bd1-d497-4a48-bfb6-5cf2c4824a60",
+ "nickname": "WWT",
+ "name": "Wando Welch Terminal",
+ "firms_code": "N598",
+ "smdg_code": "WWMT",
+ "bic_facility_code": null,
+ "provided_data": {
+ "pickup_lfd": false,
+ "pod_full_out_at": true,
+ "pickup_lfd_notes": "",
+ "available_for_pickup": true,
+ "fees_at_pod_terminal": false,
+ "holds_at_pod_terminal": true,
+ "pickup_appointment_at": false,
+ "location_at_pod_terminal": false,
+ "available_for_pickup_notes": "",
+ "fees_at_pod_terminal_notes": "",
+ "holds_at_pod_terminal_notes": "",
+ "pickup_appointment_at_notes": "",
+ "pod_full_out_chassis_number": true,
+ "location_at_pod_terminal_notes": "",
+ "pod_full_out_chassis_number_notes": ""
+ },
+ "street": "400 Long Point Rd. ",
+ "city": "Mt. Pleasant",
+ "state": "South Carolina",
+ "state_abbr": "SC",
+ "zip": "29464",
+ "country": "United States",
+ "facility_type": "ocean_terminal"
+ },
+ "relationships": {
+ "port": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ }
+ }
+ },
+ "links": {
+ "self": "https://api.terminal49.com/v2/terminals/a59b3bd1-d497-4a48-bfb6-5cf2c4824a60"
+ }
+}
diff --git a/sdks/typescript-sdk/src/fixtures/tracking-requests.get.base.json b/sdks/typescript-sdk/src/fixtures/tracking-requests.get.base.json
new file mode 100644
index 00000000..599700dd
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/tracking-requests.get.base.json
@@ -0,0 +1,124 @@
+{
+ "data": {
+ "id": "tracking_request-1",
+ "type": "tracking_request",
+ "attributes": {
+ "request_number": "HLCUSEL251063157",
+ "request_type": "bill_of_lading",
+ "scac": "HLCU",
+ "ref_numbers": [],
+ "shipment_tags": [],
+ "created_at": "2026-02-03T04:53:06Z",
+ "updated_at": "2026-02-03T04:53:33Z",
+ "status": "created",
+ "failed_reason": null,
+ "is_retrying": false,
+ "retry_count": null
+ },
+ "relationships": {
+ "tracked_object": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "customer": {
+ "data": null
+ },
+ "user": {
+ "data": {
+ "id": "user-1",
+ "type": "user"
+ }
+ }
+ },
+ "links": {
+ "self": "/v2/tracking_requests/e75b0877-b92a-4e22-a563-e7c46b74d52c"
+ }
+ },
+ "links": {
+ "self": "https://api.terminal49.com/v2/tracking_requests/e75b0877-b92a-4e22-a563-e7c46b74d52c"
+ },
+ "included": [
+ {
+ "id": "shipment-1",
+ "type": "shipment",
+ "attributes": {
+ "created_at": "2026-02-03T04:53:33Z",
+ "ref_numbers": [],
+ "tags": [],
+ "bill_of_lading_number": "HLCUSEL251063ad5",
+ "normalized_number": "HLCUSEL251063257",
+ "shipping_line_scac": "HLCU",
+ "shipping_line_name": "Hapag-Lloyd",
+ "shipping_line_short_name": "Hapag-Lloyd",
+ "customer_name": "CUSTOMER-001",
+ "port_of_lading_locode": "KRPUS",
+ "port_of_lading_name": "Busan",
+ "port_of_discharge_locode": "USCHS",
+ "port_of_discharge_name": "Charleston",
+ "pod_vessel_name": "MAERSK SHIVLING",
+ "pod_vessel_imo": "9728253",
+ "pod_voyage_number": "546E",
+ "destination_locode": null,
+ "destination_name": null,
+ "destination_timezone": null,
+ "destination_ata_at": null,
+ "destination_eta_at": null,
+ "pol_etd_at": null,
+ "pol_atd_at": "2025-11-18T15:26:00Z",
+ "pol_timezone": "Asia/Seoul",
+ "pod_eta_at": null,
+ "pod_original_eta_at": null,
+ "pol_original_etd_at": null,
+ "destination_original_eta_at": null,
+ "pod_ata_at": "2025-12-29T09:28:23Z",
+ "pod_timezone": "America/New_York",
+ "line_tracking_last_attempted_at": "2026-02-03T18:02:03Z",
+ "line_tracking_last_succeeded_at": "2026-02-03T18:02:04Z",
+ "line_tracking_stopped_at": null,
+ "line_tracking_stopped_reason": null
+ },
+ "links": {
+ "self": "/v2/shipments/a99e23fe-84ca-4dac-ab15-c4996c7fe3d6"
+ },
+ "relationships": {
+ "port_of_lading": {
+ "data": {
+ "id": "port-1",
+ "type": "port"
+ }
+ },
+ "port_of_discharge": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ },
+ "pod_terminal": {
+ "data": {
+ "id": "terminal-1",
+ "type": "terminal"
+ }
+ },
+ "destination": {
+ "data": null
+ },
+ "destination_terminal": {
+ "data": null
+ },
+ "line_tracking_stopped_by_user": {
+ "data": null
+ },
+ "containers": {
+ "data": [
+ {
+ "id": "container-1",
+ "type": "container"
+ }
+ ]
+ }
+ }
+ }
+ ]
+}
diff --git a/sdks/typescript-sdk/src/fixtures/tracking-requests.list.json b/sdks/typescript-sdk/src/fixtures/tracking-requests.list.json
new file mode 100644
index 00000000..7bdccfca
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/tracking-requests.list.json
@@ -0,0 +1,139 @@
+{
+ "data": [
+ {
+ "id": "tracking_request-1",
+ "type": "tracking_request",
+ "attributes": {
+ "request_number": "HLCUSEL251063157",
+ "request_type": "bill_of_lading",
+ "scac": "HLCU",
+ "ref_numbers": [],
+ "shipment_tags": [],
+ "created_at": "2026-02-03T04:53:06Z",
+ "updated_at": "2026-02-03T04:53:33Z",
+ "status": "created",
+ "failed_reason": null,
+ "is_retrying": false,
+ "retry_count": null
+ },
+ "relationships": {
+ "tracked_object": {
+ "data": {
+ "id": "shipment-1",
+ "type": "shipment"
+ }
+ },
+ "customer": {
+ "data": null
+ },
+ "user": {
+ "data": {
+ "id": "user-1",
+ "type": "user"
+ }
+ }
+ },
+ "links": {
+ "self": "/v2/tracking_requests/e75b0877-b92a-4e22-a563-e7c46b74d52c"
+ }
+ }
+ ],
+ "included": [
+ {
+ "id": "shipment-1",
+ "type": "shipment",
+ "attributes": {
+ "created_at": "2026-02-03T04:53:33Z",
+ "ref_numbers": [],
+ "tags": [],
+ "bill_of_lading_number": "HLCUSEL251063ad5",
+ "normalized_number": "HLCUSEL251063257",
+ "shipping_line_scac": "HLCU",
+ "shipping_line_name": "Hapag-Lloyd",
+ "shipping_line_short_name": "Hapag-Lloyd",
+ "customer_name": "CUSTOMER-001",
+ "port_of_lading_locode": "KRPUS",
+ "port_of_lading_name": "Busan",
+ "port_of_discharge_locode": "USCHS",
+ "port_of_discharge_name": "Charleston",
+ "pod_vessel_name": "MAERSK SHIVLING",
+ "pod_vessel_imo": "9728253",
+ "pod_voyage_number": "546E",
+ "destination_locode": null,
+ "destination_name": null,
+ "destination_timezone": null,
+ "destination_ata_at": null,
+ "destination_eta_at": null,
+ "pol_etd_at": null,
+ "pol_atd_at": "2025-11-18T15:26:00Z",
+ "pol_timezone": "Asia/Seoul",
+ "pod_eta_at": null,
+ "pod_original_eta_at": null,
+ "pol_original_etd_at": null,
+ "destination_original_eta_at": null,
+ "pod_ata_at": "2025-12-29T09:28:23Z",
+ "pod_timezone": "America/New_York",
+ "line_tracking_last_attempted_at": "2026-02-03T18:02:03Z",
+ "line_tracking_last_succeeded_at": "2026-02-03T18:02:04Z",
+ "line_tracking_stopped_at": null,
+ "line_tracking_stopped_reason": null
+ },
+ "links": {
+ "self": "/v2/shipments/a99e23fe-84ca-4dac-ab15-c4996c7fe3d6"
+ },
+ "relationships": {
+ "port_of_lading": {
+ "data": {
+ "id": "port-1",
+ "type": "port"
+ }
+ },
+ "port_of_discharge": {
+ "data": {
+ "id": "port-2",
+ "type": "port"
+ }
+ },
+ "pod_terminal": {
+ "data": {
+ "id": "terminal-1",
+ "type": "terminal"
+ }
+ },
+ "destination": {
+ "data": null
+ },
+ "destination_terminal": {
+ "data": null
+ },
+ "line_tracking_stopped_by_user": {
+ "data": null
+ },
+ "containers": {
+ "data": [
+ {
+ "id": "container-1",
+ "type": "container"
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "meta": {
+ "size": 1,
+ "total": 180774,
+ "pagination": {
+ "current": 1,
+ "next": 2,
+ "last": 180774,
+ "records": 180774
+ }
+ },
+ "links": {
+ "self": "https://api.terminal49.com/v2/tracking_requests?page[size]=1",
+ "current": "https://api.terminal49.com/v2/tracking_requests?page[number]=1&page[size]=1",
+ "next": "https://api.terminal49.com/v2/tracking_requests?page[number]=2&page[size]=1",
+ "last": "https://api.terminal49.com/v2/tracking_requests?page[number]=180774&page[size]=1"
+ }
+}
diff --git a/sdks/typescript-sdk/src/generated/terminal49.ts b/sdks/typescript-sdk/src/generated/terminal49.ts
new file mode 100644
index 00000000..52ce65ef
--- /dev/null
+++ b/sdks/typescript-sdk/src/generated/terminal49.ts
@@ -0,0 +1,3699 @@
+/**
+ * This file was auto-generated by openapi-typescript.
+ * Do not make direct changes to the file.
+ */
+
+export interface paths {
+ "/shipments": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List shipments
+ * @description Returns a list of your shipments. The shipments are returned sorted by creation date, with the most recent shipments appearing first.
+ *
+ * This api will return all shipments associated with the account. Shipments created via the `tracking_request` API aswell as the ones added via the dashboard will be retuned via this endpoint.
+ */
+ get: operations["get-shipments"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/shipments/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Shipment Id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get a shipment
+ * @description Retrieves the details of an existing shipment. You need only supply the unique shipment `id` that was returned upon `tracking_request` creation.
+ */
+ get: operations["get-shipment-id"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ /**
+ * Edit a shipment
+ * @description Update a shipment
+ */
+ patch: operations["patch-shipments-id"];
+ trace?: never;
+ };
+ "/shipments/{id}/stop_tracking": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ /**
+ * Stop tracking a shipment
+ * @description We'll stop tracking the shipment, which means that there will be no more updates. You can still access the shipment's previously-collected information via the API or dashboard.
+ *
+ * You can resume tracking a shipment by calling the `resume_tracking` endpoint, but keep in mind that some information is only made available by our data sources at specific times, so a stopped and resumed shipment may have some information missing.
+ */
+ patch: operations["patch-shipments-id-stop-tracking"];
+ trace?: never;
+ };
+ "/shipments/{id}/resume_tracking": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ /**
+ * Resume tracking a shipment
+ * @description Resume tracking a shipment. Keep in mind that some information is only made available by our data sources at specific times, so a stopped and resumed shipment may have some information missing.
+ */
+ patch: operations["patch-shipments-id-resume-tracking"];
+ trace?: never;
+ };
+ "/tracking_requests": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List tracking requests
+ * @description Returns a list of your tracking requests. The tracking requests are returned sorted by creation date, with the most recent tracking request appearing first.
+ */
+ get: operations["get-tracking-requests"];
+ put?: never;
+ /**
+ * Create a tracking request
+ * @description To track an ocean shipment, you create a new tracking request.
+ * Two attributes are required to track a shipment. A `bill of lading/booking number` and a shipping line `SCAC`.
+ *
+ * Once a tracking request is created we will attempt to fetch the shipment details and it's related containers from the shipping line. If the attempt is successful we will create in new shipment object including any related container objects. We will send a `tracking_request.succeeded` webhook notification to your webhooks.
+ *
+ * If the attempt to fetch fails then we will send a `tracking_request.failed` webhook notification to your `webhooks`.
+ *
+ * A `tracking_request.succeeded` or `tracking_request.failed` webhook notificaiton will only be sent if you have atleast one active webhook.
This endpoint is limited to 100 tracking requests per minute.
+ */
+ post: operations["post-track"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/tracking_requests/infer_number": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Infer Tracking Number
+ * @description Predict the carrier SCAC (VOCC) and number type from a tracking number. Provide a container number, bill of lading number, or booking number and receive the predicted carrier with confidence and a decision value. Use this to auto-populate carrier fields before creating a tracking request.
+ */
+ post: operations["post-infer-number"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/tracking_requests/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Tracking Request ID */
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get a single tracking request
+ * @description Get the details and status of an existing tracking request.
+ */
+ get: operations["get-track-request-by-id"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ /**
+ * Edit a tracking request
+ * @description Update a tracking request
+ */
+ patch: operations["patch-track-request-by-id"];
+ trace?: never;
+ };
+ "/webhooks/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get single webhook
+ * @description Get the details of a single webhook
+ */
+ get: operations["get-webhooks-id"];
+ put?: never;
+ post?: never;
+ /**
+ * Delete a webhook
+ * @description Delete a webhook
+ */
+ delete: operations["delete-webhooks-id"];
+ options?: never;
+ head?: never;
+ /**
+ * Edit a webhook
+ * @description Update a single webhook
+ */
+ patch: operations["patch-webhooks-id"];
+ trace?: never;
+ };
+ "/webhooks": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List webhooks
+ * @description Get a list of all the webhooks
+ */
+ get: operations["get-webhooks"];
+ put?: never;
+ /**
+ * Create a webhook
+ * @description You can configure a webhook via the API to be notified about events that happen in your Terminal49 account. These events can be realted to tracking_requests, shipments and containers.
+ *
+ * This is the recommended way tracking shipments and containers via the API. You should use this instead of polling our the API periodically.
+ */
+ post: operations["post-webhooks"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/webhook_notifications/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get a single webhook notification
+ * @description
+ */
+ get: operations["get-webhook-notification-id"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/webhook_notifications": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List webhook notifications
+ * @description Return the list of webhook notifications. This can be useful for reconciling your data if your endpoint has been down.
+ */
+ get: operations["get-webhook-notifications"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/webhook_notifications/examples": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get webhook notification payload examples
+ * @description Returns an example payload as it would be sent to a webhook endpoint for the provided `event`
+ */
+ get: operations["get-webhook-notifications-example"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/webhooks/ips": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List webhook IPs
+ * @description Return the list of IPs used for sending webhook notifications. This can be useful for whitelisting the IPs on the firewall.
+ */
+ get: operations["get-webhooks-ips"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/containers": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List containers
+ * @description Returns a list of container. The containers are returned sorted by creation date, with the most recently refreshed containers appearing first.
+ *
+ * This API will return all containers associated with the account.
+ */
+ get: operations["get-containers"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ /**
+ * Edit a container
+ * @description Update a container
+ */
+ patch: operations["patch-containers-id"];
+ trace?: never;
+ };
+ "/containers/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get a container
+ * @description Retrieves the details of a container.
+ */
+ get: operations["get-containers-id"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/containers/{id}/raw_events": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get a container's raw events
+ * @deprecated
+ * @description #### Deprecation warning
+ * The `raw_events` endpoint is provided as-is.
+ *
+ * For past events we recommend consuming `transport_events`.
+ *
+ * ---
+ * Get a list of past and future (estimated) milestones for a container as reported by the carrier. Some of the data is normalized even though the API is called raw_events.
+ *
+ * Normalized attributes: `event` and `timestamp` timestamp. Not all of the `event` values have been normalized. You can expect the the events related to container movements to be normalized but there are cases where events are not normalized.
+ *
+ * For past historical events we recommend consuming `transport_events`. Although there are fewer events here those events go through additional vetting and normalization to avoid false positives and get you correct data.
+ */
+ get: operations["get-containers-id-raw_events"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/containers/{id}/transport_events": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get a container's transport events
+ * @description Get a list of past transport events (canonical) for a container. All data has been normalized across all carriers. These are a verified subset of the raw events may also be sent as Webhook Notifications to a webhook endpoint.
+ *
+ * This does not provide any estimated future events. See `container/:id/raw_events` endpoint for that.
+ */
+ get: operations["get-containers-id-transport_events"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/containers/{id}/route": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get container route
+ * @description Retrieves the route details from the port of lading to the port of discharge, including transshipments. This is a paid feature. Please contact sales@terminal49.com.
+ */
+ get: operations["get-containers-id-route"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/containers/{id}/map_geojson": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get container map GeoJSON
+ * @description Returns a GeoJSON FeatureCollection containing all map-related data for a container, including port locations, current vessel position (if at sea), past vessel paths, and estimated future routes. The response can be directly used with most mapping libraries (Leaflet, Mapbox GL, Google Maps, etc.). This is a paid feature. Please contact sales@terminal49.com.
+ */
+ get: operations["get-containers-id-map-geojson"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/containers/{id}/refresh": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ /**
+ * Refresh container
+ * @description Schedules the container to be refreshed immediately from all relevant sources.
To be alerted of updates you should subscribe to the [relevant webhooks](/api-docs/in-depth-guides/webhooks). This endpoint is limited to 10 requests per minute.This is a paid feature. Please contact sales@terminal49.com.
+ */
+ patch: operations["patch-containers-id-refresh"];
+ trace?: never;
+ };
+ "/shipping_lines": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Shipping Lines
+ * @description Return a list of shipping lines supported by Terminal49.
+ * N.B. There is no pagination for this endpoint.
+ */
+ get: operations["get-shipping_lines"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/shipping_lines/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get a single shipping line
+ * @description Return the details of a single shipping line.
+ */
+ get: operations["get-shipping_lines-id"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/metro_areas/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get a metro area using the un/locode or the id
+ * @description Return the details of a single metro area.
+ */
+ get: operations["get-metro-area-id"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/ports/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get a port using the locode or the id
+ * @description Return the details of a single port.
+ */
+ get: operations["get-port-id"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/vessels/{id}": {
+ parameters: {
+ query?: {
+ /** @description ISO 8601 timestamp to filter positions from. 7 days by default. */
+ "show_positions[from_timestamp]"?: string;
+ /** @description ISO 8601 timestamp to filter positions up to. Current time by default. */
+ "show_positions[to_timestamp]"?: string;
+ };
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get a vessel using the id
+ * @description Returns a vessel by id. `show_positions` is a paid feature. Please contact sales@terminal49.com.
+ */
+ get: operations["get-vessels-id"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/vessels/{imo}": {
+ parameters: {
+ query?: {
+ /** @description ISO 8601 timestamp to filter positions from. 7 days by default. */
+ "show_positions[from_timestamp]"?: string;
+ /** @description ISO 8601 timestamp to filter positions up to. Current time by default. */
+ "show_positions[to_timestamp]"?: string;
+ };
+ header?: never;
+ path: {
+ imo: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get a vessel using the imo
+ * @description Returns a vessel by the given IMO number. `show_positions` is a paid feature. Please contact sales@terminal49.com.
+ */
+ get: operations["get-vessels-imo"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/vessels/{id}/future_positions": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get vessel future positions
+ * @description Returns the estimated route between two ports for a given vessel. The timestamp of the positions has fixed spacing of one minute. This is a paid feature. Please contact sales@terminal49.com.
+ */
+ get: operations["get-vessels-id-future-positions"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/vessels/{id}/future_positions_with_coordinates": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get vessel future positions from coordinates
+ * @description Returns the estimated route between two ports for a given vessel from a set of coordinates. The timestamp of the positions has fixed spacing of one minute. This is a paid feature. Please contact sales@terminal49.com.
+ */
+ get: operations["get-vessels-id-future-positions-with-coordinates"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/terminals/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /**
+ * Get a terminal using the id
+ * @description Return the details of a single terminal.
+ */
+ get: operations["get-terminal-id"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/parties": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** @description Get a list of parties */
+ get: operations["list-parties"];
+ put?: never;
+ /** @description Creates a new party */
+ post: operations["post-party"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/parties/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /** @description Returns a party by it's given identifier */
+ get: operations["get-parties-id"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ /** @description Updates a party */
+ patch: operations["edit-party"];
+ trace?: never;
+ };
+}
+export type webhooks = Record;
+export interface components {
+ schemas: {
+ /** Shipment model */
+ shipment: {
+ /** Format: uuid */
+ id: string;
+ relationships: {
+ destination?: {
+ data?: {
+ /** @enum {string} */
+ type: "port" | "metro_area";
+ /** Format: uuid */
+ id: string;
+ } | null;
+ };
+ port_of_lading?: {
+ data?: {
+ /** @enum {string} */
+ type: "port";
+ /** Format: uuid */
+ id: string;
+ } | null;
+ };
+ containers?: {
+ data?: {
+ /** @enum {string} */
+ type: "container";
+ /** Format: uuid */
+ id: string;
+ }[];
+ };
+ port_of_discharge?: {
+ data?: {
+ /** @enum {string} */
+ type: "port";
+ /** Format: uuid */
+ id: string;
+ } | null;
+ };
+ pod_terminal?: {
+ data?: {
+ /** @enum {string} */
+ type: "terminal";
+ /** Format: uuid */
+ id: string;
+ };
+ };
+ destination_terminal?: {
+ data?: {
+ /** @enum {string} */
+ type: "terminal" | "rail_terminal";
+ /** Format: uuid */
+ id: string;
+ };
+ };
+ line_tracking_stopped_by_user?: {
+ data?: {
+ /** @enum {string} */
+ type: "user";
+ /** Format: uuid */
+ id: string;
+ };
+ };
+ };
+ attributes: {
+ bill_of_lading_number: string;
+ /** @description The normalized version of the shipment number used for querying the carrier */
+ normalized_number?: string;
+ ref_numbers?: string[] | null;
+ /** Format: date-time */
+ created_at?: string;
+ tags?: string[];
+ /** @description UN/LOCODE */
+ port_of_lading_locode?: string | null;
+ port_of_lading_name?: string | null;
+ /** @description UN/LOCODE */
+ port_of_discharge_locode?: string | null;
+ port_of_discharge_name?: string | null;
+ /** @description UN/LOCODE */
+ destination_locode?: string | null;
+ destination_name?: string | null;
+ shipping_line_scac?: string;
+ shipping_line_name?: string;
+ shipping_line_short_name?: string;
+ customer_name?: string | null;
+ pod_vessel_name?: string | null;
+ pod_vessel_imo?: string | null;
+ pod_voyage_number?: string | null;
+ /** Format: date-time */
+ pol_etd_at?: string | null;
+ /** Format: date-time */
+ pol_atd_at?: string | null;
+ /** Format: date-time */
+ pod_eta_at?: string | null;
+ /** Format: date-time */
+ pod_original_eta_at?: string | null;
+ /** Format: date-time */
+ pod_ata_at?: string | null;
+ /** Format: date-time */
+ destination_eta_at?: string | null;
+ /** Format: date-time */
+ destination_ata_at?: string | null;
+ /** @description IANA tz */
+ pol_timezone?: string | null;
+ /** @description IANA tz */
+ pod_timezone?: string | null;
+ /** @description IANA tz */
+ destination_timezone?: string | null;
+ /**
+ * Format: date-time
+ * @description When Terminal49 last tried to update the shipment status from the shipping line
+ */
+ line_tracking_last_attempted_at?: string | null;
+ /**
+ * Format: date-time
+ * @description When Terminal49 last successfully updated the shipment status from the shipping line
+ */
+ line_tracking_last_succeeded_at?: string | null;
+ /**
+ * Format: date-time
+ * @description When Terminal49 stopped checking at the shipping line
+ */
+ line_tracking_stopped_at?: string | null;
+ /**
+ * @description The reason Terminal49 stopped checking
+ * @enum {string|null}
+ */
+ line_tracking_stopped_reason?: "all_containers_terminated" | "past_arrival_window" | "past_full_out_window" | "no_updates_at_line" | "cancelled_by_user" | "booking_cancelled" | null;
+ };
+ /** @enum {string} */
+ type: "shipment";
+ links: {
+ /** Format: uri */
+ self: string;
+ };
+ };
+ /** meta */
+ meta: {
+ size?: number;
+ total?: number;
+ };
+ /** link */
+ "link-self": {
+ /** Format: uri */
+ self?: string;
+ };
+ /** links */
+ links: {
+ /** Format: uri */
+ last?: string;
+ /** Format: uri */
+ next?: string;
+ /** Format: uri */
+ prev?: string;
+ /** Format: uri */
+ first?: string;
+ /** Format: uri */
+ self?: string;
+ };
+ /**
+ * Container model
+ * @description Represents the equipment during a specific journey.
+ */
+ container: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "container";
+ attributes: {
+ number?: string;
+ ref_numbers?: string[];
+ /** @enum {string|null} */
+ equipment_type?: "dry" | "reefer" | "open top" | "flat rack" | "bulk" | "tank" | null;
+ /** @enum {integer|null} */
+ equipment_length?: null | 10 | 20 | 40 | 45;
+ /** @enum {string|null} */
+ equipment_height?: "standard" | "high_cube" | null;
+ weight_in_lbs?: number | null;
+ /** Format: date-time */
+ created_at?: string;
+ seal_number?: string | null;
+ /**
+ * Format: date-time
+ * @description Coalesces `import_deadlines` values giving preference to `pickup_lfd_line`
+ */
+ pickup_lfd?: string | null;
+ /**
+ * Format: date-time
+ * @description When available the pickup appointment time at the terminal is returned.
+ */
+ pickup_appointment_at?: string | null;
+ /** @description Whether Terminal 49 is receiving availability status from the terminal. */
+ availability_known?: boolean;
+ /** @description If availability_known is true, then whether container is available to be picked up at terminal. */
+ available_for_pickup?: boolean | null;
+ /**
+ * Format: date-time
+ * @description Time the vessel arrived at the POD
+ */
+ pod_arrived_at?: string | null;
+ /**
+ * Format: date-time
+ * @description Discharge time at the port of discharge
+ */
+ pod_discharged_at?: string | null;
+ /**
+ * Format: date-time
+ * @description Full Out time at port of discharge. Null for inland moves.
+ */
+ pod_full_out_at?: string | null;
+ /**
+ * Format: date-time
+ * @description When the terminal was last checked.
+ */
+ terminal_checked_at?: string | null;
+ /** @description The chassis number used when container was picked up at POD (if available) */
+ pod_full_out_chassis_number?: string | null;
+ /** @description Location at port of discharge terminal */
+ location_at_pod_terminal?: string | null;
+ /**
+ * Format: date-time
+ * @description Pickup time at final destination for inland moves.
+ */
+ final_destination_full_out_at?: string | null;
+ /**
+ * Format: date-time
+ * @description Time empty container was returned.
+ */
+ empty_terminated_at?: string | null;
+ holds_at_pod_terminal?: components["schemas"]["terminal_hold"][];
+ fees_at_pod_terminal?: components["schemas"]["terminal_fee"][];
+ /** @description IANA tz. Applies to attributes pod_arrived_at, pod_discharged_at, pickup_appointment_at, pod_full_out_at. */
+ pod_timezone?: string | null;
+ /** @description IANA tz. Applies to attribute final_destination_full_out_at. */
+ final_destination_timezone?: string | null;
+ /** @description IANA tz. Applies to attribute empty_terminated_at. */
+ empty_terminated_timezone?: string | null;
+ /** @description The SCAC of the rail carrier for the pickup leg of the container's journey.(BETA) */
+ pod_rail_carrier_scac?: string | null;
+ /** @description The SCAC of the rail carrier for the delivery leg of the container's journey.(BETA) */
+ ind_rail_carrier_scac?: string | null;
+ /** Format: date-time */
+ pod_last_tracking_request_at?: string | null;
+ /** Format: date-time */
+ shipment_last_tracking_request_at?: string | null;
+ /** Format: date-time */
+ pod_rail_loaded_at?: string | null;
+ /** Format: date-time */
+ pod_rail_departed_at?: string | null;
+ /** Format: date-time */
+ ind_eta_at?: string | null;
+ /** Format: date-time */
+ ind_ata_at?: string | null;
+ /** Format: date-time */
+ ind_rail_unloaded_at?: string | null;
+ /**
+ * Format: date-time
+ * @deprecated
+ * @description Please use `import_deadlines.pickup_lfd_rail`
+ */
+ ind_facility_lfd_on?: string | null;
+ /** @description Import pickup deadlines for the container */
+ import_deadlines?: {
+ /**
+ * Format: date-time
+ * @description The last free day for pickup before demmurage accrues. Corresponding timezone is pod_timezone.
+ */
+ pickup_lfd_terminal?: string | null;
+ /**
+ * Format: date-time
+ * @description The last free day for pickup before demmurage accrues. Corresponding timezone is final_destination_timezone.
+ */
+ pickup_lfd_rail?: string | null;
+ /**
+ * Format: date-time
+ * @description The last free day as reported by the line. Corresponding timezone is final_destination_timezone or pod_timezone.
+ */
+ pickup_lfd_line?: string | null;
+ } | null;
+ /**
+ * @description The current status of the container in its journey. [Read guide to learn more.](/api-docs/in-depth-guides/container-statuses)
+ * @enum {string}
+ */
+ current_status?: "new" | "on_ship" | "available" | "not_available" | "grounded" | "on_rail" | "picked_up" | "off_dock" | "delivered" | "dropped" | "loaded" | "empty_returned" | "awaiting_inland_transfer";
+ };
+ relationships?: {
+ shipment?: {
+ data?: {
+ id?: string;
+ /** @enum {string} */
+ type?: "shipment";
+ };
+ };
+ pickup_facility?: {
+ data?: {
+ id?: string;
+ /** @enum {string} */
+ type?: "terminal";
+ };
+ };
+ pod_terminal?: {
+ data?: {
+ id?: string;
+ /** @enum {string} */
+ type?: "terminal";
+ };
+ };
+ transport_events?: {
+ data?: {
+ id?: string;
+ /** @enum {string} */
+ type?: "transport_event";
+ }[];
+ };
+ raw_events?: {
+ data?: {
+ id?: string;
+ /** @enum {string} */
+ type?: "raw_event";
+ }[];
+ };
+ };
+ };
+ /** Port model */
+ port: {
+ /** Format: uuid */
+ id: string;
+ attributes?: {
+ name?: string;
+ /** @description UN/LOCODE */
+ code?: string;
+ state_abbr?: string | null;
+ city?: string | null;
+ /** @description 2 digit country code */
+ country_code?: string;
+ /** @description IANA tz */
+ time_zone?: string;
+ latitude?: number | null;
+ longitude?: number | null;
+ };
+ /** @enum {string} */
+ type: "port";
+ };
+ /** Shipping line model */
+ shipping_line: {
+ /** Format: uuid */
+ id: string;
+ attributes: {
+ scac: string;
+ name: string;
+ /** @description Additional SCACs which will be accepted in tracking requests */
+ alternative_scacs: string[];
+ short_name: string;
+ bill_of_lading_tracking_support: boolean;
+ booking_number_tracking_support: boolean;
+ container_number_tracking_support: boolean;
+ };
+ /** @enum {string} */
+ type: "shipping_line";
+ };
+ /** Account model */
+ account: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "container";
+ attributes: {
+ company_name: string;
+ };
+ };
+ /** Error model */
+ error: {
+ detail?: string | null;
+ title: string | null;
+ source?: {
+ pointer?: string | null;
+ parameter?: string | null;
+ } | null;
+ code?: string | null;
+ status?: string | null;
+ meta?: {
+ /** Format: uuid */
+ tracking_request_id?: string | null;
+ } | null;
+ };
+ /** Metro area model */
+ metro_area: {
+ /** Format: uuid */
+ id: string;
+ attributes?: {
+ name?: string;
+ /** @description UN/LOCODE */
+ code?: string;
+ state_abbr?: string | null;
+ country_code?: string;
+ /** @description IANA tz */
+ time_zone?: string;
+ latitude?: number | null;
+ longitude?: number | null;
+ };
+ /** @enum {string} */
+ type: "metro_area";
+ ""?: string;
+ };
+ /** Terminal model */
+ terminal: {
+ /** Format: uuid */
+ id?: string;
+ relationships: {
+ port: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "port";
+ };
+ };
+ };
+ attributes: {
+ name: string;
+ nickname?: string;
+ /** @description CBP FIRMS Code or CBS Sublocation Code */
+ firms_code?: string;
+ /** @description SMDG Code */
+ smdg_code?: string;
+ /** @description BIC Facility Code */
+ bic_facility_code?: string;
+ /** @description Street part of the address */
+ street?: string;
+ /** @description City part of the address */
+ city?: string;
+ /** @description State part of the address */
+ state?: string;
+ /** @description State abbreviation for the state */
+ state_abbr?: string;
+ /** @description ZIP code part of the address */
+ zip?: string;
+ /** @description Country part of the address */
+ country?: string;
+ };
+ /** @enum {string} */
+ type?: "terminal";
+ };
+ /** Rail Terminal model */
+ rail_terminal: {
+ /** Format: uuid */
+ id?: string;
+ relationships?: {
+ port?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "port";
+ } | null;
+ };
+ metro_area?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "metro_area";
+ };
+ };
+ };
+ attributes: {
+ name: string;
+ nickname?: string;
+ /** @description CBP FIRMS Code or CBS Sublocation Code */
+ firms_code?: string;
+ };
+ /** @enum {string} */
+ type?: "rail_terminal";
+ };
+ /** Tracking Request */
+ tracking_request: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "tracking_request";
+ attributes?: {
+ /** @example ONEYSH9AME650500 */
+ request_number: string;
+ ref_numbers?: string[] | null;
+ tags?: string[];
+ /** @enum {string} */
+ status: "pending" | "awaiting_manifest" | "created" | "failed" | "tracking_stopped";
+ /**
+ * @description If the tracking request has failed, or is currently failing, the last reason we were unable to complete the request
+ * @enum {string|null}
+ */
+ failed_reason?: "booking_cancelled" | "duplicate" | "expired" | "internal_processing_error" | "invalid_number" | "not_found" | "retries_exhausted" | "shipping_line_unreachable" | "unrecognized_response" | "data_unavailable" | null;
+ /**
+ * @example bill_of_lading
+ * @enum {string}
+ */
+ request_type: "bill_of_lading" | "booking_number" | "container";
+ /** @example ONEY */
+ scac: string;
+ /** Format: date-time */
+ created_at: string;
+ /** Format: date-time */
+ updated_at?: string;
+ is_retrying?: boolean;
+ /** @description How many times T49 has attempted to get the shipment from the shipping line */
+ retry_count?: number | null;
+ };
+ relationships?: {
+ tracked_object?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "shipment";
+ } | null;
+ };
+ customer?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "party";
+ };
+ };
+ };
+ };
+ /** webhook */
+ webhook: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "webhook";
+ attributes?: {
+ /**
+ * Format: uri
+ * @description https end point
+ */
+ url: string;
+ /**
+ * @description Whether the webhook will be delivered when events are triggered
+ * @default true
+ */
+ active: boolean;
+ /** @description The list of events to enabled for this endpoint */
+ events: ("container.transport.vessel_arrived" | "container.transport.vessel_discharged" | "container.transport.vessel_loaded" | "container.transport.vessel_departed" | "container.transport.rail_departed" | "container.transport.rail_arrived" | "container.transport.rail_loaded" | "container.transport.rail_unloaded" | "container.transport.transshipment_arrived" | "container.transport.transshipment_discharged" | "container.transport.transshipment_loaded" | "container.transport.transshipment_departed" | "container.transport.feeder_arrived" | "container.transport.feeder_discharged" | "container.transport.feeder_loaded" | "container.transport.feeder_departed" | "container.transport.empty_out" | "container.transport.full_in" | "container.transport.full_out" | "container.transport.empty_in" | "container.transport.vessel_berthed" | "shipment.estimated.arrival" | "tracking_request.succeeded" | "tracking_request.failed" | "tracking_request.awaiting_manifest" | "tracking_request.tracking_stopped" | "container.created" | "container.updated" | "container.pod_terminal_changed" | "container.transport.arrived_at_inland_destination" | "container.transport.estimated.arrived_at_inland_destination" | "container.pickup_lfd.changed" | "container.pickup_lfd_line.changed" | "container.transport.available")[];
+ /** @description A random token that will sign all delivered webhooks */
+ secret: string;
+ headers?: {
+ name?: string;
+ value?: string;
+ }[] | null;
+ };
+ };
+ /** vessel */
+ vessel: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "vessel";
+ attributes?: {
+ /**
+ * @description The name of the ship or vessel
+ * @example Ever Given
+ */
+ name?: string;
+ /**
+ * @description International Maritime Organization (IMO) number
+ * @example 9811000
+ */
+ imo?: string | null;
+ /**
+ * @description Maritime Mobile Service Identity (MMSI)
+ * @example 353136000
+ */
+ mmsi?: string | null;
+ /**
+ * @description The current latitude position of the vessel
+ * @example 25.29845
+ */
+ latitude?: number | null;
+ /**
+ * @description The current longitude position of the vessel
+ * @example 121.217
+ */
+ longitude?: number | null;
+ /**
+ * @description The current speed of the ship in knots (nautical miles per hour)
+ * @example 90
+ */
+ nautical_speed_knots?: number | null;
+ /**
+ * @description The current heading of the ship in degrees, where 0 is North, 90 is East, 180 is South, and 270 is West
+ * @example 194
+ */
+ navigational_heading_degrees?: number | null;
+ /**
+ * @description The timestamp of when the ship's position was last recorded, in ISO 8601 date and time format
+ * @example 2023-07-28T14:01:37Z
+ */
+ position_timestamp?: string | null;
+ /** @description An array of historical position data for the vessel. Only included if `show_positions` is true. */
+ positions?: {
+ /** @example 1.477285 */
+ latitude?: number;
+ /** @example 104.535533333 */
+ longitude?: number;
+ /** @example 51 */
+ heading?: number | null;
+ /**
+ * Format: date-time
+ * @example 2025-05-23T19:14:22Z
+ */
+ timestamp?: string;
+ /** @example false */
+ estimated?: boolean;
+ }[] | null;
+ };
+ };
+ /** Transport Event Model */
+ transport_event: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "transport_event";
+ attributes?: {
+ /** @enum {string} */
+ event?: "container.transport.vessel_arrived" | "container.transport.vessel_discharged" | "container.transport.vessel_loaded" | "container.transport.vessel_departed" | "container.transport.rail_departed" | "container.transport.rail_arrived" | "container.transport.rail_loaded" | "container.transport.rail_unloaded" | "container.transport.transshipment_arrived" | "container.transport.transshipment_discharged" | "container.transport.transshipment_loaded" | "container.transport.transshipment_departed" | "container.transport.feeder_arrived" | "container.transport.feeder_discharged" | "container.transport.feeder_loaded" | "container.transport.feeder_departed" | "container.transport.empty_out" | "container.transport.full_in" | "container.transport.full_out" | "container.transport.empty_in" | "container.transport.vessel_berthed" | "container.transport.arrived_at_inland_destination" | "container.transport.estimated.arrived_at_inland_destination" | "container.pickup_lfd.changed" | "container.pickup_lfd_line.changed" | "container.transport.available";
+ voyage_number?: string | null;
+ /** Format: date-time */
+ timestamp?: string | null;
+ /** @description IANA tz */
+ timezone?: string | null;
+ /** @description UNLOCODE of the event location */
+ location_locode?: string | null;
+ /** Format: date-time */
+ created_at?: string;
+ /**
+ * @description The original source of the event data
+ * @example shipping_line
+ * @enum {string}
+ */
+ data_source?: "shipping_line" | "terminal" | "ais";
+ };
+ relationships?: {
+ shipment?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "shipment";
+ };
+ };
+ location?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "port" | "metro_area";
+ } | null;
+ };
+ vessel?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ name?: "vessel";
+ } | null;
+ };
+ terminal?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "terminal" | "rail_terminal";
+ } | null;
+ };
+ container?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "container";
+ };
+ };
+ };
+ };
+ /** Estimated Event Model */
+ estimated_event: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "estimated_event";
+ attributes: {
+ /**
+ * Format: date-time
+ * @description When the estimated event was created
+ */
+ created_at: string;
+ /** Format: date-time */
+ estimated_timestamp: string;
+ /** @enum {string} */
+ event: "shipment.estimated.arrival";
+ /** @description UNLOCODE of the event location */
+ location_locode?: string | null;
+ /** @description IANA tz */
+ timezone?: string | null;
+ voyage_number?: string | null;
+ /**
+ * @description The original source of the event data
+ * @enum {string}
+ */
+ data_source?: "shipping_line" | "terminal";
+ };
+ relationships: {
+ shipment: {
+ data: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "shipment";
+ };
+ };
+ port?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "port";
+ } | null;
+ };
+ /** @description */
+ vessel?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "vessel";
+ } | null;
+ };
+ };
+ };
+ /** webhook_notification */
+ webhook_notification: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "webhook_notification";
+ attributes?: {
+ /** @enum {string} */
+ event: "container.transport.vessel_arrived" | "container.transport.vessel_discharged" | "container.transport.vessel_loaded" | "container.transport.vessel_departed" | "container.transport.rail_departed" | "container.transport.rail_arrived" | "container.transport.rail_loaded" | "container.transport.rail_unloaded" | "container.transport.transshipment_arrived" | "container.transport.transshipment_discharged" | "container.transport.transshipment_loaded" | "container.transport.transshipment_departed" | "container.transport.feeder_arrived" | "container.transport.feeder_discharged" | "container.transport.feeder_loaded" | "container.transport.feeder_departed" | "container.transport.empty_out" | "container.transport.full_in" | "container.transport.full_out" | "container.transport.empty_in" | "container.transport.vessel_berthed" | "shipment.estimated.arrival" | "tracking_request.succeeded" | "tracking_request.failed" | "tracking_request.awaiting_manifest" | "tracking_request.tracking_stopped" | "container.created" | "container.updated" | "container.pod_terminal_changed" | "container.transport.arrived_at_inland_destination" | "container.transport.estimated.arrived_at_inland_destination" | "container.pickup_lfd.changed" | "container.pickup_lfd_line.changed" | "container.transport.available";
+ /**
+ * @description Whether the notification has been delivered to the webhook endpoint
+ * @default pending
+ * @enum {string}
+ */
+ delivery_status: "pending" | "succeeded" | "failed";
+ created_at: string;
+ };
+ relationships?: {
+ webhook: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "webhook";
+ };
+ };
+ reference_object?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "tracking_request" | "estimated_event" | "transport_event" | "container_updated_event";
+ };
+ };
+ };
+ };
+ /** terminal_hold */
+ terminal_hold: {
+ name: string;
+ /** @enum {string} */
+ status: "pending" | "hold";
+ /** @description Text description from the terminal (if any) */
+ description?: string | null;
+ };
+ /** terminal_fee */
+ terminal_fee: {
+ /** @enum {string} */
+ type: "demurrage" | "exam" | "extended_dwell_time" | "other" | "total";
+ /** @description The fee amount in local currency */
+ amount: number;
+ /**
+ * @description The ISO 4217 currency code of the fee is charged in. E.g. USD
+ * @example USD
+ */
+ currency_code?: string;
+ };
+ /** container_updated_event */
+ container_updated_event: {
+ id?: string;
+ type?: string;
+ attributes?: {
+ /**
+ * @description A hash of all the changed attributes with the values being an array of the before and after. E.g.
+ * `{"pickup_lfd": [null, "2020-05-20"]}`
+ *
+ * The current attributes that can be alerted on are:
+ * - `available_for_pickup`
+ * - `pickup_lfd`
+ * - `fees_at_pod_terminal`
+ * - `holds_at_pod_terminal`
+ * - `pickup_appointment_at`
+ * - `pod_terminal`
+ */
+ changeset: Record;
+ /** Format: date-time */
+ timestamp: string;
+ /** @description IANA tz */
+ timezone?: string;
+ /**
+ * @example terminal
+ * @enum {string}
+ */
+ data_source?: "terminal";
+ };
+ relationships: {
+ container: {
+ data: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "container";
+ };
+ };
+ terminal: {
+ data: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "terminal";
+ };
+ };
+ };
+ };
+ /**
+ * Raw Event Model
+ * @description Raw Events represent the milestones from the shipping line for a given container.
+ *
+ * ### About raw_event datetimes
+ *
+ * The events may include estimated future events. The event is a future event if an `estimated_` timestamp is not null.
+ *
+ * The datetime properties `timestamp` and `estimated`.
+ *
+ * When the `time_zone` property is present the datetimes are UTC timestamps, which can be converted to the local time by parsing the provided `time_zone`.
+ *
+ * When the `time_zone` property is absent, the datetimes represent local times which serialized as UTC timestamps for consistency.
+ */
+ raw_event: {
+ id?: string;
+ /** @enum {string} */
+ type?: "raw_event";
+ attributes?: {
+ /**
+ * @description Normalized string representing the event
+ * @enum {string|null}
+ */
+ event?: "empty_out" | "full_in" | "positioned_in" | "positioned_out" | "vessel_loaded" | "vessel_departed" | "transshipment_arrived" | "transshipment_discharged" | "transshipment_loaded" | "transshipment_departed" | "feeder_arrived" | "feeder_discharged" | "feeder_loaded" | "feeder_departed" | "rail_loaded" | "rail_departed" | "rail_arrived" | "rail_unloaded" | "vessel_arrived" | "vessel_discharged" | "arrived_at_destination" | "delivered" | "full_out" | "empty_in" | "vgm_received" | "carrier_release" | "customs_release" | "available" | null;
+ /** @description The event name as returned by the carrier */
+ original_event?: string;
+ /**
+ * Format: date-time
+ * @description The datetime the event either transpired or will occur in UTC
+ */
+ timestamp?: string;
+ /** @description True if the timestamp is estimated, false otherwise */
+ estimated?: boolean;
+ /**
+ * Format: date
+ * @description Deprecated: The date of the event at the event location when no time information is available.
+ */
+ actual_on?: string | null;
+ /**
+ * Format: date
+ * @description Deprecated: The estimated date of the event at the event location when no time information is available.
+ */
+ estimated_on?: string | null;
+ /**
+ * Format: date-time
+ * @description Deprecated: The datetime the event transpired in UTC
+ */
+ actual_at?: string | null;
+ /**
+ * Format: date-time
+ * @description Deprecated: The estimated datetime the event will occur in UTC
+ */
+ estimated_at?: string | null;
+ /** @description IANA tz where the event occured */
+ timezone?: string | null;
+ /**
+ * Format: date-time
+ * @description When the raw_event was created in UTC
+ */
+ created_at?: string;
+ /** @description The city or facility name of the event location */
+ location_name?: string;
+ /** @description UNLOCODE of the event location */
+ location_locode?: string | null;
+ /** @description The name of the vessel where applicable */
+ vessel_name?: string | null;
+ /** @description The IMO of the vessel where applicable */
+ vessel_imo?: string | null;
+ /** @description The order of the event. This may be helpful when only dates (i.e. actual_on) are available. */
+ index?: number;
+ voyage_number?: string | null;
+ };
+ relationships?: {
+ location?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "port" | "metro_area";
+ };
+ };
+ vessel?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "vessel";
+ };
+ };
+ };
+ };
+ /** Container Pod Terminal Changed Event */
+ container_pod_terminal_changed_event: {
+ id?: string;
+ type?: string;
+ attributes?: {
+ /**
+ * @description Where the information about the terminal change came from
+ * @example shipping_line
+ * @enum {string}
+ */
+ data_source?: "shipping_line" | "terminal" | "pierpass";
+ /**
+ * Format: date-time
+ * @description When the terminal change was recorded
+ */
+ timestamp?: string;
+ };
+ relationships?: {
+ container?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "container";
+ };
+ shipment?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "shipment";
+ };
+ /** @description The terminal the container has changed to. If this container is still on the vessel this represents an advisory. If it was previously at the terminal this represents an off-dock move. */
+ terminal?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "terminal";
+ };
+ };
+ };
+ /** Party model */
+ party: {
+ /** Format: uuid */
+ id?: string;
+ attributes: {
+ /** @description Company name */
+ company_name: string;
+ };
+ /** @enum {string} */
+ type?: "party";
+ };
+ /** Route model */
+ route: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "route";
+ attributes: {
+ /** Format: uuid */
+ id: string;
+ /** Format: date-time */
+ created_at: string;
+ /** Format: date-time */
+ updated_at: string;
+ };
+ relationships: {
+ cargo?: {
+ data?: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "container";
+ };
+ };
+ shipment?: {
+ data?: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "shipment";
+ };
+ };
+ route_locations?: {
+ data?: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "route_location";
+ }[];
+ };
+ };
+ };
+ /** Route Location model */
+ route_location: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "route_location";
+ attributes: {
+ /** Format: uuid */
+ id: string;
+ inbound_scac?: string | null;
+ /** @enum {string|null} */
+ inbound_mode?: "vessel" | "rail" | null;
+ /** Format: date-time */
+ inbound_eta_at?: string | null;
+ /** Format: date-time */
+ inbound_ata_at?: string | null;
+ inbound_voyage_number?: string | null;
+ outbound_scac?: string | null;
+ /** @enum {string|null} */
+ outbound_mode?: "vessel" | "rail" | null;
+ /** Format: date-time */
+ outbound_etd_at?: string | null;
+ /** Format: date-time */
+ outbound_atd_at?: string | null;
+ outbound_voyage_number?: string | null;
+ /** Format: date-time */
+ created_at: string;
+ /** Format: date-time */
+ updated_at: string;
+ };
+ relationships: {
+ route?: {
+ data?: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "route";
+ };
+ };
+ inbound_vessel?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "vessel";
+ } | null;
+ };
+ outbound_vessel?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "vessel";
+ } | null;
+ };
+ location?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "port" | "terminal";
+ };
+ };
+ facility?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "terminal" | "port";
+ } | null;
+ };
+ };
+ };
+ /** Vessel with positions model */
+ vessel_with_positions: {
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ type: "vessel";
+ attributes: {
+ /** @description The name of the ship or vessel */
+ name?: string;
+ /** @description International Maritime Organization (IMO) number */
+ imo?: string | null;
+ /** @description Maritime Mobile Service Identity (MMSI) */
+ mmsi?: string | null;
+ /** @description The current latitude position of the vessel */
+ latitude?: number | null;
+ /** @description The current longitude position of the vessel */
+ longitude?: number | null;
+ /** @description The current speed of the ship in knots (nautical miles per hour) */
+ nautical_speed_knots?: number | null;
+ /** @description The current heading of the ship in degrees, where 0 is North, 90 is East, 180 is South, and 270 is West */
+ navigational_heading_degrees?: number | null;
+ /**
+ * Format: date-time
+ * @description The timestamp of when the ship's position was last recorded, in ISO 8601 date and time format
+ */
+ position_timestamp?: string | null;
+ /** @description Array of estimated future positions */
+ positions?: {
+ latitude: number;
+ longitude: number;
+ heading?: number | null;
+ /** Format: date-time */
+ timestamp: string;
+ estimated: boolean;
+ }[];
+ };
+ };
+ /** Port */
+ portFeatureProperties: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ feature_type: "port";
+ /** @description The sequence number of this port in the route (1 = POL, last = POD) */
+ ports_sequence?: number;
+ /** @description Total number of ports in the route */
+ ports_total?: number;
+ /** @description Unique identifier for the port location */
+ location_id?: string;
+ /** @enum {string} */
+ location_type?: "Port";
+ /** @description Name of the port */
+ name?: string;
+ /** @description State abbreviation (if applicable) */
+ state_abbr?: string | null;
+ /** @description State name (if applicable) */
+ state?: string | null;
+ /** @description ISO country code */
+ country_code?: string;
+ /** @description Country name */
+ country?: string;
+ /** @description IANA timezone identifier */
+ time_zone?: string;
+ /** @description Port label: POL, POD, or TS1, TS2, etc. */
+ label?: string;
+ /**
+ * Format: date-time
+ * @description Estimated time of arrival (ISO 8601)
+ */
+ inbound_eta_at?: string | null;
+ /**
+ * Format: date-time
+ * @description Actual time of arrival (ISO 8601)
+ */
+ inbound_ata_at?: string | null;
+ /**
+ * Format: date-time
+ * @description Estimated time of departure (ISO 8601)
+ */
+ outbound_etd_at?: string | null;
+ /**
+ * Format: date-time
+ * @description Actual time of departure (ISO 8601)
+ */
+ outbound_atd_at?: string | null;
+ /**
+ * Format: date-time
+ * @description Last update timestamp from the shipment (ISO 8601)
+ */
+ updated_at?: string | null;
+ };
+ /** Current Vessel */
+ currentVesselFeatureProperties: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ feature_type: "current_vessel";
+ /** @description Sequence number of the departure port for this leg */
+ ports_sequence?: number;
+ /** @description Unique identifier for the vessel */
+ vessel_id?: string;
+ /** @description Name of the vessel */
+ vessel_name?: string;
+ /** @description IMO number of the vessel */
+ vessel_imo?: string;
+ /** @description Voyage number for this leg */
+ voyage_number?: string | null;
+ /**
+ * Format: date-time
+ * @description Timestamp of the vessel position (ISO 8601)
+ */
+ vessel_location_timestamp?: string;
+ /** @description Vessel heading in degrees (0-360) */
+ vessel_location_heading?: number | null;
+ /** @description Vessel speed in knots */
+ vessel_location_speed?: number | null;
+ /** @description ID of the port the vessel departed from */
+ departure_port_id?: string;
+ /** @description Name of the departure port */
+ departure_port_name?: string;
+ /** @description State abbreviation of departure port */
+ departure_port_state_abbr?: string | null;
+ /** @description State name of departure port */
+ departure_port_state?: string | null;
+ /** @description Country code of departure port */
+ departure_port_country_code?: string;
+ /** @description Country name of departure port */
+ departure_port_country?: string;
+ /** @description Label of departure port (POL, POD, TS1, etc.) */
+ departure_port_label?: string;
+ /**
+ * Format: date-time
+ * @description Actual time of departure from the port (ISO 8601)
+ */
+ departure_port_atd?: string | null;
+ /** @description Timezone of departure port */
+ departure_port_time_zone?: string;
+ /** @description ID of the next port the vessel is heading to */
+ arrival_port_id?: string | null;
+ /** @description Name of the arrival port */
+ arrival_port_name?: string | null;
+ /** @description State abbreviation of arrival port */
+ arrival_port_state_abbr?: string | null;
+ /** @description State name of arrival port */
+ arrival_port_state?: string | null;
+ /** @description Country code of arrival port */
+ arrival_port_country_code?: string | null;
+ /** @description Country name of arrival port */
+ arrival_port_country?: string | null;
+ /** @description Label of arrival port (POL, POD, TS1, etc.) */
+ arrival_port_label?: string | null;
+ /**
+ * Format: date-time
+ * @description Estimated time of arrival at the next port (ISO 8601)
+ */
+ arrival_port_eta?: string | null;
+ /** @description Timezone of arrival port */
+ arrival_port_time_zone?: string | null;
+ };
+ /** Past Vessel Locations */
+ pastVesselLocationsFeatureProperties: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ feature_type: "past_vessel_locations";
+ /** @description Sequence number of the departure port for this leg */
+ ports_sequence?: number;
+ /** @description Unique identifier for the vessel that traveled this path */
+ vessel_id?: string;
+ /**
+ * Format: date-time
+ * @description Start timestamp of the path (ISO 8601)
+ */
+ start_time?: string;
+ /**
+ * Format: date-time
+ * @description End timestamp of the path (ISO 8601)
+ */
+ end_time?: string;
+ /** @description Number of coordinate points in the LineString */
+ point_count?: number;
+ /**
+ * Format: date-time
+ * @description Actual time of departure from the origin port (ISO 8601)
+ */
+ outbound_atd_at?: string | null;
+ /**
+ * Format: date-time
+ * @description Actual time of arrival at the destination port (ISO 8601)
+ */
+ inbound_ata_at?: string | null;
+ /**
+ * Format: date-time
+ * @description Estimated time of arrival at the destination port (ISO 8601)
+ */
+ inbound_eta_at?: string | null;
+ };
+ /** Estimated Full Leg */
+ estimatedFullLegFeatureProperties: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ feature_type: "estimated_full_legs";
+ /** @description Sequence number of the departure port for this leg */
+ ports_sequence?: number;
+ /** @description ID of the origin port */
+ previous_port_id?: string;
+ /** @description ID of the destination port */
+ next_port_id?: string;
+ /** @description Number of coordinate points in the LineString */
+ point_count?: number;
+ };
+ /** Estimated Partial Leg */
+ estimatedPartialLegFeatureProperties: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ feature_type: "estimated_partial_leg";
+ /** @description Sequence number of the departure port for this leg */
+ ports_sequence?: number;
+ /** @description ID of the port the vessel departed from */
+ current_port_id?: string;
+ /** @description ID of the next port the vessel is heading to */
+ next_port_id?: string;
+ /** @description Number of coordinate points in the LineString */
+ point_count?: number;
+ };
+ /** Point */
+ pointGeometry: {
+ /** @enum {string} */
+ type: "Point";
+ /**
+ * @example [
+ * 100.896831042,
+ * 13.065302386
+ * ]
+ */
+ coordinates: number[];
+ };
+ /** LineString */
+ lineStringGeometry: {
+ /** @enum {string} */
+ type: "LineString";
+ /**
+ * @example [
+ * [
+ * 100.868768333,
+ * 13.07306
+ * ],
+ * [
+ * 100.839155,
+ * 13.079318333
+ * ]
+ * ]
+ */
+ coordinates: number[][];
+ };
+ };
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+export type $defs = Record;
+export interface operations {
+ "get-shipments": {
+ parameters: {
+ query?: {
+ /** @description */
+ "page[number]"?: number;
+ /** @description */
+ "page[size]"?: number;
+ /**
+ * @deprecated
+ * @description Search shipments by master bill of lading, reference number, or container number.
+ */
+ q?: string;
+ /** @description Comma delimited list of relations to include */
+ include?: string;
+ /** @description Search shipments by the original request tracking `request_number` */
+ number?: string;
+ /** @description Filter shipments by whether they are still tracking or not */
+ "filter[tracking_stopped]"?: boolean;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["shipment"][];
+ included?: (components["schemas"]["container"] | components["schemas"]["port"] | components["schemas"]["terminal"])[];
+ links?: components["schemas"]["links"];
+ meta?: components["schemas"]["meta"];
+ };
+ };
+ };
+ /** @description Unprocessable Entity */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: components["schemas"]["error"][];
+ };
+ };
+ };
+ };
+ };
+ "get-shipment-id": {
+ parameters: {
+ query?: {
+ /** @description Comma delimited list of relations to include */
+ include?: string;
+ };
+ header?: never;
+ path: {
+ /** @description Shipment Id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["shipment"];
+ included?: (components["schemas"]["container"] | components["schemas"]["port"] | components["schemas"]["terminal"])[];
+ };
+ };
+ };
+ /** @description Not Found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: components["schemas"]["error"][];
+ };
+ };
+ };
+ };
+ };
+ "patch-shipments-id": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Shipment Id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: {
+ content: {
+ "application/json": {
+ data?: {
+ attributes: {
+ /**
+ * @description Shipment ref numbers.
+ * @example [
+ * "REFNUMBER10"
+ * ]
+ */
+ ref_numbers?: string[];
+ /** @description Tags related to a shipment */
+ shipment_tags?: string[];
+ };
+ };
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["shipment"];
+ };
+ };
+ };
+ };
+ };
+ "patch-shipments-id-stop-tracking": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["shipment"];
+ };
+ };
+ };
+ };
+ };
+ "patch-shipments-id-resume-tracking": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["shipment"];
+ };
+ };
+ };
+ };
+ };
+ "get-tracking-requests": {
+ parameters: {
+ query?: {
+ /**
+ * @deprecated
+ * @description A search term to be applied against request_number and reference_numbers.
+ */
+ q?: string;
+ /** @description filter by `request_number` */
+ "filter[request_number]"?: string;
+ /** @description filter by `status` */
+ "filter[status]"?: "created" | "pending" | "failed";
+ /** @description filter by shipping line `scac` */
+ "filter[scac]"?: string;
+ /** @description filter by tracking_requests `created_at` after a certain ISO8601 timestamp */
+ "filter[created_at][start]"?: string;
+ /** @description filter by tracking_requests `created_at` before a certain ISO8601 timestamp */
+ "filter[created_at][end]"?: string;
+ /** @description filter by tracking_requests `updated_at` after a certain ISO8601 timestamp */
+ "filter[updated_at][start]"?: string;
+ /** @description filter by tracking_requests `updated_at` before a certain ISO8601 timestamp */
+ "filter[updated_at][end]"?: string;
+ /** @description Comma delimited list of relations to include. 'tracked_object' is included by default. */
+ include?: string;
+ "page[number]"?: number;
+ "page[size]"?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["tracking_request"][];
+ links?: components["schemas"]["links"];
+ meta?: components["schemas"]["meta"];
+ included?: (components["schemas"]["account"] | components["schemas"]["shipping_line"] | {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "shipment";
+ links?: {
+ /** Format: uri */
+ self?: string;
+ };
+ })[];
+ };
+ };
+ };
+ /** @description Not Found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: components["schemas"]["error"][];
+ };
+ };
+ };
+ };
+ };
+ "post-track": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** @description Create a shipment tracking request */
+ requestBody?: {
+ content: {
+ "application/json": {
+ data?: {
+ attributes?: {
+ /**
+ * @description The type of document number to be supplied. Container number support is currently in BETA.
+ * @example bill_of_lading
+ * @enum {string}
+ */
+ request_type: "bill_of_lading" | "booking_number" | "container";
+ /** @example MEDUFR030802 */
+ request_number: string;
+ /** @example MSCU */
+ scac: string;
+ /** @description Optional list of reference numbers to be added to the shipment when tracking request completes */
+ ref_numbers?: string[];
+ /** @description Optional list of tags to be added to the shipment when tracking request completes */
+ shipment_tags?: string[];
+ };
+ relationships?: {
+ customer?: {
+ data?: {
+ /** Format: uuid */
+ id?: string;
+ /** @enum {string} */
+ type?: "party";
+ };
+ };
+ };
+ /** @enum {string} */
+ type: "tracking_request";
+ };
+ };
+ };
+ };
+ responses: {
+ /** @description Tracking Request Created */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["tracking_request"];
+ included?: (components["schemas"]["account"] | components["schemas"]["shipping_line"])[];
+ };
+ };
+ };
+ /** @description Unprocessable Entity */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: components["schemas"]["error"][];
+ };
+ };
+ };
+ /** @description Too Many Requests - You've hit the create tracking requests limit. Please try again in a minute. */
+ 429: {
+ headers: {
+ /** @description Number of seconds to wait before making another request */
+ "Retry-After"?: number;
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: {
+ /** @example 429 */
+ status?: string;
+ /** @example Too Many Requests */
+ title?: string;
+ /** @example You've hit the create tracking requests limit. Please try again in a minute. */
+ detail?: string;
+ }[];
+ };
+ };
+ };
+ };
+ };
+ "post-infer-number": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ /**
+ * @description The tracking number to analyze (container number, bill of lading, or booking number)
+ * @example WHLU1234560
+ */
+ number: string;
+ };
+ };
+ };
+ responses: {
+ /** @description Successfully inferred number type and shipping line */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: {
+ /** @example req_123e4567-e89b-12d3-a456-426614174000 */
+ id?: string;
+ /** @example infer_number_results */
+ type?: string;
+ attributes?: {
+ /** @enum {string} */
+ number_type?: "container" | "bill_of_lading" | "booking";
+ validation?: {
+ is_valid?: boolean | null;
+ /** @enum {string} */
+ type?: "container" | "shipment";
+ check_digit_passed?: boolean | null;
+ parsed_number?: string | null;
+ reason?: string | null;
+ };
+ shipping_line?: {
+ /** @enum {string} */
+ decision?: "auto_select" | "needs_confirmation" | "no_prediction";
+ selected?: {
+ scac?: string;
+ name?: string;
+ confidence?: number;
+ } | null;
+ candidates?: {
+ scac?: string;
+ name?: string;
+ confidence?: number;
+ }[];
+ };
+ };
+ };
+ };
+ };
+ };
+ /** @description Unprocessable Entity - Invalid tracking number format */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: {
+ status?: string;
+ detail?: string;
+ }[];
+ };
+ };
+ };
+ /** @description Too Many Requests - Rate limit exceeded */
+ 429: {
+ headers: {
+ /** @description Number of seconds to wait before making another request */
+ "Retry-After"?: number;
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: {
+ status?: string;
+ detail?: string;
+ }[];
+ };
+ };
+ };
+ };
+ };
+ "get-track-request-by-id": {
+ parameters: {
+ query?: {
+ /** @description Comma delimited list of relations to include. 'tracked_object' is included by default. */
+ include?: string;
+ };
+ header?: never;
+ path: {
+ /** @description Tracking Request ID */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["tracking_request"];
+ included?: (components["schemas"]["account"] | components["schemas"]["shipment"] | components["schemas"]["shipping_line"])[];
+ };
+ };
+ };
+ /** @description Not Found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: components["schemas"]["error"][];
+ };
+ };
+ };
+ };
+ };
+ "patch-track-request-by-id": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Tracking Request ID */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: {
+ content: {
+ "application/json": {
+ data?: {
+ attributes: {
+ /**
+ * @description Tracking request ref number.
+ * @example REFNUMBER11
+ */
+ ref_number?: string;
+ };
+ };
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["tracking_request"];
+ };
+ };
+ };
+ };
+ };
+ "get-webhooks-id": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["webhook"];
+ };
+ };
+ };
+ };
+ };
+ "delete-webhooks-id": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ "patch-webhooks-id": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: {
+ content: {
+ "application/json": {
+ data: {
+ attributes: {
+ /**
+ * Format: uri
+ * @description The URL of the webhook endpoint.
+ * @example https://webhook.site/#!/39084fbb-d887-42e8-be08-b9183ad02362
+ */
+ url?: string;
+ /** @description The list of events to enable for this endpoint. */
+ events?: ("container.transport.vessel_arrived" | "container.transport.vessel_discharged" | "container.transport.vessel_loaded" | "container.transport.vessel_departed" | "container.transport.rail_departed" | "container.transport.rail_arrived" | "container.transport.rail_loaded" | "container.transport.rail_unloaded" | "container.transport.transshipment_arrived" | "container.transport.transshipment_discharged" | "container.transport.transshipment_loaded" | "container.transport.transshipment_departed" | "container.transport.feeder_arrived" | "container.transport.feeder_discharged" | "container.transport.feeder_loaded" | "container.transport.feeder_departed" | "container.transport.empty_out" | "container.transport.full_in" | "container.transport.full_out" | "container.transport.empty_in" | "container.transport.vessel_berthed" | "shipment.estimated.arrival" | "tracking_request.succeeded" | "tracking_request.failed" | "tracking_request.awaiting_manifest" | "tracking_request.tracking_stopped" | "container.created" | "container.updated" | "container.pod_terminal_changed" | "container.transport.arrived_at_inland_destination" | "container.transport.estimated.arrived_at_inland_destination" | "container.pickup_lfd.changed" | "container.pickup_lfd_line.changed" | "container.transport.available")[];
+ active?: boolean;
+ /** @description Optional custom headers to pass with each webhook invocation */
+ headers?: {
+ /** @description The name of the header. (Please not this will be auto-capitalized) */
+ name?: string;
+ /** @description The value to pass for the header */
+ value?: string;
+ }[];
+ };
+ /** @enum {string} */
+ type: "webhook";
+ };
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["webhook"];
+ };
+ };
+ };
+ };
+ };
+ "get-webhooks": {
+ parameters: {
+ query?: {
+ "page[number]"?: number;
+ "page[size]"?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["webhook"][];
+ meta?: components["schemas"]["meta"];
+ links?: components["schemas"]["links"];
+ };
+ };
+ };
+ };
+ };
+ "post-webhooks": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: {
+ content: {
+ "application/json": {
+ data: {
+ attributes: {
+ /**
+ * Format: uri
+ * @description The URL of the webhook endpoint.
+ * @example https://webhook.site/#!/39084fbb-d887-42e8-be08-b9183ad02362
+ */
+ url: string;
+ /** @description The list of events to enable for this endpoint. */
+ events?: ("container.transport.vessel_arrived" | "container.transport.vessel_discharged" | "container.transport.vessel_loaded" | "container.transport.vessel_departed" | "container.transport.rail_departed" | "container.transport.rail_arrived" | "container.transport.rail_loaded" | "container.transport.rail_unloaded" | "container.transport.transshipment_arrived" | "container.transport.transshipment_discharged" | "container.transport.transshipment_loaded" | "container.transport.transshipment_departed" | "container.transport.feeder_arrived" | "container.transport.feeder_discharged" | "container.transport.feeder_loaded" | "container.transport.feeder_departed" | "container.transport.empty_out" | "container.transport.full_in" | "container.transport.full_out" | "container.transport.empty_in" | "container.transport.vessel_berthed" | "shipment.estimated.arrival" | "tracking_request.succeeded" | "tracking_request.failed" | "tracking_request.awaiting_manifest" | "tracking_request.tracking_stopped" | "container.created" | "container.updated" | "container.pod_terminal_changed" | "container.transport.arrived_at_inland_destination" | "container.transport.estimated.arrived_at_inland_destination" | "container.pickup_lfd.changed" | "container.pickup_lfd_line.changed" | "container.transport.available")[];
+ active: boolean;
+ /** @description Optional custom headers to pass with each webhook invocation */
+ headers?: {
+ /** @description The name of the header. (Please note this will be auto-capitalized) */
+ name?: string;
+ /** @description The value to pass for the header */
+ value?: string;
+ }[];
+ };
+ /** @enum {string} */
+ type: "webhook";
+ };
+ };
+ };
+ };
+ responses: {
+ /** @description Create a test webhook endpoint */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["webhook"];
+ };
+ "application/xml": components["schemas"]["webhook"];
+ };
+ };
+ };
+ };
+ "get-webhook-notification-id": {
+ parameters: {
+ query?: {
+ /** @description Comma delimited list of relations to include. */
+ include?: string;
+ };
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["webhook_notification"];
+ included?: (components["schemas"]["webhook"] | components["schemas"]["tracking_request"] | components["schemas"]["transport_event"] | components["schemas"]["estimated_event"] | components["schemas"]["container_updated_event"])[];
+ };
+ };
+ };
+ };
+ };
+ "get-webhook-notifications": {
+ parameters: {
+ query?: {
+ "page[number]"?: number;
+ "page[size]"?: number;
+ /** @description Comma delimited list of relations to include. */
+ include?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["webhook_notification"][];
+ links?: components["schemas"]["links"];
+ meta?: components["schemas"]["meta"];
+ included?: (components["schemas"]["webhook"] | components["schemas"]["tracking_request"] | components["schemas"]["transport_event"] | components["schemas"]["estimated_event"])[];
+ };
+ };
+ };
+ };
+ };
+ "get-webhook-notifications-example": {
+ parameters: {
+ query?: {
+ /** @description The webhook notification event name you wish to see an example of */
+ event?: "container.transport.vessel_arrived" | "container.transport.vessel_discharged" | "container.transport.vessel_loaded" | "container.transport.vessel_departed" | "container.transport.rail_departed" | "container.transport.rail_arrived" | "container.transport.rail_loaded" | "container.transport.rail_unloaded" | "container.transport.transshipment_arrived" | "container.transport.transshipment_discharged" | "container.transport.transshipment_loaded" | "container.transport.transshipment_departed" | "container.transport.feeder_arrived" | "container.transport.feeder_discharged" | "container.transport.feeder_loaded" | "container.transport.feeder_departed" | "container.transport.empty_out" | "container.transport.full_in" | "container.transport.full_out" | "container.transport.empty_in" | "container.transport.vessel_berthed" | "shipment.estimated.arrival" | "tracking_request.succeeded" | "tracking_request.failed" | "tracking_request.awaiting_manifest" | "tracking_request.tracking_stopped" | "container.created" | "container.updated" | "container.pod_terminal_changed" | "container.transport.arrived_at_inland_destination" | "container.transport.estimated.arrived_at_inland_destination" | "container.pickup_lfd.changed" | "container.pickup_lfd_line.changed" | "container.transport.available";
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["webhook_notification"][];
+ links?: components["schemas"]["links"];
+ meta?: components["schemas"]["meta"];
+ included?: (components["schemas"]["webhook"] | components["schemas"]["tracking_request"] | components["schemas"]["transport_event"] | components["schemas"]["estimated_event"])[];
+ };
+ };
+ };
+ };
+ };
+ "get-webhooks-ips": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ webhook_notification_ips?: string[];
+ /** Format: date-time */
+ last_updated?: string;
+ };
+ };
+ };
+ };
+ };
+ "get-containers": {
+ parameters: {
+ query?: {
+ "page[number]"?: number;
+ "page[size]"?: number;
+ /** @description Comma delimited list of relations to include */
+ include?: string;
+ /** @description Number of seconds in which containers were refreshed */
+ terminal_checked_before?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["container"][];
+ included?: components["schemas"]["shipment"][];
+ links?: components["schemas"]["links"];
+ meta?: components["schemas"]["meta"];
+ };
+ };
+ };
+ };
+ };
+ "patch-containers-id": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: {
+ content: {
+ "application/json": {
+ data?: {
+ attributes: {
+ ref_numbers?: string[];
+ };
+ };
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["container"];
+ };
+ };
+ };
+ };
+ };
+ "get-containers-id": {
+ parameters: {
+ query?: {
+ /** @description Comma delimited list of relations to include */
+ include?: string;
+ };
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["container"];
+ included?: (components["schemas"]["shipment"] | components["schemas"]["terminal"] | components["schemas"]["transport_event"])[];
+ };
+ };
+ };
+ };
+ };
+ "get-containers-id-raw_events": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["raw_event"][];
+ };
+ };
+ };
+ };
+ };
+ "get-containers-id-transport_events": {
+ parameters: {
+ query?: {
+ /** @description Comma delimited list of relations to include */
+ include?: string;
+ };
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["transport_event"][];
+ included?: (components["schemas"]["shipment"] | components["schemas"]["container"] | components["schemas"]["port"] | components["schemas"]["metro_area"] | components["schemas"]["terminal"] | components["schemas"]["rail_terminal"] | components["schemas"]["vessel"])[];
+ links?: components["schemas"]["links"];
+ meta?: components["schemas"]["meta"];
+ };
+ };
+ };
+ };
+ };
+ "get-containers-id-route": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["route"];
+ included?: (components["schemas"]["port"] | components["schemas"]["vessel"] | components["schemas"]["route_location"] | components["schemas"]["shipment"])[];
+ };
+ };
+ };
+ /** @description Forbidden - Routing data feature is not enabled for this account */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: {
+ /** @example 403 */
+ status?: string;
+ /** @example Forbidden */
+ title?: string;
+ /** @example Routing data feature is not enabled for this account */
+ detail?: string;
+ }[];
+ };
+ };
+ };
+ };
+ };
+ "get-containers-id-map-geojson": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @enum {string} */
+ type: "FeatureCollection";
+ features: {
+ /** @enum {string} */
+ type: "Feature";
+ geometry: {
+ /** @enum {string} */
+ type: "Point";
+ /**
+ * @example [
+ * 100.896831042,
+ * 13.065302386
+ * ]
+ */
+ coordinates: number[];
+ } | {
+ /** @enum {string} */
+ type: "LineString";
+ /**
+ * @example [
+ * [
+ * 100.868768333,
+ * 13.07306
+ * ],
+ * [
+ * 100.839155,
+ * 13.079318333
+ * ]
+ * ]
+ */
+ coordinates: number[][];
+ };
+ properties: components["schemas"]["portFeatureProperties"] | components["schemas"]["currentVesselFeatureProperties"] | components["schemas"]["pastVesselLocationsFeatureProperties"] | components["schemas"]["estimatedFullLegFeatureProperties"] | components["schemas"]["estimatedPartialLegFeatureProperties"];
+ }[];
+ };
+ };
+ };
+ /** @description Forbidden - Routing data feature is not enabled for this account */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: {
+ /** @example 403 */
+ status?: string;
+ /** @example Forbidden */
+ title?: string;
+ /** @example Routing data feature is not enabled for this account */
+ detail?: string;
+ }[];
+ };
+ };
+ };
+ };
+ };
+ "patch-containers-id-refresh": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example Started refresh for Shipping line, Terminal, Rail */
+ message?: string;
+ };
+ };
+ };
+ /** @description Forbidden - This API endpoint is not enabled for your account. Please contact support@terminal49.com */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: {
+ /** @example 403 */
+ status?: string;
+ /** @example API access not enabled */
+ title?: string;
+ /** @example This API endpoint is not enabled for your account. Please contact support@terminal49.com */
+ detail?: string;
+ }[];
+ };
+ };
+ };
+ /** @description Too Many Requests - You've hit the refresh limit. Please try again in a minute. */
+ 429: {
+ headers: {
+ /** @description Number of seconds to wait before making another request */
+ "Retry-After"?: number;
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: {
+ /** @example 429 */
+ status?: string;
+ /** @example Too Many Requests */
+ title?: string;
+ /** @example You've hit the refresh limit. Please try again in a minute. */
+ detail?: string;
+ }[];
+ };
+ };
+ };
+ };
+ };
+ "get-shipping_lines": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["shipping_line"][];
+ links?: components["schemas"]["links"];
+ };
+ };
+ };
+ };
+ };
+ "get-shipping_lines-id": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["shipping_line"];
+ };
+ };
+ };
+ };
+ };
+ "get-metro-area-id": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["metro_area"];
+ };
+ };
+ };
+ };
+ };
+ "get-port-id": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["port"];
+ };
+ };
+ };
+ };
+ };
+ "get-vessels-id": {
+ parameters: {
+ query?: {
+ /** @description ISO 8601 timestamp to filter positions from. 7 days by default. */
+ "show_positions[from_timestamp]"?: string;
+ /** @description ISO 8601 timestamp to filter positions up to. Current time by default. */
+ "show_positions[to_timestamp]"?: string;
+ };
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["vessel"];
+ };
+ };
+ };
+ /** @description Forbidden - Feature not enabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: {
+ /** @example 403 */
+ status?: string;
+ source?: Record | null;
+ /** @example Forbidden */
+ title?: string;
+ /** @example Routing data feature is not enabled for this account */
+ detail?: string;
+ }[];
+ };
+ };
+ };
+ };
+ };
+ "get-vessels-imo": {
+ parameters: {
+ query?: {
+ /** @description ISO 8601 timestamp to filter positions from. 7 days by default. */
+ "show_positions[from_timestamp]"?: string;
+ /** @description ISO 8601 timestamp to filter positions up to. Current time by default. */
+ "show_positions[to_timestamp]"?: string;
+ };
+ header?: never;
+ path: {
+ imo: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["vessel"];
+ };
+ };
+ };
+ /** @description Forbidden - Feature not enabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: {
+ /** @example 403 */
+ status?: string;
+ source?: Record | null;
+ /** @example Forbidden */
+ title?: string;
+ /** @example Routing data feature is not enabled for this account */
+ detail?: string;
+ }[];
+ };
+ };
+ };
+ };
+ };
+ "get-vessels-id-future-positions": {
+ parameters: {
+ query: {
+ /** @description The destination port id */
+ port_id: string;
+ /** @description The previous port id */
+ previous_port_id: string;
+ };
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["vessel_with_positions"];
+ links?: {
+ /** Format: uri */
+ self?: string;
+ };
+ };
+ };
+ };
+ /** @description Forbidden - Routing data feature is not enabled for this account */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: {
+ /** @example 403 */
+ status?: string;
+ /** @example Forbidden */
+ title?: string;
+ /** @example Routing data feature is not enabled for this account */
+ detail?: string;
+ }[];
+ };
+ };
+ };
+ };
+ };
+ "get-vessels-id-future-positions-with-coordinates": {
+ parameters: {
+ query: {
+ /** @description The destination port id */
+ port_id: string;
+ /** @description The previous port id */
+ previous_port_id: string;
+ /** @description Starting latitude coordinate */
+ latitude: number;
+ /** @description Starting longitude coordinate */
+ longitude: number;
+ };
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["vessel_with_positions"];
+ links?: {
+ /** Format: uri */
+ self?: string;
+ };
+ };
+ };
+ };
+ /** @description Forbidden - Routing data feature is not enabled for this account */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: {
+ /** @example 403 */
+ status?: string;
+ /** @example Forbidden */
+ title?: string;
+ /** @example Routing data feature is not enabled for this account */
+ detail?: string;
+ }[];
+ };
+ };
+ };
+ };
+ };
+ "get-terminal-id": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["terminal"];
+ };
+ };
+ };
+ };
+ };
+ "list-parties": {
+ parameters: {
+ query?: {
+ "page[number]"?: number;
+ "page[size]"?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["party"][];
+ links?: components["schemas"]["links"];
+ meta?: components["schemas"]["meta"];
+ };
+ };
+ };
+ };
+ };
+ "post-party": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: {
+ content: {
+ "application/json": {
+ data?: {
+ attributes: {
+ /**
+ * @description The name of the company
+ * @example COMPANY NAME
+ */
+ company_name?: string;
+ };
+ };
+ };
+ };
+ };
+ responses: {
+ /** @description Party Created */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["party"];
+ links?: components["schemas"]["link-self"];
+ };
+ };
+ };
+ /** @description Unprocessable Entity */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: components["schemas"]["error"][];
+ };
+ };
+ };
+ };
+ };
+ "get-parties-id": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["party"];
+ links?: components["schemas"]["link-self"];
+ };
+ };
+ };
+ };
+ };
+ "edit-party": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: {
+ content: {
+ "application/json": {
+ data?: {
+ attributes: {
+ /**
+ * @description The name of the company
+ * @example COMPANY NAME
+ */
+ company_name?: string;
+ };
+ };
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data?: components["schemas"]["party"];
+ links?: components["schemas"]["link-self"];
+ };
+ };
+ };
+ /** @description Unprocessable Entity */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ errors?: components["schemas"]["error"][];
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/sdks/typescript-sdk/src/index.ts b/sdks/typescript-sdk/src/index.ts
new file mode 100644
index 00000000..8b32d3c5
--- /dev/null
+++ b/sdks/typescript-sdk/src/index.ts
@@ -0,0 +1,4 @@
+export * from './client.js';
+export * from './types/models.js';
+export * from './types/options.js';
+export type { paths } from './generated/terminal49.js';
diff --git a/sdks/typescript-sdk/src/scripts/example-deserialize.ts b/sdks/typescript-sdk/src/scripts/example-deserialize.ts
new file mode 100644
index 00000000..dac38c8e
--- /dev/null
+++ b/sdks/typescript-sdk/src/scripts/example-deserialize.ts
@@ -0,0 +1,34 @@
+import { Terminal49Client } from '@terminal49/sdk';
+
+interface SimplifiedContainer {
+ id: string;
+ number?: string;
+ status?: string;
+ shipment?: { id: string; bill_of_lading?: string } | null;
+}
+
+async function main() {
+ const token = process.env.T49_API_TOKEN;
+ const containerId = process.env.T49_CONTAINER_ID;
+
+ if (!token) throw new Error('Set T49_API_TOKEN');
+ if (!containerId) throw new Error('Set T49_CONTAINER_ID');
+
+ const client = new Terminal49Client({ apiToken: token });
+
+ // Fetch raw JSON:API document (includes shipment for mapping)
+ const doc = await client.getContainer(containerId, ['shipment']);
+
+ // Use JSONA to deserialize into plain objects
+ const simplified = client.deserialize(doc);
+
+ console.log('Raw JSON:API:');
+ console.log(JSON.stringify(doc, null, 2));
+ console.log('\nSimplified:');
+ console.log(JSON.stringify(simplified, null, 2));
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/sdks/typescript-sdk/src/scripts/example.ts b/sdks/typescript-sdk/src/scripts/example.ts
new file mode 100644
index 00000000..4368a456
--- /dev/null
+++ b/sdks/typescript-sdk/src/scripts/example.ts
@@ -0,0 +1,59 @@
+import { Terminal49Client } from '@terminal49/sdk';
+
+interface SimplifiedContainer {
+ id: string;
+ number?: string;
+ status?: string;
+ shipment?: {
+ id: string;
+ bill_of_lading?: string;
+ containers?: Array<{ id: string; number?: string }>;
+ } | null;
+}
+
+async function main() {
+ const token = process.env.T49_API_TOKEN;
+ const containerId = process.env.T49_CONTAINER_ID;
+
+ if (!token) throw new Error('Set T49_API_TOKEN');
+ if (!containerId) throw new Error('Set T49_CONTAINER_ID');
+
+ const client = new Terminal49Client({ apiToken: token });
+
+ // Fetch raw JSON:API document (includes shipment for mapping)
+ const doc = await client.getContainer(containerId, ['shipment']);
+
+ console.log('Raw JSON:API response:');
+ console.log(JSON.stringify(doc, null, 2));
+
+ // Demonstrate JSONA deserialization into a plain object
+ const deserialized = client.deserialize(doc);
+ const simplified: SimplifiedContainer = {
+ id: deserialized.id,
+ number: deserialized.number || deserialized.container_number,
+ status: deserialized.status,
+ shipment: deserialized.shipment
+ ? {
+ id: deserialized.shipment.id,
+ bill_of_lading:
+ deserialized.shipment.bill_of_lading_number ||
+ deserialized.shipment.bill_of_lading ||
+ deserialized.shipment.bl_number,
+ containers: Array.isArray(deserialized.shipment.containers)
+ ? deserialized.shipment.containers.map((c: any) => ({
+ id: c.id,
+ number: c.number || c.container_number,
+ }))
+ : undefined,
+ }
+ : null,
+ };
+
+ console.log('\nSimplified (deserialize → plucked):');
+ console.log(JSON.stringify(simplified, null, 2));
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/sdks/typescript-sdk/src/scripts/list-smoke.ts b/sdks/typescript-sdk/src/scripts/list-smoke.ts
new file mode 100644
index 00000000..0ff1a230
--- /dev/null
+++ b/sdks/typescript-sdk/src/scripts/list-smoke.ts
@@ -0,0 +1,63 @@
+import { Terminal49Client } from '@terminal49/sdk';
+
+async function main() {
+ const token = process.env.T49_API_TOKEN;
+ if (!token) throw new Error('Set T49_API_TOKEN');
+
+ const client = new Terminal49Client({
+ apiToken: token,
+ defaultFormat: 'mapped',
+ });
+
+ // Shipping lines
+ const lines = await client.shippingLines.list(undefined, {
+ format: 'mapped',
+ });
+ logSection('shipping_lines', lines, 5);
+
+ // Containers (optionally filtered)
+ const containerFilters: Record = {};
+ if (process.env.T49_CONTAINER_STATUS)
+ containerFilters['filter[status]'] = process.env.T49_CONTAINER_STATUS;
+ if (process.env.T49_CONTAINER_PORT)
+ containerFilters['filter[pod_locode]'] = process.env.T49_CONTAINER_PORT;
+ const containers = await client.listContainers(containerFilters, {
+ format: 'mapped',
+ });
+ logSection('containers', containers, 3);
+
+ // Shipments (optionally filtered)
+ const shipmentFilters: Record = {};
+ if (process.env.T49_SHIPMENT_STATUS)
+ shipmentFilters['filter[status]'] = process.env.T49_SHIPMENT_STATUS;
+ if (process.env.T49_SHIPMENT_PORT)
+ shipmentFilters['filter[pod_locode]'] = process.env.T49_SHIPMENT_PORT;
+ const shipments = await client.listShipments(shipmentFilters, {
+ format: 'mapped',
+ });
+ logSection('shipments', shipments, 3);
+
+ // Tracking requests
+ const trackingFilters: Record = {};
+ const trackingRequests = await client.listTrackingRequests(trackingFilters, {
+ format: 'mapped',
+ });
+ logSection('tracking_requests', trackingRequests, 3);
+}
+
+function logSection(name: string, data: any, sampleCount: number) {
+ const list = Array.isArray(data) ? data : data?.items || data?.data || [];
+ console.log(`\n=== ${name} ===`);
+ console.log(`count: ${Array.isArray(list) ? list.length : 'n/a'}`);
+ if (Array.isArray(list)) {
+ const sample = list.slice(0, sampleCount);
+ console.log('sample:', JSON.stringify(sample, null, 2));
+ } else {
+ console.log('raw:', JSON.stringify(data, null, 2));
+ }
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/sdks/typescript-sdk/src/scripts/smoke.ts b/sdks/typescript-sdk/src/scripts/smoke.ts
new file mode 100644
index 00000000..db36337a
--- /dev/null
+++ b/sdks/typescript-sdk/src/scripts/smoke.ts
@@ -0,0 +1,64 @@
+import { Terminal49Client } from '@terminal49/sdk';
+
+async function main() {
+ const token = process.env.T49_API_TOKEN;
+ if (!token) throw new Error('Set T49_API_TOKEN');
+
+ const containerId = process.env.T49_CONTAINER_ID;
+ const shipmentId = process.env.T49_SHIPMENT_ID;
+ const trackingRequestId = process.env.T49_TRACKING_REQUEST_ID;
+
+ const client = new Terminal49Client({
+ apiToken: token,
+ defaultFormat: 'mapped',
+ });
+
+ // Shipping lines
+ const lines = await client.shippingLines.list(undefined, {
+ format: 'mapped',
+ });
+ console.log(`Shipping lines: ${Array.isArray(lines) ? lines.length : 'n/a'}`);
+
+ if (containerId) {
+ const c = await client.containers.get(containerId, ['shipment'], {
+ format: 'both',
+ });
+ console.log(
+ 'Container:',
+ (c && (c as any).mapped?.id) || (c as any).raw?.data?.id || 'unknown',
+ );
+
+ const events = await client.containers.events(containerId, {
+ format: 'raw',
+ });
+ console.log('Events count:', events?.data?.length ?? 'n/a');
+
+ const route = await client.containers.route(containerId, {
+ format: 'mapped',
+ });
+ console.log('Route legs:', (route as any)?.totalLegs ?? 'n/a');
+ }
+
+ if (shipmentId) {
+ const s = await client.shipments.get(shipmentId, true, { format: 'both' });
+ console.log(
+ 'Shipment:',
+ (s as any).mapped?.id || (s as any).raw?.data?.id || 'unknown',
+ );
+ }
+
+ if (trackingRequestId) {
+ const tr = await client.getTrackingRequest(trackingRequestId, {
+ format: 'raw',
+ });
+ console.log(
+ 'Tracking request status:',
+ (tr as any)?.data?.attributes?.status ?? 'n/a',
+ );
+ }
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/sdks/typescript-sdk/src/smoke.test.ts b/sdks/typescript-sdk/src/smoke.test.ts
new file mode 100644
index 00000000..980841ec
--- /dev/null
+++ b/sdks/typescript-sdk/src/smoke.test.ts
@@ -0,0 +1,60 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import dotenv from 'dotenv';
+import { describe, expect, it } from 'vitest';
+import { Terminal49Client } from './client.js';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const envCandidates = [
+ process.env.DOTENV_CONFIG_PATH,
+ path.resolve(__dirname, '../../../.env.local'),
+ path.resolve(__dirname, '../../../.env'),
+ path.resolve(__dirname, '../.env.local'),
+ path.resolve(__dirname, '../.env'),
+].filter(Boolean) as string[];
+
+for (const candidate of envCandidates) {
+ if (fs.existsSync(candidate)) {
+ dotenv.config({ path: candidate });
+ break;
+ }
+}
+
+const token = process.env.T49_API_TOKEN;
+const baseUrl = process.env.T49_API_BASE_URL;
+const runSmoke = process.env.T49_RUN_SMOKE === '1';
+
+if (!token || !runSmoke) {
+ describe.skip('Terminal49Client smoke (requires T49_API_TOKEN and T49_RUN_SMOKE=1)', () => {});
+} else {
+ describe('Terminal49Client smoke', () => {
+ const client = new Terminal49Client({
+ apiToken: token as string,
+ apiBaseUrl: baseUrl,
+ defaultFormat: 'raw',
+ });
+
+ it('lists shipping lines', async () => {
+ const result = await client.shippingLines.list(undefined, {
+ format: 'raw',
+ });
+ expect((result as any)?.data).toBeDefined();
+ });
+
+ it('lists tracking requests', async () => {
+ const result = await client.trackingRequests.list();
+ expect((result as any)?.data).toBeDefined();
+ });
+
+ const inferNumber = process.env.T49_INFER_NUMBER;
+ const itIf = inferNumber ? it : it.skip;
+
+ itIf('infers tracking number', async () => {
+ const result = await client.trackingRequests.inferNumber(
+ inferNumber as string,
+ );
+ expect(result).toBeTruthy();
+ });
+ });
+}
diff --git a/sdks/typescript-sdk/src/test/mock-fetch.ts b/sdks/typescript-sdk/src/test/mock-fetch.ts
new file mode 100644
index 00000000..75db2b00
--- /dev/null
+++ b/sdks/typescript-sdk/src/test/mock-fetch.ts
@@ -0,0 +1,62 @@
+export function jsonResponse(
+ body: any,
+ status = 200,
+ headers?: Record,
+): Response {
+ return new Response(JSON.stringify(body), {
+ status,
+ headers: { 'Content-Type': 'application/json', ...(headers || {}) },
+ });
+}
+
+export function createMockFetch(
+ handlers: Record Response>,
+) {
+ const calls: Array<{ init?: RequestInit; url: URL }> = [];
+
+ const fetchImpl = async (
+ input: Request | URL | string,
+ init?: RequestInit,
+ ) => {
+ const request = input instanceof Request ? input : undefined;
+ const urlString =
+ typeof input === 'string'
+ ? input
+ : input instanceof URL
+ ? input.toString()
+ : (request?.url ?? '');
+
+ const url = new URL(urlString);
+ const derivedBody =
+ init && 'body' in init
+ ? init.body
+ : request
+ ? await request.clone().text()
+ : undefined;
+ const effectiveInit: RequestInit | undefined =
+ init || request
+ ? {
+ ...init,
+ method: init?.method || request?.method,
+ headers: init?.headers || request?.headers,
+ body: derivedBody,
+ }
+ : undefined;
+
+ const searchParams = new URLSearchParams(url.search);
+ const search = searchParams.toString()
+ ? `?${[...searchParams.entries()].map(([k, v]) => `${k}=${v}`).join('&')}`
+ : '';
+ const relative = url.pathname.replace('/v2', '') + search;
+
+ const handler = handlers[relative];
+ if (!handler) {
+ throw new Error(`No handler for ${relative}`);
+ }
+
+ calls.push({ init: effectiveInit, url });
+ return handler(effectiveInit, url);
+ };
+
+ return { fetchImpl, calls };
+}
diff --git a/sdks/typescript-sdk/src/types/models.ts b/sdks/typescript-sdk/src/types/models.ts
new file mode 100644
index 00000000..00cee640
--- /dev/null
+++ b/sdks/typescript-sdk/src/types/models.ts
@@ -0,0 +1,158 @@
+export interface ShippingLine {
+ scac: string;
+ name: string;
+ shortName?: string;
+ bolPrefix?: string;
+ notes?: string;
+}
+
+export interface PaginationLinks {
+ self?: string;
+ current?: string;
+ next?: string;
+ prev?: string;
+ first?: string;
+ last?: string;
+}
+
+export interface PaginatedResult {
+ items: T[];
+ links?: PaginationLinks;
+ meta?: Record;
+}
+
+export interface Container {
+ id: string;
+ number?: string;
+ status?: string;
+ equipment?: {
+ type?: string;
+ length?: number;
+ height?: number;
+ weightLbs?: number;
+ };
+ location?: {
+ currentLocation?: string;
+ availableForPickup?: boolean;
+ podArrivedAt?: string | null;
+ podDischargedAt?: string | null;
+ };
+ demurrage?: {
+ pickupLfd?: string | null;
+ pickupAppointmentAt?: string | null;
+ fees?: any[];
+ holds?: any[];
+ };
+ terminals?: {
+ podTerminal?: {
+ id?: string;
+ name?: string;
+ nickname?: string;
+ firmsCode?: string;
+ } | null;
+ destinationTerminal?: {
+ id?: string;
+ name?: string;
+ nickname?: string;
+ firmsCode?: string;
+ } | null;
+ };
+ shipment?: Shipment | null;
+ [key: string]: any;
+}
+
+export interface Shipment {
+ id: string;
+ billOfLading?: string;
+ shippingLineScac?: string;
+ customerName?: string;
+ ports?: {
+ portOfLading?: {
+ locode?: string | null;
+ name?: string | null;
+ code?: string | null;
+ countryCode?: string | null;
+ etd?: string | null;
+ atd?: string | null;
+ timezone?: string | null;
+ } | null;
+ portOfDischarge?: {
+ locode?: string | null;
+ name?: string | null;
+ code?: string | null;
+ countryCode?: string | null;
+ eta?: string | null;
+ ata?: string | null;
+ originalEta?: string | null;
+ timezone?: string | null;
+ terminal?: {
+ id?: string;
+ name?: string;
+ nickname?: string;
+ firmsCode?: string;
+ } | null;
+ } | null;
+ destination?: {
+ locode?: string | null;
+ name?: string | null;
+ eta?: string | null;
+ ata?: string | null;
+ timezone?: string | null;
+ terminal?: {
+ id?: string;
+ name?: string;
+ nickname?: string;
+ firmsCode?: string;
+ } | null;
+ } | null;
+ };
+ tracking?: {
+ lineTrackingLastAttemptedAt?: string | null;
+ lineTrackingLastSucceededAt?: string | null;
+ lineTrackingStoppedAt?: string | null;
+ lineTrackingStoppedReason?: string | null;
+ };
+ containers?: Array<{ id: string; number?: string }>;
+ [key: string]: any;
+}
+
+export interface Route {
+ id?: string;
+ totalLegs: number;
+ locations: Array<{
+ port?: {
+ code?: string | null;
+ name?: string | null;
+ city?: string | null;
+ countryCode?: string | null;
+ } | null;
+ inbound: {
+ mode?: string | null;
+ carrierScac?: string | null;
+ eta?: string | null;
+ ata?: string | null;
+ vessel?: { name?: string | null; imo?: string | null } | null;
+ };
+ outbound: {
+ mode?: string | null;
+ carrierScac?: string | null;
+ etd?: string | null;
+ atd?: string | null;
+ vessel?: { name?: string | null; imo?: string | null } | null;
+ };
+ }>;
+ createdAt?: string | null;
+ updatedAt?: string | null;
+}
+
+export interface TrackingRequest {
+ id: string;
+ requestType?: string;
+ requestNumber?: string;
+ status?: string;
+ scac?: string;
+ refNumbers?: string[];
+ shipment?: Shipment | null;
+ container?: Container | null;
+ [key: string]: any;
+}
diff --git a/sdks/typescript-sdk/src/types/options.ts b/sdks/typescript-sdk/src/types/options.ts
new file mode 100644
index 00000000..0ef3ea33
--- /dev/null
+++ b/sdks/typescript-sdk/src/types/options.ts
@@ -0,0 +1,10 @@
+export type ResponseFormat = 'raw' | 'mapped' | 'both';
+
+export interface CallOptions {
+ format?: ResponseFormat;
+}
+
+export interface ListOptions extends CallOptions {
+ page?: number;
+ pageSize?: number;
+}
diff --git a/sdks/typescript-sdk/tsconfig.json b/sdks/typescript-sdk/tsconfig.json
new file mode 100644
index 00000000..342ab011
--- /dev/null
+++ b/sdks/typescript-sdk/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "lib": ["ES2022"],
+ "moduleResolution": "nodenext",
+ "rootDir": "./src",
+ "outDir": "./dist",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "allowSyntheticDefaultImports": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "types": ["node"]
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/sdks/typescript-sdk/vitest.config.ts b/sdks/typescript-sdk/vitest.config.ts
new file mode 100644
index 00000000..60029544
--- /dev/null
+++ b/sdks/typescript-sdk/vitest.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig, configDefaults } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: 'node',
+ exclude: [...configDefaults.exclude, 'dist/**'],
+ env: {
+ DOTENV_CONFIG_PATH: '.env.local',
+ },
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'json', 'html'],
+ },
+ },
+});