diff --git a/docs/sdk/reference/types/models/interfaces/Container.mdx b/docs/sdk/reference/types/models/interfaces/Container.mdx
index 45acdb3a..f9545142 100644
--- a/docs/sdk/reference/types/models/interfaces/Container.mdx
+++ b/docs/sdk/reference/types/models/interfaces/Container.mdx
@@ -13,27 +13,28 @@ Simplified container model returned by mapped SDK responses.
## Properties
-| Property | Type |
-| ------ | ------ |
-| `demurrage?` | `object` |
-| `demurrage.fees?` | `any`[] |
-| `demurrage.holds?` | `any`[] |
-| `demurrage.pickupAppointmentAt?` | `string` \| `null` |
-| `demurrage.pickupLfd?` | `string` \| `null` |
-| `equipment?` | `object` |
-| `equipment.height?` | `number` |
-| `equipment.length?` | `number` |
-| `equipment.type?` | `string` |
-| `equipment.weightLbs?` | `number` |
-| `id` | `string` |
-| `location?` | `object` |
-| `location.availableForPickup?` | `boolean` |
-| `location.currentLocation?` | `string` |
-| `location.podArrivedAt?` | `string` \| `null` |
-| `location.podDischargedAt?` | `string` \| `null` |
-| `number?` | `string` |
-| `shipment?` | [`Shipment`](/sdk/reference/types/models/interfaces/Shipment) \| `null` |
-| `status?` | `string` |
-| `terminals?` | `object` |
-| `terminals.destinationTerminal?` | \{ `firmsCode?`: `string`; `id?`: `string`; `name?`: `string`; `nickname?`: `string`; \} \| `null` |
-| `terminals.podTerminal?` | \{ `firmsCode?`: `string`; `id?`: `string`; `name?`: `string`; `nickname?`: `string`; \} \| `null` |
+| Property | Type | Description |
+| ------ | ------ | ------ |
+| `currentStatus?` | `string` | Raw `current_status` from the API (also surfaced via `status`). |
+| `demurrage?` | `object` | - |
+| `demurrage.fees?` | `any`[] | - |
+| `demurrage.holds?` | `any`[] | - |
+| `demurrage.pickupAppointmentAt?` | `string` \| `null` | - |
+| `demurrage.pickupLfd?` | `string` \| `null` | - |
+| `equipment?` | `object` | - |
+| `equipment.height?` | `number` | - |
+| `equipment.length?` | `number` | - |
+| `equipment.type?` | `string` | - |
+| `equipment.weightLbs?` | `number` | - |
+| `id` | `string` | - |
+| `location?` | `object` | - |
+| `location.availableForPickup?` | `boolean` | - |
+| `location.currentLocation?` | `string` | - |
+| `location.podArrivedAt?` | `string` \| `null` | - |
+| `location.podDischargedAt?` | `string` \| `null` | - |
+| `number?` | `string` | - |
+| `shipment?` | [`Shipment`](/sdk/reference/types/models/interfaces/Shipment) \| `null` | - |
+| `status?` | `string` | - |
+| `terminals?` | `object` | - |
+| `terminals.destinationTerminal?` | \{ `firmsCode?`: `string`; `id?`: `string`; `name?`: `string`; `nickname?`: `string`; \} \| `null` | - |
+| `terminals.podTerminal?` | \{ `firmsCode?`: `string`; `id?`: `string`; `name?`: `string`; `nickname?`: `string`; \} \| `null` | - |
diff --git a/docs/sdk/reference/types/models/interfaces/ShippingLine.mdx b/docs/sdk/reference/types/models/interfaces/ShippingLine.mdx
index 3f852cc8..3b4fdcb8 100644
--- a/docs/sdk/reference/types/models/interfaces/ShippingLine.mdx
+++ b/docs/sdk/reference/types/models/interfaces/ShippingLine.mdx
@@ -9,10 +9,14 @@ Simplified shipping line returned by mapped SDK responses.
## Properties
-| Property | Type |
-| ------ | ------ |
-| `bolPrefix?` | `string` |
-| `name` | `string` |
-| `notes?` | `string` |
-| `scac` | `string` |
-| `shortName?` | `string` |
+| Property | Type | Description |
+| ------ | ------ | ------ |
+| `alternativeScacs?` | `string`[] | Additional SCACs the carrier tracks under. |
+| `billOfLadingTrackingSupport?` | `boolean` | Whether the carrier supports tracking by bill of lading number. |
+| `bolPrefix?` | `string` | - |
+| `bookingNumberTrackingSupport?` | `boolean` | Whether the carrier supports tracking by booking number. |
+| `containerNumberTrackingSupport?` | `boolean` | Whether the carrier supports tracking by container number. |
+| `name` | `string` | - |
+| `notes?` | `string` | - |
+| `scac` | `string` | - |
+| `shortName?` | `string` | - |
diff --git a/sdks/typescript-sdk/src/client.errors.test.ts b/sdks/typescript-sdk/src/client.errors.test.ts
index f6dfc656..be867203 100644
--- a/sdks/typescript-sdk/src/client.errors.test.ts
+++ b/sdks/typescript-sdk/src/client.errors.test.ts
@@ -53,7 +53,7 @@ describe('Terminal49Client error handling', () => {
it('maps 401 to AuthenticationError', async () => {
const { fetchImpl } = createMockFetch({
- '/containers/abc?include=shipment,pod_terminal': () =>
+ '/containers/abc?include=shipment,pod_terminal,pickup_facility': () =>
jsonResponse({ errors: [{ detail: 'invalid token' }] }, 401),
});
@@ -69,7 +69,7 @@ describe('Terminal49Client error handling', () => {
it('maps 403 with feature message to FeatureNotEnabledError', async () => {
const { fetchImpl } = createMockFetch({
- '/containers/abc?include=shipment,pod_terminal': () =>
+ '/containers/abc?include=shipment,pod_terminal,pickup_facility': () =>
jsonResponse({ errors: [{ detail: 'Feature not enabled' }] }, 403),
});
@@ -85,7 +85,7 @@ describe('Terminal49Client error handling', () => {
it('maps 403 without feature message to AuthorizationError', async () => {
const { fetchImpl } = createMockFetch({
- '/containers/abc?include=shipment,pod_terminal': () =>
+ '/containers/abc?include=shipment,pod_terminal,pickup_facility': () =>
jsonResponse({ errors: [{ detail: 'Access forbidden' }] }, 403),
});
@@ -101,7 +101,7 @@ describe('Terminal49Client error handling', () => {
it('maps 404 to NotFoundError', async () => {
const { fetchImpl } = createMockFetch({
- '/containers/abc?include=shipment,pod_terminal': () =>
+ '/containers/abc?include=shipment,pod_terminal,pickup_facility': () =>
jsonResponse({ errors: [{ detail: 'missing' }] }, 404),
});
@@ -117,7 +117,7 @@ describe('Terminal49Client error handling', () => {
it('maps 429 to RateLimitError', async () => {
const { fetchImpl } = createMockFetch({
- '/containers/abc?include=shipment,pod_terminal': () =>
+ '/containers/abc?include=shipment,pod_terminal,pickup_facility': () =>
jsonResponse({ errors: [{ detail: 'too many requests' }] }, 429),
});
@@ -133,7 +133,7 @@ describe('Terminal49Client error handling', () => {
it('maps 5xx to UpstreamError', async () => {
const { fetchImpl } = createMockFetch({
- '/containers/abc?include=shipment,pod_terminal': () =>
+ '/containers/abc?include=shipment,pod_terminal,pickup_facility': () =>
jsonResponse({ errors: [{ detail: 'server down' }] }, 503),
});
@@ -149,7 +149,7 @@ describe('Terminal49Client error handling', () => {
it('maps unexpected status to Terminal49Error with status in message', async () => {
const { fetchImpl } = createMockFetch({
- '/containers/abc?include=shipment,pod_terminal': () =>
+ '/containers/abc?include=shipment,pod_terminal,pickup_facility': () =>
jsonResponse({ errors: [{ detail: 'teapot' }] }, 418),
});
diff --git a/sdks/typescript-sdk/src/client.mapping.test.ts b/sdks/typescript-sdk/src/client.mapping.test.ts
index 0f9d5ebb..9c58bd16 100644
--- a/sdks/typescript-sdk/src/client.mapping.test.ts
+++ b/sdks/typescript-sdk/src/client.mapping.test.ts
@@ -141,11 +141,18 @@ describe('Terminal49Client mapping helpers', () => {
: null;
if (!routeLocation) return;
- const portId = routeLocation?.relationships?.port?.data?.id;
- const port = portId ? findIncluded(fixture, 'port', portId) : null;
+ // The port for a route leg lives under the `location` relationship of a
+ // route_location (type port|terminal), NOT a `port` relationship.
+ const locationId = routeLocation?.relationships?.location?.data?.id;
+ const port = locationId
+ ? findIncluded(fixture, 'port', locationId)
+ : null;
if (port) {
+ // Regression guard: the leg must resolve its port, not be null.
+ expect(result.locations[0]?.port).toBeTruthy();
expectIfDefined(result.locations[0]?.port?.code, port.attributes?.code);
expectIfDefined(result.locations[0]?.port?.name, port.attributes?.name);
+ expectIfDefined(result.locations[0]?.port?.city, port.attributes?.city);
expectIfDefined(
result.locations[0]?.port?.countryCode,
port.attributes?.country_code,
@@ -491,7 +498,8 @@ describe('Terminal49Client mapping helpers', () => {
it('maps container list items from base fixture', async () => {
const fixture = loadFixture('containers.list');
const { fetchImpl } = createMockFetch({
- '/containers?include=shipment,pod_terminal': () => jsonResponse(fixture),
+ '/containers?include=shipment,pod_terminal,pickup_facility': () =>
+ jsonResponse(fixture),
});
const client = new Terminal49Client({
@@ -590,7 +598,7 @@ describe('Terminal49Client mapping helpers', () => {
it('returns empty lists when container or shipment list data is not an array', async () => {
const { fetchImpl } = createMockFetch({
- '/containers?include=shipment,pod_terminal': () =>
+ '/containers?include=shipment,pod_terminal,pickup_facility': () =>
jsonResponse({ data: {} }),
'/shipments?include=containers,pod_terminal,port_of_lading,port_of_discharge,destination,destination_terminal':
() => jsonResponse({ data: {} }),
@@ -614,4 +622,215 @@ describe('Terminal49Client mapping helpers', () => {
expect(containers.items).toEqual([]);
expect(shipments.items).toEqual([]);
});
+
+ it('resolves every route leg port from the `location` relationship', 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(2);
+ expect(result.locations.length).toBe(2);
+
+ // Each leg must carry a non-null port resolved via `location`.
+ expect(result.locations[0]?.port?.code).toBe('CNSHA');
+ expect(result.locations[0]?.port?.name).toBe('Shanghai');
+ expect(result.locations[0]?.port?.city).toBe('Shanghai');
+ expect(result.locations[0]?.port?.countryCode).toBe('CN');
+
+ expect(result.locations[1]?.port?.code).toBe('USLAX');
+ expect(result.locations[1]?.port?.name).toBe('Los Angeles');
+ expect(result.locations[1]?.port?.countryCode).toBe('US');
+
+ // Outbound leg + vessel resolution sanity.
+ expect(result.locations[0]?.outbound?.carrierScac).toBe('MAEU');
+ expect(result.locations[0]?.outbound?.vessel?.name).toBe(
+ 'MAERSK EDINBURGH',
+ );
+ expect(result.locations[1]?.inbound?.vessel?.imo).toBe('9456769');
+ });
+
+ it('maps port `code` into the shipment locode fields', 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 relationships = fixture?.data?.relationships || {};
+ const polRef = relationships.port_of_lading?.data;
+ const pol = polRef ? findIncluded(fixture, polRef.type, polRef.id) : null;
+
+ if (pol?.attributes?.code) {
+ // Port resources expose `code` (e.g. KRPUS), never `locode`.
+ expect(pol.attributes.locode).toBeUndefined();
+ expect(result.ports?.portOfLading?.locode).toBe(pol.attributes.code);
+ expect(result.ports?.portOfLading?.code).toBe(pol.attributes.code);
+ }
+
+ const podRef = relationships.port_of_discharge?.data;
+ const pod = podRef ? findIncluded(fixture, podRef.type, podRef.id) : null;
+ if (pod?.attributes?.code) {
+ expect(result.ports?.portOfDischarge?.locode).toBe(pod.attributes.code);
+ expect(result.ports?.portOfDischarge?.code).toBe(pod.attributes.code);
+ }
+ });
+
+ it('maps container current_status and pickup_facility from real paths', async () => {
+ const fixture = loadFixture('containers.get.pickup');
+ const { fetchImpl } = createMockFetch({
+ '/containers?include=shipment,pod_terminal,pickup_facility': () =>
+ jsonResponse(fixture),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const list = (await client.listContainers(
+ { include: 'shipment,pod_terminal,pickup_facility' },
+ { format: 'mapped' },
+ )) as any;
+ const result = list.items[0];
+
+ const item = fixture.data[0];
+ const attrs = item.attributes;
+
+ // status must come from current_status (there is no `status` attribute).
+ expect(attrs.status).toBeUndefined();
+ expect(result.status).toBe(attrs.current_status);
+ expect(result.currentStatus).toBe(attrs.current_status);
+
+ // pod terminal resolves as before.
+ expect(result.terminals?.podTerminal?.name).toBe('APM Terminals Pier 400');
+ expect(result.terminals?.podTerminal?.firmsCode).toBe('Y258');
+
+ // pickup_facility (the real inland/destination facility relationship) must
+ // be surfaced; the legacy non-existent `destination_terminal` relationship
+ // never resolves anything.
+ const pickupRef = item.relationships.pickup_facility?.data;
+ const pickup = findIncluded(fixture, 'terminal', pickupRef?.id);
+ expect(pickup).toBeTruthy();
+ expect(result.terminals?.destinationTerminal?.name).toBe(
+ pickup.attributes.name,
+ );
+ expect(result.terminals?.destinationTerminal?.firmsCode).toBe(
+ pickup.attributes.firms_code,
+ );
+
+ expect(result.equipment?.type).toBe(attrs.equipment_type);
+ expect(result.location?.availableForPickup).toBe(
+ attrs.available_for_pickup,
+ );
+ expect(result.demurrage?.pickupLfd).toBe(attrs.pickup_lfd);
+ });
+
+ it('does not let the raw-attr spread clobber curated nested fields', async () => {
+ const fixture = loadFixture('containers.get.pickup');
+ const { fetchImpl } = createMockFetch({
+ '/containers?include=shipment,pod_terminal,pickup_facility': () =>
+ jsonResponse(fixture),
+ });
+
+ const client = new Terminal49Client({
+ apiToken: 'token-123',
+ apiBaseUrl: baseUrl,
+ fetchImpl,
+ });
+
+ const list = (await client.listContainers(
+ { include: 'shipment,pod_terminal,pickup_facility' },
+ { format: 'mapped' },
+ )) as any;
+ const result = list.items[0];
+
+ // Curated nests must remain structured objects, never overwritten by the
+ // flat camelCased raw attributes (e.g. an `equipment*` scalar must not
+ // replace the curated `equipment` object).
+ expect(typeof result.equipment).toBe('object');
+ expect(result.equipment).not.toBeNull();
+ expect(typeof result.location).toBe('object');
+ expect(typeof result.demurrage).toBe('object');
+ expect(typeof result.rail).toBe('object');
+ expect(typeof result.terminals).toBe('object');
+
+ // The flattened raw scalars that feed curated nests should not also appear
+ // as top-level duplicate keys.
+ expect(result).not.toHaveProperty('equipmentType');
+ expect(result).not.toHaveProperty('equipmentLength');
+ expect(result).not.toHaveProperty('availableForPickup');
+ expect(result).not.toHaveProperty('podArrivedAt');
+ expect(result).not.toHaveProperty('pickupLfd');
+ expect(result).not.toHaveProperty('podRailCarrierScac');
+ });
+
+ it('restores shipping-line alternative_scacs and tracking-support flags', 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 sourceWithAlt = (fixture?.data || []).find(
+ (item: any) =>
+ Array.isArray(item?.attributes?.alternative_scacs) &&
+ item.attributes.alternative_scacs.length > 0,
+ );
+ expect(sourceWithAlt).toBeTruthy();
+ const mappedAlt = result.find(
+ (line) => line.scac === sourceWithAlt.attributes.scac,
+ );
+ expect(mappedAlt?.alternativeScacs).toEqual(
+ sourceWithAlt.attributes.alternative_scacs,
+ );
+
+ const sourceWithFlag = (fixture?.data || []).find(
+ (item: any) =>
+ item?.attributes?.container_number_tracking_support === false,
+ );
+ expect(sourceWithFlag).toBeTruthy();
+ const mappedFlag = result.find(
+ (line) => line.scac === sourceWithFlag.attributes.scac,
+ );
+ expect(mappedFlag?.containerNumberTrackingSupport).toBe(false);
+ expect(mappedFlag?.billOfLadingTrackingSupport).toBe(
+ sourceWithFlag.attributes.bill_of_lading_tracking_support,
+ );
+ expect(mappedFlag?.bookingNumberTrackingSupport).toBe(
+ sourceWithFlag.attributes.booking_number_tracking_support,
+ );
+ });
});
diff --git a/sdks/typescript-sdk/src/client.test.ts b/sdks/typescript-sdk/src/client.test.ts
index e7dfa500..c9e83aee 100644
--- a/sdks/typescript-sdk/src/client.test.ts
+++ b/sdks/typescript-sdk/src/client.test.ts
@@ -103,7 +103,7 @@ describe('Terminal49Client', () => {
it('maps 404 responses to NotFoundError', async () => {
const { fetchImpl } = createMockFetch({
- '/containers/missing?include=shipment,pod_terminal': () =>
+ '/containers/missing?include=shipment,pod_terminal,pickup_facility': () =>
jsonResponse({ errors: [{ detail: 'not found' }] }, 404),
});
@@ -120,7 +120,7 @@ describe('Terminal49Client', () => {
it('adds auth header and include params when fetching container', async () => {
const { fetchImpl, calls } = createMockFetch({
- '/containers/abc?include=shipment,pod_terminal': () =>
+ '/containers/abc?include=shipment,pod_terminal,pickup_facility': () =>
jsonResponse({ data: { id: 'abc', attributes: {} } }),
});
@@ -138,13 +138,13 @@ describe('Terminal49Client', () => {
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',
+ 'shipment,pod_terminal,pickup_facility',
);
});
it('preserves bearer auth and sends account header when configured', async () => {
const { fetchImpl, calls } = createMockFetch({
- '/containers/abc?include=shipment,pod_terminal': () =>
+ '/containers/abc?include=shipment,pod_terminal,pickup_facility': () =>
jsonResponse({ data: { id: 'abc', attributes: {} } }),
});
@@ -351,7 +351,8 @@ describe('Terminal49Client', () => {
};
const { fetchImpl } = createMockFetch({
- '/containers?include=shipment,pod_terminal': () => jsonResponse(doc),
+ '/containers?include=shipment,pod_terminal,pickup_facility': () =>
+ jsonResponse(doc),
});
const client = new Terminal49Client({
diff --git a/sdks/typescript-sdk/src/client.ts b/sdks/typescript-sdk/src/client.ts
index fabbcb57..a9035df3 100644
--- a/sdks/typescript-sdk/src/client.ts
+++ b/sdks/typescript-sdk/src/client.ts
@@ -146,7 +146,11 @@ export class Terminal49Client {
/** Fetch a container by ID with optional included relationships. */
async getContainer(
id: string,
- include: IncludeParam = ['shipment', 'pod_terminal'],
+ include: IncludeParam = [
+ 'shipment',
+ 'pod_terminal',
+ 'pickup_facility',
+ ],
options?: CallOptions,
): Promise {
return this.containers.get(id, include, options);
diff --git a/sdks/typescript-sdk/src/client/jsonapi.ts b/sdks/typescript-sdk/src/client/jsonapi.ts
index 8f32da4f..614e6b78 100644
--- a/sdks/typescript-sdk/src/client/jsonapi.ts
+++ b/sdks/typescript-sdk/src/client/jsonapi.ts
@@ -47,3 +47,21 @@ export class JsonApiDocument {
return camelCase ? JsonApiDocument.toCamelCase(attrs) : attrs;
}
}
+
+/**
+ * Returns a shallow copy of `obj` with the given keys removed. Used by the
+ * mappers to keep the raw camelCased attribute spread from clobbering curated
+ * nested fields (and from emitting duplicate top-level scalars for the same
+ * underlying value).
+ */
+export function omitKeys>(
+ obj: T,
+ keys: readonly string[],
+): Record {
+ const omit = new Set(keys);
+ const result: Record = {};
+ for (const [key, value] of Object.entries(obj || {})) {
+ if (!omit.has(key)) result[key] = value;
+ }
+ return result;
+}
diff --git a/sdks/typescript-sdk/src/client/managers/containers.ts b/sdks/typescript-sdk/src/client/managers/containers.ts
index b64a5d48..552298f9 100644
--- a/sdks/typescript-sdk/src/client/managers/containers.ts
+++ b/sdks/typescript-sdk/src/client/managers/containers.ts
@@ -16,12 +16,17 @@ import { BaseManager } from './base.js';
const DEFAULT_CONTAINER_INCLUDES = [
'shipment',
'pod_terminal',
+ 'pickup_facility',
] as const satisfies readonly ContainerInclude[];
export class ContainerManager extends BaseManager {
async get(
id: string,
- include: IncludeParam = ['shipment', 'pod_terminal'],
+ include: IncludeParam = [
+ 'shipment',
+ 'pod_terminal',
+ 'pickup_facility',
+ ],
options?: CallOptions,
): Promise {
const includeParam = normalizeInclude(include);
diff --git a/sdks/typescript-sdk/src/client/mappers.ts b/sdks/typescript-sdk/src/client/mappers.ts
index d4a38369..0fe27011 100644
--- a/sdks/typescript-sdk/src/client/mappers.ts
+++ b/sdks/typescript-sdk/src/client/mappers.ts
@@ -5,7 +5,41 @@ import type {
ShippingLine,
TrackingRequest,
} from '../types/models.js';
-import { JsonApiDocument } from './jsonapi.js';
+import { JsonApiDocument, omitKeys } from './jsonapi.js';
+
+/**
+ * CamelCased raw container attribute keys that already feed a curated nested
+ * field (equipment/location/demurrage/terminals/rail) or a curated top-level
+ * field. They are dropped from the raw-attribute spread so the spread cannot
+ * clobber the curated nests or emit duplicate top-level scalars for the same
+ * value.
+ */
+const CONTAINER_CURATED_ATTR_KEYS = [
+ 'number',
+ 'containerNumber',
+ 'status',
+ 'currentStatus',
+ 'equipmentType',
+ 'equipmentLength',
+ 'equipmentHeight',
+ 'weightInLbs',
+ 'locationAtPodTerminal',
+ 'availableForPickup',
+ 'podArrivedAt',
+ 'podDischargedAt',
+ 'pickupLfd',
+ 'pickupAppointmentAt',
+ 'feesAtPodTerminal',
+ 'holdsAtPodTerminal',
+ 'podRailCarrierScac',
+ 'indRailCarrierScac',
+ 'podRailLoadedAt',
+ 'podRailDepartedAt',
+ 'indRailArrivedAt',
+ 'indRailUnloadedAt',
+ 'indEtaAt',
+ 'indAtaAt',
+] as const;
export function mapTransportEvents(doc: any) {
const apiDoc = new JsonApiDocument(doc);
@@ -23,7 +57,8 @@ export function mapTransportEvents(doc: any) {
? {
id: location.id,
name: location.attributes?.name,
- locode: location.attributes?.locode,
+ // Port/terminal resources expose `code`, not `locode`.
+ locode: location.attributes?.code ?? location.attributes?.locode,
}
: undefined,
terminal: terminal
@@ -51,7 +86,9 @@ export function mapRoute(doc: any): Route {
if (!location) return null;
const attrs = location.attributes || {};
- const port = apiDoc.getRelationship(location, 'port');
+ // A route leg's port lives under the `location` relationship
+ // (type port|terminal); there is no `port` relationship.
+ const port = apiDoc.getRelationship(location, 'location');
const inboundVessel = apiDoc.getRelationship(location, 'inbound_vessel');
const outboundVessel = apiDoc.getRelationship(
location,
@@ -107,7 +144,7 @@ export function mapRoute(doc: any): Route {
export function mapShippingLines(doc: any): ShippingLine[] {
const data = Array.isArray(doc?.data) ? doc.data : [];
return data
- .map((item: any) => {
+ .map((item: any): ShippingLine | null => {
const attrs = item?.attributes || {};
const scac = attrs.scac || item?.scac;
if (!scac) return null;
@@ -117,9 +154,15 @@ export function mapShippingLines(doc: any): ShippingLine[] {
shortName: attrs.short_name || attrs.nickname || undefined,
bolPrefix: attrs.bol_prefix || undefined,
notes: attrs.notes || undefined,
- } as ShippingLine;
+ alternativeScacs: Array.isArray(attrs.alternative_scacs)
+ ? attrs.alternative_scacs
+ : undefined,
+ billOfLadingTrackingSupport: attrs.bill_of_lading_tracking_support,
+ bookingNumberTrackingSupport: attrs.booking_number_tracking_support,
+ containerNumberTrackingSupport: attrs.container_number_tracking_support,
+ };
})
- .filter(Boolean) as ShippingLine[];
+ .filter((line: ShippingLine | null): line is ShippingLine => line !== null);
}
export function mapContainer(doc: any): Container {
@@ -130,7 +173,10 @@ export function mapContainer(doc: any): Container {
const shipment = apiDoc.getRelationship(data, 'shipment');
const podTerminal = apiDoc.getRelationship(data, 'pod_terminal');
- const destTerminal = apiDoc.getRelationship(data, 'destination_terminal');
+ // The inland/destination facility is exposed via the `pickup_facility`
+ // relationship; there is no `destination_terminal` relationship on a
+ // container resource.
+ const pickupFacility = apiDoc.getRelationship(data, 'pickup_facility');
const transportEvents = apiDoc.included
.filter((item: any) => item.type === 'transport_event')
@@ -145,7 +191,8 @@ export function mapContainer(doc: any): Container {
? {
id: location.id,
name: location.attributes?.name,
- locode: location.attributes?.locode,
+ // Port/terminal resources expose `code`, not `locode`.
+ locode: location.attributes?.code ?? location.attributes?.locode,
}
: undefined,
terminal: terminal
@@ -161,9 +208,10 @@ export function mapContainer(doc: any): Container {
return {
id: data?.id,
- ...attrCamel,
+ ...omitKeys(attrCamel, CONTAINER_CURATED_ATTR_KEYS),
number: attrs.number || attrs.container_number,
- status: attrs.status,
+ status: attrs.current_status ?? attrs.status,
+ currentStatus: attrs.current_status,
equipment: {
type: attrs.equipment_type,
length: attrs.equipment_length,
@@ -191,12 +239,14 @@ export function mapContainer(doc: any): Container {
firmsCode: podTerminal.attributes?.firms_code,
}
: null,
- destinationTerminal: destTerminal
+ // The container's inland/final pickup facility (real `pickup_facility`
+ // relationship) is surfaced here as the destination terminal.
+ destinationTerminal: pickupFacility
? {
- id: destTerminal.id,
- name: destTerminal.attributes?.name,
- nickname: destTerminal.attributes?.nickname,
- firmsCode: destTerminal.attributes?.firms_code,
+ id: pickupFacility.id,
+ name: pickupFacility.attributes?.name,
+ nickname: pickupFacility.attributes?.nickname,
+ firmsCode: pickupFacility.attributes?.firms_code,
}
: null,
},
@@ -280,7 +330,8 @@ export function mapShipment(doc: any): Shipment {
shipment.ports = {
portOfLading: pol
? {
- locode: pol.attributes?.locode,
+ // Port resources expose `code` (e.g. KRPUS), not `locode`.
+ locode: pol.attributes?.code ?? pol.attributes?.locode,
name: pol.attributes?.name,
code: pol.attributes?.code,
countryCode: pol.attributes?.country_code,
@@ -291,7 +342,7 @@ export function mapShipment(doc: any): Shipment {
: null,
portOfDischarge: pod
? {
- locode: pod.attributes?.locode,
+ locode: pod.attributes?.code ?? pod.attributes?.locode,
name: pod.attributes?.name,
code: pod.attributes?.code,
countryCode: pod.attributes?.country_code,
diff --git a/sdks/typescript-sdk/src/fixtures/containers.get.pickup.json b/sdks/typescript-sdk/src/fixtures/containers.get.pickup.json
new file mode 100644
index 00000000..c79aebc6
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/containers.get.pickup.json
@@ -0,0 +1,65 @@
+{
+ "data": [
+ {
+ "id": "cont-pickup-1",
+ "type": "container",
+ "attributes": {
+ "number": "MSCU1234567",
+ "seal_number": "SEAL987",
+ "current_status": "available",
+ "equipment_type": "dry",
+ "equipment_length": 40,
+ "equipment_height": "high_cube",
+ "weight_in_lbs": 38000,
+ "available_for_pickup": true,
+ "location_at_pod_terminal": "Yard A-12",
+ "pod_arrived_at": "2026-06-15T03:00:00Z",
+ "pod_discharged_at": "2026-06-15T09:30:00Z",
+ "pickup_lfd": "2026-06-20T23:59:00Z",
+ "pickup_appointment_at": "2026-06-19T15:00:00Z",
+ "fees_at_pod_terminal": [],
+ "holds_at_pod_terminal": [],
+ "pod_rail_carrier_scac": "BNSF",
+ "ind_rail_carrier_scac": "UP",
+ "ind_eta_at": "2026-06-25T12:00:00Z"
+ },
+ "relationships": {
+ "shipment": { "data": { "id": "ship-pickup-1", "type": "shipment" } },
+ "pod_terminal": { "data": { "id": "term-pod-1", "type": "terminal" } },
+ "pickup_facility": {
+ "data": { "id": "term-pickup-1", "type": "terminal" }
+ },
+ "transport_events": { "data": [] },
+ "raw_events": { "data": [] }
+ }
+ }
+ ],
+ "included": [
+ {
+ "id": "ship-pickup-1",
+ "type": "shipment",
+ "attributes": {
+ "bill_of_lading_number": "MAEU123456789",
+ "shipping_line_scac": "MAEU"
+ }
+ },
+ {
+ "id": "term-pod-1",
+ "type": "terminal",
+ "attributes": {
+ "name": "APM Terminals Pier 400",
+ "nickname": "Pier 400",
+ "firms_code": "Y258"
+ }
+ },
+ {
+ "id": "term-pickup-1",
+ "type": "terminal",
+ "attributes": {
+ "name": "BNSF Logistics Park Chicago",
+ "nickname": "LPC",
+ "firms_code": "Z901"
+ }
+ }
+ ]
+}
diff --git a/sdks/typescript-sdk/src/fixtures/containers.route.json b/sdks/typescript-sdk/src/fixtures/containers.route.json
new file mode 100644
index 00000000..0c18fcf2
--- /dev/null
+++ b/sdks/typescript-sdk/src/fixtures/containers.route.json
@@ -0,0 +1,108 @@
+{
+ "data": {
+ "id": "route-1",
+ "type": "route",
+ "attributes": {
+ "created_at": "2026-06-01T10:00:00Z",
+ "updated_at": "2026-06-10T12:30:00Z"
+ },
+ "relationships": {
+ "route_locations": {
+ "data": [
+ { "id": "rl-1", "type": "route_location" },
+ { "id": "rl-2", "type": "route_location" }
+ ]
+ }
+ }
+ },
+ "included": [
+ {
+ "id": "rl-1",
+ "type": "route_location",
+ "attributes": {
+ "id": "rl-1",
+ "inbound_scac": null,
+ "inbound_mode": null,
+ "inbound_eta_at": null,
+ "inbound_ata_at": null,
+ "inbound_voyage_number": null,
+ "outbound_scac": "MAEU",
+ "outbound_mode": "vessel",
+ "outbound_etd_at": "2026-06-02T08:00:00Z",
+ "outbound_atd_at": "2026-06-02T09:15:00Z",
+ "outbound_voyage_number": "123W",
+ "created_at": "2026-06-01T10:00:00Z",
+ "updated_at": "2026-06-10T12:30:00Z"
+ },
+ "relationships": {
+ "route": { "data": { "id": "route-1", "type": "route" } },
+ "location": { "data": { "id": "port-cnsha", "type": "port" } },
+ "inbound_vessel": { "data": null },
+ "outbound_vessel": { "data": { "id": "vessel-1", "type": "vessel" } }
+ }
+ },
+ {
+ "id": "rl-2",
+ "type": "route_location",
+ "attributes": {
+ "id": "rl-2",
+ "inbound_scac": "MAEU",
+ "inbound_mode": "vessel",
+ "inbound_eta_at": "2026-06-20T14:00:00Z",
+ "inbound_ata_at": null,
+ "inbound_voyage_number": "123W",
+ "outbound_scac": null,
+ "outbound_mode": null,
+ "outbound_etd_at": null,
+ "outbound_atd_at": null,
+ "outbound_voyage_number": null,
+ "created_at": "2026-06-01T10:00:00Z",
+ "updated_at": "2026-06-10T12:30:00Z"
+ },
+ "relationships": {
+ "route": { "data": { "id": "route-1", "type": "route" } },
+ "location": { "data": { "id": "port-uslax", "type": "port" } },
+ "inbound_vessel": { "data": { "id": "vessel-1", "type": "vessel" } },
+ "outbound_vessel": { "data": null }
+ }
+ },
+ {
+ "id": "port-cnsha",
+ "type": "port",
+ "attributes": {
+ "id": "port-cnsha",
+ "name": "Shanghai",
+ "code": "CNSHA",
+ "state_abbr": "SH",
+ "city": "Shanghai",
+ "country_code": "CN",
+ "latitude": "31.22222",
+ "longitude": "121.45806",
+ "time_zone": "Asia/Shanghai"
+ }
+ },
+ {
+ "id": "port-uslax",
+ "type": "port",
+ "attributes": {
+ "id": "port-uslax",
+ "name": "Los Angeles",
+ "code": "USLAX",
+ "state_abbr": "CA",
+ "city": "Los Angeles",
+ "country_code": "US",
+ "latitude": "33.74537",
+ "longitude": "-118.27370",
+ "time_zone": "America/Los_Angeles"
+ }
+ },
+ {
+ "id": "vessel-1",
+ "type": "vessel",
+ "attributes": {
+ "name": "MAERSK EDINBURGH",
+ "imo": "9456769"
+ }
+ }
+ ]
+}
diff --git a/sdks/typescript-sdk/src/types/models.ts b/sdks/typescript-sdk/src/types/models.ts
index 309386ab..ce1a6cc9 100644
--- a/sdks/typescript-sdk/src/types/models.ts
+++ b/sdks/typescript-sdk/src/types/models.ts
@@ -5,6 +5,14 @@ export interface ShippingLine {
shortName?: string;
bolPrefix?: string;
notes?: string;
+ /** Additional SCACs the carrier tracks under. */
+ alternativeScacs?: string[];
+ /** Whether the carrier supports tracking by bill of lading number. */
+ billOfLadingTrackingSupport?: boolean;
+ /** Whether the carrier supports tracking by booking number. */
+ bookingNumberTrackingSupport?: boolean;
+ /** Whether the carrier supports tracking by container number. */
+ containerNumberTrackingSupport?: boolean;
}
/** Pagination links returned by Terminal49 list endpoints. */
@@ -29,6 +37,8 @@ export interface Container {
id: string;
number?: string;
status?: string;
+ /** Raw `current_status` from the API (also surfaced via `status`). */
+ currentStatus?: string;
equipment?: {
type?: string;
length?: number;
diff --git a/sdks/typescript-sdk/src/types/options.ts b/sdks/typescript-sdk/src/types/options.ts
index e112c4d7..79d71d28 100644
--- a/sdks/typescript-sdk/src/types/options.ts
+++ b/sdks/typescript-sdk/src/types/options.ts
@@ -30,6 +30,9 @@ export type ShipmentInclude =
export type ContainerInclude =
| 'shipment'
| 'pod_terminal'
+ // The inland/final-destination facility is exposed via `pickup_facility`;
+ // containers have no `destination_terminal` relationship (that one is on
+ // shipments). The container mapper resolves `destinationTerminal` from this.
| 'pickup_facility'
| 'transport_events';