# AGENTS Source: https://terminal49.com/docs/AGENTS # Documentation agent instructions These instructions guide automated changes for the Terminal49 docs in this repository. ## Scope * Primary docs live in `docs/` (MDX pages, `docs/docs.json`, and `docs/openapi.json`). * Do not edit generated files unless explicitly asked (e.g., `Terminal49-API.postman_collection.json`). ## Audience focus * Primary persona: integration engineers at BCOs/shippers/exporters who may not know logistics terms. * Secondary personas: logistics operators and decision-makers who know the domain but are less technical. * Each page should be laser-focused on one persona and one goal. ## Content goals by section * Getting Started: tutorial-style onboarding and first success within 30 minutes. * In Depth Guides: how-to and explanation content for workflows and best practices. * Useful Info: explanation/FAQ content that supports decisions and integrations. * API Reference: reference-only endpoint lookups, no narrative. For substantive documentation writing, use the repo-local skill at `../skills/terminal49-docs-writing/SKILL.md`. It includes content-type-specific guides for tutorials, how-to guides, reference pages, concepts, webhook/event docs, SDK/MCP docs, DataSync/coverage docs, changelog/update posts, and navigation. ## Voice and terminology * Sound like a domain expert but stay friendly and easy to understand. * Use active voice and second person ("you"). * Use consistent product terms: "Terminal49", "tracking request", "shipment", "container", "webhook". * Define acronyms on first use (e.g., Bill of Lading (BOL)); link to a glossary if available. * Approved positioning phrases (use where relevant, do not invent new claims): * Automated Container Tracking API * Tracking shipments and containers from empty-out at the origin to empty-return at the destination * Single API to track bill of ladings, bookings, and container numbers with global coverage * Complete import milestones in North America including rail data ## API and code examples * Base URL is `https://api.terminal49.com/v2` unless a page says otherwise. * Examples should be realistic but safe (no real keys, emails, or customer data). * Use JSON with 2-space indentation; label code fences (e.g., `json, `bash, \`\`\`json http). * Prefer copy-pasteable snippets with complete headers. * For auth examples, use `Authorization: Token YOUR_API_KEY`. ## When updating API reference * If you change API behavior or schemas, update `docs/openapi.json` first. * Regenerate the Postman collection with: `openapi2postmanv2 -s docs/openapi.json -o Terminal49-API.postman_collection.json -p -O folderStrategy=Tags` ## MDX conventions * Every page must include frontmatter with a `title`. * Use Mintlify components like `` or `` sparingly for emphasis. * Keep headings concise and action-oriented. # Create container custom field Source: https://terminal49.com/docs/api-docs/api-reference/containers/create-container-custom-field post /containers/{container_id}/custom_fields Create or update a custom field value on a container in the Terminal49 API. Attach internal metadata like reference codes, priority, or cost centers. Creates or updates a custom field on a container. If a custom field with the specified `api_slug` already exists, it will be updated. ## Path parameters | Parameter | Required | Description | | -------------- | -------- | ----------------------- | | `container_id` | Yes | The ID of the container | ## Request body | Parameter | Required | Description | | -------------------------- | -------- | ------------------------------------------------------------- | | `data.type` | Yes | Must be `custom_field` | | `data.attributes.api_slug` | Yes | The slug of the custom field definition | | `data.attributes.value` | Yes | The value to set (type depends on the definition's data type) | The container is implied by the path, so do not send `data.relationships.entity` on this endpoint. ## Authorization Requires `update` permission on the container. ## Response Returns `201 Created` with the custom field resource on success. ## Behavior * Uses `find_or_initialize_by` internally, so it creates if missing or updates if it exists * Values are validated against the definition's data type * For enum fields, values are validated against the definition's options ## Example request ```json theme={null} { "data": { "type": "custom_field", "attributes": { "api_slug": "customer_reference_number", "value": "ABC124" } } } ``` ## Example response ```json theme={null} { "data": { "id": "YOUR_CUSTOM_FIELD_ID", "type": "custom_field", "attributes": { "api_slug": "customer_reference_number", "value": "ABC124", "display_value": "ABC124" }, "relationships": { "entity": { "data": { "id": "YOUR_CONTAINER_ID", "type": "container" } }, "definition": { "data": { "id": "YOUR_DEFINITION_ID", "type": "custom_field_definition" } } } } } ``` # Delete container custom field Source: https://terminal49.com/docs/api-docs/api-reference/containers/delete-container-custom-field delete /containers/{container_id}/custom_fields/{api_slug} Remove a custom field value from a container in the Terminal49 API by referencing the custom field definition's api_slug for that container record. Deletes a specific custom field from a container by its `api_slug`. ## Path parameters | Parameter | Required | Description | | -------------- | -------- | -------------------------------------------- | | `container_id` | Yes | The ID of the container | | `api_slug` | Yes | The api\_slug of the custom field definition | ## Authorization Requires `update` permission on the container. ## Response Returns `204 No Content` on success. # Edit a container Source: https://terminal49.com/docs/api-docs/api-reference/containers/edit-a-container patch /containers Update editable container attributes in the Terminal49 API, including weight, seal number, and reference fields, on active or completed shipments. # Get a container Source: https://terminal49.com/docs/api-docs/api-reference/containers/get-a-container get /containers/{id} Retrieve a single container's full record from the Terminal49 API, including status, holds, fees, and last free day, using its container resource ID. # Get a container's raw events Source: https://terminal49.com/docs/api-docs/api-reference/containers/get-a-containers-raw-events get /containers/{id}/raw_events Retrieve raw, carrier-sourced container events from the Terminal49 API. Note: deprecated — use transport events for normalized milestone data instead. This endpoint is deprecated. For past milestone data, use [Get a container's transport events](/docs/api-docs/api-reference/containers/get-a-containers-transport-events). Returns past and estimated future milestones for a container as reported by the carrier. The `event` and `timestamp` attributes are normalized where possible; other attributes are passed through as-is. Not every value in `event` has a normalized name — container movement events are usually normalized, but exceptions occur. The response includes an `original_event` field with the raw carrier event name, which is not present on `transport_events`. ## `raw_events` vs `transport_events` | | `raw_events` | `transport_events` | | ------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------- | | Status | Deprecated | Recommended | | Source | Carrier feed, mostly as-is | Vetted and normalized across carriers | | Estimated events | Any event type can be estimated, flagged by `attributes.estimated` | Only three normalized `estimated.*` event types (see below) | | Includes `original_event` | Yes | No | | Data quality | More events, less consistent normalization | Fewer events, higher confidence | Use `transport_events` for past milestones. Use `raw_events` only when you need the carrier's original event name or estimated events that have no `transport_events` equivalent. ## Estimated events In `raw_events`, any event type can carry an estimated timestamp. The `attributes.estimated` boolean is `true` when the timestamp is an estimate, so estimated feeder, rail, and transshipment events appear here. In `transport_events`, estimates are separate event types, and only three exist: * `container.transport.estimated.vessel_departed` * `container.transport.estimated.vessel_arrived` * `container.transport.estimated.arrived_at_inland_destination` There are no estimated equivalents for feeder, rail, or transshipment events in `transport_events`. If you need estimated timestamps for those milestones, `raw_events` is currently the only endpoint that provides them. # Get a container's transport events Source: https://terminal49.com/docs/api-docs/api-reference/containers/get-a-containers-transport-events get /containers/{id}/transport_events List normalized transport events for a single container — vessel discharge, gate-out, rail ramp arrival, and more — from the Terminal49 tracking API. This endpoint returns the container's normalized transport event history — vessel, rail, transshipment, terminal/gate, delivery, and document events — across the entire journey. It does not accept filters for event type, data source, or timestamp. To narrow results to a subset (for example, terminal-only events such as `full_in`, `full_out`, `empty_out`, `empty_in`, `vessel_discharged`, `vessel_arrived`, or `vessel_berthed`), fetch the list and filter client-side on `attributes.event` or `attributes.data_source`. ## Pagination The response follows the standard Terminal49 pagination shape: a `links` object (`self`, `current`, `next`, `prev`, `last`) and a `meta` object with `size` (page size) and `total` (total events for the container). Most containers have far fewer transport events than one page, so `links.next` is usually absent and a single request returns the full history. When a container does have more events than fit on one page, follow `links.next` until it is absent. Do not construct pagination URLs by hand — use the URLs returned in `links`. ## Event types The `attributes.event` field is one of a fixed set of normalized event names covering vessel, rail, transshipment, feeder, terminal/gate, inland-destination, availability, and delivery milestones. The complete enum is defined on the `transport_event` schema in the OpenAPI spec and mirrored in the [Webhook Event Catalog](/docs/api-docs/webhooks/event-catalog), which describes what each event means. The `attributes.data_source` field indicates where the event originated: `shipping_line`, `terminal`, or `ais`. ### Estimated event types Three estimated event types can appear in the response: * `container.transport.estimated.vessel_departed` * `container.transport.estimated.vessel_arrived` * `container.transport.estimated.arrived_at_inland_destination` These are the only estimated events in `transport_events`. Feeder, rail, and transshipment events exist only as actual milestones here. For estimated timestamps on those event types, use the deprecated [raw events endpoint](/docs/api-docs/api-reference/containers/get-a-containers-raw-events), which flags estimates with an `attributes.estimated` boolean on any event type. ## Null locations and timezones Some events may have a `null` `location_locode` and `timezone` — most commonly on estimated events. See [Event Timestamps](/docs/api-docs/in-depth-guides/event-timestamps) for details on how to interpret those timestamps. ## `transport_events` vs `raw_events` `transport_events` is the recommended endpoint for milestone data. Events are normalized across carriers and go through additional vetting to reduce false positives. `raw_events` is deprecated and returns the carrier feed as-is; it contains more events but with less consistent normalization. See [Get a container's raw events](/docs/api-docs/api-reference/containers/get-a-containers-raw-events) for details. # Get container map GeoJSON Source: https://terminal49.com/docs/api-docs/api-reference/containers/get-container-map-geojson get /containers/{id}/map_geojson Retrieve a GeoJSON FeatureCollection for a container with port locations, current vessel position, past path, and estimated future route in one call. This endpoint returns a GeoJSON FeatureCollection containing all map-related data for a container in a single response. The response includes port locations, current vessel position (if at sea), past vessel paths, and estimated future routes. For detailed documentation on the response structure, feature types, and their properties, see the [Container Map GeoJSON Data guide](/docs/api-docs/in-depth-guides/routing). # List container custom fields Source: https://terminal49.com/docs/api-docs/api-reference/containers/list-container-custom-fields get /containers/{container_id}/custom_fields List every custom field value attached to a container in the Terminal49 API, including the api_slug, current value, and resolved option for enum fields. Lists all custom fields attached to a specific container. ## Path parameters | Parameter | Required | Description | | -------------- | -------- | ----------------------- | | `container_id` | Yes | The ID of the container | ## Authorization Requires `show` permission on the container. ## Response Returns a JSONAPI array of custom field resources including: * `value` - The raw stored value * `display_value` - Formatted value for display * Relationships to the definition and user who last updated the field # List containers Source: https://terminal49.com/docs/api-docs/api-reference/containers/list-containers get /containers List all containers in your Terminal49 account with filters for status, shipment, last free day, and pagination cursors for large result sets. # Refresh a container Source: https://terminal49.com/docs/api-docs/api-reference/containers/refresh-container patch /containers/{id}/refresh Force an immediate data refresh for a container in the Terminal49 API, pulling new status, milestones, holds, and last free day data from all sources. # Update container custom field Source: https://terminal49.com/docs/api-docs/api-reference/containers/update-container-custom-field patch /containers/{container_id}/custom_fields/{api_slug} Update an existing custom field value on a container in the Terminal49 API, identified by the custom field definition's api_slug for that container. Updates a specific custom field on a container by its `api_slug`. ## Path parameters | Parameter | Required | Description | | -------------- | -------- | -------------------------------------------- | | `container_id` | Yes | The ID of the container | | `api_slug` | Yes | The api\_slug of the custom field definition | ## Request body | Parameter | Required | Description | | --------- | -------- | -------------------- | | `value` | Yes | The new value to set | ## Authorization Requires `update` permission on the container. ## Response Returns `200 OK` with the updated custom field resource on success. # Create a custom field Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/create-a-custom-field post /custom_fields Create a custom field value record in the Terminal49 API, attaching a definition and value to a target shipment, container, or other supported resource. Use this endpoint to create a custom field value on a shipment or container when you need to send the full JSON:API relationship payload yourself. The field must reference an existing custom field definition. ## Request body | Parameter | Required | Description | | ------------------------------------- | -------- | ------------------------------------------------------- | | `data.type` | Yes | Must be `custom_field` | | `data.attributes.api_slug` | Yes | The slug of the custom field definition | | `data.attributes.value` | Yes | The field value (must match the definition's data type) | | `data.relationships.entity.data.type` | Yes | `shipment` or `container` | | `data.relationships.entity.data.id` | Yes | The shipment or container ID | ## Value formats by data type | Data type | Expected value format | | ------------ | --------------------------------------------------------------------------------------------------- | | `short_text` | Any string | | `number` | Numeric value | | `date` | Date string (parsed using definition's `default_format` or flexible parsing) | | `datetime` | DateTime string | | `boolean` | `true` or `false` | | `enum` | String matching one of the definition's option values | | `enum_multi` | Array of strings matching the definition's option values | | `reference` | Object identifying the referenced record, for example `{ "type": "shipment", "id": "SHIPMENT_ID" }` | ## Validation * Values are validated against the definition's data type * Enum values must match one of the definition's configured options * Reference values must match the definition's configured `reference_type` * The `api_slug` must reference a definition belonging to your account or a Terminal49 template ## Example request ```json theme={null} { "data": { "type": "custom_field", "attributes": { "api_slug": "customer_reference_number", "value": "ABC124" }, "relationships": { "entity": { "data": { "type": "shipment", "id": "YOUR_SHIPMENT_ID" } } } } } ``` ## Example response ```json theme={null} { "data": { "id": "YOUR_CUSTOM_FIELD_ID", "type": "custom_field", "attributes": { "api_slug": "customer_reference_number", "value": "ABC124", "display_value": "ABC124" }, "relationships": { "entity": { "data": { "id": "YOUR_SHIPMENT_ID", "type": "shipment" } }, "definition": { "data": { "id": "YOUR_DEFINITION_ID", "type": "custom_field_definition" } } } } } ``` # Create a custom field definition Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/create-a-custom-field-definition post /custom_field_definitions Create a new custom field definition in the Terminal49 API to attach structured metadata like reference codes or enums to shipments and containers. Create a custom field definition to describe metadata you want to store on shipments or containers. ## Request body | Parameter | Required | Description | | ---------------- | -------- | ------------------------------------------------------------------------------- | | `entity_type` | Yes | The entity type this field applies to (`Shipment` or `Container`) | | `api_slug` | Yes | Unique identifier for the field | | `display_name` | Yes | Human-readable name for the field | | `data_type` | Yes | Data type for values (for example: `short_text`, `number`, `date`, `reference`) | | `description` | No | Optional description of the field's purpose | | `validation` | No | Validation rules (for example: `required`, `pattern`, `max_length`) | | `default_format` | No | Default format string for numbers or dates | | `default_value` | No | Default value for new custom fields | | `reference_type` | No | Required when `data_type` is `reference` | # Create a custom field option Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/create-a-custom-field-option post /custom_field_definitions/{definition_id}/options Add an enum option to a custom field definition in the Terminal49 API so users can select it when applying the custom field to shipments or containers. Create a new option for an `enum` or `enum_multi` custom field definition. ## Path parameters | Parameter | Description | | --------------- | ---------------------------------------------------- | | `definition_id` | The unique identifier of the custom field definition | ## Request body | Parameter | Required | Description | | ---------- | -------- | ------------------------------------ | | `label` | Yes | Display label shown to users | | `value` | Yes | Stored value (unique per definition) | | `position` | No | Sort order for the option | ## Notes Options can only be added to definitions with `data_type` of `enum` or `enum_multi`. # Delete a custom field Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/delete-a-custom-field delete /custom_fields/{id} Delete a custom field value record from the Terminal49 API. The custom field definition is preserved for use on other shipments and containers. Use this endpoint to delete a custom field value from a shipment or container. ## Path parameters | Parameter | Description | | --------- | --------------------------------------------------------- | | `id` | The unique identifier of the custom field value to delete | ## Behavior * The custom field value is removed from the associated entity * Deleting a custom field value does not affect the underlying custom field definition # Delete a custom field definition Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/delete-a-custom-field-definition delete /custom_field_definitions/{id} Delete a custom field definition from your Terminal49 account, removing the field type and its applied values from shipments and containers permanently. Delete a custom field definition by its ID. ## Path parameters | Parameter | Description | | --------- | ---------------------------------------------------- | | `id` | The unique identifier of the custom field definition | ## Behavior Deleting a custom field definition also removes all associated custom field values. # Delete a custom field option Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/delete-a-custom-field-option delete /custom_field_definitions/{definition_id}/options/{option_id} Remove an enum option from a custom field definition in the Terminal49 API. Existing records using the deleted option are cleared on next save. Delete a custom field option by its ID. ## Path parameters | Parameter | Description | | --------------- | ---------------------------------------------------- | | `definition_id` | The unique identifier of the custom field definition | | `option_id` | The unique identifier of the option | ## Notes Deleting an option does not automatically update existing custom field values that reference it. # Get a custom field Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/get-a-custom-field get /custom_fields/{id} Retrieve a single custom field value record from the Terminal49 API, including the resolved value, related definition slug, and parent resource reference. Use this endpoint to retrieve a single custom field value by its ID. ## Path parameters | Parameter | Description | | --------- | ----------------------------------------------- | | `id` | The unique identifier of the custom field value | ## Response The response includes: * `value` - The raw stored value (type depends on the field's data type) * `display_value` - Human-readable formatted value * Relationships to the associated entity (shipment or container), definition, and the user who last updated it ## Data types Custom fields support these data types, each with specific value handling: | Data type | Storage | Display format | | ------------ | --------------------------------- | -------------------------------------- | | `short_text` | String | As-is | | `number` | Decimal (precision: 18, scale: 6) | Formatted per `default_format` | | `date` | Date | `YYYY-MM-DD` or custom format | | `datetime` | DateTime | `YYYY-MM-DD HH:MM:SS` or custom format | | `boolean` | Boolean | `Yes` or `No` | | `enum` | String | Option label | | `enum_multi` | Comma-separated string | Comma-separated labels | # Get a custom field definition Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/get-a-custom-field-definition get /custom_field_definitions/{id} Retrieve a single custom field definition from the Terminal49 API, including its data type, slug, target resource type, and configured enum options. Use this endpoint to retrieve a single custom field definition by its ID. ## Path parameters | Parameter | Description | | --------- | ---------------------------------------------------- | | `id` | The unique identifier of the custom field definition | # Get a custom field option Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/get-a-custom-field-option get /custom_field_definitions/{definition_id}/options/{option_id} Retrieve a single enum option for a custom field definition from the Terminal49 API, including the option's display label and persisted value identifier. Retrieve a single custom field option by its ID. ## Path parameters | Parameter | Description | | --------------- | ---------------------------------------------------- | | `definition_id` | The unique identifier of the custom field definition | | `option_id` | The unique identifier of the option | # List custom field definitions Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/list-custom-field-definitions get /custom_field_definitions List every custom field definition configured for your Terminal49 account, including data type, slug, and the target resource each definition applies to. List all custom field definitions available to your account. ## Query filters | Filter | Description | | ---------------------- | ------------------------------------------------- | | `filter[entity_type]` | Filter by entity type (`Shipment` or `Container`) | | `filter[data_type]` | Filter by data type | | `filter[display_name]` | Filter by display name (prefix match) | # List custom field options Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/list-custom-field-options get /custom_field_definitions/{definition_id}/options List every enum option configured for a custom field definition in the Terminal49 API. Use this to populate selectors in your tracking UI or app. List all options for a custom field definition. ## Path parameters | Parameter | Description | | --------------- | ---------------------------------------------------- | | `definition_id` | The unique identifier of the custom field definition | # List custom fields Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/list-custom-fields get /custom_fields List custom field value records across your account in the Terminal49 API, with filters for definition slug, target resource type, and resource ID. Use this endpoint to retrieve custom field values attached to your shipments and containers. Custom fields let you store additional metadata on entities to support your business workflows. ## Query filters Filter results using these query parameters: | Filter | Description | | ----------------------- | ------------------------------------------------- | | `filter[entity_type]` | Filter by entity type (`Shipment` or `Container`) | | `filter[entity_id]` | Filter by the ID of the shipment or container | | `filter[definition_id]` | Filter by custom field definition ID | ## Response The response includes: * `value` - The raw stored value * `display_value` - Formatted value for display (e.g., formatted numbers, date strings, enum labels) * Relationships to the entity, definition, and user who last updated the field # Update a custom field Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/update-a-custom-field patch /custom_fields/{id} Update the stored value on a custom field record in the Terminal49 API. The associated definition slug and parent resource reference remain unchanged. Use this endpoint to update an existing custom field value. ## Path parameters | Parameter | Description | | --------- | --------------------------------------------------------- | | `id` | The unique identifier of the custom field value to update | ## Request body | Parameter | Required | Description | | --------- | -------- | ----------------------------------------------------------- | | `value` | Yes | The new field value (must match the definition's data type) | ## Behavior * The new value is validated against the field definition's data type * For enum fields, the value must match one of the definition's configured options * The `updated_by` user is recorded for audit purposes * Update pathway tracking records the source of the change # Update a custom field definition Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/update-a-custom-field-definition patch /custom_field_definitions/{id} Update a custom field definition in the Terminal49 API, including its display label, data type constraints, and enum option set for shipments or containers. Update an existing custom field definition. ## Path parameters | Parameter | Description | | --------- | ---------------------------------------------------- | | `id` | The unique identifier of the custom field definition | ## Request body Provide the fields you want to update, such as `display_name`, `description`, `validation`, or `default_format`. ## Notes You cannot change `api_slug`, `entity_type`, or `data_type` after creation. # Update a custom field option Source: https://terminal49.com/docs/api-docs/api-reference/custom-fields/update-a-custom-field-option patch /custom_field_definitions/{definition_id}/options/{option_id} Update an enum option on a custom field definition in the Terminal49 API. Change the option's display label without invalidating records that reference it. Update an existing custom field option. ## Path parameters | Parameter | Description | | --------------- | ---------------------------------------------------- | | `definition_id` | The unique identifier of the custom field definition | | `option_id` | The unique identifier of the option | ## Request body Provide the fields you want to update, such as `label` or `position`. # Document representations resource Source: https://terminal49.com/docs/api-docs/api-reference/document-representations/document-representations-resource Understand the Terminal49 document_representation resource — how extracted document payloads appear via include parameters and document webhook events. **Beta Feature** - This endpoint is currently in beta. The API is stable, but the schema and behavior may evolve based on feedback. `document_representation` is a **resource type**, not a standalone endpoint. You receive it through: * document includes: `include=last_document_representation` * email submission nested includes: `include=documents.last_document_representation` * document webhook payloads (`document_representation.created`, `document_representation.failed`) in `included` ## Resource shape * `type`: `document_representation` * `attributes.schema_version`: public schema version string * `attributes.payload`: extracted key/value payload object * `attributes.created_at` * `attributes.updated_at` ## Where to fetch related schemas Use [`GET /document_schemas/{id}`](/docs/api-docs/api-reference/document-schemas/get-a-document-schema) to retrieve schema metadata and payload contracts for document extraction outputs. # Get a document schema Source: https://terminal49.com/docs/api-docs/api-reference/document-schemas/get-a-document-schema get /document_schemas/{id} Retrieve a Terminal49 document schema for an extraction output, including the schema version and the structured payload contract for parsed fields. **Beta Feature** - This endpoint is currently in beta. The API is stable, but the schema and behavior may evolve based on feedback. # Delete a document Source: https://terminal49.com/docs/api-docs/api-reference/documents/delete-a-document delete /documents/{id} Soft-delete (discard) a document record in the Terminal49 API. The underlying file is retained for compliance, but the record is hidden from list endpoints. **Beta Feature** - This endpoint is currently in beta. The API is stable, but the schema and behavior may evolve based on feedback. # Edit a document Source: https://terminal49.com/docs/api-docs/api-reference/documents/edit-a-document patch /documents/{id} Update manual extraction fields and classification metadata on a document in the Terminal49 API, including document type and operator-edited values. **Beta Feature** - This endpoint is currently in beta. The API is stable, but the schema and behavior may evolve based on feedback. # Get a document Source: https://terminal49.com/docs/api-docs/api-reference/documents/get-a-document get /documents/{id} Retrieve a single document record from the Terminal49 API, including classification, extraction status, and links to the file blob and last representation. **Beta Feature** - This endpoint is currently in beta. The API is stable, but the schema and behavior may evolve based on feedback. # Get a document download URL Source: https://terminal49.com/docs/api-docs/api-reference/documents/get-a-document-download-url get /documents/{id}/download_url Get a presigned download URL for a document file in the Terminal49 API to view, share, or stream the original file securely without proxying through your app. **Beta Feature** - This endpoint is currently in beta. The API is stable, but the schema and behavior may evolve based on feedback. # List document types Source: https://terminal49.com/docs/api-docs/api-reference/documents/list-document-types get /documents/types List the allowed document types and labels configured for your Terminal49 account so your app can populate selectors and validate uploads correctly. **Beta Feature** - This endpoint is currently in beta. The API is stable, but the schema and behavior may evolve based on feedback. # List documents Source: https://terminal49.com/docs/api-docs/api-reference/documents/list-documents get /documents List documents from the Terminal49 API with filters for shipment, container, document type, and extraction status, plus sorting and include parameters. **Beta Feature** - This endpoint is currently in beta. The API is stable, but the schema and behavior may evolve based on feedback. # Re-classify a document Source: https://terminal49.com/docs/api-docs/api-reference/documents/re-classify-a-document post /documents/{id}/reclassify Trigger asynchronous re-classification of a document in the Terminal49 API to recompute the document type using the latest classification model output. **Beta Feature** - This endpoint is currently in beta. The API is stable, but the schema and behavior may evolve based on feedback. # Re-extract a document Source: https://terminal49.com/docs/api-docs/api-reference/documents/re-extract-a-document post /documents/{id}/reextract Trigger asynchronous re-extraction of a document in the Terminal49 API to refresh structured field values using the latest extraction model and schema. **Beta Feature** - This endpoint is currently in beta. The API is stable, but the schema and behavior may evolve based on feedback. # Re-link a document Source: https://terminal49.com/docs/api-docs/api-reference/documents/re-link-a-document post /documents/{id}/relink Re-run reference linking on a document in the Terminal49 API so updated bill of lading, booking, or container numbers attach to the right shipment record. **Beta Feature** - This endpoint is currently in beta. The API is stable, but the schema and behavior may evolve based on feedback. # Rotate a document Source: https://terminal49.com/docs/api-docs/api-reference/documents/rotate-a-document post /documents/{id}/rotate Queue a rotation update for an image-based document in the Terminal49 API. PDFs and other non-image document types are not supported by this endpoint. **Beta Feature** - This endpoint is currently in beta. The API is stable, but the schema and behavior may evolve based on feedback. After rotation is accepted, request [`GET /documents/{id}/download_url`](/docs/api-docs/api-reference/documents/get-a-document-download-url) again to retrieve the updated image. # Upload a document Source: https://terminal49.com/docs/api-docs/api-reference/documents/upload-a-document post /documents Create a document record in the Terminal49 API by attaching an ActiveStorage signed blob ID. Triggers downstream classification and field extraction. **Beta Feature** - This endpoint is currently in beta. The API is stable, but the schema and behavior may evolve based on feedback. # Get an email submission Source: https://terminal49.com/docs/api-docs/api-reference/email-submissions/get-an-email-submission get /email_submissions/{id} Retrieve a single email submission from the Terminal49 API, including parsed sender metadata, attached documents, and reference linking results. # List email submissions Source: https://terminal49.com/docs/api-docs/api-reference/email-submissions/list-email-submissions get /email_submissions List inbound email submissions to your Terminal49 account with filters and includes, useful for auditing parsed documents and reference linking outcomes. # Introduction Source: https://terminal49.com/docs/api-docs/api-reference/introduction Complete REST API reference for tracking ocean shipments and containers. Covers BOL, booking, and container number endpoints with real-time webhooks. The Terminal49 API gives you a single integration to track Bills of Lading (BOLs), bookings, and container numbers across global ocean carriers. You get complete import milestones — from empty-out at origin to empty-return at destination, including rail data in North America. ## Prerequisites Before making API calls, you need: * A Terminal49 account ([start a free trial](https://app.terminal49.com/register)) * An API key from the [Developer Portal](https://app.terminal49.com/developers/api-keys) ## Base URL ``` https://api.terminal49.com/v2 ``` ## Authentication Include your API key in the `Authorization` header, prefixed with `Token` (not `Bearer`): ```bash theme={null} curl https://api.terminal49.com/v2/shipments \ -H "Authorization: Token YOUR_API_KEY" ``` Replace `YOUR_API_KEY` with the raw key value from the [developer portal](https://app.terminal49.com/developers/api-keys) — no quotes, no `Bearer` prefix, and no extra whitespace. A `401 Unauthorized` response with `"Terminal49 API key could not be verified"` means the key is missing, malformed, revoked, or truncated when it was copied. The full API key value is only shown once, right after you create it. Copy the complete token from that screen and store it in a secret manager before navigating away — after that, the value is masked and cannot be revealed again. If you lost the full value, create a new key, copy it immediately, then delete the older key. ### Restricted API access on Free plans Free-plan accounts can authenticate and create tracking requests, but read endpoints are gated. A valid key on a restricted account returns `401 Unauthorized` with `"You do not have permissions for using the API, except for creating tracking requests. All other permissions require a paid plan. See https://app.terminal49.com/settings/billing"` when you call any endpoint other than `POST /v2/tracking_requests`. To read tracking data (`GET /v2/shipments`, `GET /v2/containers`, `GET /v2/tracking_requests/{id}`, and other endpoints), your account needs full API access enabled. Full API access is not automatic on the Free plan. Contact [support@terminal49.com](mailto:support@terminal49.com) to enable a 7-day API trial, or see [Pricing](/docs/api-docs/useful-info/pricing) for plans that include ongoing API read access. ## Request and response format * The API follows the [JSON:API](https://jsonapi.org/) specification * All responses return `application/vnd.api+json` content type * Requests that include a body should set `Content-Type: application/vnd.api+json` * [JSON:API client libraries](https://jsonapi.org/implementations/#client-libraries) are available in most languages ### Example response ```json theme={null} { "data": { "id": "dabf9e1c-4ddc-4e4b-a701-2de1cdb38010", "type": "shipment", "attributes": { "status": "in_transit", "pod_eta": "2025-04-15T00:00:00Z", "shipping_line_name": "Maersk" } } } ``` ## Rate limits | Detail | Value | | ------------------- | ------------------------ | | Default limit | 100 requests per minute | | Scope | Per API key/account | | Window | Rolling 60 seconds | | Over-limit response | `429 Too Many Requests` | | Retry header | `Retry-After` in seconds | Some endpoints have their own rate-limit bucket. For example, Infer Tracking Number allows 200 requests per minute, Create Tracking Request allows 100 requests per minute, and Refresh Container allows 10 requests per minute. See the [rate limiting guide](/docs/api-docs/in-depth-guides/rate-limiting) for endpoint-specific limits, retry semantics, and best practices. Use [webhooks](/docs/api-docs/in-depth-guides/webhooks) instead of polling to receive real-time updates. This keeps you well within rate limits and gives you faster data. ## Core resources Create and manage tracking requests by BOL, booking, or container number. Retrieve shipment details, ETAs, and milestones. Container-level statuses, transport events, and map data. Subscribe to real-time push notifications when shipment data changes. ## Supporting resources Attach your own metadata to shipments and containers. Look up supported carriers and SCACs. Vessel details and future position data. Port lookups by UN/LOCODE. Terminal details at destination ports. Manage customer and partner references. # Get a metro area Source: https://terminal49.com/docs/api-docs/api-reference/metro-areas/get-a-metro-area-using-the-unlocode-or-the-id get /metro_areas/{id} Retrieve a single metro area from the Terminal49 API by metro area ID or by UN/LOCODE so you can resolve city and region metadata for shipments. # Create a party Source: https://terminal49.com/docs/api-docs/api-reference/parties/create-a-party post /parties Create a party in the Terminal49 API. Use its ID as the customer on a tracking request or assign it to shipments and containers via party roles. # Edit a party Source: https://terminal49.com/docs/api-docs/api-reference/parties/edit-a-party patch /parties/{id} Update the company name of an existing party in the Terminal49 API with a PATCH request. Roles already assigned to the party remain unchanged. # Get a party Source: https://terminal49.com/docs/api-docs/api-reference/parties/get-a-party get /parties/{id} Retrieve a single party by ID from the Terminal49 API. The response returns the company name for the party record stored in your account. # List parties Source: https://terminal49.com/docs/api-docs/api-reference/parties/list-parties get /parties List the parties in your Terminal49 account. Parties are companies you assign to tracking requests, shipments, and containers through party roles. # Assign a container party role Source: https://terminal49.com/docs/api-docs/api-reference/party-roles/assign-a-container-party-role post /containers/{container_id}/party_roles Assign a party to a container in the Terminal49 API as pickup dray carrier. Roles are additive, and duplicate party-role pairs return a 422 error. Attaches a party from your account to the container in one role. ## Path parameters | Parameter | Required | Description | | -------------- | -------- | ----------------------- | | `container_id` | Yes | The ID of the container | ## Roles `pickup_dray_carrier` only ## Behavior * Roles are additive. Posting a second party with the same role keeps both * To replace a party, delete its role and create a new one * Posting the same party and role twice returns `422` * The party must belong to your account, otherwise the request returns `401` * Any other role returns `422` # Assign a shipment party role Source: https://terminal49.com/docs/api-docs/api-reference/party-roles/assign-a-shipment-party-role post /shipments/{shipment_id}/party_roles Assign a party to a shipment in the Terminal49 API as shipper, consignee, notify party, customs broker, customer, freight forwarder, or dray carrier. Attaches a party from your account to the shipment in one role. ## Path parameters | Parameter | Required | Description | | ------------- | -------- | ---------------------- | | `shipment_id` | Yes | The ID of the shipment | ## Roles `shipper`, `consignee`, `notify_party`, `customs_broker`, `customer`, `freight_forwarder`, `pickup_dray_carrier` ## Behavior * Roles are additive. Posting a second party with the same role keeps both * To replace a party, delete its role and create a new one * Posting the same party and role twice returns `422` * The party must belong to your account, otherwise the request returns `401` # List container party roles Source: https://terminal49.com/docs/api-docs/api-reference/party-roles/list-container-party-roles get /containers/{container_id}/party_roles List the party roles on a container in the Terminal49 API. Returns each role, such as pickup dray carrier, with the linked party in included. Returns every party role on the container. The linked parties are returned in `included`. ## Path parameters | Parameter | Required | Description | | -------------- | -------- | ----------------------- | | `container_id` | Yes | The ID of the container | # List shipment party roles Source: https://terminal49.com/docs/api-docs/api-reference/party-roles/list-shipment-party-roles get /shipments/{shipment_id}/party_roles List the party roles on a shipment in the Terminal49 API. Returns each role, such as shipper or consignee, with the linked party in included. Returns every party role on the shipment. The linked parties are returned in `included`. ## Path parameters | Parameter | Required | Description | | ------------- | -------- | ---------------------- | | `shipment_id` | Yes | The ID of the shipment | # Remove a container party role Source: https://terminal49.com/docs/api-docs/api-reference/party-roles/remove-a-container-party-role delete /containers/{container_id}/party_roles/{id} Remove a party role from a container in the Terminal49 API. Detaches the party from the container while keeping the party record in your account. Detaches the party from the container. The party itself is not deleted. ## Path parameters | Parameter | Required | Description | | -------------- | -------- | ------------------------------------------------ | | `container_id` | Yes | The ID of the container | | `id` | Yes | The ID of the party role, from the list endpoint | # Remove a shipment party role Source: https://terminal49.com/docs/api-docs/api-reference/party-roles/remove-a-shipment-party-role delete /shipments/{shipment_id}/party_roles/{id} Remove a party role from a shipment in the Terminal49 API. Detaches the party from the shipment while keeping the party record in your account. Detaches the party from the shipment. The party itself is not deleted. ## Path parameters | Parameter | Required | Description | | ------------- | -------- | ------------------------------------------------ | | `shipment_id` | Yes | The ID of the shipment | | `id` | Yes | The ID of the party role, from the list endpoint | # Get a port Source: https://terminal49.com/docs/api-docs/api-reference/ports/get-a-port-using-the-locode-or-the-id get /ports/{id} Retrieve a single port from the Terminal49 API by port ID or UN/LOCODE so you can resolve port names, country codes, and timezones for ocean shipments. # Search Source: https://terminal49.com/docs/api-docs/api-reference/search/search get /search Full-text search across shipments, containers, and tracking requests in your Terminal49 account by BL number, container number, or reference number. # Create shipment custom field Source: https://terminal49.com/docs/api-docs/api-reference/shipments/create-shipment-custom-field post /shipments/{shipment_id}/custom_fields Create or update a custom field value on a shipment in the Terminal49 API. Attach internal metadata like PO numbers, customer codes, or workflow tags. Creates or updates a custom field on a shipment. If a custom field with the specified `api_slug` already exists, it will be updated. ## Path parameters | Parameter | Required | Description | | ------------- | -------- | ---------------------- | | `shipment_id` | Yes | The ID of the shipment | ## Request body | Parameter | Required | Description | | -------------------------- | -------- | ------------------------------------------------------------- | | `data.type` | Yes | Must be `custom_field` | | `data.attributes.api_slug` | Yes | The slug of the custom field definition | | `data.attributes.value` | Yes | The value to set (type depends on the definition's data type) | The shipment is implied by the path, so do not send `data.relationships.entity` on this endpoint. ## Authorization Requires `update` permission on the shipment. ## Response Returns `201 Created` with the custom field resource on success. ## Behavior * Uses `find_or_initialize_by` internally, so it creates if missing or updates if it exists * Values are validated against the definition's data type * For enum fields, values are validated against the definition's options ## Example request ```json theme={null} { "data": { "type": "custom_field", "attributes": { "api_slug": "customer_reference_number", "value": "ABC124" } } } ``` ## Example response ```json theme={null} { "data": { "id": "YOUR_CUSTOM_FIELD_ID", "type": "custom_field", "attributes": { "api_slug": "customer_reference_number", "value": "ABC124", "display_value": "ABC124" }, "relationships": { "entity": { "data": { "id": "YOUR_SHIPMENT_ID", "type": "shipment" } }, "definition": { "data": { "id": "YOUR_DEFINITION_ID", "type": "custom_field_definition" } } } } } ``` # Delete shipment custom field Source: https://terminal49.com/docs/api-docs/api-reference/shipments/delete-shipment-custom-field delete /shipments/{shipment_id}/custom_fields/{api_slug} Remove a custom field value from a shipment in the Terminal49 API by referencing the custom field definition's api_slug for that shipment record. Deletes a specific custom field from a shipment by its `api_slug`. ## Path parameters | Parameter | Required | Description | | ------------- | -------- | -------------------------------------------- | | `shipment_id` | Yes | The ID of the shipment | | `api_slug` | Yes | The api\_slug of the custom field definition | ## Authorization Requires `update` permission on the shipment. ## Response Returns `204 No Content` on success. # Edit a shipment Source: https://terminal49.com/docs/api-docs/api-reference/shipments/edit-a-shipment patch /shipments/{id} Update editable shipment attributes in the Terminal49 API, including reference numbers, customer party, and operator-managed fields without retracking. # Get a shipment Source: https://terminal49.com/docs/api-docs/api-reference/shipments/get-a-shipment get /shipments/{id} Retrieve a single shipment from the Terminal49 API by ID, returning the full shipment record with carrier, ports, ETA, and references in one call. # List shipment custom fields Source: https://terminal49.com/docs/api-docs/api-reference/shipments/list-shipment-custom-fields get /shipments/{shipment_id}/custom_fields List every custom field value attached to a shipment in the Terminal49 API, including the api_slug, current value, and resolved option for enum fields. Lists all custom fields attached to a specific shipment. ## Path parameters | Parameter | Required | Description | | ------------- | -------- | ---------------------- | | `shipment_id` | Yes | The ID of the shipment | ## Authorization Requires `show` permission on the shipment. ## Response Returns a JSONAPI array of custom field resources including: * `value` - The raw stored value * `display_value` - Formatted value for display * Relationships to the definition and user who last updated the field # List shipments Source: https://terminal49.com/docs/api-docs/api-reference/shipments/list-shipments get /shipments List all shipments in your Terminal49 account with filters for status, carrier, and date, plus pagination cursors for working through large result sets. # Resume tracking a shipment Source: https://terminal49.com/docs/api-docs/api-reference/shipments/resume-tracking-shipment patch /shipments/{id}/resume_tracking Resume tracking on a previously paused shipment in the Terminal49 API to start receiving milestone updates and webhook notifications again. # Stop tracking a shipment Source: https://terminal49.com/docs/api-docs/api-reference/shipments/stop-tracking-shipment patch /shipments/{id}/stop_tracking Stop tracking a shipment in the Terminal49 API to pause data refreshes and webhook notifications while preserving previously collected shipment history. # Update shipment custom field Source: https://terminal49.com/docs/api-docs/api-reference/shipments/update-shipment-custom-field patch /shipments/{shipment_id}/custom_fields/{api_slug} Update an existing custom field value on a shipment in the Terminal49 API, identified by the custom field definition's api_slug for that shipment record. Updates a specific custom field on a shipment by its `api_slug`. ## Path parameters | Parameter | Required | Description | | ------------- | -------- | -------------------------------------------- | | `shipment_id` | Yes | The ID of the shipment | | `api_slug` | Yes | The api\_slug of the custom field definition | ## Request body | Parameter | Required | Description | | --------- | -------- | -------------------- | | `value` | Yes | The new value to set | ## Authorization Requires `update` permission on the shipment. ## Response Returns `200 OK` with the updated custom field resource on success. # Get a single shipping line Source: https://terminal49.com/docs/api-docs/api-reference/shipping-lines/get-a-single-shipping-line get /shipping_lines/{id} Retrieve a single ocean shipping line from the Terminal49 API by ID, returning the carrier name, SCAC, and supported tracking number formats. # List shipping lines Source: https://terminal49.com/docs/api-docs/api-reference/shipping-lines/shipping-lines get /shipping_lines List every ocean shipping line supported by Terminal49, including SCAC codes and carrier names. This endpoint returns the full list without pagination. # Get a terminal Source: https://terminal49.com/docs/api-docs/api-reference/terminals/get-a-terminal-using-the-id get /terminals/{id} Retrieve a single terminal from the Terminal49 API by terminal ID, including the terminal name, port, country, timezone, and operator metadata. # Infer Tracking Number (Beta) Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/auto-detect-carrier post /tracking_requests/infer_number Predict the carrier SCAC and tracking number type from a bill of lading, booking, or container number using the Terminal49 Infer endpoint. **Beta Feature** - This endpoint is currently in beta. The API is stable, but the schema and behavior may evolve based on feedback. ## What this endpoint does Provide a tracking number (container, bill of lading, or booking). The endpoint returns: * The **predicted VOCC carrier SCAC** to use for tracking * The **predicted number type** * A confidence-driven **decision** (`auto_select`, `needs_confirmation`, `no_prediction`) Terminal49 uses machine learning prediction across container, bill of lading, and booking numbers. For container numbers, Terminal49 leverages tens of millions of historical container movements to predict which carrier is moving the container (about 9 out of 10 times). ## How to use the result This endpoint has its own rate-limit bucket: 200 requests per minute per API key. Learn how to use Infer Tracking Number to reliably create tracking requests # Create a tracking request Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/create-a-tracking-request post /tracking_requests Create a new tracking request in the Terminal49 API with a bill of lading, booking, or container number plus a carrier SCAC to start tracking a shipment. **Don't know the SCAC?** Set `auto_detect_vocc_scac` to `true` and omit `scac`, and Terminal49 will infer the carrier SCAC for you. Detection runs asynchronously: the request is created immediately with `status: "pending"` and `scac: null`, then resolves to `created` (with the detected `scac` populated) or `failed` (`failed_reason: "scac_auto_detect_failed"`) — poll the tracking request or use webhooks to observe the outcome. Use [Auto-Detect Carrier](/docs/api-docs/api-reference/tracking-requests/auto-detect-carrier) first when your workflow needs to preview or confirm carrier candidates before submitting. This endpoint has its own rate-limit bucket: 100 tracking requests per minute per API key/account. ## Setting custom field values before the shipment exists Use `initial_custom_fields` to stage custom field values at creation time, before the shipment and containers exist. Terminal49 applies `shipment` entries to the shipment and `containers` entries to their containers once the tracking request resolves. Each `api_slug` must match an existing [custom field definition](/docs/api-docs/api-reference/custom-fields/create-a-custom-field-definition) on your account — a `Shipment`-scoped definition for `shipment` entries, a `Container`-scoped definition for `containers` entries. Container entries without a `number` — or with `number` set to an empty string — are applied to every container on the shipment; include a non-empty `number` to target one specific container. ```json theme={null} { "data": { "type": "tracking_request", "attributes": { "request_number": "MEDUAI047070", "request_type": "bill_of_lading", "scac": "MSCU", "initial_custom_fields": { "shipment": [ { "api_slug": "booking_reference", "value": "BOOK-2026-001" } ], "containers": [ { "api_slug": "po_number", "value": "PO-123", "number": "MSCU1234567" }, { "api_slug": "po_number", "value": "PO-999", "number": "TCLU7654321" }, { "api_slug": "customs_broker", "value": "Acme Brokerage" }, { "api_slug": "seal_number", "value": "SEAL-0001", "number": "" } ] } } } } ``` In this example, `booking_reference` is set on the shipment; `po_number` is set per container by `number`; and `customs_broker` and `seal_number` — which omit `number` or pass an empty string — are applied to every container on the shipment. # Create tracking request custom field Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/create-tracking-request-custom-field post /tracking_requests/{tracking_request_id}/custom_fields Create or update a custom field value on a tracking request in the Terminal49 API. Attach internal metadata like references, intake tags, or workflow flags. Creates or updates a custom field on a tracking request. If a custom field with the specified `api_slug` already exists, it will be updated. ## Path parameters | Parameter | Required | Description | | --------------------- | -------- | ------------------------------ | | `tracking_request_id` | Yes | The ID of the tracking request | ## Request body | Parameter | Required | Description | | -------------------------- | -------- | ------------------------------------------------------------- | | `data.type` | Yes | Must be `custom_field` | | `data.attributes.api_slug` | Yes | The slug of the custom field definition | | `data.attributes.value` | Yes | The value to set (type depends on the definition's data type) | The tracking request is implied by the path, so do not send `data.relationships.entity` on this endpoint. ## Authorization Requires `update` permission on the tracking request. ## Response Returns `201 Created` with the custom field resource on success. ## Behavior * Uses `find_or_initialize_by` internally, so it creates if missing or updates if it exists * Values are validated against the definition's data type * For enum fields, values are validated against the definition's options ## Example request ```json theme={null} { "data": { "type": "custom_field", "attributes": { "api_slug": "customer_reference_number", "value": "ABC124" } } } ``` ## Example response ```json theme={null} { "data": { "id": "YOUR_CUSTOM_FIELD_ID", "type": "custom_field", "attributes": { "api_slug": "customer_reference_number", "value": "ABC124", "display_value": "ABC124" }, "relationships": { "entity": { "data": { "id": "YOUR_TRACKING_REQUEST_ID", "type": "tracking_request" } }, "definition": { "data": { "id": "YOUR_DEFINITION_ID", "type": "custom_field_definition" } } } } } ``` # Delete tracking request custom field Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/delete-tracking-request-custom-field delete /tracking_requests/{tracking_request_id}/custom_fields/{api_slug} Remove a custom field value from a tracking request in the Terminal49 API by referencing the custom field definition's api_slug for that tracking request. Deletes a specific custom field from a tracking request by its `api_slug`. ## Path parameters | Parameter | Required | Description | | --------------------- | -------- | -------------------------------------------- | | `tracking_request_id` | Yes | The ID of the tracking request | | `api_slug` | Yes | The api\_slug of the custom field definition | ## Authorization Requires `update` permission on the tracking request. ## Response Returns `204 No Content` on success. # Edit a tracking request Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/edit-a-tracking-request patch /tracking_requests/{id} Update an existing tracking request in the Terminal49 API, including its associated party reference, customer metadata, and operator-managed fields. # Get a tracking request Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/get-a-single-tracking-request get /tracking_requests/{id} Retrieve a single tracking request from the Terminal49 API by ID, including its current status, failure reason, retry count, and associated shipment ID. # List tracking request custom fields Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/list-tracking-request-custom-fields get /tracking_requests/{tracking_request_id}/custom_fields List every custom field value attached to a tracking request in the Terminal49 API, including the api_slug, current value, and resolved option for enum fields. Lists all custom fields attached to a specific tracking request. ## Path parameters | Parameter | Required | Description | | --------------------- | -------- | ------------------------------ | | `tracking_request_id` | Yes | The ID of the tracking request | ## Authorization Requires `show` permission on the tracking request. ## Response Returns a JSONAPI array of custom field resources including: * `value` - The raw stored value * `display_value` - Formatted value for display * Relationships to the definition and user who last updated the field # List tracking requests Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/list-tracking-requests get /tracking_requests List tracking requests in your Terminal49 account with filters for status, carrier SCAC, and date, sorted with the most recent tracking requests first. # Update tracking request custom field Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/update-tracking-request-custom-field patch /tracking_requests/{tracking_request_id}/custom_fields/{api_slug} Update an existing custom field value on a tracking request in the Terminal49 API, identified by the custom field definition's api_slug for that record. Updates a specific custom field on a tracking request by its `api_slug`. ## Path parameters | Parameter | Required | Description | | --------------------- | -------- | -------------------------------------------- | | `tracking_request_id` | Yes | The ID of the tracking request | | `api_slug` | Yes | The api\_slug of the custom field definition | ## Request body | Parameter | Required | Description | | --------- | -------- | -------------------- | | `value` | Yes | The new value to set | ## Authorization Requires `update` permission on the tracking request. ## Response Returns `200 OK` with the updated custom field resource on success. # Get a vessel by ID Source: https://terminal49.com/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id get /vessels/{id} Retrieve a vessel from the Terminal49 API using its internal vessel ID, including name, IMO number, and optional position data for paid plan accounts. # Get a vessel by IMO Source: https://terminal49.com/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo get /vessels/{imo} Retrieve a vessel from the Terminal49 API by IMO number, including the vessel name, internal ID, and optional AIS position data on paid plan accounts. # Get vessel future positions Source: https://terminal49.com/docs/api-docs/api-reference/vessels/get-vessel-future-positions get /vessels/{id}/future_positions Retrieve a vessel's estimated future route between two ports from the Terminal49 API, returning a sequence of positions spaced one minute apart. This is a paid Routing Data endpoint. See [Entitlements and Paid Features](/docs/api-docs/useful-info/entitlements) for access requirements and non-entitled responses. This endpoint requires the destination port ID (`port_id`) and the previous port ID (`previous_port_id`) for the vessel leg you want to forecast. ## How to find the port IDs If you are starting from a tracked container, call [`GET /v2/containers/{id}/map_geojson`](/docs/api-docs/api-reference/containers/get-container-map-geojson) and inspect the `port` features. Each port feature includes: * `properties.location_id` - the port UUID to pass as `port_id` or `previous_port_id` * `properties.ports_sequence` - the route order * `properties.label` - route label such as `POL`, `POD`, or transshipment labels Use the destination port's `location_id` as `port_id`, and the preceding port's `location_id` as `previous_port_id`. # Get vessel future positions from coordinates Source: https://terminal49.com/docs/api-docs/api-reference/vessels/get-vessel-future-positions-with-coordinates get /vessels/{id}/future_positions_with_coordinates Retrieve a vessel's estimated future route between two coordinate pairs from the Terminal49 API, with one-minute spacing on returned position timestamps. # Get a single webhook notification Source: https://terminal49.com/docs/api-docs/api-reference/webhook-notifications/get-a-single-webhook-notification get /webhook_notifications/{id} Retrieve a single webhook notification record from the Terminal49 API to inspect its delivery status, payload, and the originating shipment or container event. # Get webhook notification payload examples Source: https://terminal49.com/docs/api-docs/api-reference/webhook-notifications/get-webhook-notification-payload-examples get /webhook_notifications/examples Get an example webhook notification payload from the Terminal49 API for a given event type. Use these payloads to build and test webhook consumers. # List webhook notifications Source: https://terminal49.com/docs/api-docs/api-reference/webhook-notifications/list-webhook-notifications get /webhook_notifications List webhook notifications from the Terminal49 API to reconcile delivered and undelivered events when your endpoint has been down or behind on processing. # Create a webhook Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/create-a-webhook post /webhooks Register a new webhook endpoint with the Terminal49 API to receive real-time notifications for tracking request, shipment, and container event changes. # Delete a webhook Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/delete-a-webhook delete /webhooks/{id} Permanently delete a webhook subscription from the Terminal49 API. Once deleted, the endpoint will stop receiving event notifications immediately. # Edit a webhook Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/edit-a-webhook patch /webhooks/{id} Update an existing webhook subscription in the Terminal49 API, including its target URL, subscribed event types, and active status flag without losing history. # Get a single webhook Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/get-single-webhook get /webhooks/{id} Retrieve a single webhook subscription from the Terminal49 API, including its target URL, subscribed event types, signing secret, and active status flag. # List webhook events Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/list-webhook-events get /webhooks/events Retrieve a paginated list of webhook events delivered by the Terminal49 API, including delivery status, payload metadata, and endpoint details. # List webhook IPs Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/list-webhook-ips get /webhooks/ips Retrieve the list of IP addresses Terminal49 uses to send webhook notifications. Use this list to whitelist Terminal49 traffic on your firewall or WAF. # List webhooks Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/list-webhooks get /webhooks List every webhook subscription configured for your Terminal49 account, including each endpoint URL, subscribed event types, and active status flag. # Trigger a webhook test delivery Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/trigger-a-webhook post /webhooks/trigger Send a one-time test webhook notification to a target HTTPS URL via the Terminal49 API without creating a persistent webhook endpoint configuration. # List Shipments and Containers Source: https://terminal49.com/docs/api-docs/getting-started/list-shipments-and-containers List tracked shipments and containers via the Terminal49 API, filter results by status, and retrieve the tracking data your integration needs. In this tutorial, you will list the shipment and container records created from your tracking requests. Use this step after you have created at least one tracking request. ## Shipment and container data in Terminal49 After Terminal49 accepts a tracking request, it starts collecting available data from carriers and terminals. You can retrieve the latest stored data at any time with the Shipments and Containers endpoints. Use these endpoints for on-demand lookups. For ongoing status monitoring, use webhooks instead of polling. ## Which object holds which field? Tracking data is split across two resources. If you query the wrong endpoint you will not see the field you expect — for example, `pod_eta_at` is **not** returned by `GET /containers` because it lives on the shipment. | Field | Object | Endpoint | | ------------------------------------------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------ | | `pod_eta_at` — current ETA at the port of discharge | `shipment` | `GET /shipments/{id}` | | `pod_original_eta_at` — first ETA reported by the carrier | `shipment` | `GET /shipments/{id}` | | `destination_eta_at` — ETA at the final destination (carrier view) | `shipment` | `GET /shipments/{id}` | | `pod_ata_at` — actual arrival at the port of discharge | `shipment` | `GET /shipments/{id}` | | `bill_of_lading_number` | `shipment` | `GET /shipments/{id}` | | `port_of_lading_name` / `port_of_discharge_name` | `shipment` | `GET /shipments/{id}` | | `shipping_line_scac` / `shipping_line_name` | `shipment` | `GET /shipments/{id}` | | `ref_numbers` | `shipment` | `GET /shipments/{id}` | | `number` — container number | `container` | `GET /containers/{id}` | | `pod_arrived_at` / `pod_discharged_at` | `container` | `GET /containers/{id}` | | `pod_full_out_at` — gated out of the port terminal | `container` | `GET /containers/{id}` | | `empty_terminated_at` — empty returned | `container` | `GET /containers/{id}` | | `pickup_lfd` — last free day | `container` | `GET /containers/{id}` | | `holds_at_pod_terminal` / `fees_at_pod_terminal` | `container` | `GET /containers/{id}` | | `available_for_pickup` / `availability_known` | `container` | `GET /containers/{id}` | | `ind_eta_at` / `ind_ata_at` — rail carrier ETA/ATA at inland destination | `container` | `GET /containers/{id}` (see [Rail integration guide](/docs/api-docs/in-depth-guides/rail-integration-guide)) | ### Fetching shipment fields alongside a container If you already have a container ID (or are filtering by container number) and want the shipment ETA fields in the same response, use the `include` query parameter to embed the related shipment: ```bash theme={null} curl "https://api.terminal49.com/v2/containers/{id}?include=shipment" \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" ``` The shipment record — including `pod_eta_at`, `pod_original_eta_at`, and `destination_eta_at` — is returned in the top-level `included` array. See [Include related resources](/docs/api-docs/in-depth-guides/including-resources) for the full syntax. ## Authentication As in the previous steps, every request sends your API key in the `Authorization` header: ```http theme={null} Authorization: Token YOUR_API_KEY ``` If you don't have an API key yet, get one from the [developer portal](https://app.terminal49.com/developers/api-keys) as described in [Start Here](/docs/api-docs/getting-started/start-here). ## List all your tracked shipments If your tracking request was successful, you will now be able to list your tracked shipments. Replace `YOUR_API_KEY` with your API key: ```bash theme={null} curl "https://api.terminal49.com/v2/shipments" \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" ``` Sometimes it takes a few minutes for a new tracking request to appear as a shipment. Copy the response into a text editor so you can inspect it while continuing the tutorial. Responses follow the JSON:API format, which is why they are larger and more structured than plain JSON. See the JSON:API note in [Track Shipments and Containers](/docs/api-docs/getting-started/tracking-shipments-and-containers#anatomy-of-a-tracking-request-response) for tips on parsing it. ## Inspect the shipment response The `/shipments` response returns an array of `shipment` objects. Each shipment includes attributes, relationships to related records, and a `self` link. For clarity, some fields have been replaced with ellipses (`...`), and inline comments call out the key fields. The **data** attribute contains an array of objects. Each object is of type `shipment` and includes attributes such as bill of lading number and port of lading. Each shipment object also has relationships to structured data objects like ports and terminals, as well as a list of containers on the shipment. You can access these structured elements through the API. Terminal49 cleans and enhances the data from the shipping line, so you get a pre-defined object for each port, terminal, and other entity. ```jsonc theme={null} { "data": [ { /* this is an internal id that you can use to query the API directly, i.e by hitting https://api.terminal49.com/v2/shipments/123456789 */ "id": "123456789", // the object type is a shipment, per below. "type": "shipment", "attributes": { // Your BOL number that you used in the tracking request "bill_of_lading_number": "99999999", ... "shipping_line_scac": "MAEU", "shipping_line_name": "Maersk", "port_of_lading_locode": "INVTZ", "port_of_lading_name": "Visakhapatnam", ... }, "relationships": { "port_of_lading": { "data": { "id": "bde5465a-1160-4fde-a026-74df9c362f65", "type": "port" } }, "port_of_discharge": { "data": { "id": "3d892622-def8-4155-94c5-91d91dc42219", "type": "port" } }, "pod_terminal": { "data": { "id": "99e1f6ba-a514-4355-8517-b4720bdc5f33", "type": "terminal" } }, "destination": { "data": null }, "containers": { "data": [ { "id": "593f3782-cc24-46a9-a6ce-b2f1dbf3b6b9", "type": "container" } ] } }, "links": { // this is a link to this specific shipment in the API. "self": "/v2/shipments/7f8c52b2-c255-4252-8a82-f279061fc847" } }, ... ], ... } ``` ## Sample code: listing tracked shipments in a Google Sheet Below is code written in Google App Script that lists the current shipments into the current sheet of a spreadsheet. App Script is very similar to Javascript. Because Google App Script does not have native JSON:API support, you need to parse the JSON directly, making this example an ideal real world application of the API. ```javascript theme={null} function listTrackedShipments(){ // first we construct the request. var options = { "method" : "GET", "headers" : { "content-type": "application/vnd.api+json", "authorization" : "Token YOUR_API_KEY" }, "payload" : "" }; try { // note that URLFetchApp is a function of Google App Script, not a standard JS function. var response = UrlFetchApp.fetch("https://api.terminal49.com/v2/shipments", options); var json = response.getContentText(); var shipments = JSON.parse(json)["data"]; var shipment_values = []; shipment_values = extractShipmentValues(shipments); listShipmentValues(shipment_values); } catch (error){ //In JS you would use console.log(), but App Script uses Logger.log(). Logger.log("error communicating with t49 / shipments: " + error); } } function extractShipmentValues(shipments){ var shipment_values = []; shipments.forEach(function(shipment){ // iterating through the shipments. shipment_values.push(extractShipmentData(shipment)); }); return shipment_values; } function extractShipmentData(shipment){ var shipment_val = []; //for each shipment I'm extracting some of the key info i want to display. shipment_val.push(shipment["attributes"]["shipping_line_scac"], shipment["attributes"]["shipping_line_name"], shipment["attributes"]["bill_of_lading_number"], shipment["attributes"]["pod_vessel_name"], shipment["attributes"]["port_of_lading_name"], shipment["attributes"]["pol_etd_at"], shipment["attributes"]["pol_atd_at"], shipment["attributes"]["port_of_discharge_name"], shipment["attributes"]["pod_eta_at"], shipment["attributes"]["pod_ata_at"], shipment["relationships"]["containers"]["data"].length, shipment["id"] ); return shipment_val; } function listShipmentValues(shipment_values){ // now, list the data in the spreadsheet. var ss = SpreadsheetApp.getActiveSpreadsheet(); var homesheet = ss.getActiveSheet(); var STARTING_ROW = 1; var MAX_TRACKED = 500; try { // clear the contents of the sheet first. homesheet.getRange(STARTING_ROW,1,MAX_TRACKED,shipment_values[0].length).clearContent(); // now insert all the shipment values directly into the sheet. homesheet.getRange(STARTING_ROW,1,shipment_values.length,shipment_values[0].length).setValues(shipment_values); } catch (error){ Logger.log("there was an error in listShipmentValues: " + error); } } ``` ## List all your tracked containers You can also list out all of your containers. Container data includes terminal availability, last free day, holds, fees, and other logistical information that you might use for drayage operations at port. To learn how to use holds and fees data to determine if a container is ready for pickup, see [Container Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). Replace `YOUR_API_KEY` with your API key: ```bash theme={null} curl "https://api.terminal49.com/v2/containers" \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" ``` We suggest copying the response into a text editor so you can examine it while continuing the tutorial. ## Anatomy of containers JSON response Now that you've got a list of containers, let's examine the response you've received. The example below is partial: it shows a single container object from the `data` array, with some fields omitted and inline comments calling out the key fields. ```jsonc theme={null} // We have an array of objects in the data returned. "data": [ { // "id": "internalid", // this object is of type Container. "type": "container", "attributes": { // Here is your container number "number": "OOLU-xxxx", // Seal Numbers aren't always returned by the carrier. "seal_number": null, "created_at": "2020-09-13T19:16:47Z", "equipment_type": "reefer", "equipment_length": null, "equipment_height": null, "weight_in_lbs": 54807, "fees_at_pod_terminal": [], "holds_at_pod_terminal": [], // here is your last free day. "pickup_lfd": "2020-09-17T07:00:00Z", "pickup_appointment_at": null, "availability_known": true, "available_for_pickup": false, "pod_arrived_at": "2020-09-13T22:05:00Z", "pod_discharged_at": "2020-09-15T05:27:00Z", "location_at_pod_terminal": "CC1-162-B-3(Deck)", "final_destination_full_out_at": null, "pod_full_out_at": "2020-09-18T10:30:00Z", "empty_terminated_at": null }, "relationships": { // linking back to the shipment object, found above. "shipment": { "data": { "id": "894befec-e7e2-4e48-ab97-xxxxxxxxx", "type": "shipment" } }, "pod_terminal": { "data": { "id": "39d09f18-cf98-445b-b6dc-xxxxxxxxx", "type": "terminal" } }, ... } }, ... ``` ## Next up: receive status updates You can now list your tracked shipments and containers on demand. The final step is to register a webhook so Terminal49 pushes updates to you as they happen. Register a webhook endpoint and handle your first notification. # Receive shipment status updates with webhooks Source: https://terminal49.com/docs/api-docs/getting-started/receive-status-updates Set up Terminal49 webhooks to receive real-time shipment and container status updates, including milestone events, whenever tracking data changes. In this tutorial, you will register a webhook endpoint and confirm the shape of the status updates Terminal49 sends. Use webhooks for ongoing tracking updates. Polling is useful for on-demand lookups, but it adds latency and consumes API rate limits. ## Before you start You need: * A Terminal49 API key. * A public HTTPS endpoint that can receive `POST` requests. * At least one active tracking request. For local testing, use a temporary endpoint from a tool such as webhook.site. For production, use an endpoint in your own application. ## Create a webhook endpoint You can create a webhook from the dashboard or the API. To use the dashboard: Go to [Developer Webhooks](https://app.terminal49.com/developers/webhooks) in your Terminal49 dashboard. Click **Create Webhook Endpoint** and enter your HTTPS endpoint URL. Select the events you want to receive, then save the webhook. To use the API, send: ```bash theme={null} curl -X POST "https://api.terminal49.com/v2/webhooks" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Content-Type: application/vnd.api+json" \ -d '{ "data": { "type": "webhook", "attributes": { "url": "https://example.com/webhooks/terminal49", "active": true, "events": ["tracking_request.succeeded", "container.updated"] } } }' ``` The response includes the webhook `id` and `secret`. Store the `secret` securely; you use it to verify webhook signatures. ## Receive the first event After Terminal49 detects a change for one of your tracked shipments or containers, it sends a `POST` request to your endpoint. Every notification has the same top-level shape: ```json theme={null} { "data": { "id": "87d4f5e3-df7b-4725-85a3-b80acc572e5d", "type": "webhook_notification", "attributes": { "event": "tracking_request.succeeded", "delivery_status": "pending", "created_at": "2026-05-11T18:30:00Z" } }, "included": [] } ``` Check `data.attributes.event` first. This tells your handler which code path to run. Common first events are: * `tracking_request.succeeded`: Terminal49 found the shipment and created tracking records. * `tracking_request.failed`: Terminal49 could not create tracking for the submitted number. * `container.updated`: One or more container attributes changed. ## Return a successful response Your endpoint should return a success status (200, 201, 202, or 204) after it durably accepts the event — persist or enqueue the payload first, then process it asynchronously. ```javascript theme={null} app.post("/webhooks/terminal49", express.raw({ type: "*/*" }), (req, res) => { const payload = JSON.parse(req.body.toString("utf8")); queueWebhookForProcessing(payload); res.sendStatus(202); }); ``` If Terminal49 receives another response code or the request times out, it retries the notification. ## Before you use webhooks in production Production webhook handlers should: 1. Verify the `X-T49-Webhook-Signature` header against the raw request body. 2. Allowlist Terminal49 webhook IPs. 3. Deduplicate by `data.id`. 4. Process asynchronously when work may take more than a few seconds. Follow [Setting up webhooks](/docs/api-docs/in-depth-guides/webhooks) for signature examples and [Webhook Best Practices](/docs/api-docs/webhooks/best-practices) for retry handling. ## Next steps Select the events your integration should subscribe to. Review the notification envelope and example payloads. # Open the TypeScript SDK Quickstart Source: https://terminal49.com/docs/api-docs/getting-started/sdk-quickstart Redirect page for the Terminal49 TypeScript SDK quickstart. Track a container and retrieve live shipment data in a few lines of code. The SDK documentation has moved to the top-level SDK Docs section. * [Open the SDK quickstart](/docs/sdk/quickstart) # Start Here: Track Shipments with the Terminal49 API Source: https://terminal49.com/docs/api-docs/getting-started/start-here Set up Terminal49 API access, generate an API key, and make your first tracking request to start monitoring ocean shipments and containers. This getting-started path walks you through a first successful Terminal49 integration: 1. Get an API key. 2. Create a tracking request for a bill of lading, booking, or container number. 3. List the shipment and container data Terminal49 stores for you. 4. Register a webhook so your system receives updates when data changes. By the end, you will have made an authenticated request to the API and seen the basic request/response shape used by the rest of the documentation. ## Before you start You need: * A Terminal49 account with API access. * A shipment identifier from a carrier: master Bill of Lading (BOL), booking number, or container number. * The carrier Standard Carrier Alpha Code (SCAC), unless you plan to use carrier auto-detection. You can use any HTTP client. [Postman](https://www.postman.com/) is useful for a first pass because it can run the published OpenAPI collection with your API key. Explore the Terminal49 API with the published Postman collection. ## Get an API key Sign in to your Terminal49 account and go to the [developer portal](https://app.terminal49.com/developers/api-keys) to get your API key. The full API key value is only shown once, right after you create it. Copy the complete token from that screen and store it somewhere safe (for example, a password manager or your deployment's secret manager) before you navigate away. Once you leave the page, the key is masked and cannot be revealed again. If you did not capture the full value, create a new key and copy it immediately. You can then delete the older key from the [developer portal](https://app.terminal49.com/developers/api-keys). ## Send the Authorization header The API uses a Token-prefixed API key in the `Authorization` header. Send it with every request: ```http theme={null} Authorization: Token YOUR_API_KEY ``` Keep your API key on your server. Do not put it in browser code or public repositories. ## Continue the tutorial Send your first shipment identifier to Terminal49. # Track Shipments and Containers Source: https://terminal49.com/docs/api-docs/getting-started/tracking-shipments-and-containers Create tracking requests for bill of lading, booking, and container numbers in the Terminal49 API and start receiving shipment milestone updates. In this tutorial, you will create a tracking request. A tracking request tells Terminal49 which shipment or container to monitor. Each tracking request needs two values: * A Bill of Lading (BOL), booking number, or container number from the carrier. * The carrier Standard Carrier Alpha Code (SCAC). You can see a complete list of supported SCACs in the [ocean carrier coverage list](/docs/coverage/ocean-carriers). **Don't know the SCAC?** Use the [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) endpoint (also called Auto-Detect Carrier) to identify the shipping line from your tracking number. ## Choose a tracking number **Supported numbers** 1. Master Bill of Lading number from the carrier (recommended) 2. Booking number from the carrier 3. Container number Container number tracking support varies by ocean carrier. Check the [ocean carrier coverage list](/docs/coverage/ocean-carriers) to see which carriers support container number tracking. **Unsupported numbers** * House Bill of Lading (HBOL) numbers * Customs entry numbers * Seal numbers * Internally generated numbers, such as purchase order numbers or customer reference numbers ## Authentication Every request in this tutorial sends your API key in the `Authorization` header: ```http theme={null} Authorization: Token YOUR_API_KEY ``` If you don't have an API key yet, get one from the [developer portal](https://app.terminal49.com/developers/api-keys) as described in [Start Here](/docs/api-docs/getting-started/start-here). ## Create a tracking request Replace `YOUR_API_KEY`, `REQUEST_NUMBER`, and `SCAC` before running this example. The request number must be a master bill of lading, booking, or container number from the carrier. ```bash cURL theme={null} curl -X POST "https://api.terminal49.com/v2/tracking_requests" \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "tracking_request", "attributes": { "request_type": "bill_of_lading", "request_number": "REQUEST_NUMBER", "scac": "SCAC" } } }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.terminal49.com/v2/tracking_requests", { method: "POST", headers: { "Content-Type": "application/vnd.api+json", "Authorization": "Token YOUR_API_KEY" }, body: JSON.stringify({ data: { type: "tracking_request", attributes: { request_type: "bill_of_lading", request_number: "REQUEST_NUMBER", scac: "SCAC" } } }) }); console.log(await response.json()); ``` Rate limiting: You can create up to 100 tracking requests per minute. ## Anatomy of a tracking request response The response confirms that Terminal49 accepted the request. A new request usually starts with `status: "pending"` while Terminal49 checks the carrier. ```json theme={null} { "data": { "id": "478cd7c4-a603-4bdf-84d5-3341c37c43a3", "type": "tracking_request", "attributes": { "request_number": "xxxxxx", "request_type": "bill_of_lading", "scac": "MAEU", "ref_numbers": [], "created_at": "2020-09-17T16:13:30Z", "updated_at": "2020-09-17T17:13:30Z", "status": "pending", "failed_reason": null, "is_retrying": false, "retry_count": null }, "relationships": { "tracked_object": { "data": null } }, "links": { "self": "/v2/tracking_requests/478cd7c4-a603-4bdf-84d5-3341c37c43a3" } } } ``` Note that if you try to track the same shipment again, you will receive an error like this: ```json theme={null} { "errors": [ { "status": "422", "source": { "pointer": "/data/attributes/request_number" }, "title": "Unprocessable Entity", "detail": "Request number 'xxxxxxx' with scac 'MAEU' already exists in a tracking_request with a pending or created status", "code": "duplicate" } ] } ``` **Why so much JSON? (A note on JSON:API)** The Terminal49 API is JSON:API compliant. JSON:API libraries can translate the response into a full object model compatible with an ORM, which is powerful but produces larger, more structured payloads. If you parse JSON directly, this can feel verbose. For production use, consider adopting a [JSON:API client library](https://jsonapi.org/implementations/#client-libraries) to get the most out of the format. For this tutorial, you will work with the data directly. ## What happens after you create a tracking request Terminal49 works asynchronously: 1. You send a tracking request with a shipment identifier and SCAC. 2. Terminal49 accepts the request and returns a `tracking_request` with `status: "pending"`. 3. Terminal49 monitors the carrier and creates shipment and container records as data becomes available. 4. You list shipments and containers at any time, or receive updates through a webhook. A webhook is a callback URL that Terminal49 sends `POST` requests to whenever tracking data changes: you receive `tracking_request.succeeded` when the shipment is created, or `tracking_request.failed` if there is a problem. You will register a webhook in [step 4 of this path](/docs/api-docs/getting-started/receive-status-updates). Until then, you can poll the tracking request as shown below. ## Check your tracking request status If you have not set up a webhook yet, poll the Tracking Requests endpoint to check whether your request succeeded or failed. Replace `YOUR_API_KEY` with your API key. ```bash theme={null} curl "https://api.terminal49.com/v2/tracking_requests" \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" ``` To check a single request, append the `id` from the create response: `GET /v2/tracking_requests/{id}`. ## Troubleshooting **Tracking request troubleshooting** The most common issue is entering the wrong number. Check that you are entering a Bill of Lading number, booking number, or container number — not an internal reference from your company or freight forwarder. Verify the number by going to the carrier's website and tracking the shipment with it. If that works and Terminal49 supports the SCAC, you should be able to track it through the API. If you are unsure of the correct SCAC, try the [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) endpoint first. Sometimes the issue is on the shipping line's side. Temporary network problems, unpopulated manifests, and other issues can occur. See [Tracking Request Retrying](/docs/api-docs/useful-info/tracking-request-retrying) for how Terminal49 handles these cases. You can always email us at [support@terminal49.com](mailto:support@terminal49.com) if you have persistent issues. ## Next up: get your shipments Now that you've made a tracking request, the next step is to list your shipments and retrieve the tracking data. Retrieve the shipment and container records Terminal49 created for you. See [How to initiate shipment tracking on Terminal49](https://help.terminal49.com/en/articles/8074102-how-to-initiate-shipment-tracking-on-terminal49) for other ways of initiating shipment tracking. # Add a Customer to a Tracking Request Source: https://terminal49.com/docs/api-docs/in-depth-guides/adding-customer Associate a customer party with a Terminal49 tracking request so new shipments automatically inherit the correct party relationship and metadata. This guide covers the `customer` role. For shipper, consignee, freight forwarder, notify party, customs broker, and dray carrier, see [Assign Shipper, Consignee, and Other Parties](/docs/api-docs/in-depth-guides/assigning-parties). ## Why add a party to a tracking request? Adding a party to a tracking request associates customer information with the request. The customer is assigned to the shipment when it is created, just like reference numbers and tags. This helps you organize and manage your shipments more effectively. ## How to get the party ID You can either find an existing party or create a new one. * To find an existing party, jump to [Listing all parties](#listing-all-parties) section. * To create a new party, jump to [Adding party for a customer](#adding-party-for-a-customer) section. ## List all parties You can list all parties associated with your account through the [API](/docs/api-docs/api-reference/parties/list-parties). Endpoint: **GET** - [https://api.terminal49.com/v2/parties](/docs/api-docs/api-reference/parties/list-parties) ```json Response theme={null} { "data": [ { "id": "PARTY_ID_1", "type": "party", "attributes": { "company_name": "COMPANY NAME 1", } }, { "id": "PARTY_ID_2", "type": "party", "attributes": { "company_name": "COMPANY NAME 2", } } ], "links": { "last": "", "next": "", "prev": "", "first": "", "self": "" }, "meta": { "size": 2, "total": 2 } } ``` After you get all the parties you would filter the parties by `company_name` to find the correct ID, either by looking through the list manually or using code to automate the process. ## Add a party to a tracking request To add a customer to a tracking request, include the party as a customer relationship when creating the request. Shipper, consignee, and the other roles are passed the same way, as relationships named after the role. To change roles on an existing shipment, use [party roles](/docs/api-docs/in-depth-guides/assigning-parties). Endpoint: **POST** - [https://api.terminal49.com/v2/tracking\_requests](/docs/api-docs/api-reference/tracking-requests/create-a-tracking-request) ```json Request theme={null} { "data": { "type": "tracking_request", "attributes": { "request_type": "bill_of_lading", "request_number": "MEDUFR030802", "ref_numbers": [ "PO12345", "HBL12345", "CUSREF1234" ], "shipment_tags": [ "camembert" ], "scac": "MSCU" }, "relationships": { "customer": { "data": { "id": "PARTY_ID", "type": "party" } } } } } ``` The response carries the tracking request ID. Its `customer` relationship references the party's linked account and is `null` when the party has none, which is the case for parties created through this API. Once the shipment exists, read the assigned party with [`GET /v2/shipments/SHIPMENT_ID/party_roles`](/docs/api-docs/api-reference/party-roles/list-shipment-party-roles). ```json Response theme={null} { "data": { "id": "TRACKING_REQUEST_ID", "type": "tracking_request", "attributes": { "request_type": "bill_of_lading", "request_number": "MEDUFR030802", "ref_numbers": [ "PO12345", "HBL12345", "CUSREF1234" ], "shipment_tags": [ "camembert" ], "scac": "MSCU" }, "relationships": { "tracked_object": { "data": null }, "customer": { "data": null } }, "links": { "self": "/v2/tracking_requests/TRACKING_REQUEST_ID" } } } ``` ## Create a party for a customer To add a customer to a tracking request, you first need to create a party. You can create a party through the [API](/docs/api-docs/api-reference/parties/create-a-party). Endpoint: **POST** - [https://api.terminal49.com/v2/parties](/docs/api-docs/api-reference/parties/create-a-party) ```json Request theme={null} { "data": { "type": "party", "attributes": { "company_name": "COMPANY NAME" } } } ``` After you send a **POST** request to create a party, you will receive a response with the Party ID. You can use this Party ID to add the customer to a tracking request. ```json Response theme={null} { "data": { "id": "PARTY_ID", "type": "party", "attributes": { "company_name": "COMPANY NAME" } } } ``` ## Edit a party You can update existing parties through the [API](/docs/api-docs/api-reference/parties/edit-a-party). Endpoint: **PATCH** - [https://api.terminal49.com/v2/parties/PARTY\_ID](/docs/api-docs/api-reference/parties/edit-a-party) ## Read a party You can retrieve the details of an existing party through the [API](/docs/api-docs/api-reference/parties/get-a-party). Endpoint: **GET** - [https://api.terminal49.com/v2/parties/PARTY\_ID](/docs/api-docs/api-reference/parties/get-a-party) # Assign Shipper, Consignee, and Other Parties Source: https://terminal49.com/docs/api-docs/in-depth-guides/assigning-parties Set the shipper, consignee, freight forwarder, notify party, customs broker, or dray carrier on shipments and containers through the Terminal49 API. Use this guide to assign parties to your shipments from code instead of the dashboard bulk update. ## How parties and roles work * A **party** is a company in your account, managed through [`/v2/parties`](/docs/api-docs/api-reference/parties/list-parties). * A **party role** links one party to one shipment or container in one role. * Roles are a list, not a field. A shipment can carry two parties as `consignee`. To replace a party, remove its role and assign a new one. | Role | Tracking request creation | Shipment | Container | | --------------------- | ------------------------- | -------- | --------- | | `customer` | Yes | Yes | No | | `shipper` | Yes | Yes | No | | `consignee` | Yes | Yes | No | | `notify_party` | Yes | Yes | No | | `customs_broker` | Yes | Yes | No | | `freight_forwarder` | Yes | Yes | No | | `pickup_dray_carrier` | Yes | Yes | Yes | You need an API key. See [Start here](/docs/api-docs/getting-started/start-here). ## Find or create the party Search your parties by name: ```bash theme={null} curl -s "https://api.terminal49.com/v2/parties?query=ACME" \ -H "Authorization: Token YOUR_API_KEY" ``` If the party does not exist, [create it](/docs/api-docs/api-reference/parties/create-a-party): ```json Request theme={null} { "data": { "type": "party", "attributes": { "company_name": "ACME LOGISTICS" } } } ``` Keep the returned `data.id`. It is the `PARTY_ID` below. ## Assign roles when creating the tracking request Pass each party as a relationship named after its role. One party per role. Endpoint: **POST** - [https://api.terminal49.com/v2/tracking\_requests](/docs/api-docs/api-reference/tracking-requests/create-a-tracking-request) ```json Request theme={null} { "data": { "type": "tracking_request", "attributes": { "request_type": "bill_of_lading", "request_number": "MEDUFR030802", "scac": "MSCU" }, "relationships": { "customer": { "data": { "id": "CUSTOMER_PARTY_ID", "type": "party" } }, "shipper": { "data": { "id": "SHIPPER_PARTY_ID", "type": "party" } }, "consignee": { "data": { "id": "CONSIGNEE_PARTY_ID", "type": "party" } }, "freight_forwarder": { "data": { "id": "FORWARDER_PARTY_ID", "type": "party" } } } } } ``` The roles are copied to the shipment when it is created. The tracking request response does not list them; read them on the shipment as shown below. A party from another account fails the whole request with `422` and a pointer to the relationship. ## Assign roles to an existing shipment Use this to add roles later, or to add a second party in the same role. Endpoint: **POST** - [https://api.terminal49.com/v2/shipments/SHIPMENT\_ID/party\_roles](/docs/api-docs/api-reference/party-roles/assign-a-shipment-party-role) ```json Request theme={null} { "data": { "type": "party_role", "attributes": { "role": "shipper" }, "relationships": { "party": { "data": { "id": "PARTY_ID", "type": "party" } } } } } ``` ```json Response theme={null} { "data": { "id": "PARTY_ROLE_ID", "type": "party_role", "attributes": { "role": "shipper", "roleable_type": "Shipment", "roleable_id": "SHIPMENT_ID", "created_at": "2026-09-01T14:02:11Z", "updated_at": "2026-09-01T14:02:11Z" }, "relationships": { "party": { "data": { "id": "PARTY_ID", "type": "party" } } } }, "included": [ { "id": "PARTY_ID", "type": "party", "attributes": { "company_name": "ACME LOGISTICS" } } ] } ``` Send one request per role. Repeat with `"role": "consignee"` and `"role": "freight_forwarder"`. ## Read the roles on the shipment Request the shipment with `flag[parties]=true` and include the parties: ```bash theme={null} curl -sg "https://api.terminal49.com/v2/shipments/SHIPMENT_ID?flag[parties]=true&include=party_roles.party" \ -H "Authorization: Token YOUR_API_KEY" ``` The shipment carries a `party_roles` relationship and the `included` array holds each `party_role` and its `party`. The same flag works on `GET /v2/shipments`. `-g` stops curl from treating the brackets in `flag[parties]` as a range. To list the roles alone, use [`GET /v2/shipments/SHIPMENT_ID/party_roles`](/docs/api-docs/api-reference/party-roles/list-shipment-party-roles). ## Replace a party To replace the consignee: 1. List the roles and find the `party_role` with `"role": "consignee"`. 2. **DELETE** [https://api.terminal49.com/v2/shipments/SHIPMENT\_ID/party\_roles/PARTY\_ROLE\_ID](/docs/api-docs/api-reference/party-roles/remove-a-shipment-party-role). Returns `204`. 3. **POST** the new consignee. ## Assign a dray carrier to a container Containers accept one role, `pickup_dray_carrier`: Endpoint: **POST** - [https://api.terminal49.com/v2/containers/CONTAINER\_ID/party\_roles](/docs/api-docs/api-reference/party-roles/assign-a-container-party-role) Read it back with [`GET /v2/containers/CONTAINER_ID/party_roles`](/docs/api-docs/api-reference/party-roles/list-container-party-roles). ## Errors | Status | Cause | Fix | | ------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------- | | `401` | The party or the record belongs to another account | Use a party from `GET /v2/parties` and a shipment you created | | `404` | Unknown shipment, container, or party role ID | Check the ID | | `422` `Party has already been taken` | The same party already has this role on the record | Nothing to do, the role is set | | `422` `'x' is not a valid role` | Unknown role name | Use a role from the table above | | `422` `Role is not allowed for Cargo` | A role other than `pickup_dray_carrier` on a container | Assign it on the shipment instead | # Identify your carrier with Infer Tracking Number Source: https://terminal49.com/docs/api-docs/in-depth-guides/auto-detect-carrier Use the Terminal49 Infer Tracking Number endpoint to identify a carrier SCAC from a bill of lading or container number before you create a tracking request. **Beta Feature** — This guide covers the [Infer Tracking Number](/docs/api-docs/api-reference/tracking-requests/auto-detect-carrier) endpoint (also known as Auto-Detect Carrier), currently in beta. The API is stable for production use, but features may expand based on feedback. Every tracking request requires two things: **your tracking number** and **the shipping line's (carrier's) SCAC code**. But what if you don't know the SCAC? That's where Infer Tracking Number comes in. You've seen this feature in action — when you enter a number, Terminal49 auto-suggests the carrier. Now this same intelligence is available via API. ## Why SCAC matters To track a shipment or container, Terminal49 needs to know **which shipping line to ask** (also called the vessel-operating common carrier (VOCC)). The SCAC (Standard Carrier Alpha Code) used here is the **shipping line SCAC for tracking** — that is, the carrier operating the move you're querying for events and shipment data. | You Have | You Need | The Challenge | | ------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | Bill of Lading: `MAEU123456789` | Shipping line SCAC (VOCC SCAC) | Many MBOLs **do not include a prefix**, and even when they do, it may not reliably identify the shipping line you need for tracking. | | Container: `WHLU1234560` | Shipping line SCAC (VOCC SCAC) | The container owner code / leasing company is not always the carrier moving it, so the prefix alone is not enough. | | Booking: `987654321` | Shipping line SCAC (VOCC SCAC) | Booking formats vary widely and often contain no carrier identifier. | Without the correct **shipping line SCAC (VOCC SCAC)**, your tracking request can fail even if the number is valid. Infer Tracking Number predicts the shipping line SCAC + number type to increase the likelihood your tracking request succeeds. ## How Infer Tracking Number helps Submit any tracking number, and the API returns: * **The predicted shipping line (SCAC)** — so you don't have to guess * **The number type** — container, bill of lading, or booking * **Validation results** — catches typos and invalid formats before you submit Just the number — no need to specify the shipping line or type Terminal49 uses machine learning and historical data from millions of shipments to predict the shipping line. Use high-confidence results automatically, or prompt users to confirm With the right SCAC, your tracking request is far more likely to succeed ## Examples by number type Container numbers follow the ISO 6346 format. While the first three letters (owner code) often indicate the owner, the container might be moved by a different shipping line (VOCC). Terminal49 analyzes the number against tens of millions of historical records to predict which shipping line is moving the container. **Example Input:** `MSCU1234567` ```json Request theme={null} { "number": "MSCU1234567" } ``` ```json Response theme={null} { "data": { "attributes": { "number_type": "container", "validation": { "is_valid": true, "type": "container", "check_digit_passed": true }, "shipping_line": { "decision": "auto_select", "selected": { "scac": "MSCU", "name": "Mediterranean Shipping Company", "confidence": 1.0 }, "candidates": [ { "scac": "MSCU", "name": "Mediterranean Shipping Company", "confidence": 1.0 } ] } } } } ``` For container numbers, Terminal49 uses historical data to identify the shipping line with high accuracy (9/10 times). Always check the `decision` field to know if you should ask the user for confirmation. **What to do next:** ```bash theme={null} # Create tracking request with the detected SCAC POST /tracking_requests { "data": { "type": "tracking_request", "attributes": { "request_number": "MSCU1234567", "scac": "MSCU", "request_type": "container" } } } ``` Bill of lading numbers vary by carrier. Some contain prefixes, but others don't. The API uses machine learning to identify the carrier pattern. **Example Input:** `MAEU123456789` ```json Request theme={null} { "number": "MAEU123456789" } ``` ```json Response theme={null} { "data": { "attributes": { "number_type": "bill_of_lading", "validation": { "is_valid": true, "type": "shipment" }, "shipping_line": { "decision": "auto_select", "selected": { "scac": "MAEU", "name": "Maersk", "confidence": 0.98 }, "candidates": [ { "scac": "MAEU", "name": "Maersk", "confidence": 0.98 } ] } } } } ``` Maersk BLs typically start with `MAEU`, but other carriers may not have prefixes. The API analyzes the full format. **What to do next:** ```bash theme={null} # Create tracking request with the detected SCAC POST /tracking_requests { "data": { "type": "tracking_request", "attributes": { "request_number": "MAEU123456789", "scac": "MAEU", "request_type": "bill_of_lading" } } } ``` Booking numbers are the **hardest to identify** — they often don't contain carrier identifiers. **Example Input:** `987654321` ```json Request theme={null} { "number": "987654321" } ``` ```json Response theme={null} { "data": { "attributes": { "number_type": "booking", "validation": { "is_valid": null, "type": "shipment" }, "shipping_line": { "decision": "needs_confirmation", "selected": { "scac": "HLCU", "name": "Hapag-Lloyd", "confidence": 0.72 }, "candidates": [ { "scac": "HLCU", "name": "Hapag-Lloyd", "confidence": 0.72 }, { "scac": "ONE", "name": "Ocean Network Express", "confidence": 0.18 } ] } } } } ``` When `decision` is `needs_confirmation`, show the suggestion but **ask the user to verify**. Display the `candidates` list as options. **What to do next:** ```bash theme={null} # Show user the suggested carrier and candidates # After user confirms, create tracking request POST /tracking_requests { "data": { "type": "tracking_request", "attributes": { "request_number": "987654321", "scac": "HLCU", "request_type": "booking" } } } ``` ## Understanding the response The `decision` field tells you how confident the prediction is and what action to take: | Decision | When it's used | What to do | | -------------------- | ---------------------------- | ----------------------------------------------------- | | `auto_select` | Confidence ≥ 95% | ✅ Safe to use automatically without user confirmation | | `needs_confirmation` | Confidence 70-95% | ⚠️ Show suggestion, ask user to confirm | | `no_prediction` | Confidence \< 70% or unknown | ❌ User must select carrier manually | For the best user experience, always handle all three decision types. Even when `no_prediction` is returned, you can still show the list of `candidates` as suggestions. The API validates numbers before returning predictions: | Field | Description | | -------------------- | ---------------------------------------------------------------- | | `is_valid` | `true` if format is valid, `false` if invalid, `null` if unknown | | `check_digit_passed` | For containers: ISO 6346 check digit verification | | `reason` | If invalid, explains why (e.g., "Invalid check digit") | Invalid numbers may still return a carrier prediction, but you should validate the format before creating a tracking request. | Field | Type | Description | | -------------------------- | ------ | ---------------------------------------------------------- | | `number_type` | string | Detected type: `container`, `bill_of_lading`, or `booking` | | `shipping_line.decision` | string | Confidence level for the prediction | | `shipping_line.selected` | object | Best match: `scac`, `name`, `confidence` | | `shipping_line.candidates` | array | All possible matches, ranked by confidence | See the [API Reference](/docs/api-docs/api-reference/tracking-requests/auto-detect-carrier) for complete schema details. ## Integration guide If you're implementing this API, here are code examples in different languages: ```javascript theme={null} async function getCarrierForNumber(trackingNumber, apiKey) { const response = await fetch( "https://api.terminal49.com/v2/tracking_requests/infer_number", { method: "POST", headers: { Authorization: `Token ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ number: trackingNumber }), } ); const { data } = await response.json(); const { decision, selected, candidates } = data.attributes.shipping_line; return { scac: selected?.scac, carrier: selected?.name, confidence: selected?.confidence, autoSelect: decision === "auto_select", needsConfirmation: decision === "needs_confirmation", candidates: candidates, }; } // Usage const result = await getCarrierForNumber("MSCU1234567", "YOUR_API_KEY"); if (result.autoSelect) { // Auto-fill carrier dropdown carrierDropdown.value = result.scac; } else if (result.needsConfirmation) { // Show suggestion with confirmation prompt showCarrierSuggestion(result.carrier, result.candidates); } ``` ```python theme={null} import requests def get_carrier_for_number(tracking_number: str, api_key: str) -> dict: """Get carrier prediction for a tracking number.""" response = requests.post( 'https://api.terminal49.com/v2/tracking_requests/infer_number', headers={ 'Authorization': f'Token {api_key}', 'Content-Type': 'application/json' }, json={'number': tracking_number} ) result = response.json() shipping_line = result['data']['attributes']['shipping_line'] return { 'scac': shipping_line['selected']['scac'] if shipping_line.get('selected') else None, 'carrier': shipping_line['selected']['name'] if shipping_line.get('selected') else None, 'confidence': shipping_line['selected']['confidence'] if shipping_line.get('selected') else None, 'auto_select': shipping_line['decision'] == 'auto_select', 'needs_confirmation': shipping_line['decision'] == 'needs_confirmation', 'candidates': shipping_line.get('candidates', []) } # Usage result = get_carrier_for_number("MSCU1234567", "YOUR_API_KEY") if result['auto_select']: # Auto-fill carrier dropdown carrier_dropdown.value = result['scac'] elif result['needs_confirmation']: # Show suggestion with confirmation prompt show_carrier_suggestion(result['carrier'], result['candidates']) ``` ```bash theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests/infer_number \ -H "Authorization: Token YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"number": "MSCU1234567"}' ``` ## Rate limits | Setting | Value | | ------------------- | ------------------------------------------- | | Requests per minute | 200 | | Scope | Per API key, in an endpoint-specific bucket | | Rate limit header | `Retry-After` (seconds) | Rate limit errors return HTTP 429 with a `Retry-After` header. Respect this header before sending another Infer Tracking Number request. ## What's next? Full API specification with request/response schemas and try it in the playground Use the detected SCAC to start tracking your shipment See which carriers Terminal49 supports and their data availability Learn what happens after you submit a tracking request # Terminal49 Container Statuses Source: https://terminal49.com/docs/api-docs/in-depth-guides/container-statuses Understand every Terminal49 container status value, from on-ship to empty-returned, so your integration can interpret each shipment lifecycle stage. The `current_status` attribute on container objects provides a high-level view of where a container is in its journey. This guide explains the different status values and their meanings. The API returns the raw backend value (for example `grounded`). The Terminal49 dashboard often displays a friendlier label for the same value (for example **At Terminal**), so each status below lists both. Integrations should always key off the backend `current_status` value, not the dashboard label. ## Quick reference | `current_status` (API) | Dashboard label | Meaning | | -------------------------- | ------------------------ | ----------------------------------------------- | | `new` | No Status | Tracking started, no status milestone yet | | `on_ship` | On Ship | On the vessel | | `grounded` | At Terminal | Discharged at the terminal | | `available` | Available | Available for pickup | | `not_available` | Not available | At the terminal but held / not released | | `awaiting_inland_transfer` | Awaiting Inland Transfer | Discharged at POD, awaiting the inland rail leg | | `on_rail` | On Rail | On a rail car to the inland destination | | `off_dock` | Available at Shippers | Moved to an off-dock / shipper's-own yard | | `picked_up` | Picked up | Picked up by a trucker | | `delivered` | Delivered | Delivered (manually marked) | | `empty_returned` | Empty Returned | Returned empty; tracking ends | ## Status values ### new **Dashboard label: "No Status"** **Default state** — The container is being tracked but no status milestone has been received yet. This is the initial state when tracking begins, typically before the container has been loaded onto a vessel at the port of lading. ### on\_ship **Dashboard label: "On Ship"** **In transit by vessel** — The container is on a vessel. In the lifecycle this status first appears when the container is loaded or the vessel departs the port of lading, and it persists through the ocean voyage until the container is discharged at the Port of Discharge (POD). Triggered by the *vessel loaded* or *vessel departed* milestone (and kept by subsequent on-vessel milestones such as *vessel arrived*, *vessel berthed*, and transshipment events). ### grounded **Dashboard label: "At Terminal"** **At the terminal** — The container has been discharged and is physically at the terminal, but its availability for pickup has not yet been confirmed (the terminal isn't yet providing availability data). * For containers **without** an inland destination: discharged at the POD terminal. Triggered by the *vessel discharged* milestone. * For containers **with** an inland destination: unloaded from the rail car at the inland terminal. Triggered by the *rail unloaded* milestone. ### available **Dashboard label: "Available"** **Ready for pickup** — The container has arrived at the POD or inland destination and is confirmed available for pickup, with no clearance issues preventing it from leaving the facility. Triggered by the *available* milestone (or terminal availability data). For a definitive readiness check that combines `available_for_pickup` with hold data, see [Container Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). ### not\_available **Dashboard label: "Not available"** **At the terminal but not ready** — The container is at the POD or inland destination but has not been cleared to leave. This could be due to: * Terminal holds * Customs holds * Line holds * Documentation requirements * Other restrictions While a hold is in place, the terminal will not release the container for pickup until it is cleared, and the LFD countdown continues (which can result in demurrage fees). Triggered by the *not available* milestone from the terminal. See [Container Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees) for details on specific hold types and how to determine when the container is released. ### awaiting\_inland\_transfer **Dashboard label: "Awaiting Inland Transfer"** **Awaiting the inland leg** — The container has been discharged at the POD but still has an inland destination, so it is waiting to be loaded onto rail for the inland move. Triggered by the *vessel discharged* milestone when the shipment has an inland destination distinct from the POD. This status is specific to shipments with inland rail movements. ### on\_rail **Dashboard label: "On Rail"** **In transit by rail** — The container has been loaded onto a rail car and is being transported toward its inland destination after being discharged from the POD. Triggered by the *rail departed* or *rail loaded* milestone. ### off\_dock **Dashboard label: "Available at Shippers"** **At an off-dock facility** — The terminal reported that the container has moved off the main terminal to an off-dock or shipper's-own yard, where it's available for pickup. When this happens, Terminal49 attempts to identify the off-dock facility and re-point tracking to it, so this status can also appear briefly during that transition before status is pulled from the new facility. This value comes from terminal data (not the steamship line) and is uncommon. ### picked\_up **Dashboard label: "Picked up"** **Out for delivery** — The container has been picked up by a trucker from the POD or inland destination and is on its way to the warehouse. Triggered by the *full out* milestone. ### delivered **Dashboard label: "Delivered"** **Delivery confirmed** — The container has been delivered to the warehouse. This status is only set when delivery is [manually marked as delivered](https://help.terminal49.com/articles/4713318249-how-to-mark-containers-as-delivered) through the Terminal49 dashboard, because delivery date/time data is only available to the customer (Terminal49 does not have access to it). ### empty\_returned **Dashboard label: "Empty Returned"** **Container returned empty** — The container has been emptied and returned to the shipping line or designated return location, completing its journey. Terminal49 stops tracking the container at this point. Triggered by the *empty returned* or *empty in* milestone. ## Important considerations ### Status accuracy The logic to derive container statuses is complex and involves processing data from multiple sources, including: * Shipping line updates * Terminal systems * Rail carrier feeds * Manual updates **There can sometimes be errors in the reported `current_status`.** When making critical, time-sensitive business decisions, consider: * Cross-referencing with the container's transport events * Contacting the terminal directly for time-sensitive pickups ### Status transitions Containers don't always follow a linear path through these statuses. For example: * A container may go from `on_ship` directly to `available` if terminal data arrives quickly * A container might alternate between `available` and `not_available` as holds are placed and removed * The status may remain `new` for some time if data from the shipping line is delayed ### API usage To get the current status of a container, read the container's `current_status` attribute in your API responses: ```bash theme={null} GET /v2/containers/{id} ``` The response will include: ```json theme={null} { "data": { "id": "ff77a822-23a7-4ccd-95ca-g534c071baaf3", "type": "container", "attributes": { "number": "KOCU4959010", "current_status": "available", ... } } } ``` # Direct Links to Shipments and Containers Source: https://terminal49.com/docs/api-docs/in-depth-guides/dashboard-deep-linking Link directly to a shipment or container tracking page using container numbers, BOL numbers, or references — no internal IDs needed. You can link directly to any shipment or container in Terminal49 using an identifier you already have — a container number, bill of lading, booking number, or any reference number you've attached to a shipment. The link resolves to the correct tracking page automatically, so your team doesn't need to know Terminal49's internal UUIDs. This is especially useful when linking from a TMS, ERP, spreadsheet, internal tool, or automated notification. ## URL format The deep link URL pattern is: ``` https://app.terminal49.com/shipments/find?q={identifier} ``` Replace `{identifier}` with the container number, BOL, booking number, or reference number you want to look up. | Identifier type | Example | URL | | --------------------- | --------------- | ----------------------------------------------------------- | | Container number | `TCLU6718159` | `https://app.terminal49.com/shipments/find?q=TCLU6718159` | | Bill of lading number | `MEDUFR030802` | `https://app.terminal49.com/shipments/find?q=MEDUFR030802` | | Booking number | `BKG12345678` | `https://app.terminal49.com/shipments/find?q=BKG12345678` | | Reference number | `PO-2024-00123` | `https://app.terminal49.com/shipments/find?q=PO-2024-00123` | The query is case-insensitive and normalizes formatting — spaces and dashes in container numbers are stripped automatically, so `MSCU 1234567` and `MSCU1234567` both resolve to the same container. ## How resolution works The deep link searches your Terminal49 account and redirects to the best match, in this order: 1. **Exact container number match** → Opens the shipment detail page with that container selected 2. **Exact shipment number or reference number match** → Opens that shipment's detail page 3. **First container result (partial match)** → Opens the associated shipment with that container selected 4. **First shipment result (partial match)** → Opens that shipment 5. **No match found** → Falls back to the shipments list with the search query pre-filled, so the user can refine manually The deep link only matches shipments and containers tracked in your Terminal49 account. If an identifier isn't found, check that the shipment is actively tracked and that the reference number has been added. When multiple shipments match (e.g., the same reference number on two shipments), the deep link resolves to the first match. Use unique reference numbers to ensure deterministic resolution. ## Use cases ### Link from a TMS or ERP If your TMS or ERP stores container numbers or purchase order numbers, you can build a deep link for each record. This gives your logistics team one-click access to real-time tracking data without leaving their primary workflow. For example, if your system stores a container number per shipment record, construct the URL as: ``` https://app.terminal49.com/shipments/find?q={container_number} ``` Most TMS platforms support configurable URL fields or "external link" columns — configure one pointing to this URL pattern. ### Link from a spreadsheet Add a formula column to your shipment spreadsheet that generates a clickable Terminal49 link. Works in Google Sheets, Excel, and most spreadsheet tools. ``` =HYPERLINK("https://app.terminal49.com/shipments/find?q=" & A2, "View in Terminal49") ``` Where `A2` contains the container number, BOL, or reference number. ### Link from Slack or email notifications Include deep links in automated alerts so recipients can jump directly to the relevant shipment: ``` Container TCLU6718159 has been discharged at port. View details: https://app.terminal49.com/shipments/find?q=TCLU6718159 ``` This works in Slack messages, email templates, PagerDuty alerts, or any notification channel that renders URLs as clickable links. ### Link from webhook handlers When processing Terminal49 webhook events, you already have the container number or BOL in the payload. Construct a deep link to include in your internal tools, ticketing systems, or dashboards: ```javascript theme={null} function buildDashboardLink(containerNumber) { return `https://app.terminal49.com/shipments/find?q=${encodeURIComponent(containerNumber)}`; } ``` ### Link from internal dashboards or BI tools Embed deep links in Looker, Metabase, Retool, or similar tools. For example, in a SQL-based dashboard, construct the link in your query: ```sql theme={null} SELECT container_number, CONCAT('https://app.terminal49.com/shipments/find?q=', container_number) AS terminal49_link FROM shipments ``` ### Customer portal integration If you build a customer-facing portal, deep link your customers directly to their shipment status in Terminal49 using the PO number or reference they already know. No need to store or expose Terminal49 internal IDs. ## Supported identifiers The deep link matches against these identifier types: * **Container number** — the standard ISO container number (e.g., `TCLU6718159`). Matched against tracked containers in your account. * **Master bill of lading** — the original BOL number used to create the tracking request. * **Booking number** — the carrier booking reference. * **Reference numbers** — any custom reference numbers you've added to the shipment or container (e.g., purchase order numbers, house bill of lading numbers, internal IDs). ## Adding reference numbers To deep link using your internal identifiers, attach reference numbers to shipments or containers in Terminal49. There are three ways to do this. ### When creating a tracking request Include `ref_numbers` in the tracking request payload: ```json theme={null} { "data": { "type": "tracking_request", "attributes": { "request_type": "bill_of_lading", "request_number": "MEDUFR030802", "scac": "MSCU", "ref_numbers": ["PO-2024-00123", "HBL-5678"] } } } ``` See [Create a tracking request](/docs/api-docs/api-reference/tracking-requests/create-a-tracking-request) for full details. ### By editing a shipment ```bash theme={null} curl -X PATCH https://api.terminal49.com/v2/shipments/{shipment_id} \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "shipment", "attributes": { "ref_numbers": ["PO-2024-00123", "HBL-5678"] } } }' ``` See [Edit a shipment](/docs/api-docs/api-reference/shipments/edit-a-shipment) for full details. ### By editing a container ```bash theme={null} curl -X PATCH https://api.terminal49.com/v2/containers/{container_id} \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "container", "attributes": { "ref_numbers": ["PO-2024-00123"] } } }' ``` See [Edit a container](/docs/api-docs/api-reference/containers/edit-a-container) for full details. Reference numbers added at tracking request creation are propagated to both the shipment and its containers. You can also add different reference numbers to individual containers for more granular deep linking. ## Related guides Embed tracking on your website Get notified when shipments update Understand request statuses # Direct Upload for Documents Source: https://terminal49.com/docs/api-docs/in-depth-guides/direct-upload-documents Upload files with the Terminal49 direct upload flow, retrieve a signed_id, and create document resources linked to your shipments or containers. Use this guide when your app needs to upload a file first and then create a Terminal49 document. Any client stack can use this flow as long as it can make standard HTTP requests. ## Overview 1. Request a direct upload blob payload from Terminal49. 2. Upload the file bytes to the returned `direct_upload.url` using the returned headers. 3. Store the returned `signed_id`. 4. Create a Terminal49 document with `attached_document = signed_id`. ## 1) Request a direct upload blob Endpoint: `POST /rails/active_storage/direct_uploads` Send metadata for the file you want to upload: ```json theme={null} { "blob": { "filename": "1462486 order.pdf", "content_type": "application/pdf", "byte_size": 35672, "checksum": "tZTfawHSrI1hiuOZQ0cQRg==" } } ``` ### Blob attributes explained | Attribute | Type | What it is | How to create it | | -------------- | ------- | ------------------------------------------------ | --------------------------------------------------------------------------- | | `filename` | string | Original file name users see. | Use the file name from the uploaded file (for example `1462486 order.pdf`). | | `content_type` | string | MIME type of the file. | Detect from file extension or file bytes (for PDF use `application/pdf`). | | `byte_size` | integer | Exact file size in bytes. | Read the file size from your filesystem or uploaded file object. | | `checksum` | string | Base64-encoded MD5 digest of the raw file bytes. | Compute MD5 on file bytes, then Base64-encode the binary MD5 result. | ### Example ways to generate values Get file size in bytes: ```bash theme={null} wc -c < "1462486 order.pdf" ``` Compute checksum (`Base64(MD5(file_bytes))`): ```bash theme={null} openssl md5 -binary "1462486 order.pdf" | openssl base64 ``` `checksum` must match the exact bytes you upload in step 2, or the upload will fail. Example response: ```json theme={null} { "id": "96b6d878-0341-4ce3-8b3c-06767f6f08eb", "key": "883c4cf4-b086-4698-a620-5ffa16cc95ef/pqjug51un0gobs6x72ez0q9e4so4", "filename": "1462486 order.pdf", "content_type": "application/pdf", "metadata": {}, "service_name": "amazon", "byte_size": 35672, "checksum": "tZTfawHSrI1hiuOZQ0cQRg==", "created_at": "2026-03-26T18:49:37Z", "signed_id": "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJaWs1Tm1JMlpEZzNPQzB3TXpReExUUmpaVE10T0dJell5MHdOamMyTjJZMlpqQTRaV0lHT2daRlZBPT0iLCJleHAiOm51bGwsInB1ciI6ImJsb2JfaWQifX0=--f5605d2c1e90ce3b54ef1a193f84530f954184a5", "direct_upload": { "url": "https://...s3.amazonaws.com/...signature...", "headers": { "Content-Type": "application/pdf", "Content-MD5": "tZTfawHSrI1hiuOZQ0cQRg==", "Content-Disposition": "inline; filename=\"1462486 order.pdf\"; filename*=UTF-8''1462486%20order.pdf" } } } ``` ## 2) Upload the file bytes to `direct_upload.url` Use `direct_upload.url` to send the file, and send the `direct_upload.headers` object as request headers. Use whatever response is returned by that upload request. ```bash theme={null} curl -X PUT "$DIRECT_UPLOAD_URL" \ -H "Content-Length: 35672" \ -H "Content-Type: application/pdf" \ -H "Content-MD5: tZTfawHSrI1hiuOZQ0cQRg==" \ -H "Content-Disposition: inline; filename=\"1462486 order.pdf\"; filename*=UTF-8''1462486%20order.pdf" \ --data-binary @"/path/to/1462486 order.pdf" ``` ## 3) Persist `signed_id` in your app Save the `signed_id` with your internal record. You will use this value in the next step. Do not send the S3 URL to `POST /documents`. Send `signed_id` in `attached_document`. ## 4) Create the document using `attached_document` ```json theme={null} { "data": { "type": "document", "attributes": { "name": "1462486 order.pdf", "attached_document": "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJaWs1Tm1JMlpEZzNPQzB3TXpReExUUmpaVE10T0dJell5MHdOamMyTjJZMlpqQTRaV0lHT2daRlZBPT0iLCJleHAiOm51bGwsInB1ciI6ImJsb2JfaWQifX0=--f5605d2c1e90ce3b54ef1a193f84530f954184a5" } } } ``` Endpoint: [`POST /documents`](/docs/api-docs/api-reference/documents/upload-a-document) # Document processing workflows Source: https://terminal49.com/docs/api-docs/in-depth-guides/document-processing-workflows Submit shipping documents by email or API, then consume structured extraction results via webhooks in your Terminal49 document processing workflow. Shipping documents (House Bills of Lading, Master Bills of Lading, arrival notices, delivery orders, and more) arrive in shared inboxes and have traditionally required manual classification, data entry, and filing. This integration automates that workflow: Terminal49 receives each document by email, classifies it, extracts structured fields, and delivers the results to your system via webhook. The outcome is less manual re-keying, faster time-to-file, and fewer errors from misfiled or delayed documents, freeing your team from routine data entry to focus on true exceptions. Submit documents by emailing attachments to your account docs alias. Terminal49 then handles the rest: classify -> extract -> webhook result. ## Coming soon * **`email_submission.created` event:** a webhook fired immediately on email receipt, before classification and extraction complete. * **Action required flow:** documentation in progress. ## Before you start Make sure you have the following in place before building: Confirm you can log in and switch between the test and production accounts (see [Environments](#environments) below). Generate an API key from **User > Developers > API Keys**. You'll need this to register your webhook and call the API. Your server needs a reachable HTTPS URL to receive webhook POST requests from Terminal49. For local development, use a tool like [ngrok](https://ngrok.com) to expose a local port. Register your endpoint and subscribe to `document_representation.created` and `document_representation.failed` (see [Subscribing to events](#subscribing-to-events) below). ## Environments Your Terminal49 account may have separate test and production environments, accessible from the account switcher in the top-left corner when you log in. Both environments make live calls. Documents submitted under either account are processed and costs will be incurred. There is no free sandbox for document processing at this time. ## Authentication All API calls require an API key passed as a Bearer token: ``` Authorization: Token YOUR_API_KEY ``` To get your API key, go to **User > Developers > API Keys** (click your username in the bottom-left corner of the navigation). For more detail, see [Start Here](/docs/api-docs/getting-started/start-here). ## Subscribing to events Register a webhook endpoint to receive document processing notifications: ```bash theme={null} curl -X POST https://api.terminal49.com/v2/webhooks \ -H "Authorization: Token YOUR_API_KEY" \ -H "Content-Type: application/vnd.api+json" \ -d '{ "data": { "type": "webhook", "attributes": { "url": "https://your-server.com/webhooks/t49-documents", "active": true, "events": [ "document_representation.created", "document_representation.failed" ] } } }' ``` You can also configure webhooks from the Terminal49 dashboard: 1. Click your username in the bottom-left corner of the navigation. 2. Go to **User > Developers > Webhooks**. 3. To add a new endpoint, click **Create Webhook**, fill in your URL, and select the relevant events under **Document Events**. 4. To update an existing endpoint, click into it and toggle on the document events you need. ## Webhook endpoint requirements Your endpoint must meet the following requirements to reliably receive webhook notifications: **Response codes:** Return HTTP `200`, `201`, `202`, or `204`. Any other response (including a timeout) is treated as a delivery failure and will trigger retries. **Retries:** Terminal49 will retry failed deliveries multiple times. Design your endpoint to be idempotent. Use `data.id` (the `webhook_notification` UUID) as your idempotency key to avoid processing the same event twice. **HTTPS:** Your endpoint must be accessible over HTTPS. **IP allowlist:** Webhook notifications are sent from the following IP addresses. Allowlist these if your infrastructure restricts inbound traffic: ``` 35.222.62.171 3.230.67.145 44.217.15.129 ``` **Signature verification (recommended):** Each webhook is signed using HMAC SHA-256. The signature is included in the `X-T49-Webhook-Signature` header. To verify, retrieve the `secret` from your webhook configuration and compute the HMAC digest of the raw request body; it should match the header value. ```ruby theme={null} secret = ENV.fetch('T49_WEBHOOK_SECRET') hmac = OpenSSL::HMAC.hexdigest('SHA256', secret, request.body.read) verified = request.headers['X-T49-Webhook-Signature'] == hmac ``` ## Workflow diagrams ```mermaid theme={null} flowchart LR A[Email sent to docs alias] --> B[Terminal49 receives document] B --> C{Duplicate?} C -- Yes --> Z[Ignored, no webhook fired] C -- No --> D[Terminal49 classifies document] D --> E[Terminal49 extracts structured data] E --> F{Extraction outcome} F -- Success --> G[document_representation.created] F -- Failure --> H[document_representation.failed] ``` ## Workflow: step-by-step Email attachments to your account's unique docs alias (for example, `youraccount-42@docs.terminal49.com`). Find your alias under **User > Developers > API Keys**. **Supported file types:** PDF, PNG, JPEG, XLSX, XLS, CSV, Word (.doc, .docx). **Multiple attachments:** Each attachment in a single email is processed independently and generates its own webhook event. All resulting webhooks reference the same `email_submission`. **Unsupported files:** Encrypted or password-protected files cannot be processed and will result in a `document_representation.failed` event. Terminal49 classifies and extracts structured data asynchronously. Processing typically completes within seconds to a few minutes depending on document complexity. You receive `document_representation.created` (extraction succeeded) or `document_representation.failed` (extraction could not complete). Parse the payload, route by `document_type`, store the extracted fields, and trigger your downstream processes. Treat submission as fire-and-forget. Do not poll or wait for a response after sending the email. The webhook is the signal that processing is complete. If the same file content has already been processed for your account, it is treated as a duplicate and ignored. No webhook is fired. ## Webhooks you should handle A `document_representation` is the structured extraction result for a document. `document_representation.created` means extraction succeeded and structured data is available in the payload. `document_representation.failed` means Terminal49 could not produce an extraction result. | Event | Meaning | Signal | | --------------------------------- | --------------------------------- | ------------------------------------------------------------------------ | | `document_representation.created` | Extraction completed successfully | `document_type` is set; `payload` contains extracted fields | | `document_representation.failed` | Extraction did not complete | `document_type` is `"unknown"`; `last_document_representation` is `null` | ## Webhook payload structure Every document webhook follows the same envelope structure. The `payload` object inside `document_representation` contains the extracted fields and varies by document type. See [Document Types in Scope](#document-types-in-scope) for full examples. `document_representation.created` envelope: ```json theme={null} { "data": { "id": "89ec3520-cea3-447d-8404-341e0bfd3aa6", "type": "webhook_notification", "attributes": { "event": "document_representation.created", "delivery_status": "pending", "created_at": "2026-03-27T20:05:39Z" }, "relationships": { "reference_object": { "data": { "id": "e75541c0-9ad5-408b-9747-23415adfbca0", "type": "document" } } } }, "included": [ { "id": "b3abc297-624a-4eaa-a0e9-4ac4ebbd064f", "type": "document_representation", "attributes": { "schema_version": "draft_house_bill_of_lading@2026-03-23", "payload": {}, "created_at": "2026-03-27T20:05:39Z", "updated_at": "2026-03-27T20:05:39Z" } }, { "id": "e75541c0-9ad5-408b-9747-23415adfbca0", "type": "document", "attributes": { "document_type": "draft_house_bill_of_lading", "source": "email", "file_name": "invoice.pdf", "file_url": "https://t49-documents-prod.s3.amazonaws.com/..." }, "relationships": { "email_submission": { "data": { "id": "7de2c356-5d2a-4d6e-99f4-6f0d2d63e357", "type": "email_submission" } }, "last_document_representation": { "data": { "id": "b3abc297-624a-4eaa-a0e9-4ac4ebbd064f", "type": "document_representation" } } } } ] } ``` When the webhook document is a child document (split from a larger packet), the document include also contains `attributes.parsed.packetSegment` and a `parent_document` relationship: ```json theme={null} { "id": "child-document-id", "type": "document", "attributes": { "document_type": "arrival_notice", "file_name": "packet_child_1.pdf", "file_url": "https://t49-documents-prod.s3.amazonaws.com/...", "parsed": { "packetSegment": { "startPage": 3, "endPage": 7 } } }, "relationships": { "parent_document": { "data": { "id": "parent-document-id", "type": "document" } }, "last_document_representation": { "data": { "id": "b3abc297-624a-4eaa-a0e9-4ac4ebbd064f", "type": "document_representation" } } } } ``` `file_url` is a pre-signed S3 URL and expires after 1 hour. Download the file promptly after receiving the webhook, or fetch a fresh URL using the endpoint below. ### Fetching a fresh download URL If the `file_url` from the webhook has expired, request a new one: ```bash theme={null} curl -X GET https://api.terminal49.com/v2/documents/{id}/download_url \ -H "Authorization: Token YOUR_API_KEY" ``` Replace `{id}` with the document `id` from the webhook payload. Response: ```json theme={null} { "download_url": "https://t49-documents-prod.s3.amazonaws.com/..." } ``` ### Schema versioning Every webhook payload includes a `schema_version` field that identifies the document type and the schema date in use: ``` "schema_version": "draft_house_bill_of_lading@2026-03-23" ``` Your account is pinned to a specific schema date. All document types will use the latest schema version up to and including that date. Terminal49 can update your pinned version when you are ready to migrate. **What changes the version:** * Breaking changes (fields removed, renamed, or restructured) increment the date. Terminal49 will either support parallel versions during a migration window or coordinate a cutover date with you. * Non-breaking additions (new optional fields) do not change the version. Use `schema_version` to route your parsing logic. If you support multiple versions, branch on this field. ### Persisting extracted data Use the `document_type` and `schema_version` to look up the expected `payload` fields for that document type, then store the extracted data in your system. ### Handling a failed extraction If extraction fails, you will receive `document_representation.failed` instead. The key signal is `"document_type": "unknown"` means the document was received but could not be classified or extracted. There is no `document_representation` in `included` and `last_document_representation` will be `null`. ```json theme={null} { "data": { "id": "014551bd-32c8-46c1-b17c-3a9f1984e39f", "type": "webhook_notification", "attributes": { "event": "document_representation.failed", "delivery_status": "pending", "created_at": "2026-03-27T20:46:56Z" }, "relationships": { "reference_object": { "data": { "id": "31e9df4a-7539-4b44-8409-8e9c350d2ac7", "type": "document" } } } }, "included": [ { "id": "90df411d-b836-498b-bf0d-b320d56ab311", "type": "email_submission", "attributes": { "subject": "[ediDocManager SHP HBL MBL HLCUSHA2601APKY2 / HBL CGGMSGH5110912]", "from": ["sender@example.com"], "sent_at": "2026-03-27T13:46:32-07:00" } }, { "id": "31e9df4a-7539-4b44-8409-8e9c350d2ac7", "type": "document", "attributes": { "document_type": "unknown", "file_name": "f134a7229b5cf7b6c241c566448b9293_fail-1234.pdf", "file_url": "https://t49-documents-prod.s3.amazonaws.com/..." }, "relationships": { "last_document_representation": { "data": null }, "email_submission": { "data": { "id": "90df411d-b836-498b-bf0d-b320d56ab311", "type": "email_submission" } } } } ] } ``` `document_type: "unknown"` means Terminal49 could not classify or extract the document. Log the document `id` and `file_url` for investigation. If failures recur on the same document type, contact Terminal49 support. ## Document types in scope The table below lists the document types Terminal49 currently classifies and extracts, along with the `document_type` value returned by the API. | Document type | `document_type` value | Notes | | -------------------------- | ---------------------------- | --------------------------------------------------------------- | | Draft House Bill of Lading | `draft_house_bill_of_lading` | `hbl_type: "DRAFT"` in payload | | Final House Bill of Lading | `final_house_bill_of_lading` | `hbl_type: "TELEX"` in payload; often issued as a Sea Waybill | | Importer Security Filing | `importer_security_filing` | | | Master Bill of Lading | `master_bill_of_lading` | Often issued as a Sea Waybill | | Delivery Order | `dray_delivery_order` | | | Arrival Notice | `arrival_notice` | | | General Notice | `general_notice` | Includes container available notices | | Customs Entry | `customs_entry` | Includes in-bond documents | | Other | `other` | Used when no dedicated schema exists yet, e.g. freight invoices | Additional document types will be added in future phases. **Null fields are intentional.** A `null` value means Terminal49 looked for that field in the source document but did not find it. Treat `null` as "checked, not present" rather than "field not supported" or "not checked". ### Draft House Bill of Lading: full webhook payload `schema_version: draft_house_bill_of_lading@2026-03-23` ```json theme={null} { "data": { "id": "925298f4-dd2e-43c7-bdc6-690d92cc55bc", "type": "webhook_notification", "attributes": { "event": "document_representation.created", "delivery_status": "pending", "created_at": "2026-03-27T20:05:03Z" }, "relationships": { "reference_object": { "data": { "id": "1272e0d7-7989-4d3a-88a9-fceac6c9d239", "type": "document" } } } }, "included": [ { "id": "86bd0705-6478-4e14-9a8a-2cea84329636", "type": "document_representation", "attributes": { "schema_version": "draft_house_bill_of_lading@2026-03-23", "created_at": "2026-03-27T20:05:03Z", "updated_at": "2026-03-27T20:05:03Z", "payload": { "hbl_type": "DRAFT", "hbl_number": "CGGMSGH5110912", "carrier_booking_number": "HLCUSHA2601APKY2", "reference_number_isf": "CGGMSGH5110912", "fmc_oti_number": "026564N", "date_of_issue": "13-02-2026", "shipped_on_board_date": "17-Feb-26", "freight_payment_terms": "PREPAID", "service_mode": "CFS/CY", "vessel_name": "GUSTAV MAERSK", "voyage_number": "606E", "port_of_loading": "SHANGHAI,CHINA", "port_of_discharge": "LOS ANGELES, CALIFORNIA, USA", "place_of_receipt": "SHANGHAI,CHINA", "place_of_delivery": "PERRIS, CALIFORNIA, USA", "point_and_country_of_origin": "SHANGHAI,CHINA", "signed_at": "SHANGHAI", "signed_by": "MAERSK LOGISTICS & SERVICE CHINA LIMITED as agent of the Carrier", "shipper": { "name": "FUZHOU LIGHT INDUSTRY IMPORT & EXPORT CO.,LTD", "address": "8/F.,TAIYANG PLAZA,NO.278,HUDONG AVENUE FUZHOU,CHINA" }, "consignee": { "name": "GOLDENSEE LIMITED", "address": "C/O ACME LOGISTICS INC 100 MAIN ST, ANYTOWN, CA 90001" }, "notify_party": { "name": "MOHAWK GLOBAL LOGISTICS", "email": "NJIMPORTS@MOHAWKGLOBAL.COM", "phone": "732-218-9164", "address": "105 FIELDCREST AVE, SUITE 404 EDISON, NJ 08837" }, "containers": [ { "container_number": "FDCU0184438", "seal_number": "HLK6247958", "container_size": "40HIGH", "gross_weight": "1116.75", "weight_unit": "KGS", "measurement": "5.736", "measurement_unit": "CBM", "number_of_packages": "24", "package_unit": "CARTONS", "cargo_references": ["WFH1G9800163", "WFH1G9800162"] } ], "line_items": [ { "description": "THE SOFA", "quantity": "24", "quantity_unit": "CARTONS", "weight": "1116.75", "weight_unit": "KGS", "measurement": "5.736", "measurement_unit": "CBM", "hts_codes": ["9401616031"], "cargo_references": ["WFH1G9800163", "WFH1G9800162"] } ], "totals": { "number_of_packages": "24", "package_unit": "CARTONS", "gross_weight": "1116.75 KGS", "weight_unit": "KGS", "measurement": "5.736 CBM", "measurement_unit": "CBM" } } } }, { "id": "3bdf78ea-a86c-40b5-b650-a1d79542808a", "type": "email_submission", "attributes": { "subject": "[ediDocManager SHP HBL MBL HLCUSHA2601APKY2 / HBL CGGMSGH5110912]", "from": ["sender@example.com"], "sent_at": "2026-03-27T13:03:05-07:00" } }, { "id": "1272e0d7-7989-4d3a-88a9-fceac6c9d239", "type": "document", "attributes": { "document_type": "draft_house_bill_of_lading", "source": "email", "file_name": "f134a7229b5cf7b6c241c566448b9293.pdf", "file_url": "https://t49-documents-prod.s3.amazonaws.com/..." }, "relationships": { "last_document_representation": { "data": { "id": "86bd0705-6478-4e14-9a8a-2cea84329636", "type": "document_representation" } }, "email_submission": { "data": { "id": "3bdf78ea-a86c-40b5-b650-a1d79542808a", "type": "email_submission" } } } } ] } ``` ### Final House Bill of Lading: full webhook payload `schema_version: final_house_bill_of_lading@2026-03-23` ```json theme={null} { "data": { "id": "89ec3520-cea3-447d-8404-341e0bfd3aa6", "type": "webhook_notification", "attributes": { "event": "document_representation.created", "delivery_status": "pending", "created_at": "2026-03-27T20:05:39Z" }, "relationships": { "reference_object": { "data": { "id": "e75541c0-9ad5-408b-9747-23415adfbca0", "type": "document" } } } }, "included": [ { "id": "b3abc297-624a-4eaa-a0e9-4ac4ebbd064f", "type": "document_representation", "attributes": { "schema_version": "final_house_bill_of_lading@2026-03-23", "created_at": "2026-03-27T20:05:39Z", "updated_at": "2026-03-27T20:05:39Z", "payload": { "hbl_type": "TELEX", "hbl_number": "CGGMXIM1554317", "carrier_booking_number": "MAEU263829038", "reference_number_isf": "CGGMXIM1554317", "fmc_oti_number": "026564N", "date_of_issue": "13-02-2026", "shipped_on_board_date": "31-Jan-26", "freight_payment_terms": "PREPAID", "service_mode": "CFS/CY", "vessel_name": "SYNERGY BUSAN", "voyage_number": "604N", "port_of_loading": "XIAMEN,CHINA", "port_of_discharge": "JACKSONVILLE,FLORIDA,USA", "place_of_receipt": "XIAMEN,CHINA", "place_of_delivery": "JACKSONVILLE,FLORIDA,USA", "point_and_country_of_origin": "XIAMEN,CHINA", "signed_at": "XIAMEN", "signed_by": "MAERSK LOGISTICS & SERVICE CHINA LIMITED as agent of the Carrier", "shipper": { "name": "TOTAL WIN HOME PRODUCTS CO.,LTD", "address": "ROOM 601-604, ZHONGXI TIMES TOWER, NO. 26 OF HONGQI ROAD, NANCHENG DISTRICT, DONGGUAN CITY GUANGDONG PROVINCE, CHINA" }, "consignee": { "name": "UNIVERSE HOME INC.", "address": "1546 NW 56TH STREET, SEATTLE, WA, 98107, UNITED STATES" }, "notify_party": { "name": "MOHAWK GLOBAL LOGISTICS", "email": "NJIMPORTS@MOHAWKGLOBAL.COM", "phone": "732-218-9164", "address": "105 FIELDCREST AVE, SUITE 404 EDISON, NJ 08837" }, "containers": [ { "container_number": "CAAU4786387", "seal_number": "CN5274554", "container_size": "40HIGH", "gross_weight": "451.7", "weight_unit": "KGS", "measurement": "2.412", "measurement_unit": "CBM", "number_of_packages": "37", "package_unit": "CARTONS", "cargo_references": ["ACN SPO WHS-41414-42378538"] }, { "container_number": "CAAU4786387", "seal_number": "CN5274554", "container_size": "40HIGH", "gross_weight": "862", "weight_unit": "KGS", "measurement": "4.724", "measurement_unit": "CBM", "number_of_packages": "26", "package_unit": "CARTONS", "cargo_references": ["ACN SPO WHS-41414-42378549"] } ], "line_items": [ { "description": "PET GATE", "weight": "1313.7", "weight_unit": "KGS", "measurement": "7.136", "measurement_unit": "CBM", "hts_codes": ["442199"] }, { "description": "PET RAMP", "hts_codes": ["442199"], "cargo_references": ["ACI SPO: WHS-41414-42267479"] }, { "description": "DOG HOUSE", "hts_codes": ["442199"] }, { "description": "COMMODITY SHELF", "hts_codes": ["442199"], "cargo_references": ["ACI SPO: WHS-41414-42267481"] } ], "totals": { "number_of_packages": "63", "package_unit": "CARTONS", "gross_weight": "1313.7 KGS", "weight_unit": "KGS", "measurement": "7.136 CBM", "measurement_unit": "CBM" } } } }, { "id": "048bfec4-1249-4239-ab5b-63ad1e8f70cf", "type": "email_submission", "attributes": { "subject": null, "from": ["sender@example.com"], "sent_at": "2026-03-27T23:03:18+03:00" } }, { "id": "e75541c0-9ad5-408b-9747-23415adfbca0", "type": "document", "attributes": { "document_type": "final_house_bill_of_lading", "source": "email", "file_name": "c4b16d8360049ea688367f6192dba07b.pdf", "file_url": "https://t49-documents-prod.s3.amazonaws.com/..." }, "relationships": { "last_document_representation": { "data": { "id": "b3abc297-624a-4eaa-a0e9-4ac4ebbd064f", "type": "document_representation" } }, "email_submission": { "data": { "id": "048bfec4-1249-4239-ab5b-63ad1e8f70cf", "type": "email_submission" } } } } ] } ``` ### ISF (Importer Security Filing): full webhook payload `schema_version: importer_security_filing@2026-03-30` ```json theme={null} { "data": { "id": "89ec3520-cea3-447d-8404-341e0bfd3aa6", "type": "webhook_notification", "attributes": { "event": "document_representation.created", "delivery_status": "pending", "created_at": "2026-03-27T20:05:39Z" }, "relationships": { "reference_object": { "data": { "id": "e75541c0-9ad5-408b-9747-23415adfbca0", "type": "document" } } } }, "included": [ { "id": "b3abc297-624a-4eaa-a0e9-4ac4ebbd064f", "type": "document_representation", "attributes": { "schema_version": "importer_security_filing@2026-03-30", "created_at": "2026-03-27T20:05:39Z", "updated_at": "2026-03-27T20:05:39Z", "payload": { "importer_name": "Hulk International Limited", "importer_of_record_number": null, "po_numbers": ["WHS-64134-41828780"], "description_of_goods": null, "bol_type": null, "hbl_number": "CGGMSGH5115217", "mbl_number": "MAEU265407596", "etd": "17-Feb-26", "eta": "04-Mar-26", "consignee_name": "Hulk International Limited", "consignee_address_1": "C/O ACME LOGISTICS INC 100 MAIN ST, ANYTOWN, CA 90001", "consignee_address_2": null, "consignee_city": null, "consignee_state": null, "consignee_postal_code": null, "consignee_country": "US", "consignee_irs_tax_id": null, "buyer_name": "Hulk International Limited", "buyer_address_1": "C/O ACME LOGISTICS INC 100 MAIN ST, ANYTOWN, CA 90001", "buyer_address_2": null, "buyer_city": null, "buyer_state": null, "buyer_postal_code": null, "buyer_country": "US", "buyer_duns": null, "buyer_duns4": null, "ship_to_name": "Acme Distribution Center 2", "ship_to_address_1": "100 Main St, Anytown, CA 90001", "ship_to_address_2": null, "ship_to_city": null, "ship_to_state": null, "ship_to_postal_code": null, "ship_to_country": "US", "seller_name": "Jili Creation Technology Co., Ltd", "seller_address_1": "Room(2803), Aidu international,#72,Jianshe Dong Street,Tiexi District,Shenyang,Liaoning,110000", "seller_address_2": null, "seller_city": null, "seller_state_province": null, "seller_postal_code": "110000", "seller_country": "China", "seller_duns": null, "seller_duns4": null, "consolidator_name": "A.P. Moller – Maersk", "consolidator_address_1": "1-3/F, D3, Tianfu Software Park, Chengdu, China, 610041", "consolidator_address_2": null, "consolidator_city": null, "consolidator_province": null, "consolidator_postal_code": "610041", "consolidator_country": "China", "consolidator_duns": null, "consolidator_duns4": null, "stuffing_location_name": "Shanghai Yangshan Free Trade Port Area Logistics Service Co.,Ltd", "stuffing_location_address_1": "No 666, Tongshun Avenue, Pudong District, Shanghai", "stuffing_location_address_2": null, "stuffing_location_city": null, "stuffing_location_province": null, "stuffing_location_postal_code": "201306", "stuffing_location_country": "China", "stuffing_location_duns": null, "stuffing_location_duns4": null, "manufacturer_name": "ZHEJIANG ANJI SHUYE FURNITURE CO.,LTD", "manufacturer_address_1": "DISTRICT 2, SUNSHINE INDUSTRIAL PARK OF DIPU SUBDISTRICT, ANJI COUNTY, HUZHOU CITY, ZHEJIANG PROVINCE, CHINA", "manufacturer_address_2": null, "manufacturer_city": null, "manufacturer_province": null, "manufacturer_postal_code": "313300", "manufacturer_country": "China", "manufacturer_duns": null, "manufacturer_duns4": null, "hts_code": "9401719000", "country_of_origin": "China" } } }, { "id": "e75541c0-9ad5-408b-9747-23415adfbca0", "type": "document", "attributes": { "document_type": "importer_security_filing", "source": "email", "file_name": "isf.pdf", "file_url": "https://t49-documents-prod.s3.amazonaws.com/..." }, "relationships": { "last_document_representation": { "data": { "id": "b3abc297-624a-4eaa-a0e9-4ac4ebbd064f", "type": "document_representation" } }, "email_submission": { "data": { "id": "048bfec4-1249-4239-ab5b-63ad1e8f70cf", "type": "email_submission" } } } } ] } ``` ### Master Bill of Lading: full webhook payload `schema_version: master_bill_of_lading@2026-03-23` ```json theme={null} { "data": { "id": "89ec3520-cea3-447d-8404-341e0bfd3aa6", "type": "webhook_notification", "attributes": { "event": "document_representation.created", "delivery_status": "pending", "created_at": "2026-03-27T20:05:39Z" }, "relationships": { "reference_object": { "data": { "id": "e75541c0-9ad5-408b-9747-23415adfbca0", "type": "document" } } } }, "included": [ { "id": "b3abc297-624a-4eaa-a0e9-4ac4ebbd064f", "type": "document_representation", "attributes": { "schema_version": "master_bill_of_lading@2026-03-23", "created_at": "2026-03-27T20:05:39Z", "updated_at": "2026-03-27T20:05:39Z", "payload": { "document_state": "NON-NEGOTIABLE", "waybill_number": "235600052920", "booking_number": "EGLV235600052920", "carrier_reference": null, "carrier_name": "Evergreen Line", "scac_code": "EGLV", "export_references": null, "service_contract_number": null, "service_type": "FCL/FCL", "movement_type": "O/O", "number_of_original_waybills": "NIL (0)", "rider_pages": null, "shipper": { "name": "MAERSK LOGISTICS & SERVICES VIETNAM COMPANY LIMITED", "address": "FLOOR 16&17,OFFICE - COMMERCIAL - SERVICE BUILDING AT LOT 5.5,NO.8-10 MAI CHI THO STREET,AN KHANH WARD,HO CHI MINH CITY,VIETNAM", "email": null, "phone": null }, "consignee": { "name": "ACME LOGISTICS INC", "address": "4 COPLEY PLACE FLOOR 7 BOSTON MA 02116 UNITED STATES", "email": "OCEANIMPORT@ACMELOGISTICS.COM", "phone": "1 617-532-5100" }, "notify_party": { "name": "ACME LOGISTICS INC.", "address": "4 COPLEY PLACE FLOOR 7 BOSTON MA 02116 UNITED STATES", "email": "OCEANIMPORT@ACMELOGISTICS.COM", "phone": "1 617-532-5100" }, "also_notify": null, "actual_shipper": null, "forwarding_agent_references": null, "point_and_country_of_origin": null, "pre_carriage_by": null, "place_of_receipt": "HO CHI MINH CITY", "date_of_receipt": null, "ocean_vessel": "EVER MAGNA", "voyage_number": "1440-003E", "port_of_loading": "CAI MEP", "transshipment_port": null, "port_of_discharge": "LOS ANGELES, CA", "place_of_delivery": "LOS ANGELES, CA", "onward_inland_routing": null, "marks_and_numbers": "N/M", "containers": [ { "container_number": "EGSU9849000", "seal_number": "EMCCYG3555", "container_size": "40H", "slac": null, "gross_weight": null, "weight_unit": null, "measurement": null, "measurement_unit": null, "number_of_packages": null, "package_unit": null } ], "description_of_goods": "SOFA", "hts_codes": ["9401616011"], "invoice_references": [], "hbl_reference": null, "cargo_references": ["SPO#WHS-58624-41973686"], "total_packages": 135, "package_unit": "CARTONS", "gross_weight": 6439.5, "tare_weight": null, "tare_weight_unit": null, "weight_unit": "KGS", "measurement": 64.8, "measurement_unit": "CBM", "total_containers_received": 1, "total_containers_in_words": "ONE(1) CONTAINER ONLY", "freight_payment_terms": "COLLECT", "prepaid_at": null, "collect_at": "DESTINATION", "freight_payable_at": null, "place_of_issue": "HO CHI MINH", "date_of_issue": "FEB.02,2026", "laden_on_board_date": "FEB.02,2026", "signed_by": "EVERGREEN SHIPPING AGENCY (VIETNAM) COMPANY LTD. As agent for the Carrier and the Vessel Provider Evergreen Marine (Asia) Pte. Ltd. doing business as \"Evergreen Line\"" } } }, { "id": "e75541c0-9ad5-408b-9747-23415adfbca0", "type": "document", "attributes": { "document_type": "master_bill_of_lading", "source": "email", "file_name": "c4b16d8360049ea688367f6192dba07b.pdf", "file_url": "https://t49-documents-prod.s3.amazonaws.com/..." }, "relationships": { "last_document_representation": { "data": { "id": "b3abc297-624a-4eaa-a0e9-4ac4ebbd064f", "type": "document_representation" } }, "email_submission": { "data": { "id": "048bfec4-1249-4239-ab5b-63ad1e8f70cf", "type": "email_submission" } } } } ] } ``` ### Delivery Order: full webhook payload `schema_version: dray_delivery_order@2026-03-30` ```json theme={null} { "data": { "id": "89ec3520-cea3-447d-8404-341e0bfd3aa6", "type": "webhook_notification", "attributes": { "event": "document_representation.created", "delivery_status": "pending", "created_at": "2026-03-27T20:05:39Z" }, "relationships": { "reference_object": { "data": { "id": "e75541c0-9ad5-408b-9747-23415adfbca0", "type": "document" } } } }, "included": [ { "id": "b3abc297-624a-4eaa-a0e9-4ac4ebbd064f", "type": "document_representation", "attributes": { "schema_version": "dray_delivery_order@2026-03-30", "created_at": "2026-03-27T20:05:39Z", "updated_at": "2026-03-27T20:05:39Z", "payload": { "issuer_name": "Acme Logistics Inc", "issuer_fmc_oti_number": "026564N", "document_date": null, "document_number": null, "consol_number": null, "prepared_by": null, "prepared_date": null, "pickup_terminal_name": "APM Terminal, New Jersey", "pickup_address_name": "New York APM Terminal, New Jersey", "pickup_address": null, "pickup_address_phone": null, "delivery_name": "Acme Distribution Center", "delivery_address": "100 Main St, Anytown, NJ 08512 US", "ocean_carrier": "Hapag-Lloyd", "mbol": "HLCUSGN2512AWAC8", "hbol": null, "vessel_name": "CAUTIN", "voyage_number": "2516E", "transport_mode": null, "port_of_loading": "Vung Tau", "port_of_discharge": "New York", "eta": "02/23/2026 EST", "door_eta": null, "entry_number": null, "freight_payment_terms": null, "service_type": null, "movement_type": null, "customer": "Haomaijia Technology (Shenzhen) Co., LTD", "goods_description": null, "package_count": null, "measurement_cbm": null, "measurement_cft": null, "cargo_references": ["WHS-58624-41985386"], "delivery_notes": null, "delivery_carrier": "Cargomatic", "routing_legs": null, "containers": [ { "container_number": "HAMU1717557", "seal_number": "HLC3251157", "container_type": "ISO_45G0", "gross_weight_kg": null, "gross_weight_lb": 16762.0 } ] } } }, { "id": "e75541c0-9ad5-408b-9747-23415adfbca0", "type": "document", "attributes": { "document_type": "dray_delivery_order", "source": "email", "file_name": "delivery_order_2902654.pdf", "file_url": "https://t49-documents-prod.s3.amazonaws.com/..." }, "relationships": { "last_document_representation": { "data": { "id": "b3abc297-624a-4eaa-a0e9-4ac4ebbd064f", "type": "document_representation" } }, "email_submission": { "data": { "id": "048bfec4-1249-4239-ab5b-63ad1e8f70cf", "type": "email_submission" } } } } ] } ``` ## Use these endpoints while integrating * [`GET /webhook_notifications/examples`](/docs/api-docs/api-reference/webhook-notifications/get-webhook-notification-payload-examples) * [`POST /webhooks/trigger`](/docs/api-docs/api-reference/webhooks/trigger-a-webhook) You can trigger a test payload for a specific document type without sending an email: ```bash theme={null} curl -X POST https://api.terminal49.com/v2/webhooks/trigger \ -H "Authorization: Token YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-endpoint.example/webhooks", "event": "document_representation.created", "sample": { "document_type": "dray_delivery_order" } }' ``` Webhook event availability depends on your account configuration. If you are not receiving expected events, contact Terminal49 support. ## APIs involved * [`GET /documents`](/docs/api-docs/api-reference/documents/list-documents) * [`GET /documents/{id}`](/docs/api-docs/api-reference/documents/get-a-document) * [`GET /documents/{id}/download_url`](/docs/api-docs/api-reference/documents/get-a-document-download-url) * [`GET /email_submissions`](/docs/api-docs/api-reference/email-submissions/list-email-submissions) * [`GET /email_submissions/{id}`](/docs/api-docs/api-reference/email-submissions/get-an-email-submission) * [`GET /document_schemas/{id}`](/docs/api-docs/api-reference/document-schemas/get-a-document-schema) * [`Document representations resource`](/docs/api-docs/api-reference/document-representations/document-representations-resource) # Event Timestamps Source: https://terminal49.com/docs/api-docs/in-depth-guides/event-timestamps Learn how Terminal49 stores transport event timestamps in UTC and how to convert them to local time using the matching IANA timezone field. Through the typical container lifecycle, events occur across multiple timezones. Wherever you see a timestamp for a transport event, there should be a corresponding [IANA timezone](https://www.iana.org/time-zones). Event timestamps are stored and returned in UTC. If you wish to present them in the local time you need to convert that UTC timestamp using the corresponding timezone. ### Example If you receive a container model with the attributes ``` 'pod_arrived_at': '2022-12-22T07:00:00Z', 'pod_timezone': 'America/Los_Angeles', ``` then the local time of the `pod_arrived_at` timestamp would be `2022-12-21T23:00:00 PST -08:00` ## When the corresponding timezone is null When an event occurs and Terminal49 cannot determine the location (and therefore the timezone), the system cannot store the event in true UTC. In this scenario, Terminal49 takes the timestamp as given from the source and parses it in UTC. ### Example ``` 'pod_arrived_at': '2022-12-22T07:00:00Z', 'pod_timezone': null, ``` then the local time of the `pod_arrived_at` timestamp would be `2022-12-22T07:00:00` and the timezone is unknown. (Assuming the source was returning localized timestamps) ## When `location` and `location_locode` are null On transport events, both `location_locode` (and any related `location` object) are nullable. They may be `null` when: * The event is **estimated** (`estimated: true`) and the carrier does not publish a location for the prediction. * The carrier or data provider omits the location field on a given event. This varies by carrier and event type. * Terminal49 could not normalize the source location to a known UNLOCODE. This is expected behavior, not a delivery error. The location is not back-filled later for the same event — if a subsequent actual event (for example, `container.transport.vessel_arrived` replacing `container.transport.estimated.vessel_arrived`) includes a location, it will be delivered as a new event. Treat `location_locode == null` as "location unknown" rather than waiting for an update on the original event. When `location_locode` is null, `timezone` is typically null as well; see [When the corresponding timezone is null](#when-the-corresponding-timezone-is-null) for how timestamps are stored in that case. ## System timestamps Timestamps representing changes within the Terminal49 system (e.g. `created_at`, `updated_at`, `terminal_checked_at`) are stored and represented in UTC and do not have a time zone. # Container Holds, Fees, and Release Readiness Source: https://terminal49.com/docs/api-docs/in-depth-guides/holds-and-fees Determine when an import container is released for pickup by reading holds, fees, last free day, and availability data from the Terminal49 API. After a container is discharged at the Port of Discharge (POD), the terminal and government agencies may place holds or assess fees before the container can be picked up. For shipments with inland rail moves, holds and fees can also apply at the inland destination. Your integration needs to monitor these fields to determine when a container is actually released and ready for pickup. Terminal49 normalizes hold and fee data from supported terminal sources into two structured arrays on the container object: `holds_at_pod_terminal` and `fees_at_pod_terminal`. This guide shows you how to use them. The field names reference `pod_terminal` for historical reasons, but these fields report hold and fee data regardless of whether the container is at a port terminal or an inland rail destination. The same readiness logic applies in both scenarios. ## Determine if a container is ready for pickup The most common question is straightforward: **can I pick up this container?** You need two fields from the container's `attributes` to answer it: * `available_for_pickup` — a boolean the terminal sets when the container is cleared for release * `holds_at_pod_terminal` — an array of active holds blocking pickup Use them together. A container is ready for pickup when `available_for_pickup` is `true` **and** there are no active holds: ```javascript theme={null} function isReadyForPickup(container) { const { available_for_pickup, holds_at_pod_terminal } = container.attributes; const hasActiveHolds = holds_at_pod_terminal.some(h => h.status === 'hold'); return available_for_pickup === true && !hasActiveHolds; } ``` Here is the full decision logic: ```mermaid theme={null} flowchart TD Start["Container discharged at POD"] --> CheckAvailable{"available_for_pickup\n== true?"} CheckAvailable -->|Yes| VerifyHolds{"holds array\nempty?"} CheckAvailable -->|No| CheckHolds{"Any active\nholds?"} VerifyHolds -->|Yes| Ready["Ready for pickup"] VerifyHolds -->|No| OutOfSync["Data may be out of sync\nWait for next update"] CheckHolds -->|Yes| Blocked["Blocked — resolve\nthe active holds"] CheckHolds -->|No| NotYet["Not yet released\nMonitor for updates"] ``` Treat `available_for_pickup: true` with an empty holds array as the definitive signal that the container is ready. When holds and `available_for_pickup` disagree — for example, holds are cleared but `available_for_pickup` is still `false` — wait for the next `container.updated` webhook or poll the container again. Terminal data is sourced from multiple systems on varying schedules, so brief inconsistencies can occur. ## Where to find holds and fees Both fields live on the container's `attributes` object in the V2 API: * `holds_at_pod_terminal` — active holds blocking or flagging pickup * `fees_at_pod_terminal` — fees assessed at the terminal [`GET /v2/containers/{id}`](/docs/api-docs/api-reference/containers/get-a-container) ```json theme={null} { "data": { "id": "3cd51f0e-eb18-4399-9f90-4c8a22250f63", "type": "container", "attributes": { "number": "COSU1186800", "available_for_pickup": false, "holds_at_pod_terminal": [ { "name": "customs", "status": "hold", "description": "CBP HOLD" } ], "fees_at_pod_terminal": [ { "type": "demurrage", "amount": 850.00, "currency_code": "USD" } ] } } } ``` An empty array (`[]`) means there are no active holds or fees of that type. ## Hold types at a glance Each item in `holds_at_pod_terminal` is a `terminal_hold` object: | Field | Type | Description | | ------------- | -------------- | ----------------------------------------- | | `name` | string | The canonical hold type (see table below) | | `status` | string | `"hold"` or `"pending"` | | `description` | string \| null | Raw text from the terminal, if provided | When a hold is cleared, the object is removed from the array entirely. There is no `"released"` status. An empty array means no active holds. | Hold name | Description | Who resolves it | | --------- | ------------------------------------ | -------------------------------------- | | `freight` | Carrier freight charges unpaid | Shipping line or freight forwarder | | `customs` | CBP hold — docs, exam, or inspection | Licensed customs broker | | `USDA` | USDA phytosanitary inspection | Customs broker or USDA compliance team | | `VACIS` | Non-intrusive X-ray / gamma-ray scan | Customs broker | | `TMF` | Terminal management fee (pier pass) | Pay terminal directly | | `other` | Unmapped hold — check `description` | Terminal or broker | Hold names are case-sensitive. `USDA`, `VACIS`, and `TMF` are uppercase. `freight`, `customs`, and `other` are lowercase. Match values exactly in your code. The ocean carrier has placed a freight hold because the freight charges have not been paid or confirmed. The container will not be released until the carrier lifts this hold. ```json theme={null} { "name": "freight", "status": "hold", "description": null } ``` **Who resolves it:** Contact the shipping line or your freight forwarder to confirm payment status. US Customs and Border Protection (CBP) has placed a hold. This can occur due to documentation issues, a random examination, or a targeted inspection. The container cannot be released until CBP clears it. ```json theme={null} { "name": "customs", "status": "hold", "description": "CBP HOLD" } ``` **Who resolves it:** Your licensed customs broker. Resolution time varies from hours to several days depending on the examination type. The US Department of Agriculture (USDA) has flagged the shipment for a phytosanitary inspection. Common for shipments containing food, plants, wood packaging, or agricultural products. ```json theme={null} { "name": "USDA", "status": "hold", "description": null } ``` **Who resolves it:** Your customs broker or USDA compliance team. Inspections typically happen at the terminal or a USDA-approved facility. The container has been flagged for a VACIS (Vehicle and Cargo Inspection System) scan — a non-intrusive gamma-ray or X-ray inspection conducted by CBP. You may also see this referred to as an NII (Non-Intrusive Inspection) exam. ```json theme={null} { "name": "VACIS", "status": "hold", "description": "VACIS EXAM" } ``` **Who resolves it:** Your customs broker. The exam fee (if assessed) will appear separately in `fees_at_pod_terminal` as type `"exam"`. A Terminal Management Fee (TMF) hold is placed by the terminal itself — sometimes called a pier pass or terminal gate fee. This hold is typically resolved by paying the fee directly to the terminal. ```json theme={null} { "name": "TMF", "status": "hold", "description": null } ``` **Who resolves it:** Pay the terminal fee. Your drayage carrier or port agent can assist. A hold that Terminal49 could not map to a specific type. The raw terminal text, when available, appears in the `description` field. ```json theme={null} { "name": "other", "status": "hold", "description": "TERMINAL HOLD - SEE CUSTOMER SERVICE" } ``` **What to do:** Use the `description` to identify the specific issue and contact the terminal or your broker for resolution. A `status` of `"pending"` means the terminal has flagged a hold as expected but not yet active. Treat it as a warning that a hold is likely incoming. When the hold becomes active, the status changes to `"hold"` and you receive a `container.updated` webhook notification. ## Fee types at a glance Each item in `fees_at_pod_terminal` is a `terminal_fee` object: | Field | Type | Description | | --------------- | ------ | ----------------------------------------- | | `type` | string | The canonical fee type (see table below) | | `amount` | number | Fee amount in local currency | | `currency_code` | string | ISO 4217 currency code, typically `"USD"` | | Fee type | Description | Charged by | | --------------------- | -------------------------------------------------------- | ------------------------- | | `demurrage` | Daily charge after carrier free time expires | Ocean carrier | | `extended_dwell_time` | Terminal charge for prolonged dwell | Terminal | | `exam` | CBP/USDA inspection cost | Terminal or exam facility | | `total` | Combined total of all fees (may overlap with line items) | See individual items | | `other` | Unmapped fee type | Varies | A daily charge assessed by the **ocean carrier** when the container is not picked up within the free time period. Demurrage starts accruing after the carrier's free time expires and increases every day. ```json theme={null} { "type": "demurrage", "amount": 1250.00, "currency_code": "USD" } ``` **Note:** Demurrage is charged by the carrier, not the terminal. The terminal reports it, but you pay the carrier. An Extended Dwell Time (EDT) fee charged by the **terminal** (separate from carrier demurrage) when a container sits at the terminal beyond a threshold. Common at major US gateways like the Ports of LA and Long Beach. ```json theme={null} { "type": "extended_dwell_time", "amount": 300.00, "currency_code": "USD" } ``` Covers the cost of a physical or non-intrusive (VACIS) inspection by CBP or USDA. Exam fees are typically paid to the terminal or a government-approved exam facility. Amounts vary widely — from a few hundred to several thousand dollars depending on the exam type. ```json theme={null} { "type": "exam", "amount": 450.00, "currency_code": "USD" } ``` A combined total of all fees at the terminal, reported as a single line item. Some terminals report only a total rather than individual fee breakdowns. If you see a `total` fee in the array alongside individual line items, filter it out when summing to avoid double-counting. ```json theme={null} { "type": "total", "amount": 2000.00, "currency_code": "USD" } ``` A fee that Terminal49 could not map to a specific type. ```json theme={null} { "type": "other", "amount": 75.00, "currency_code": "USD" } ``` ## Full example: container with multiple holds and fees ```json theme={null} { "holds_at_pod_terminal": [ { "name": "customs", "status": "hold", "description": "CBP HOLD" }, { "name": "freight", "status": "hold", "description": null } ], "fees_at_pod_terminal": [ { "type": "demurrage", "amount": 850.00, "currency_code": "USD" }, { "type": "exam", "amount": 450.00, "currency_code": "USD" } ] } ``` In this example, the container has two active holds (`customs` and `freight`) and two fees. Both holds must be resolved before the container can be released. The demurrage fee will continue increasing daily until the container is picked up. ## Getting notified when holds or fees change Subscribe to the `container.updated` webhook to run your release-readiness check in real time whenever holds or fees change. The `changeset` on the `container_updated_event` shows the old value and new value side by side — old first, new second. For full details on setting up webhooks, see [Webhooks](/docs/api-docs/in-depth-guides/webhooks). A customs hold appeared on the container: ```json theme={null} { "changeset": { "holds_at_pod_terminal": [ [], [ { "name": "customs", "status": "hold", "description": "CBP HOLD" } ] ] } } ``` The customs hold was lifted — the container is now clear: ```json theme={null} { "changeset": { "holds_at_pod_terminal": [ [ { "name": "customs", "status": "hold", "description": "CBP HOLD" } ], [] ] } } ``` A pending hold escalated to an active hold: ```json theme={null} { "changeset": { "holds_at_pod_terminal": [ [ { "name": "customs", "status": "pending", "description": null } ], [ { "name": "customs", "status": "hold", "description": "CBP HOLD" } ] ] } } ``` Demurrage increased as another day accrued: ```json theme={null} { "changeset": { "fees_at_pod_terminal": [ [ { "type": "demurrage", "amount": 850.00, "currency_code": "USD" } ], [ { "type": "demurrage", "amount": 1250.00, "currency_code": "USD" } ] ] } } ``` ## Edge cases * **Empty arrays mean no holds or fees.** An empty `holds_at_pod_terminal: []` or `fees_at_pod_terminal: []` is the normal state for most containers. Do not treat it as missing data or an error. **Avoid double-counting when `total` is present.** Some terminals report a `total` fee alongside individual line items. Filter it out before summing: ```javascript theme={null} const lineItems = container.fees_at_pod_terminal.filter(f => f.type !== 'total'); const totalAmount = lineItems.reduce((acc, f) => acc + f.amount, 0); ``` **Fee amount of `0` is valid.** A fee amount of `0` means the terminal reported the fee type but has not yet calculated or posted the dollar amount. This is common for demurrage in the first day or two after discharge. Poll the container or wait for the next `container.updated` event. **The `description` field is raw terminal text.** The `description` on hold objects is unstructured text scraped directly from the terminal. It is useful context for humans but should not be used for programmatic decision-making. Use the `name` field to drive automation logic. ## Frequently asked questions Check two fields together: `available_for_pickup` must be `true` **and** the `holds_at_pod_terminal` array must have no items with `status: "hold"`. See the [decision logic and code example](#determine-if-a-container-is-ready-for-pickup) above. Yes. Holds and fees are independent. Holds block pickup. Your container cannot be released until all holds are cleared. Fees are charges you owe (demurrage, exam costs, etc.) that may continue accruing whether or not holds are present. The hold object is removed from the `holds_at_pod_terminal` array entirely. There is no `"released"` status. An empty array means no active holds. You receive a `container.updated` webhook when this happens. Terminal data is sourced from multiple systems on varying schedules. A hold can clear before the terminal updates `available_for_pickup`, or vice versa. Wait for the next `container.updated` webhook or poll the container again. Treat `available_for_pickup: true` with an empty holds array as the definitive readiness signal. Yes. The `holds_at_pod_terminal` and `fees_at_pod_terminal` fields report data regardless of whether the container is at a port terminal or an inland rail destination. The field names reference `pod_terminal` for historical reasons, but the same readiness logic applies in both scenarios. No. The Terminal49 API reports holds, fees, and last free day as read-only data. It does not expose payment endpoints and cannot request an LFD extension. Pay demurrage or terminal fees directly with the carrier or terminal, and request LFD extensions through the carrier or your freight forwarder. Once the terminal updates its records, the change flows back into `holds_at_pod_terminal`, `fees_at_pod_terminal`, and `pickup_lfd` on the next `container.updated` webhook. Some terminals report a `total` fee alongside individual line items. Filter it out before summing: ```javascript theme={null} const lineItems = container.fees_at_pod_terminal.filter(f => f.type !== 'total'); const totalAmount = lineItems.reduce((acc, f) => acc + f.amount, 0); ``` ## Related guides How `available_for_pickup` and `current_status` are derived Subscribe to `container.updated` events When terminal data was captured Inland rail moves and container tracking at rail destinations # Include related resources in API responses Source: https://terminal49.com/docs/api-docs/in-depth-guides/including-resources Use the include query parameter to return related resources like shipments, containers, terminals, and transport events in a single API response. Throughout the documentation you will notice that many of the endpoints include a `relationships` object inside of the `data` attribute. For example, if you are [requesting a container](/docs/api-docs/api-reference/containers/get-a-container) the relationships will include `shipment`, and possibly `pod_terminal` and `transport_events` If you want to load the `shipment` and `pod_terminal` without making any additional requests you can add the query parameter `include` and provide a comma delimited list of the related resources: ``` containers/{id}?include=shipment,pod_terminal ``` You can even traverse the relationships up or down. For example if you wanted to know the port of lading for the container you could get that with: ``` containers/{id}?include=shipment,shipment.port_of_lading ``` # Terminal49 MCP Server Quickstart Source: https://terminal49.com/docs/api-docs/in-depth-guides/mcp Set up the Terminal49 MCP server in Claude, ChatGPT, Cursor, Copilot, or any MCP client to query live shipment and container tracking data from your AI tool. This guide covers everything you need to connect an MCP client to Terminal49's container tracking data. Just want to get started fast? See the per-tool setup guides for [Claude](/docs/mcp/setup/claude), [Claude Code](/docs/mcp/setup/claude-code), [ChatGPT](/docs/mcp/setup/chatgpt), [Cursor](/docs/mcp/setup/cursor), [Microsoft Copilot](/docs/mcp/setup/microsoft-copilot), and [VS Code](/docs/mcp/setup/vs-code), or the [MCP Overview](/docs/mcp/home) for a 5-minute setup. ## Prerequisites Before you begin, make sure you have: You sign in with your Terminal49 credentials during the OAuth flow — no API key needed Only needed for clients without OAuth support or the local stdio server — create one in the [developer portal](https://app.terminal49.com/developers/api-keys) Required if running the MCP server locally Claude, ChatGPT, Cursor, Microsoft Copilot, VS Code, or any MCP-compatible client **Technical Details:** * **MCP SDK**: `@modelcontextprotocol/sdk ^1.29.0` * **TypeScript SDK**: `@terminal49/sdk 0.3.0` * **Sentry MCP Monitoring**: `@sentry/node ^10.55.0` (optional) * **Runtime**: Node.js 24.x *** ## Transports | Transport | Endpoint | Best For | | ----------------- | --------------------------------- | -------------------------------- | | HTTP (streamable) | `POST https://mcp.terminal49.com` | Serverless, short-lived requests | **Authentication**: OAuth 2.1 (recommended) or API key. * **OAuth 2.1** – no API key needed. Add `https://mcp.terminal49.com` to your client; it discovers the authorization server (`https://auth.terminal49.com`) via protected resource metadata, registers itself with Dynamic Client Registration, and opens your browser so you can sign in with your Terminal49 credentials. Tokens are stored and refreshed by the client. * **API key** – for clients that can't run a browser OAuth flow, pass `Authorization: Token YOUR_API_KEY`. Use the `Token` scheme for API keys; the `Bearer` scheme is used for OAuth access tokens, which OAuth clients obtain automatically. Only the [local stdio server](#local-stdio-development) reads the `T49_API_TOKEN` environment variable instead of a header. For hosted production usage, connect to `https://mcp.terminal49.com`. The root origin is the canonical connector URL and OAuth resource identifier. *** ## Observability The MCP server supports optional [Sentry MCP Monitoring](https://docs.sentry.io/ai/monitoring/mcp/). Set `SENTRY_DSN` to capture MCP server connections, tool executions, resource access, prompts, performance spans, and errors in Sentry. ```bash theme={null} SENTRY_DSN=___PUBLIC_DSN___ SENTRY_TRACES_SAMPLE_RATE=1.0 SENTRY_MCP_RECORD_INPUTS=false SENTRY_MCP_RECORD_OUTPUTS=false SENTRY_SEND_DEFAULT_PII=false ``` `SENTRY_MCP_RECORD_INPUTS` and `SENTRY_MCP_RECORD_OUTPUTS` are disabled by default because MCP payloads can include shipment identifiers, references, and customer data. Enable them only if your Sentry project is approved for that data. *** ## Configure your MCP client ### OAuth setup (recommended) Most clients connect with just the server URL and a browser sign-in — no API key. Follow the guide for your tool: * [Claude](/docs/mcp/setup/claude) – claude.ai and Claude Desktop * [Claude Code](/docs/mcp/setup/claude-code) – one `claude mcp add` command * [ChatGPT](/docs/mcp/setup/chatgpt) – install from the ChatGPT Plugins Directory * [Cursor](/docs/mcp/setup/cursor) – `mcp.json` or Cursor Settings → MCP * [Microsoft Copilot](/docs/mcp/setup/microsoft-copilot) – Copilot Studio agent tools * [VS Code](/docs/mcp/setup/vs-code) – GitHub Copilot agent mode * [Agent plugins](/docs/mcp/setup/agent-plugins) – Terminal49 plugin for Claude Code, Cursor, Codex, and GitHub Copilot CLI * [Other MCP clients](/docs/mcp/setup/other-clients) – generic configuration The manual configurations below use an API key instead. Use them for clients or environments where the browser OAuth flow isn't practical. claude.ai and Claude Desktop cannot send a static API-key header — they always authenticate through the OAuth connector flow. Follow the [Claude setup guide](/docs/mcp/setup/claude) to connect them. For API-key-based local development in Claude Desktop, use the [local stdio server](#local-stdio-development) below instead. ### Cursor IDE (manual, API key) Add to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for all projects): ```json theme={null} { "mcpServers": { "terminal49": { "url": "https://mcp.terminal49.com", "headers": { "Authorization": "Token YOUR_API_KEY" } } } } ``` ### Local stdio (development) For local development without a hosted server: ```json theme={null} { "mcpServers": { "terminal49": { "command": "node", "args": ["/path/to/API/packages/mcp/dist/index.js"], "env": { "T49_API_TOKEN": "YOUR_API_KEY" } } } } ``` Build the MCP server first: `cd packages/mcp && npm install && T49_SDK_SOURCE=published npm run sdk:setup && npm run build` Use published SDK by default: ```bash theme={null} cd packages/mcp T49_SDK_SOURCE=published npm run sdk:setup ``` Use local SDK build during development: ```bash theme={null} cd packages/mcp T49_SDK_SOURCE=local npm run sdk:setup ``` *** ## Test your setup Once configured, verify everything works: Close and reopen Claude Desktop or Cursor to load the new config. > "List the tools available in the Terminal49 MCP server." Claude should respond with a list of 10 tools including `search_container`, `track_container`, and list tools. > "Using the Terminal49 MCP server, search for container TCLU1234567 and summarize its status." If configured correctly, Claude will call `search_container` and return container details. > "Using Terminal49, find container CAIU1234567, check its demurrage risk, and tell me if I need to pick it up urgently." Claude should chain multiple tools together to answer. Need test container numbers? See [Test Numbers](/docs/api-docs/useful-info/test-numbers) for containers you can use during development. *** ## Troubleshooting | Symptom | Likely Cause | How to Fix | | ------------------------------ | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | "Cannot connect to MCP server" | Wrong URL or config path | Confirm URL is `https://mcp.terminal49.com` and config file path matches your OS | | `401 Unauthorized` | Expired OAuth session, or missing/invalid API key | Disconnect and reconnect the connector to re-run the OAuth sign-in. If using an API key, create a new one in the [developer portal](https://app.terminal49.com/developers/api-keys) and ensure the `Authorization: Token YOUR_API_KEY` header is set | | `429 Too Many Requests` | Rate limit exceeded | See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting); use webhooks instead of polling | | Tools list is empty | Config not loaded | Restart Claude/Cursor; check MCP inspector for errors | | "Tool not found" | Typo in tool name | Use exact names: `search_container`, `get_container`, etc. | | Slow responses | Large data requests | Use `include` parameter to load only what you need | If using the hosted server, check your Terminal49 dashboard for API logs. If running locally: ```bash theme={null} cd packages/mcp T49_API_TOKEN=YOUR_API_KEY npm run mcp:stdio 2>&1 | head -20 ``` *** ## MCP capabilities ### Tools | Tool | Description | Parameters | | -------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------- | | `search_container` | Find containers by number, BL, booking, or ref | `query: string` | | `track_container` | Start tracking a container | `number`, `numberType?`, `scac?`, `refNumbers?` | | `get_container` | Get container with optional includes | `id: uuid`, `include?: ['shipment', 'pod_terminal', 'transport_events']` | | `get_shipment_details` | Get shipment and containers | `id: uuid`, `include_containers?: boolean` | | `get_container_transport_events` | Get event timeline | `id: uuid` | | `get_supported_shipping_lines` | List carriers with SCAC codes | `search?: string` | | `get_container_route` | Get multi-leg routing (paid feature) | `id: uuid` | | `list_shipments` | List shipments with filters + pagination | `status?`, `port?`, `carrier?`, `updated_after?`, `include_containers?`, `page?`, `page_size?` | | `list_containers` | List containers with filters + pagination | `status?`, `port?`, `carrier?`, `updated_after?`, `include?`, `page?`, `page_size?` | | `list_tracking_requests` | List tracking requests with filters | `filters?`, `status?`, `request_type?`, `page?`, `page_size?` | ### Prompts | Prompt | Description | Arguments | | ----------------- | ----------------------- | ------------------------------ | | `track-shipment` | Quick tracking workflow | `container_number`, `carrier?` | | `check-demurrage` | Demurrage risk analysis | `container_id` | | `analyze-delays` | Journey delay analysis | `container_id` | ### Resources | URI | Description | | -------------------------------------- | -------------------------- | | `terminal49://container/{id}` | Container data as resource | | `terminal49://docs/milestone-glossary` | Event/milestone reference | For detailed examples and response formats, see [MCP Overview → Tools Reference](/docs/mcp/home#tools-reference). *** ## SDK usage The TypeScript SDK provides the same capabilities as MCP tools, plus additional APIs not yet exposed via MCP. ```bash theme={null} npm install @terminal49/sdk ``` ```typescript theme={null} import { Terminal49Client } from '@terminal49/sdk'; const client = new Terminal49Client({ apiToken: process.env.T49_API_TOKEN!, defaultFormat: 'mapped' }); // Get container with shipment and terminal const container = await client.containers.get( 'container-uuid', ['shipment', 'pod_terminal'] ); // Search for containers const results = await client.search('CAIU1234567'); // List shipments with filters (not available via MCP) const shipments = await client.shipments.list({ status: 'in_transit', carrier: 'MAEU' }); ``` ### Response formats | Format | Description | | -------- | ------------------------------------------------------------ | | `raw` | JSON:API response with `data`, `attributes`, `relationships` | | `mapped` | Simplified, camelCase objects with IDs resolved | | `both` | `{ raw, mapped }` for debugging | **Raw format:** ```json theme={null} { "data": { "type": "container", "id": "abc-123", "attributes": { "container_number": "CAIU1234567", "available_for_pickup": true } } } ``` **Mapped format:** ```json theme={null} { "id": "abc-123", "containerNumber": "CAIU1234567", "availableForPickup": true } ``` *** ## Deployment ### Vercel (production) The `vercel.json` configures the MCP server (excerpt): ```json theme={null} { "installCommand": "npm ci", "buildCommand": "npm run build --workspace @terminal49/sdk && npm run build --workspace @terminal49/mcp", "functions": { "api/mcp.ts": { "maxDuration": 30 } }, "rewrites": [ { "source": "/mcp", "destination": "/api/mcp" }, { "source": "/", "destination": "/api/mcp" } ] } ``` ### Environment variables | Variable | Required | Description | | --------------------------- | --------------- | --------------------------------------------------------------------------------------------------------- | | `T49_API_TOKEN` | For local stdio | Terminal49 API key. The hosted HTTP endpoint authenticates callers via the `Authorization` header instead | | `T49_API_BASE_URL` | No | Override API URL (default: `https://api.terminal49.com/v2`) | | `T49_MCP_ALLOWED_HOSTS` | No | Comma-separated host allowlist for request Host validation | | `T49_MCP_ALLOWED_ORIGINS` | No | Comma-separated origin allowlist for request Origin validation | | `SENTRY_DSN` | No | Enables Sentry MCP Monitoring | | `SENTRY_ENVIRONMENT` | No | Sentry environment name; defaults to `NODE_ENV` | | `SENTRY_RELEASE` | No | Sentry release identifier; defaults to `VERCEL_GIT_COMMIT_SHA` | | `SENTRY_TRACES_SAMPLE_RATE` | No | Trace sampling rate from `0` to `1`; defaults to `1.0` | | `SENTRY_MCP_RECORD_INPUTS` | No | Records MCP inputs in Sentry when set to `true`; defaults to `false` | | `SENTRY_MCP_RECORD_OUTPUTS` | No | Records MCP outputs in Sentry when set to `true`; defaults to `false` | | `SENTRY_SEND_DEFAULT_PII` | No | Enables Sentry default PII behavior; defaults to `false` | *** ## Testing locally ```bash theme={null} # Build the MCP server cd packages/mcp npm install npm run build # Test tools/list echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | T49_API_TOKEN=YOUR_API_KEY npm run mcp:stdio # Test search_container echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"search_container","arguments":{"query":"CAIU1234567"}},"id":2}' | T49_API_TOKEN=YOUR_API_KEY npm run mcp:stdio # Test the hosted endpoint curl -X POST https://mcp.terminal49.com \ -H "Authorization: Token $T49_API_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' ``` *** ## Related guides * [MCP Overview](/docs/mcp/home) — Quick start and tools reference * Setup guides: [Claude](/docs/mcp/setup/claude), [Claude Code](/docs/mcp/setup/claude-code), [ChatGPT](/docs/mcp/setup/chatgpt), [Cursor](/docs/mcp/setup/cursor), [Microsoft Copilot](/docs/mcp/setup/microsoft-copilot), [VS Code](/docs/mcp/setup/vs-code), [Agent plugins](/docs/mcp/setup/agent-plugins), [Other clients](/docs/mcp/setup/other-clients) * [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) — API limits (same for MCP) * [Test Numbers](/docs/api-docs/useful-info/test-numbers) — Containers for testing * [Webhooks](/docs/api-docs/in-depth-guides/webhooks) — Real-time updates * [Data Coverage](/docs/coverage/home) — Data freshness and coverage # Terminal49 API Quickstart Source: https://terminal49.com/docs/api-docs/in-depth-guides/quickstart Follow the Terminal49 API quickstart to create a tracking request, monitor your first shipment, and retrieve live container and shipment data. ## Before you begin You need four things to get started. 1. **A Bill of Lading (BOL) number.** This is issued by your carrier. BOL numbers are found on your [bill of lading](https://en.wikipedia.org/wiki/Bill_of_lading) document. Ideally, this will be a shipment that is currently on the water or in terminal, but this is not necessary. 2. **The SCAC of the carrier that issued your bill of lading.** The Standard Carrier Alpha Code of your carrier is used to identify carriers in computer systems and in shipping documents. You can learn more about these [here](https://en.wikipedia.org/wiki/Standard_Carrier_Alpha_Code). 3. **A Terminal49 Account.** If you don't have one yet, [sign up here.](https://app.terminal49.com/register) 4. **An API key.** Sign in to your Terminal49 account and go to your [developer portal page](https://app.terminal49.com/developers/api-keys) to get your API key. Not sure which SCAC to use? The [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) endpoint (Auto-Detect Carrier) can identify it from your tracking number. ## Track a shipment Use the request example below as a starting point, or copy the same values into Postman or cURL. 1. Replace `YOUR_API_KEY` in the `Authorization` header with your API key. 2. Replace `request_number` and `scac` with your shipment details. The request number must be a shipping line booking number, master bill of lading number, or container number. The SCAC must be a shipping line SCAC. See [Data Coverage](/docs/coverage/home) for coverage details. ```json POST /tracking_requests theme={null} { "method": "post", "url": "https://api.terminal49.com/v2/tracking_requests", "headers": { "Content-Type": "application/vnd.api+json", "Authorization": "Token YOUR_API_KEY" }, "body": { "data": { "attributes": { "request_type": "bill_of_lading", "request_number": "", "scac": "" }, "type": "tracking_request" } } } ``` ## Check your tracking request succeeded If you have not set up a webhook to receive status updates from the Terminal49 API, you need to poll manually to check whether the tracking request succeeded or failed. **Tracking request troubleshooting** The most common issue is entering the wrong number. Check that you are entering a Bill of Lading number, booking number, or container number — not an internal reference from your company or freight forwarder. Verify the number by going to the carrier's website and tracking the shipment with it. If that works and Terminal49 supports the SCAC, you should be able to track it through the API. Email [support@terminal49.com](mailto:support@terminal49.com) if you have persistent issues. Use this request to list your recent tracking requests. Replace `YOUR_API_KEY` in the `Authorization` header with your API key. ```json GET /tracking_requests theme={null} { "method": "get", "url": "https://api.terminal49.com/v2/tracking_requests", "headers": { "Content-Type": "application/vnd.api+json", "Authorization": "Token YOUR_API_KEY" } } ``` ## List your tracked shipments If your tracking request was successful, you will now be able to list your tracked shipments. Use this request to list tracked shipments. Replace `YOUR_API_KEY` in the `Authorization` header with your API key. Sometimes it may take a while for the tracking request to show up, but usually no more than a few minutes. If you had trouble adding your first shipment, try adding a few more. ```json GET /shipments theme={null} { "method": "get", "url": "https://api.terminal49.com/v2/shipments", "headers": { "Content-Type": "application/vnd.api+json", "Authorization": "Token YOUR_API_KEY" } } ``` ## List all your tracked containers You can also list out all of your containers, if you'd like to track at that level. Use this request to list tracked containers. Replace `YOUR_API_KEY` in the `Authorization` header with your API key. ```json GET /containers theme={null} { "method": "get", "url": "https://api.terminal49.com/v2/containers", "headers": { "Content-Type": "application/vnd.api+json", "Authorization": "Token YOUR_API_KEY" } } ``` ## Listening for updates with webhooks The real power of Terminal49's API is that it is asynchronous. You can register a webhook — a callback URL that Terminal49 sends HTTP POST requests to when updates occur. To try this, first set up a URL on the open web to receive POST requests. Once configured, you receive status updates from containers and shipments as they happen, so you do not need to poll for updates. Choose the events you want to subscribe to (for example vessel departed, arrived, or discharged). Terminal49 sends those events to your webhook endpoint as soon as they happen. You can test your endpoint before creating the actual webhook by sending a sample notification with the Trigger endpoint: ```json POST /webhooks/trigger theme={null} { "method": "post", "url": "https://api.terminal49.com/v2/webhooks/trigger", "headers": { "Content-Type": "application/json", "Authorization": "Token YOUR_API_KEY" }, "body": { "url": "https://webhook.site/", "event": "container.transport.vessel_arrived", "secret": "optional-test-secret" } } ``` Trigger sends an example payload for the event you choose. Once tested, create the actual webhook to receive the real notifications as they happen: ```json POST /webhooks theme={null} { "method": "post", "url": "https://api.terminal49.com/v2/webhooks", "headers": { "Content-Type": "application/vnd.api+json", "Authorization": "Token YOUR_API_KEY" }, "body": { "data": { "type": "webhook", "attributes": { "url": "https://webhook.site/", "active": true, "events": ["container.transport.vessel_arrived"] } } } } ``` Learn more about [Webhooks](/docs/api-docs/in-depth-guides/webhooks). # Integrate rail container tracking data Source: https://terminal49.com/docs/api-docs/in-depth-guides/rail-integration-guide Integrate North American rail container tracking data with Terminal49 for unified shipment visibility across Class I rail and intermodal carriers. This is a technical article about rail data within Terminal49's API and DataSync. For a broader overview, including the reasons why you'd want rail visibility and how to use it in the Terminal49 dashboard, [read the Terminal49 announcement post](https://www.terminal49.com/blog/launching-north-american-intermodal-rail-visibility-on-terminal49/). ## Table of contents * [Supported rail carriers](#supported-rail-carriers) * [Supported rail events and data attributes](#supported-rail-events-and-data-attributes) * [Rail-specific transport events](#rail-specific-transport-events) * [Webhook notifications](#webhook-notifications) * [Rail container attributes](#rail-container-attributes) * [Integration methods](#integration-methods) * [Integration via API](#a-integration-via-api) * [Integration via DataSync](#b-integration-via-datasync) ## Supported rail carriers Terminal49's container tracking platform integrates with all North American Class I railroads that handle container shipping, providing comprehensive visibility into your rail container movements. * BNSF Railway * Canadian National Railway (CN) * Canadian Pacific Railway (CP) * CSX Transportation * Norfolk Southern Railway (NS) * Union Pacific Railroad (UP) By integrating with these carriers, Terminal49 ensures that you have direct access to critical tracking data, enabling better decision-making and operational efficiency. ## Supported rail events and data attributes Terminal49 seamlessly tracks your containers as they go from container ship, to ocean terminal, to rail carrier. Terminal49 provides a [set of transport events](#webhook-notifications) that let you track the status of your containers as they move through the rail system. You can receive webhook notifications whenever these events occur. Terminal49 also provides a set of attributes [on the container model](/docs/api-docs/api-reference/containers/get-a-container) with the current status of your container at any given time, including ETA, pickup facility, and availability information. For details on hold types, fee types, and how to determine release readiness at the port or an inland destination, see [Container Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). ### Rail-specific transport events Several core transport events occur on most rail journeys. Some rail carriers do not share all events, but in general these are the key events for a container. ```mermaid theme={null} graph LR A[Rail Loaded] --> B[Rail Departed] B --> C[Arrived at Inland Destination] C --> D[Rail Unloaded] D --> G[Available for Pickup] G --> E[Full Out] E --> F[Empty Return] ``` `Available for Pickup`, `Full Out` and `Empty Return` are not specific to rail, but are included here since they are a key part of the rail journey. ### Webhook notifications Terminal49 provides webhook notifications to keep you updated on key transport events in a container's rail journey. These notifications let you integrate near real-time tracking data directly into your applications. Here's a list of the rail-specific events which support webhook notifications: | Transport Event | Webhook Notification | Description | Example | | ----------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | Rail Loaded | `container.transport.rail_loaded` | The container is loaded onto a railcar. | [Example](/docs/api-docs/useful-info/webhook-events-examples#container-transport-rail_loaded) | | Rail Departed | `container.transport.rail_departed` | The container departs on the railcar (not always from port of discharge). | [Example](/docs/api-docs/useful-info/webhook-events-examples#container-transport-rail_departed) | | Rail Arrived | `container.transport.rail_arrived` | The container arrives at a rail terminal (not always at the destination terminal). | [Example](/docs/api-docs/useful-info/webhook-events-examples#container-transport-rail_arrived) | | Arrived At Inland Destination | `container.transport.arrived_at_inland_destination` | The container arrives at the destination terminal. | [Example](/docs/api-docs/useful-info/webhook-events-examples#container-transport-arrived_at_inland_destination) | | Rail Unloaded | `container.transport.rail_unloaded` | The container is unloaded from a railcar. | [Example](/docs/api-docs/useful-info/webhook-events-examples#container-transport-rail_unloaded) | | Rail LFD Changed | `container.pickup_lfd_rail.changed` | The Rail Last Free Day (LFD) for the container has changed ([Rail Plan only](/docs/api-docs/useful-info/entitlements)). | [Example](/docs/api-docs/useful-info/webhook-events-examples#container-pickup_lfd_rail-changed) | An additional set of events triggers when the container's status changes at the destination rail terminal. For containers without rail, these events fire at the ocean terminal instead. | Transport Event | Webhook Notification | Description | Example | | --------------- | ------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------------ | | Full Out | `container.transport.full_out` | The full container leaves the rail terminal. | [Example](/docs/api-docs/useful-info/webhook-events-examples#container-transport-full_out) | | Empty In | `container.transport.empty_in` | The empty container is returned to the terminal. | [Example](/docs/api-docs/useful-info/webhook-events-examples#container-transport-empty_in) | Finally, there is a webhook notification for when the destination ETA changes. | Transport Event | Webhook Notification | Description | Example | | ----------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | Estimated Destination Arrival | `container.transport.estimated.arrived_at_inland_destination` | Estimated time of arrival for the container at the destination rail terminal. | [Example](/docs/api-docs/useful-info/webhook-events-examples#container-transport-estimated-arrived_at_inland_destination) | Integrate these notifications by subscribing to the webhooks and handling the incoming data to update your systems. #### Set up a webhook for rail events Rail events use the same webhook infrastructure as ocean events. There is no separate rail-only endpoint or configuration. Create one webhook and subscribe it to the rail events you care about. You can create a webhook either from the [Developer Webhooks](https://app.terminal49.com/developers/webhooks) page in the dashboard or by calling [`POST /webhooks`](/docs/api-docs/api-reference/webhooks/create-a-webhook) with the rail events in the `events` array: ```json theme={null} { "data": { "type": "webhook", "attributes": { "url": "https://your-endpoint.example.com/webhooks/terminal49", "active": true, "events": [ "container.transport.rail_loaded", "container.transport.rail_departed", "container.transport.rail_arrived", "container.transport.arrived_at_inland_destination", "container.transport.rail_unloaded", "container.transport.estimated.arrived_at_inland_destination", "container.pickup_lfd_rail.changed" ] } } } ``` Notes: * `container.pickup_lfd_rail.changed` requires the [Rail Plan entitlement](/docs/api-docs/useful-info/entitlements). Other rail events are available on standard API access. * `container.transport.rail_arrived` fires **every time** a container arrives at a rail terminal — including intermediate interchanges, not just the final inland destination. Use the included location data (terminal name, city, FIRMS code, LOCODE) to identify each stop. * Store the `secret` returned in the create response and verify the `X-T49-Webhook-Signature` header on each delivery. See the [Webhooks in-depth guide](/docs/api-docs/in-depth-guides/webhooks) and [Webhooks best practices](/docs/api-docs/webhooks/best-practices). ### Rail container attributes The following attributes are specific to rail container tracking and live on the [container object](/docs/api-docs/api-reference/containers/get-a-container). * **pod\_rail\_loaded\_at**: Time when the container is loaded onto a railcar at the POD. * **pod\_rail\_departed\_at**: Time when the container departs from the POD. * **ind\_eta\_at**: Estimated Time of Arrival at the inland destination, sourced from the rail carrier. * **ind\_ata\_at**: Actual Time of Arrival at the inland destination, sourced from the rail carrier. * **ind\_rail\_unloaded\_at**: Time when the container is unloaded from rail at the inland destination. * **ind\_facility\_lfd\_on**: **Deprecated.** Last Free Day for demurrage charges at the inland destination terminal. Use `import_deadlines.pickup_lfd_rail` instead (timezone: `final_destination_timezone`). * **pod\_rail\_carrier\_scac**: SCAC code of the rail carrier that picks up the container from the POD (this could be different than the rail carrier that delivers to the inland destination). * **ind\_rail\_carrier\_scac**: SCAC code of the rail carrier that delivers the container to the inland destination. #### Inland destination ETA/ATA: rail carrier vs. shipping line For an inland (rail) move, two pairs of arrival fields exist and can differ: | Field | Lives on | Source | Notes | | ------------------------------------------- | --------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ind_eta_at` / `ind_ata_at` | container | Rail carrier | Estimated and actual arrival at the inland destination, reported directly by the rail carrier. | | `destination_eta_at` / `destination_ata_at` | shipment | Shipping line (SSL) | Estimated and actual arrival at the shipment's destination as reported by the ocean carrier. For inland moves, this is the SSL's view of the inland destination. | Use `ind_*` when you want the rail carrier's view (typically more granular and updated more frequently for inland legs). Use `destination_*` when you want the SSL-reported view on the shipment. The corresponding timezone for `ind_*` fields is `final_destination_timezone` (on the container); for `destination_*` fields it is `destination_timezone` (on the shipment). #### Rail Last Free Day (LFD) The container's top-level `pickup_lfd` attribute is a coalesced value derived from the `import_deadlines` object, in this order of preference: 1. `import_deadlines.pickup_lfd_line` — LFD as reported by the shipping line (preferred). 2. `import_deadlines.pickup_lfd_terminal` — LFD from the POD terminal (timezone: `pod_timezone`). 3. `import_deadlines.pickup_lfd_rail` — LFD from the rail carrier at the inland destination (timezone: `final_destination_timezone`). For rail moves, subscribe to the `container.pickup_lfd_rail.changed` webhook to be notified when the rail carrier updates the inland LFD. The legacy `ind_facility_lfd_on` field is deprecated. Read `import_deadlines.pickup_lfd_rail` instead. ## Integration methods There are two methods to integrate Terminal49's rail tracking data programmatically: via API and DataSync. ### A. Integration via API Terminal49 provides a robust API that allows you to programmatically access rail container tracking data and receive updates via webhooks. You will receive rail events and attributes alongside events and attributes from the ocean terminal and carrier. [Here's a step-by-step guide to get started](/docs/api-docs/getting-started/start-here). ### B. Integration via DataSync Terminal49's DataSync service automatically syncs up-to-date tracking data with your system. The rail data lives in the same tables alongside the ocean terminal and carrier data. [Learn more about DataSync](/docs/datasync/overview) # Rate Limiting Source: https://terminal49.com/docs/api-docs/in-depth-guides/rate-limiting Understand Terminal49 API rate limits, HTTP 429 responses, and how to design clients that handle throttling with retries and exponential backoff. ## Overview Terminal49 API implements rate limiting to ensure fair usage and maintain service quality for all users. The default API limit is 100 requests per minute per API key/account on a rolling 60-second window. Some high-volume or expensive endpoints use their own bucket. ## Rate limit details All limits apply per API key/account on a rolling 60-second window. | Bucket | Endpoint | Limit | | ----------------------- | ----------------------------------------- | -------------------------------- | | Default API requests | All endpoints without a dedicated bucket | 100 requests per minute | | Create Tracking Request | `POST /v2/tracking_requests` | 100 tracking requests per minute | | Infer Tracking Number | `POST /v2/tracking_requests/infer_number` | 200 requests per minute | | Refresh Container | `PATCH /v2/containers/{id}/refresh` | 10 requests per minute | ## Rate limit response When you exceed the rate limit, the API will return: **HTTP Status Code**: `429 Too Many Requests` **Response Headers**: * `Retry-After`: Number of seconds to wait before making another request against the same rate-limit bucket `Retry-After` is the only rate-limit header the API documents. Base your 429 handling on it. **Response Body**: ```json theme={null} { "errors": [ { "status": "429", "title": "Too Many Requests", "detail": "Your account has exceeded its API rate limit. Please reduce request frequency or contact support to increase your limit. Consider using webhooks for real-time updates instead of polling." } ] } ``` ## Best practices ### 1. Use webhooks instead of polling The most effective way to avoid rate limits is to use **webhooks** for real-time updates instead of repeatedly polling the API: * Configure webhooks to receive push notifications when shipment data changes * Eliminates the need for frequent polling * Provides instant updates without consuming your rate limit * See the [Webhooks](/docs/api-docs/in-depth-guides/webhooks) section for setup instructions ### 2. Implement exponential backoff If you receive a `429` response: 1. Check the `Retry-After` header and, when present, wait at least that many seconds 2. If `Retry-After` is missing, back off exponentially (for example 1s, 2s, 4s) and add jitter so retries don't synchronize 3. Cap the number of retries and surface an error once the cap is reached 4. Don't retry immediately, as this will consume your limit further ### 3. Batch your requests * Use list endpoints with filtering instead of multiple individual requests * Leverage the [`include` parameter](/docs/api-docs/in-depth-guides/including-resources) to fetch related resources in a single request * Cache responses when appropriate to reduce redundant calls ### 4. Monitor your usage * Track your request patterns * Identify and optimize high-frequency operations * Consider spreading requests over time rather than bursting ## Need a higher limit? If your use case requires a higher rate limit: 1. **Evaluate webhook usage first** - Most polling use cases can be replaced with webhooks 2. **Contact support** at [support@terminal49.com](mailto:support@terminal49.com) 3. **Provide details** about your use case and expected request volume 4. **Our team will work with you** to find an appropriate solution ## Example: handling rate limits Here's an example of how to properly handle rate limit responses in Python. It honors `Retry-After` when the server provides it and falls back to exponential backoff with jitter otherwise: ```python theme={null} import random import time import requests def make_request_with_retry(url, headers, max_retries=5): """ Make an API request with automatic retry on rate limit. Honors the Retry-After header when present; otherwise falls back to exponential backoff with jitter. Args: url: The API endpoint URL headers: Request headers including Authorization max_retries: Maximum number of retry attempts Returns: Response object if successful Raises: Exception: If max retries exceeded """ for attempt in range(max_retries): response = requests.get(url, headers=headers) if response.status_code != 429: # Return response for any other status code return response retry_after = response.headers.get('Retry-After') if retry_after is not None: # Honor the server's instruction wait_seconds = int(retry_after) else: # Exponential backoff with jitter: ~1s, 2s, 4s, 8s, ... wait_seconds = (2 ** attempt) + random.uniform(0, 1) print(f"Rate limited. Waiting {wait_seconds:.1f} seconds " f"(attempt {attempt + 1} of {max_retries})...") time.sleep(wait_seconds) raise Exception("Max retries exceeded") # Example usage headers = { 'Authorization': 'Token YOUR_API_KEY' } response = make_request_with_retry( 'https://api.terminal49.com/v2/shipments', headers ) ``` ## Tips for high-volume applications If you're building a high-volume application: * **Design for webhooks from the start**: Don't rely on polling for data updates * **Implement request queuing**: Spread your requests evenly across the rate limit window * **Use pagination efficiently**: Fetch larger pages less frequently rather than small pages frequently * **Cache aggressively**: Store and reuse data that doesn't change frequently * **Honor `Retry-After`**: When a `429` response includes `Retry-After`, wait at least that many seconds before retrying against the same bucket. # Vessel and Container Route Data Source: https://terminal49.com/docs/api-docs/in-depth-guides/routing Access detailed container route segments and vessel position data from the Terminal49 API to build richer map and shipment visibility experiences. This is a technical article describing how to use the Routing Data feature, using the map as an example. Routing Data (Container Map GeoJSON API) is a paid feature. These APIs are subject to additional terms of usage and pricing. See [Entitlements and Paid Features](/docs/api-docs/useful-info/entitlements) for the required entitlement and non-entitled error response. ## Table of contents * [Overview](#overview) * [Getting started](#getting-started) * [Understanding the response](#understanding-the-response) * [GeoJSON FeatureCollection structure](#geojson-featurecollection-structure) * [Feature types](#feature-types) * [Port](#port) * [Current vessel](#current-vessel) * [Past vessel locations](#past-vessel-locations) * [Estimated full leg](#estimated-full-leg) * [Estimated partial leg](#estimated-partial-leg) * [Building your map](#building-your-map) * [Use cases](#use-cases) * [Recommendations and best practices](#recommendations-and-best-practices) * [Frequently asked questions](#frequently-asked-questions) ## Overview The `GET /v2/containers/{id}/map_geojson` endpoint provides all the map-related data for a container in a single GeoJSON response. The endpoint returns a GeoJSON FeatureCollection containing: * **Port locations** (Point geometries): Port of lading (POL), port of discharge (POD), and transshipment ports (TS1, TS2, etc.) * **Current vessel location** (Point geometry): The current position of the vessel if the container is currently at sea * **Past vessel paths** (LineString geometries): Historical positions of vessels for completed and in-progress legs of the journey * **Estimated future paths** (LineString geometries): Predicted vessel routes for upcoming or in-progress legs Example of a shipment map ## Getting started To retrieve the map data for a container, make a simple GET request to the endpoint: ```shell Request theme={null} curl --request GET \ --url https://api.terminal49.com/v2/containers/{id}/map_geojson \ --header "Authorization: Token YOUR_API_KEY" ``` The response is a standard GeoJSON FeatureCollection that can be directly used with most mapping libraries (Leaflet, Mapbox GL, Google Maps, etc.). ```json theme={null} { "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Point", "coordinates": [ 100.896831042, 13.065302386 ] }, "properties": { "feature_type": "port", "ports_sequence": 1, "ports_total": 3, "label": "POL", "name": "Laem Chabang", // ... more properties } }, { "type": "Feature", "geometry": { "type": "LineString", "coordinates": [ [ 100.868768333, 13.07306 ], [ 100.839155, 13.079318333 ], // ... more coordinates [ 118.03862, 24.440998333 ] ] }, "properties": { "feature_type": "past_vessel_locations", "vessel_id": "87a12f43-766c-4078-89bc-ac6595082f7b", // ... more path properties } }, // ... more features ] } ``` ## Understanding the response ### GeoJSON FeatureCollection structure The response follows the [GeoJSON specification](https://geojson.org/) and contains: * `type`: Always `"FeatureCollection"` * `features`: An array of GeoJSON Feature objects, each representing a map element (port, vessel, or route path) Each feature contains: * `type`: Always `"Feature"` * `geometry`: A GeoJSON geometry object (Point or LineString) * `properties`: An object containing metadata specific to the feature type ### Feature types The `properties.feature_type` field identifies what each feature represents. The following feature types are available: #### Port Geometry Type: `Point` Port features represent all ports in the container's route: the port of lading (POL), port of discharge (POD), and any transshipment ports (TS1, TS2, etc.). ```json theme={null} { "type": "Feature", "geometry": { "type": "Point", "coordinates": [ 100.896831042, 13.065302386 ] }, "properties": { "feature_type": "port", "ports_sequence": 1, "ports_total": 3, "location_id": "c5adae24-6fd4-4720-8813-976cf206feb1", "location_type": "Port", "name": "Laem Chabang", "state_abbr": "20", "state": null, "country_code": "TH", "country": "Thailand", "time_zone": "Asia/Bangkok", "inbound_eta_at": null, "inbound_ata_at": null, "outbound_etd_at": null, "outbound_atd_at": "2025-11-08T00:44:52Z", "label": "POL", "updated_at": "2025-12-11T09:01:08Z" } } ``` | Property | Type | Description | | ----------------- | -------------- | ------------------------------------------------------------------- | | `feature_type` | string | Always `"port"` | | `ports_sequence` | integer | The sequence number of this port in the route (1 = POL, last = POD) | | `ports_total` | integer | Total number of ports in the route | | `location_id` | string | Unique identifier for the port location | | `location_type` | string | Always `"Port"` | | `name` | string | Name of the port | | `state_abbr` | string \| null | State abbreviation (if applicable) | | `state` | string \| null | State name (if applicable) | | `country_code` | string | ISO country code | | `country` | string | Country name | | `time_zone` | string | IANA timezone identifier | | `label` | string | Port label: `"POL"`, `"POD"`, or `"TS1"`, `"TS2"`, etc. | | `inbound_eta_at` | string \| null | Estimated time of arrival (ISO 8601) | | `inbound_ata_at` | string \| null | Actual time of arrival (ISO 8601) | | `outbound_etd_at` | string \| null | Estimated time of departure (ISO 8601) | | `outbound_atd_at` | string \| null | Actual time of departure (ISO 8601) | | `updated_at` | string \| null | Last update timestamp from the shipment (ISO 8601) | #### Current vessel Geometry Type: `Point` This feature is only present when the container is currently on a vessel at sea. It represents the vessel's current position. ```json theme={null} { "type": "Feature", "geometry": { "type": "Point", "coordinates": [ -131.128473333, 31.023033333 ] }, "properties": { "feature_type": "current_vessel", "ports_sequence": 2, "vessel_id": "93fc5dce-4c7f-4089-bd28-f20cd9202ab0", "vessel_name": "ZIM BANGKOK", "vessel_imo": "9936525", "voyage_number": "13E", "vessel_location_timestamp": "2025-12-11T11:46:03Z", "vessel_location_heading": 108, "vessel_location_speed": 21, "departure_port_id": "ed64d446-9098-420c-ab08-c127e62509fe", "departure_port_name": "Xiamen", "departure_port_state_abbr": "FJ", "departure_port_state": null, "departure_port_country_code": "CN", "departure_port_country": "China", "departure_port_label": "TS1", "departure_port_atd": "2025-11-19T16:00:00Z", "departure_port_time_zone": "Asia/Shanghai", "arrival_port_id": "6129528d-846e-4571-ae16-b5328a4285ab", "arrival_port_name": "Savannah", "arrival_port_state_abbr": "GA", "arrival_port_state": "Georgia", "arrival_port_country_code": "US", "arrival_port_country": "United States", "arrival_port_label": "POD", "arrival_port_eta": "2025-12-31T05:00:00Z", "arrival_port_time_zone": "America/New_York" } } ``` | Property | Type | Description | | ----------------------------- | -------------- | ----------------------------------------------------- | | `feature_type` | string | Always `"current_vessel"` | | `ports_sequence` | integer | Sequence number of the departure port for this leg | | `vessel_id` | string | Unique identifier for the vessel | | `vessel_name` | string | Name of the vessel | | `vessel_imo` | string | IMO number of the vessel | | `voyage_number` | string \| null | Voyage number for this leg | | `vessel_location_timestamp` | string | Timestamp of the vessel position (ISO 8601) | | `vessel_location_heading` | number \| null | Vessel heading in degrees (0-360) | | `vessel_location_speed` | number \| null | Vessel speed in knots | | `departure_port_id` | string | ID of the port the vessel departed from | | `departure_port_name` | string | Name of the departure port | | `departure_port_state_abbr` | string \| null | State abbreviation of departure port | | `departure_port_state` | string \| null | State name of departure port | | `departure_port_country_code` | string | Country code of departure port | | `departure_port_country` | string | Country name of departure port | | `departure_port_label` | string | Label of departure port (POL, POD, TS1, etc.) | | `departure_port_atd` | string \| null | Actual time of departure from the port (ISO 8601) | | `departure_port_time_zone` | string | Timezone of departure port | | `arrival_port_id` | string \| null | ID of the next port the vessel is heading to | | `arrival_port_name` | string \| null | Name of the arrival port | | `arrival_port_state_abbr` | string \| null | State abbreviation of arrival port | | `arrival_port_state` | string \| null | State name of arrival port | | `arrival_port_country_code` | string \| null | Country code of arrival port | | `arrival_port_country` | string \| null | Country name of arrival port | | `arrival_port_label` | string \| null | Label of arrival port (POL, POD, TS1, etc.) | | `arrival_port_eta` | string \| null | Estimated time of arrival at the next port (ISO 8601) | | `arrival_port_time_zone` | string \| null | Timezone of arrival port | #### Past vessel locations Geometry Type: `LineString` These features represent the actual historical paths taken by vessels for completed and in-progress legs of the journey. Each LineString contains a series of coordinates showing where the vessel traveled between two ports. ```json theme={null} { "type": "Feature", "geometry": { "type": "LineString", "coordinates": [ [ 100.868768333, 13.07306 ], [ 100.839155, 13.079318333 ], // ... many more coordinates [ 118.03862, 24.440998333 ] ] }, "properties": { "feature_type": "past_vessel_locations", "ports_sequence": 1, "vessel_id": "87a12f43-766c-4078-89bc-ac6595082f7b", "start_time": "2025-11-08T00:44:52Z", "end_time": "2025-11-15T16:00:00Z", "point_count": 546, "outbound_atd_at": "2025-11-08T00:44:52Z", "inbound_ata_at": "2025-11-15T16:00:00Z", "inbound_eta_at": null } } ``` | Property | Type | Description | | ----------------- | -------------- | ------------------------------------------------------------ | | `feature_type` | string | Always `"past_vessel_locations"` | | `ports_sequence` | integer | Sequence number of the departure port for this leg | | `vessel_id` | string | Unique identifier for the vessel that traveled this path | | `start_time` | string | Start timestamp of the path (ISO 8601) | | `end_time` | string | End timestamp of the path (ISO 8601) | | `point_count` | integer | Number of coordinate points in the LineString | | `outbound_atd_at` | string \| null | Actual time of departure from the origin port (ISO 8601) | | `inbound_ata_at` | string \| null | Actual time of arrival at the destination port (ISO 8601) | | `inbound_eta_at` | string \| null | Estimated time of arrival at the destination port (ISO 8601) | #### Estimated full leg Geometry Type: `LineString` These features represent predicted vessel paths for future legs that have not yet started. The LineString shows the estimated route between two ports. ```json theme={null} { "type": "Feature", "geometry": { "type": "LineString", "coordinates": [ [55.059917502, 24.987353081], [55.234, 24.856], [56.123, 24.567], // ... intermediate estimated points [79.851136851, 6.942742853] ] }, "properties": { "feature_type": "estimated_full_legs", "ports_sequence": 2, "previous_port_id": "94892d07-ef8f-4f76-a860-97a398c2c177", "next_port_id": "818ef299-aed3-49c9-b3f7-7ee205f697f6", "point_count": 87 } } ``` | Property | Type | Description | | ------------------ | ------- | -------------------------------------------------- | | `feature_type` | string | Always `"estimated_full_legs"` | | `ports_sequence` | integer | Sequence number of the departure port for this leg | | `previous_port_id` | string | ID of the origin port | | `next_port_id` | string | ID of the destination port | | `point_count` | integer | Number of coordinate points in the LineString | #### Estimated partial leg Geometry Type: `LineString` This feature represents the predicted path from the vessel's current position to the next port. It is only present when the container is currently on a vessel at sea. ```json theme={null} { "type": "Feature", "geometry": { "type": "LineString", "coordinates": [ [ -131.128473333, 31.023033333 ], [ -130.9177, 30.67224 ], // ... many more coordinates [ -80.91232, 32.03728 ] ] }, "properties": { "feature_type": "estimated_partial_leg", "ports_sequence": 2, "current_port_id": "ed64d446-9098-420c-ab08-c127e62509fe", "next_port_id": "6129528d-846e-4571-ae16-b5328a4285ab", "point_count": 364 } } ``` | Property | Type | Description | | ----------------- | ------- | -------------------------------------------------- | | `feature_type` | string | Always `"estimated_partial_leg"` | | `ports_sequence` | integer | Sequence number of the departure port for this leg | | `current_port_id` | string | ID of the port the vessel departed from | | `next_port_id` | string | ID of the next port the vessel is heading to | | `point_count` | integer | Number of coordinate points in the LineString | This feature is only present when there is a `current_vessel` feature. The LineString starts from the vessel's current position (which matches the `current_vessel` feature coordinates) and extends to the next port. ## Building your map To visualize a container's journey using the GeoJSON response on your own map (similar to [the embeddable map](/docs/api-docs/in-depth-guides/terminal49-map)): 1. **Load the GeoJSON data** into your mapping library (Leaflet, Mapbox GL, Google Maps, etc.) 2. **Filter features by type** to style them differently: * **Ports**: Display as markers with labels (POL, POD, TS1, etc.) * **Current vessel**: Display as a special marker (e.g., a ship icon) with vessel information * **Past vessel locations**: Display as solid lines (representing completed journeys) * **Estimated partial leg** and **Estimated full legs**: Display as dashed lines (representing future predictions) 3. **Use the properties** to add interactivity: * Show port details (name, country, timestamps) on click/hover * Display vessel information (name, IMO, speed, heading) for the current vessel * Show leg information (departure/arrival times, vessel ID) for path segments ```javascript theme={null} // Fetch the GeoJSON data fetch('https://api.terminal49.com/v2/containers/{id}/map_geojson', { headers: { 'Authorization': 'Token YOUR_API_KEY' } }) .then(response => response.json()) .then(geojson => { // Create a map const map = L.map('map').setView([20, 70], 3); // Add base layer L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map); // Process each feature geojson.features.forEach(feature => { const props = feature.properties; if (props.feature_type === 'port') { // Add port marker const marker = L.marker([feature.geometry.coordinates[1], feature.geometry.coordinates[0]]) .addTo(map) .bindPopup(`${props.label}
${props.name}`); } else if (props.feature_type === 'current_vessel') { // Add current vessel marker const vesselIcon = L.icon({ iconUrl: 'vessel-icon.png', iconSize: [32, 32] }); L.marker([feature.geometry.coordinates[1], feature.geometry.coordinates[0]], {icon: vesselIcon}) .addTo(map) .bindPopup(`${props.vessel_name}
Speed: ${props.vessel_location_speed} knots`); } else if (props.feature_type === 'past_vessel_locations') { // Add past path as solid line const coordinates = feature.geometry.coordinates.map(coord => [coord[1], coord[0]]); L.polyline(coordinates, {color: 'green', weight: 3}) .addTo(map); } else if (props.feature_type === 'estimated_full_legs' || props.feature_type === 'estimated_partial_leg') { // Add estimated path as dashed line const coordinates = feature.geometry.coordinates.map(coord => [coord[1], coord[0]]); L.polyline(coordinates, {color: 'blue', weight: 2, dashArray: '10, 10'}) .addTo(map); } }); }); ```
## Use cases Integrating Terminal49's Vessel and Container Route APIs enables a variety of advanced capabilities: * **Track Complete Shipment Journeys Visually:** Monitor shipments across multiple legs on a map, from the port of lading to the port of discharge, including all transshipment points. * **Identify Transshipment Details Geographically:** Clearly see where transshipments occur and the routes taken between them. * **Correlate Timestamps with Locations:** Visually connect ETDs, ETAs, ATDs, and ATAs for every leg with their geographical points on the map for precise planning and exception management. * **Improve Internal Logistics Dashboards:** Offer your operations team a clear visual overview of all ongoing shipments and their current locations. ## Recommendations and best practices * **Polling Intervals**: For active containers (currently at sea), refresh the map data up to once per hour to get updated vessel positions. For containers that have completed their journey, you can cache the data as it won't change. * **Error Handling**: Implement proper error handling for API requests. If a container doesn't have route data yet, the endpoint will return an empty FeatureCollection (`{"type": "FeatureCollection", "features": []}`). If you decide to create your own map: * **Data Layering:** Consider layering information on your map. Start with basic port markers and paths, then add details like vessel names, ETAs, or status on hover or click. * **Map Library Integration:** Use a robust mapping library (e.g., Leaflet, Mapbox GL, Google Maps, OpenLayers) to handle the rendering of markers, lines, and map interactivity. * **Styling Guidelines**: * Use distinct colors/styles for different feature types (ports, current vessel, past paths, estimated paths) * Consider using dashed lines for estimated paths and solid lines for completed paths * Add labels to port markers showing POL, POD, TS1, etc. * Display vessel information in popups or info panels * **Data Interpretation**: * The `ports_sequence` property helps you understand the order of ports in the journey * Use `inbound_ata_at` and `outbound_atd_at` to determine which legs are completed * The presence of a `current_vessel` feature indicates the container is currently at sea * **Handling Antimeridian Crossings**: When container routes cross the International Date Line (antimeridian at ±180° longitude), standard map projections can display routes incorrectly, showing lines that wrap around the entire globe. For mapping libraries that don't natively handle antimeridian crossings, the recommended approach is to: (1) detect and split crossing LineStrings into separate segments, and (2) render map features across multiple world views (standard, East, and West) as needed. Below are more details: * **Detection**: Identify LineString features (past vessel locations or estimated paths) that cross the antimeridian by checking if consecutive coordinates have a longitude difference greater than 180°. * **Single Crossing Solution**: When one antimeridian crossing is detected: * Split the route into two segments: features before the crossing (based on `ports_sequence`) are drawn in the standard world view * Features after the crossing are drawn in an extended world view (East or West, depending on crossing direction) * Split the crossing LineString into two separate lines: one ending at the antimeridian in the standard view, and one starting from the antimeridian in the extended view * **Multiple Crossings**: For routes with more than one antimeridian crossing (rare but possible), render all features across three world views (standard, East, and West) with duplicated features. Split all crossing lines to prevent lines from wrapping across the globe. * **No Crossings**: If no antimeridian crossings are detected, render all features in the standard world view without any special handling. ## Frequently asked questions Vessel location data is updated every 15 minutes, although that does not guarantee there will be a new position every 15 minutes due to factors like whether the vessel is transmitting or within range of a satellite or base station. Predicted future positions are based on algorithms and historical data. Their accuracy can vary based on many factors such as temporary deviations, weather conditions, seasonality, or how frequently the shipping lane is used. Predicted paths are estimates. The historical path (once available as a `past_vessel_locations` feature) will show the actual route taken. Regularly refreshing data for active shipments is key to getting the most accurate information. The `current_vessel` feature is only present when: * The container is currently on a vessel at sea * The vessel has departed from a port (`outbound_atd_at` is present) * The next port hasn't been reached yet (`inbound_ata_at` is not present) * A valid vessel location can be retrieved Currently, the endpoint returns data for a single container. You'll need to make separate API calls for each container you want to display on your map. All coordinates follow the GeoJSON standard: `[longitude, latitude]` in WGS84 (EPSG:4326) format. The endpoint applies simplification to reduce the number of points in LineStrings for better performance. The simplification tolerance can vary, but the paths remain accurate for visualization purposes. # Terminal49 Map Embed Guide Source: https://terminal49.com/docs/api-docs/in-depth-guides/terminal49-map Embed the Terminal49 container map in your website with a publishable API key to display live vessel positions and shipment location data. ## Prerequisites * A Terminal49 account. * A publishable map API key. See [Entitlements and Paid Features](/docs/api-docs/useful-info/entitlements) for access requirements. * Familiarity with the [Shipments API](/docs/api-docs/api-reference/shipments/list-shipments) and [Containers API](/docs/api-docs/api-reference/containers/list-containers). The following examples pass `containerId` and `shipmentId` variables to the embedded map. They relate to `id` attributes of the container and shipment objects that are returned by the API. The map embed works from a local development environment. Open your HTML file directly in a browser or serve it from `http://localhost`. There is no domain allowlisting on the embed itself. Your page just needs internet access to load `bundle.js` and `bundle.css` from `https://map.terminal49.com`, and a valid publishable API key. ## Embed the map on your website Once you have the API key, you can embed the map on your website. Copy and paste the code below and insert it on your website. Once loaded, this will make the map code available through the global `window` object. Just before the closing `` tag, add the following link tag to load the map styles. ```html theme={null} Document ``` Just before the closing `` tag, add the following script tag to load the map code. ```html theme={null} ``` Define a container where you want the map to be displayed. ```html theme={null}
```
After the code is loaded, you can use the `window.TntMap` class to create a map instance. ```javascript theme={null} const map = new window.TntMap("#map", { authToken: publishableApiKey, }); ``` Notice that the `authToken` option is required. This is where you pass your publishable map API key. This tells the map to initialize and hook into the element designated during initialization. ```javascript theme={null} await map.start(); ``` You can pass shipment and container ids to the map where it'll fetch the data and display it. ```javascript theme={null} await map.load(shipmentId, containerId); ```
Putting it all together, here is the JavaScript code that you need to embed the map on your website. ```javascript theme={null} const map = new window.TntMap("#map", { authToken: publishableApiKey, }); await map.start(); await map.load(shipmentId, containerId); ``` If you want to use inside the browser you can use the IIFE pattern. ```html theme={null} ``` Or you can use the module attribute to use top-level async/await. ```html theme={null} ``` terminal49-map.png Additionally, the map element doesn't have to be an element id but can be a DOM element reference instead. Consider this example, which uses a query selector to select the map element. ```javascript theme={null} const element = document.querySelector("#map"); const map = new window.TntMap(element, { authToken: publishableApiKey, }); ``` ## Styling the map All of the map styles are written as human-readable CSS classes and variables. You can use these to customize the map to your liking. The styles are written in [BEM](https://getbem.com/) style as well as they're scoped under a `.tntm` class to avoid style conflicts with your website. ### Sizing By default the map will take the full width of its container and some height. The map is expandable by clicking on the expand button on the bottom left corner of the map. You can also override the default styles to customize the map to your liking. For example, to tell the map to take 60% of the total viewport size when expanded, do the following: ```css theme={null} .tntm .tntm__container.--expanded { height: 60vh; } ``` terminal49-map-expanded.png ### Colors Terminal49 exposes a number of CSS variables that you can use to customize the map colors. All of the variables are bound to the `.tntm` class to avoid style conflicts with your website. ```css theme={null} .tntm { --marker-background-color: var(--athens-gray-500); --marker-border-color: var(--athens-gray-500); --marker-text-color: var(--white); --marker-secondary-background-color: var(--athens-gray-100); --marker-secondary-text-color: var(--athens-gray-500); } ``` By default, their values are set to the Terminal49 brand colors. Don't change these — focus on the `--marker` variants instead. Additionally, the variables might require adjusting for different states of the map markers. For example, to display markers 'visited' by a vessel as orange and others (in the 'on-the-way' state) as blue: First, define the default, blue color: ```css theme={null} .tntm [data-journey-state='on-the-way'] { --marker-background-color: blue; --marker-border-color: lightblue; --marker-text-color: var(--white); --marker-secondary-background-color: lightblue; --marker-secondary-text-color: black; } .tntm [data-journey-state='visited'] { --marker-background-color: orange; --marker-border-color: #FFD580; --marker-text-color: var(--white); --marker-secondary-background-color: #FFD580; --marker-secondary-text-color: black; } ``` Result: terminal49-map-colors.png You can also change the marker colors based on whether they are hovered over or not. This is how the Terminal49 website styles the map markers: ```css theme={null} [data-journey-state='visited'] { --marker-background-color: var(--green-600); --marker-border-color: var(--green-600); --marker-text-color: var(--white); --marker-secondary-background-color: var(--green-50); --marker-secondary-text-color: var(--green-600); } [data-journey-state='on-the-way'] { --marker-background-color: var(--athens-gray-500); --marker-border-color: var(--athens-gray-500); --marker-text-color: var(--white); --marker-secondary-background-color: var(--athens-gray-100); --marker-secondary-text-color: var(--athens-gray-500); } [data-hovered][data-journey-state='visited'], [data-hovered] [data-journey-state='visited'] { --marker-secondary-background-color: var(--green-200); --marker-secondary-text-color: var(--green-700); --marker-border-color: var(--green-700); } [data-hovered][data-journey-state='on-the-way'], [data-hovered] [data-journey-state='on-the-way'] { --marker-secondary-background-color: var(--athens-gray-200); --marker-secondary-text-color: var(--athens-gray-600); --marker-border-color: var(--athens-gray-600); } ``` You might want to copy this code and adjust it to your needs. # Tracking widget embed guide Source: https://terminal49.com/docs/api-docs/in-depth-guides/terminal49-widget Embed the Terminal49 track-and-trace widget on your website so customers can look up live shipment and container status with a publishable API key. ## Embed the widget on your website First, you need a publishable widget API key. See [Entitlements and Paid Features](/docs/api-docs/useful-info/entitlements) for access requirements. Once you have the key, you can embed the widget on your website. Create a dedicated page for tracking, typically at `company-website.com/track`. You can also embed the widget directly on your homepage. If you create a dedicated tracking page, add an `h1` tag above the script. Feel free to customize the `h1` contents in the script. Copy and paste the code below and insert it at the top of the page (under your navigation if you have a horizontal top navigation). Replace `REPLACE_WITH_PUBLISHABLE_KEY` with the API key you receive. To query a bill of lading, container, or reference number, replace `REPLACE_WITH_NUMBER_TO_QUERY` with the specific number you want to search for. If `data-number` exists, the query will be performed only once. ```html theme={null}

Tracking

``` ## Frequently asked questions With a few lines of code, you can embed an interactive container tracking form. Once the widget is live on your website, your customer can enter a master bill of lading, container number, or reference numbers that a shipment is tagged with. After the number has been entered, the widget will retrieve and display shipment and container details from your Terminal49 account. Yes. The information fetched and displayed by the widget is based on the shipments and containers tracked within your Terminal49 account. No. Customers can only track the shipments and containers that are tracked in your Terminal49 account. Yes. Widget access is priced separately from standard API access. Contact [support@terminal49.com](mailto:support@terminal49.com) for widget pricing and to enable access. See [Entitlements and Paid Features](/docs/api-docs/useful-info/entitlements) for access requirements. ## Terminal49 container tracking widget one-pager Here is a one-pager that describes the benefits of the Track & Trace Widget. Feel free to share it with your team or management if you want to demonstrate the benefits of adding track and trace functionality to your website. The Track & Trace Widget provides a number of advantages: * It offers your customers a convenient way to track their shipments and containers. * It helps to improve customer satisfaction by providing accurate container status. * It can reduce customer service costs by providing customers with the information they need without having to contact customer service. * It can help you differentiate from other service providers. Terminal49 container tracking widget one-pager # Tracking request lifecycle Source: https://terminal49.com/docs/api-docs/in-depth-guides/tracking-request-lifecycle Learn how Terminal49 processes tracking requests through pending, created, and failed states, including retry logic and awaiting_manifest handling. When you submit a tracking request, your request is added to a queue to be checked with the shipping line. What happens if the request does not go through correctly? If Terminal49 has difficulty connecting to the shipping line or cannot parse the response, it retries up to 14 times. This process can take up to approximately 24 hours. You will not receive a `tracking_request.failed` webhook notification until Terminal49 has exhausted the retries, and Terminal49 does not change the `status` field to `failed` until then. ## Request number not found / awaiting manifest If the shipping line returns a response that it cannot find the provided number, Terminal49 either immediately fails the tracking request or keeps trying, depending on the `request_type`: * **Containers** fail straight away after a not found response from the shipping line. * **Bill of lading** and **booking numbers** do not fail instantly. The `status` changes to `awaiting_manifest` and Terminal49 keeps checking your request daily. You receive a `tracking_request.awaiting_manifest` webhook notification the first time this happens. If the request number cannot be found after 7 days, the tracking request is marked as failed with the `status` set to `failed` and the `tracking_request.failed` event sent to your webhook. * To adjust the duration before marking tracking requests as failed, contact [support@terminal49.com](mailto:support@terminal49.com). * **Incorrect request number type** if the request number type (ex. booking number) is incorrect, the tracking request will still fail even though the request number is correct. To reduce tracking failures, use [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) (Auto-Detect Carrier) to validate numbers and identify the correct SCAC before submitting. ## Failed reason ### Temporary reasons The `failed_reason` field can take one of the following temporary values: * `unrecognized_response` when Terminal49 could not parse the response from the shipping line, * `shipping_line_unreachable` if the shipping line was unreachable, * `internal_processing_error` when Terminal49 faced another issue, * `awaiting_manifest` if the shipping line indicates a bill of lading number is found but data is not yet available, or if the requested number could not be found. ### Permanent reasons Temporary reasons can become permanent when the `status` changes to `failed`: * `duplicate` when the shipment already existed, * `expired` when the tracking request was created more than 7 days ago and still has not succeeded, * `retries_exhausted` if Terminal49 tried 14 times to no avail, * `not_found` if the shipping line could not find the BL number. * `invalid_number` if the shipping line rejects the formatting of the number. * `booking_cancelled` if the shipping line indicates that the booking has been cancelled. * `data_unavailable` if the number is valid but the shipping line will not provide the data. Examples include shipments that are flagged as private or results that are removed due to data retention policies. [Failed Reasons when tracking request through dashboard](https://help.terminal49.com/en/articles/6116676-what-happens-after-i-add-a-shipment-to-terminal49-recently-added-shipments#h_ac9b93504f) ## Stopped When a shipment is no longer being updated then the tracking request `status` is marked as `tracking_stopped`. You may subscribe to the event `tracking_request.tracking_stopped` for notifications when this occurs. Terminal49 will stop tracking requests for the following reasons: * The booking was cancelled. * The data is no longer available at the shipping line. * All shipment containers are marked `empty_returned`. * More than 56 days have passed since the shipment arrived at its destination. * There have been no updates from the shipping line for more than 56 days. You can also stop tracking a shipment from the dashboard. ### Stopping tracking programmatically Tracking is stopped at the **shipment level**, not on the tracking request or on an individual container. Use [Stop tracking a shipment](/docs/api-docs/api-reference/shipments/stop-tracking-shipment) (`PATCH /shipments/{id}/stop_tracking`). * A tracking request initiates tracking and can create a shipment. Once the shipment exists, the shipment is the resource you manage. * There is no `DELETE` or "stop" endpoint on the `tracking_requests` resource. The tracking request `status` reflects the request outcome (`pending`, `created`, `failed`, `tracking_stopped`); the shipment is where active tracking is stopped or resumed. * Stopping a shipment stops tracking for **all containers on that shipment**. There is no per-container stop endpoint. This behavior is the same whether the shipment was originally tracked by bill of lading, booking, or container number. * To resume, use [Resume tracking a shipment](/docs/api-docs/api-reference/shipments/resume-tracking-shipment). ## Retrieving status If you want to see the status of your tracking request you can make a [GET request](/docs/api-docs/api-reference/tracking-requests/get-a-single-tracking-request) on what the most recent failure reason was (`failed_reason` field). # Set up webhooks Source: https://terminal49.com/docs/api-docs/in-depth-guides/webhooks Create a Terminal49 webhook endpoint, subscribe to shipment events, verify HMAC signatures, and handle retries so your consumer stays reliable. This guide shows how to configure a production webhook consumer for Terminal49 tracking updates. For background on why Terminal49 recommends webhooks instead of polling, see [Why Webhooks](/docs/api-docs/webhooks/overview). For event names and payload details, use the [Event Catalog](/docs/api-docs/webhooks/event-catalog) and [Webhook Payloads](/docs/api-docs/webhooks/payloads) references. ## Prerequisites You need: * A Terminal49 API key from the [developer portal](https://app.terminal49.com/developers/api-keys). * A public HTTPS endpoint that accepts `POST` requests. * A list of events your integration should receive. * A place to store the webhook secret securely. Use the [List Webhook Events](/docs/api-docs/api-reference/webhooks/list-webhook-events) endpoint to fetch the event categories available to your account. Use the [Trigger Webhook](/docs/api-docs/api-reference/webhooks/trigger-a-webhook) endpoint to send a sample event payload to an HTTPS URL while you test your handler. ## Create the webhook You can create a webhook from the dashboard or API. To use the dashboard, open [Developer Webhooks](https://app.terminal49.com/developers/webhooks) and click **Create Webhook Endpoint**. To use the API: ```bash theme={null} curl -X POST "https://api.terminal49.com/v2/webhooks" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Content-Type: application/vnd.api+json" \ -d '{ "data": { "type": "webhook", "attributes": { "url": "https://example.com/webhooks/terminal49", "active": true, "events": [ "tracking_request.succeeded", "tracking_request.failed", "container.updated" ] } } }' ``` Subscribe only to the events your integration handles. Use the [Event Catalog](/docs/api-docs/webhooks/event-catalog) for the canonical event list. ## Store the webhook secret The webhook response includes a `secret` attribute. Store it in your secrets manager. Terminal49 signs every webhook delivery with an HMAC SHA-256 digest of the raw request body and sends the digest in the `X-T49-Webhook-Signature` header. ## Verify the signature Verify the signature before parsing or trusting the JSON body. ```ruby Ruby theme={null} def valid_signature?(raw_body, signature, secret) digest = OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body) Rack::Utils.secure_compare(digest, signature.to_s) end ``` ```javascript Node.js theme={null} import crypto from "crypto"; function validSignature(rawBody, signature, secret) { const digest = crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex"); const received = Buffer.from(signature || "", "hex"); const expected = Buffer.from(digest, "hex"); return received.length === expected.length && crypto.timingSafeEqual(received, expected); } ``` ```python Python theme={null} import hashlib import hmac def valid_signature(raw_body: bytes, signature: str, secret: str) -> bool: digest = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(signature or "", digest) ``` ```go Go theme={null} import ( "crypto/hmac" "crypto/sha256" "encoding/hex" ) func validSignature(rawBody []byte, signature string, secret string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(rawBody) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expected)) } ``` ## Allowlist Terminal49 webhook IPs Fetch the current source IP list from the [List Webhook IPs](/docs/api-docs/api-reference/webhooks/list-webhook-ips) endpoint and allowlist those addresses in your firewall or application. Cache the list and refresh it periodically. Do not rely on a hard-coded list in application code. ## Accept the notification Your endpoint should return `200`, `201`, `202`, or `204` after it safely accepts the event. Any other response, including a timeout, triggers retries. Terminal49 retries failed deliveries up to 12 times before marking the notification as failed. ```javascript theme={null} app.post("/webhooks/terminal49", express.raw({ type: "*/*" }), async (req, res) => { const signature = req.header("X-T49-Webhook-Signature"); if (!validSignature(req.body, signature, process.env.T49_WEBHOOK_SECRET)) { return res.sendStatus(401); } const payload = JSON.parse(req.body.toString("utf8")); await enqueueWebhook(payload); res.sendStatus(202); }); ``` ## Handle duplicate deliveries Retries can deliver the same notification more than once. Use `data.id` as the idempotency key for your processing log. ```javascript theme={null} async function processWebhook(payload) { const notificationId = payload.data.id; if (await alreadyProcessed(notificationId)) { return; } await handleEvent(payload); await markProcessed(notificationId); } ``` ## Monitor failed notifications Use the [Webhook Notifications API](/docs/api-docs/api-reference/webhook-notifications/list-webhook-notifications) to review recent deliveries and spot the ones marked `delivery_status: failed`: ```bash theme={null} curl -s "https://api.terminal49.com/v2/webhook_notifications" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Content-Type: application/vnd.api+json" ``` Missed notifications cannot be replayed. After you fix the consumer, use [Trigger a Webhook](/docs/api-docs/api-reference/webhooks/trigger-a-webhook) to re-test your handler with a sample payload, then re-fetch the affected shipments and containers from the REST API to backfill any missed state changes. For more help, see [How to Troubleshoot Missing Webhook Notifications](https://help.terminal49.com/en/articles/7851422-missing-webhook-notifications). ## Related * [Why Webhooks](/docs/api-docs/webhooks/overview) — conceptual overview * [Event Catalog](/docs/api-docs/webhooks/event-catalog) — canonical event names * [Webhook Payloads](/docs/api-docs/webhooks/payloads) — payload envelope and included resources * [Webhook Best Practices](/docs/api-docs/webhooks/best-practices) — reliability checklist * [Create a Webhook API Reference](/docs/api-docs/api-reference/webhooks/create-a-webhook) — request and response fields # Entitlements and Paid Features Source: https://terminal49.com/docs/api-docs/useful-info/entitlements Understand which Terminal49 API, map, widget, vessel, rail, and MCP features require additional account enablement and what non-entitled callers receive. Some Terminal49 features require account enablement beyond standard API access. ## Entitlement overview | Surface | Entitlement | How to request access | Non-entitled behavior | | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Read endpoints (`GET /v2/shipments`, `GET /v2/containers`, `GET /v2/tracking_requests/{id}`, and other GETs) | Full API access (paid plan or 7-day API trial) | Contact [support@terminal49.com](mailto:support@terminal49.com) to enable the trial, or upgrade at [Pricing](/docs/api-docs/useful-info/pricing) | `401 Unauthorized` with `You do not have permissions for using the API, except for creating tracking requests. All other permissions require a paid plan. See https://app.terminal49.com/settings/billing`. `POST /v2/tracking_requests` still works. | | Container Map GeoJSON API (`GET /v2/containers/{id}/map_geojson`) | Routing Data | Contact [sales@terminal49.com](mailto:sales@terminal49.com) | `403 Forbidden` with `Routing data feature is not enabled for this account` | | Vessel position and future-position endpoints | Routing Data / vessel positions | Contact [sales@terminal49.com](mailto:sales@terminal49.com) | `403 Forbidden` with `Routing data feature is not enabled for this account` | | MCP `get_container_route` tool | Routing Data | Contact [sales@terminal49.com](mailto:sales@terminal49.com) | The tool returns a feature-not-enabled response; use `get_container_transport_events` for historical milestones | | Terminal49 Map Embed | Publishable map API key | Contact [support@terminal49.com](mailto:support@terminal49.com) | The browser embed cannot authenticate without a valid publishable key | | Tracking Widget Embed | Publishable widget API key | Contact [support@terminal49.com](mailto:support@terminal49.com) | The browser widget cannot authenticate without a valid publishable key | | Container refresh (`PATCH /v2/containers/{id}/refresh`) | On-demand refresh | Contact [sales@terminal49.com](mailto:sales@terminal49.com) | `403 Forbidden`; the endpoint is also rate-limited to 10 requests per minute for entitled accounts | | Rail LFD data (`import_deadlines.pickup_lfd_rail` and the `container.pickup_lfd_rail.changed` webhook event) | Rail Plan | Contact [sales@terminal49.com](mailto:sales@terminal49.com) | `container.pickup_lfd_rail.changed` notifications are not delivered, and rail-carrier LFD updates are not surfaced on the container | ## Pricing See [Pricing](/docs/api-docs/useful-info/pricing) for current public pricing pointers. If a page says to contact sales or support, treat that feature as account-gated until the entitlement appears on your account. ## Related pages * [Vessel and Container Route Data](/docs/api-docs/in-depth-guides/routing) * [Terminal49 Map Embed Guide](/docs/api-docs/in-depth-guides/terminal49-map) * [Tracking Widget Embed Guide](/docs/api-docs/in-depth-guides/terminal49-widget) * [Rail Integration Guide](/docs/api-docs/in-depth-guides/rail-integration-guide) * [MCP Server](/docs/mcp/home) # Terminal49 API Pricing Source: https://terminal49.com/docs/api-docs/useful-info/pricing Review Terminal49 API pricing plans and find resources for evaluating access to ocean shipment and container tracking data for your integration. View the [standard Terminal49 API pricing](https://www.terminal49.com/pricing-plans#API-Section). # Terminal49 Test Tracking Numbers Source: https://terminal49.com/docs/api-docs/useful-info/test-numbers Use Terminal49 test tracking numbers to simulate successful, failed, and edge-case tracking request outcomes when validating your sandbox integration. ## Overview This page includes test tracking numbers you can use to make sure your tracking request integration works as planned. ## What are test numbers? Test numbers are deterministic request numbers for the Tracking Request API. Each number has a specific outcome, so you can test how your integration handles `tracking_request.succeeded` and `tracking_request.failed` without using a live shipment. Test numbers do not simulate every milestone, ETA, LFD, or availability event. To test your webhook handler for a specific event payload, use the [Trigger Webhook](/docs/api-docs/api-reference/webhooks/trigger-a-webhook) endpoint. ## Tracking request API You create shipments by making requests to the tracking request API. When using the API , ensure that: * you set the test number in `request_number` attribute in the request body * you set the `scac` attribute to `TEST` in the request body ## Test numbers | Number. | Use Case | | ----------------- | --------------------------------------------------------- | | TEST-TR-SUCCEEDED | Test the `tracking_request.succeeded` outcome and webhook | | TEST-TR-FAILED | Test the `tracking_request.failed` outcome and webhook | # Tracking request retry behavior Source: https://terminal49.com/docs/api-docs/useful-info/tracking-request-retrying See how Terminal49 retries failed tracking requests, which failure types trigger automatic retries, and how to inspect the current retry status via the API. When you submit a tracking request, your request is added to a queue to be checked with the shipping line. What happens if the request does not go through correctly? If Terminal49 has difficulty connecting to the shipping line or cannot parse the response, it retries up to 14 times with exponential backoff. This process can take up to approximately 24 hours. You do not receive a `tracking_request.failed` webhook notification until all retries are exhausted. If the shipping line returns a response that it cannot find the provided number, Terminal49 immediately sends the `tracking_request.failed` event to your webhook. To check the status of your tracking request, make a [GET request](/docs/api-docs/api-reference/tracking-requests/get-a-single-tracking-request) using its `id` to see how many times it has retried and what the most recent failure reason was. # Terminal49 Webhook Event Examples Source: https://terminal49.com/docs/api-docs/useful-info/webhook-events-examples Review sample Terminal49 webhook payloads for shipment, container, and tracking request events to build, test, and debug your webhook consumer. For the shared webhook notification envelope and included-resource rules, see [Webhook Payloads](/docs/api-docs/webhooks/payloads). Container payloads in these examples include `holds_at_pod_terminal`, `fees_at_pod_terminal`, and `available_for_pickup` fields. To understand the possible values and how to use them for pickup readiness, see [Container Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). ## Tracking request events These events fire when a tracking request changes status. ### tracking\_request.succeeded Shipment created and linked to the tracking request. Your tracking is active. ```json expandable theme={null} { "data": { "id": "0b27a595-e531-4f93-8d5a-22e1675d863a", "type": "webhook_notification", "attributes": { "id": "0b27a595-e531-4f93-8d5a-22e1675d863a", "event": "tracking_request.succeeded", "delivery_status": "succeeded", "created_at": "2022-10-21T20:18:36Z" }, "relationships": { "reference_object": { "data": { "id": "bf1d2f9d-f88a-4aed-901b-a86cddb0a665", "type": "tracking_request" } }, "webhook": { "data": { "id": "b1617bb8-d713-4450-8dd7-81be1317631c", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "2444986c-5ebe-4bc5-ad55-d24293424943", "type": "container", "attributes": { "number": "MRKU3700927", "seal_number": null, "created_at": "2022-10-21T20:18:36Z", "ref_numbers": [ ], "pod_arrived_at": null, "pod_discharged_at": null, "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "dry", "equipment_length": 40, "equipment_height": "standard", "weight_in_lbs": null, "pod_full_out_at": null, "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": null, "availability_known": false, "pod_timezone": null, "final_destination_timezone": null, "empty_terminated_timezone": null }, "relationships": { "shipment": { "data": { "id": "6af82332-9bff-4b4a-a1e1-a382e0f82ca5", "type": "shipment" } }, "pod_terminal": { "data": null }, "transport_events": { "data": [ ] }, "raw_events": { "data": [ ] } } }, { "id": "6af82332-9bff-4b4a-a1e1-a382e0f82ca5", "type": "shipment", "attributes": { "created_at": "2022-10-21T20:18:36Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "MAEU221876618", "normalized_number": "221876618", "shipping_line_scac": "MAEU", "shipping_line_name": "Maersk", "shipping_line_short_name": "Maersk", "customer_name": "Schuster-Barrows", "port_of_lading_locode": "CNNGB", "port_of_lading_name": "Ningbo", "port_of_discharge_locode": null, "port_of_discharge_name": null, "pod_vessel_name": null, "pod_vessel_imo": null, "pod_voyage_number": null, "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": null, "pol_timezone": "Asia/Shanghai", "pod_eta_at": "2022-11-25T08:00:00Z", "pod_original_eta_at": "2022-11-25T08:00:00Z", "pod_ata_at": null, "pod_timezone": null, "line_tracking_last_attempted_at": null, "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "d741a6bc-13dd-4b62-a5c2-f65050c9403d", "type": "port" } }, "port_of_discharge": { "data": null }, "pod_terminal": { "data": null }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "2444986c-5ebe-4bc5-ad55-d24293424943", "type": "container" } ] } }, "links": { "self": "/v2/shipments/e5a39855-f438-467a-9c18-ae91cd46cfaf" } }, { "id": "bf1d2f9d-f88a-4aed-901b-a86cddb0a665", "type": "tracking_request", "attributes": { "request_number": "MAEU221876618", "request_type": "bill_of_lading", "scac": "MAEU", "ref_numbers": [ ], "shipment_tags": [ ], "created_at": "2022-10-17T14:17:30Z", "updated_at": "2022-10-17T15:17:30Z", "status": "created", "failed_reason": null, "is_retrying": false, "retry_count": null }, "relationships": { "tracked_object": { "data": { "id": "6af82332-9bff-4b4a-a1e1-a382e0f82ca5", "type": "shipment" } }, "customer": { "data": null }, "user": { "data": null } }, "links": { "self": "/v2/tracking_requests/61e7fc09-e1a0-4bfa-b559-49d7576c790e" } } ] } ``` ### tracking\_request.failed The tracking request failed. The carrier could not find the shipment. ```json expandable theme={null} { "data": { "id": "dfa9f92b-dbc5-403e-b03f-d5683abbd074", "type": "webhook_notification", "attributes": { "id": "dfa9f92b-dbc5-403e-b03f-d5683abbd074", "event": "tracking_request.failed", "delivery_status": "pending", "created_at": "2022-10-21T20:19:14Z" }, "relationships": { "reference_object": { "data": { "id": "5fe62a78-86af-4408-a79c-2d03c907a68b", "type": "tracking_request" } }, "webhook": { "data": { "id": "5c7cbf41-37f4-4e76-b06b-4b17860fab02", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "5fe62a78-86af-4408-a79c-2d03c907a68b", "type": "tracking_request", "attributes": { "request_number": "MAEU11875506", "request_type": "bill_of_lading", "scac": "MAEU", "ref_numbers": [ ], "shipment_tags": [ ], "created_at": "2022-10-21T20:19:13Z", "updated_at": "2022-10-21T21:19:13Z", "status": "failed", "failed_reason": "not_found", "is_retrying": false, "retry_count": null }, "relationships": { "tracked_object": { "data": null }, "customer": { "data": null }, "user": { "data": null } }, "links": { "self": "/v2/tracking_requests/93d0d469-cbcf-4b6c-ae59-526c176942c7" } } ] } ``` ### tracking\_request.awaiting\_manifest The carrier has not yet manifested this shipment. Terminal49 will retry automatically. ```json expandable theme={null} { "data": { "id": "b7235d00-2617-434e-9e28-a5cf83a0a0d3", "type": "webhook_notification", "attributes": { "id": "b7235d00-2617-434e-9e28-a5cf83a0a0d3", "event": "tracking_request.awaiting_manifest", "delivery_status": "succeeded", "created_at": "2022-10-21T20:15:54Z" }, "relationships": { "reference_object": { "data": { "id": "ff77f76c-5e73-47c4-ab1e-d499cb1fa10f", "type": "tracking_request" } }, "webhook": { "data": { "id": "bb30c47d-db43-4fa0-9287-7bfade47e4ec", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "ff77f76c-5e73-47c4-ab1e-d499cb1fa10f", "type": "tracking_request", "attributes": { "request_number": "IZ12208APRV6", "request_type": "bill_of_lading", "scac": "HLCU", "ref_numbers": [ ], "shipment_tags": [ ], "created_at": "2022-10-21T20:15:49Z", "updated_at": "2022-10-21T21:15:49Z", "status": "awaiting_manifest", "failed_reason": null, "is_retrying": false, "retry_count": null }, "relationships": { "tracked_object": { "data": null }, "customer": { "data": null }, "user": { "data": null } }, "links": { "self": "/v2/tracking_requests/ca388055-8e3a-4b85-8401-e4aa1abf7228" } } ] } ``` ### tracking\_request.tracking\_stopped Terminal49 is no longer updating this tracking request (shipment delivered or manually stopped). ```json expandable theme={null} { "data": { "id": "00cbaa34-c487-419c-b5c4-415da5478971", "type": "webhook_notification", "attributes": { "id": "00cbaa34-c487-419c-b5c4-415da5478971", "event": "tracking_request.tracking_stopped", "delivery_status": "pending", "created_at": "2022-11-22T16:39:42Z" }, "relationships": { "reference_object": { "data": { "id": "94f2d7a0-4a10-42e0-81d8-83cbabc4ef6c", "type": "tracking_request" } }, "webhook": { "data": { "id": "85336ef9-8901-45fc-95c1-d26bc8f2bf68", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "36917159-1982-4c0e-bb7e-7a5e972d9c1b", "type": "shipment", "attributes": { "created_at": "2022-09-15T17:52:08Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "CMDUAMC1863476", "normalized_number": "AMC1863476", "shipping_line_scac": "CMDU", "shipping_line_name": "CMA CGM", "shipping_line_short_name": "CMA CGM", "customer_name": "Miller-Gleason", "port_of_lading_locode": "INNSA", "port_of_lading_name": "Nhava Sheva", "port_of_discharge_locode": "USLAX", "port_of_discharge_name": "Los Angeles", "pod_vessel_name": "EVER LOVELY", "pod_vessel_imo": "9629110", "pod_voyage_number": "0TBD0W1MA", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": "2022-07-20T17:59:00Z", "pol_timezone": "Asia/Calcutta", "pod_eta_at": "2022-09-14T14:00:00Z", "pod_original_eta_at": "2022-09-14T14:00:00Z", "pod_ata_at": "2022-09-16T08:11:18Z", "pod_timezone": "America/Los_Angeles", "line_tracking_last_attempted_at": "2022-10-21T20:01:32Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": "2022-11-22T16:39:42Z", "line_tracking_stopped_reason": "account_closed" }, "relationships": { "port_of_lading": { "data": { "id": "256aec7d-4915-48d4-b8e5-911e097b05e7", "type": "port" } }, "port_of_discharge": { "data": { "id": "786ff548-7e55-4d18-b4a6-b6ee31b4cc62", "type": "port" } }, "pod_terminal": { "data": { "id": "46648f15-fcf3-49fc-b86f-e8550b95c40c", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "bb77de85-2e89-4596-8a74-6100f3180296", "type": "container" } ] } }, "links": { "self": "/v2/shipments/7fd3135d-9da7-4dad-87e3-63242343e182" } }, { "id": "94f2d7a0-4a10-42e0-81d8-83cbabc4ef6c", "type": "tracking_request", "attributes": { "request_number": "CMDUAMC1863476", "request_type": "bill_of_lading", "scac": "CMDU", "ref_numbers": [ ], "shipment_tags": [ ], "created_at": "2022-09-15T17:52:07Z", "updated_at": "2022-09-15T18:52:07Z", "status": "tracking_stopped", "failed_reason": null, "is_retrying": false, "retry_count": null }, "relationships": { "tracked_object": { "data": { "id": "36917159-1982-4c0e-bb7e-7a5e972d9c1b", "type": "shipment" } }, "customer": { "data": null }, "user": { "data": { "id": "3d28c8cb-9cbb-471d-b946-80e7345c9572", "type": "user" } } }, "links": { "self": "/v2/tracking_requests/c871c4b8-0436-410c-8b39-fd426e06869e" } } ] } ``` ## Transport milestone events These events map to physical milestones in a container's journey. They fire in roughly chronological order as a container moves from origin to destination. ### Origin #### container.transport.empty\_out Empty container picked up at port of lading. ```json expandable theme={null} { "data": { "id": "6dded288-6b72-483a-9f33-c79aa8e9c1ff", "type": "webhook_notification", "attributes": { "id": "6dded288-6b72-483a-9f33-c79aa8e9c1ff", "event": "container.transport.empty_out", "delivery_status": "succeeded", "created_at": "2022-10-21T20:17:02Z" }, "relationships": { "reference_object": { "data": { "id": "fb7533ea-7afc-4a7c-a831-0b36bd28bf26", "type": "transport_event" } }, "webhook": { "data": { "id": "feb4bb16-deff-4249-8fc6-5ae67c2fe8d2", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "1fe11df6-143d-4d6c-bbc8-b5963e19611f", "type": "shipment", "attributes": { "created_at": "2022-10-21T20:16:02Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "SA00846884", "normalized_number": "SA00846884", "shipping_line_scac": "ACLU", "shipping_line_name": "Atlantic Container Line", "shipping_line_short_name": "ACL", "customer_name": "Stracke Inc", "port_of_lading_locode": "BEANR", "port_of_lading_name": "Antwerp", "port_of_discharge_locode": "USNYC", "port_of_discharge_name": "New York / New Jersey", "pod_vessel_name": null, "pod_vessel_imo": null, "pod_voyage_number": null, "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": "2022-11-04T13:00:00Z", "pol_atd_at": null, "pol_timezone": "Europe/Brussels", "pod_eta_at": "2022-11-15T00:00:00Z", "pod_original_eta_at": "2022-11-15T00:00:00Z", "pod_ata_at": null, "pod_timezone": "America/New_York", "line_tracking_last_attempted_at": null, "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "fc6a6c8c-4f6f-459b-be6c-814d34ec312b", "type": "port" } }, "port_of_discharge": { "data": { "id": "dfcc3bcd-a63d-4481-b68b-a91da48b5d79", "type": "port" } }, "pod_terminal": { "data": null }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "3f92cb0c-b7b2-4f08-ae65-677fc4d7712d", "type": "container" } ] } }, "links": { "self": "/v2/shipments/6e9625a2-ea71-49ad-8441-13a3a44926f2" } }, { "id": "3f92cb0c-b7b2-4f08-ae65-677fc4d7712d", "type": "container", "attributes": { "number": "GCNU8802957", "seal_number": null, "created_at": "2022-10-21T20:16:02Z", "ref_numbers": [ ], "pod_arrived_at": null, "pod_discharged_at": null, "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "reefer", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": null, "pod_full_out_at": null, "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": null, "availability_known": false, "pod_timezone": "America/New_York", "final_destination_timezone": null, "empty_terminated_timezone": "America/New_York" }, "relationships": { "shipment": { "data": { "id": "1fe11df6-143d-4d6c-bbc8-b5963e19611f", "type": "shipment" } }, "pod_terminal": { "data": null }, "transport_events": { "data": [ { "id": "fb7533ea-7afc-4a7c-a831-0b36bd28bf26", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "e453e33d-3ef7-4fdb-b012-e83ba5903466", "type": "raw_event" } ] } } }, { "id": "2381793f-8f43-4bd1-a4e0-1135c322f441", "type": "metro_area", "attributes": { "id": "2381793f-8f43-4bd1-a4e0-1135c322f441", "name": "Antwerp Churchill Terminal", "state_abbr": "Vlaanderen", "code": "BEANT", "latitude": "51.2806024", "longitude": "4.3551883", "country_code": "BE", "time_zone": "Europe/Brussels" } }, { "id": "fb7533ea-7afc-4a7c-a831-0b36bd28bf26", "type": "transport_event", "attributes": { "event": "container.transport.empty_out", "created_at": "2022-10-21T20:16:02Z", "voyage_number": null, "timestamp": "2022-10-20T11:12:00Z", "data_source": "shipping_line", "location_locode": "BEANT", "timezone": "Europe/Brussels" }, "relationships": { "shipment": { "data": { "id": "1fe11df6-143d-4d6c-bbc8-b5963e19611f", "type": "shipment" } }, "container": { "data": { "id": "3f92cb0c-b7b2-4f08-ae65-677fc4d7712d", "type": "container" } }, "vessel": { "data": null }, "location": { "data": { "id": "2381793f-8f43-4bd1-a4e0-1135c322f441", "type": "metro_area" } }, "terminal": { "data": null } } } ] } ``` #### container.transport.full\_in Full container returned to port of lading. ```json expandable theme={null} { "data": { "id": "63fb3158-375e-417f-a31e-baba60a17afa", "type": "webhook_notification", "attributes": { "id": "63fb3158-375e-417f-a31e-baba60a17afa", "event": "container.transport.full_in", "delivery_status": "succeeded", "created_at": "2022-10-21T20:18:14Z" }, "relationships": { "reference_object": { "data": { "id": "83e82be7-1791-48cd-a595-e4c92c3ddd09", "type": "transport_event" } }, "webhook": { "data": { "id": "fad16f92-e418-49eb-b004-55eeff8e28c6", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "84aedf7a-a3ec-48e6-bc36-e3234454795c", "type": "shipment", "attributes": { "created_at": "2022-10-20T17:02:14Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "CMDUSHZ5223740", "normalized_number": "SHZ5223740", "shipping_line_scac": "CMDU", "shipping_line_name": "CMA CGM", "shipping_line_short_name": "CMA CGM", "customer_name": "Kris LLC", "port_of_lading_locode": "CNSHK", "port_of_lading_name": "Shekou", "port_of_discharge_locode": "USMIA", "port_of_discharge_name": "Miami Seaport", "pod_vessel_name": "CMA CGM OTELLO", "pod_vessel_imo": "9299628", "pod_voyage_number": "0PGDNE1MA", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": "2022-10-23T05:30:00Z", "pol_atd_at": null, "pol_timezone": "Asia/Shanghai", "pod_eta_at": "2022-12-16T12:00:00Z", "pod_original_eta_at": "2022-12-16T12:00:00Z", "pod_ata_at": null, "pod_timezone": "America/New_York", "line_tracking_last_attempted_at": "2022-10-21T20:18:06Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "57f55608-c9fb-47d1-8cd6-0e78b340061b", "type": "port" } }, "port_of_discharge": { "data": { "id": "b802e728-e01a-400f-9687-81e9d7f4da51", "type": "port" } }, "pod_terminal": { "data": { "id": "21ff320e-ddb7-4199-8873-a819e9dcfc31", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "975be82b-d16a-4b0f-818a-ea1ba83c3fde", "type": "container" } ] } }, "links": { "self": "/v2/shipments/d08ffcbf-43c6-4f68-85c4-7f2199211723" } }, { "id": "975be82b-d16a-4b0f-818a-ea1ba83c3fde", "type": "container", "attributes": { "number": "TGSU5023798", "seal_number": null, "created_at": "2022-10-20T17:02:14Z", "ref_numbers": [ ], "pod_arrived_at": null, "pod_discharged_at": null, "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "dry", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": null, "pod_full_out_at": null, "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": null, "availability_known": false, "pod_timezone": "America/New_York", "final_destination_timezone": null, "empty_terminated_timezone": "America/New_York" }, "relationships": { "shipment": { "data": { "id": "84aedf7a-a3ec-48e6-bc36-e3234454795c", "type": "shipment" } }, "pod_terminal": { "data": { "id": "21ff320e-ddb7-4199-8873-a819e9dcfc31", "type": "terminal" } }, "transport_events": { "data": [ { "id": "3a60833d-df16-438d-ad8e-5b3d9a1c44ed", "type": "transport_event" }, { "id": "83e82be7-1791-48cd-a595-e4c92c3ddd09", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "ffbab92a-7f6d-45f3-bfa1-c88a79feb19c", "type": "raw_event" }, { "id": "2323c45e-a4ab-42fc-95a7-5b4f14af6835", "type": "raw_event" }, { "id": "64bd26a3-8393-41bb-ab7c-64b8d705d054", "type": "raw_event" }, { "id": "2e08535f-7a3f-4dc1-87f9-939101e46d53", "type": "raw_event" } ] } } }, { "id": "57f55608-c9fb-47d1-8cd6-0e78b340061b", "type": "port", "attributes": { "id": "57f55608-c9fb-47d1-8cd6-0e78b340061b", "name": "Shekou", "code": "CNSHK", "state_abbr": null, "city": null, "country_code": "CN", "latitude": "22.459940331", "longitude": "113.892910965", "time_zone": "Asia/Shanghai" } }, { "id": "83e82be7-1791-48cd-a595-e4c92c3ddd09", "type": "transport_event", "attributes": { "event": "container.transport.full_in", "created_at": "2022-10-21T20:18:14Z", "voyage_number": null, "timestamp": "2022-10-20T16:29:00Z", "data_source": "shipping_line", "location_locode": "CNSHK", "timezone": "Asia/Shanghai" }, "relationships": { "shipment": { "data": { "id": "84aedf7a-a3ec-48e6-bc36-e3234454795c", "type": "shipment" } }, "container": { "data": { "id": "975be82b-d16a-4b0f-818a-ea1ba83c3fde", "type": "container" } }, "vessel": { "data": null }, "location": { "data": { "id": "57f55608-c9fb-47d1-8cd6-0e78b340061b", "type": "port" } }, "terminal": { "data": null } } } ] } ``` #### container.transport.vessel\_loaded Container loaded onto the vessel at port of lading. ```json expandable theme={null} { "data": { "id": "a3baf7bb-3ffe-485e-bf9d-7b7dd17a08a8", "type": "webhook_notification", "attributes": { "id": "a3baf7bb-3ffe-485e-bf9d-7b7dd17a08a8", "event": "container.transport.vessel_loaded", "delivery_status": "succeeded", "created_at": "2022-10-21T20:16:47Z" }, "relationships": { "reference_object": { "data": { "id": "58114b8d-7aab-491d-97ce-7b32c0c1d198", "type": "transport_event" } }, "webhook": { "data": { "id": "7c353e67-cab6-422e-a17f-8483bf250dd9", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "2bae2c8b-c682-47a6-81c7-f1ac9d4404ef", "type": "shipment", "attributes": { "created_at": "2022-10-18T23:20:40Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "HDMUNBOZ32457200", "normalized_number": "NBOZ32457200", "shipping_line_scac": "HDMU", "shipping_line_name": "Hyundai Merchant Marine", "shipping_line_short_name": "Hyundai", "customer_name": "Roberts LLC", "port_of_lading_locode": "CNNGB", "port_of_lading_name": "Ningbo", "port_of_discharge_locode": "USLAX", "port_of_discharge_name": "Los Angeles", "pod_vessel_name": "NYK THEMIS", "pod_vessel_imo": "9356696", "pod_voyage_number": "0082E", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": "2022-09-23T00:30:00Z", "pol_timezone": "Asia/Shanghai", "pod_eta_at": "2022-10-22T12:30:00Z", "pod_original_eta_at": "2022-10-22T12:30:00Z", "pod_ata_at": null, "pod_timezone": "America/Los_Angeles", "line_tracking_last_attempted_at": "2022-10-21T20:13:02Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "b80acdc3-e2e7-4d09-8c7a-48ce12b4dd38", "type": "port" } }, "port_of_discharge": { "data": { "id": "5d0f3e26-a5a7-4aff-a73f-2eacb3ca0a05", "type": "port" } }, "pod_terminal": { "data": { "id": "43cf9fea-eb03-428a-8e57-6da700f95adc", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "e7fd77a6-5b1d-49c6-8790-9597e154c3c6", "type": "container" }, { "id": "d1a33088-16fa-475c-9ea4-d5098ae9b8df", "type": "container" }, { "id": "c7876dc6-ccad-4219-b0a2-1e9e9845f474", "type": "container" }, { "id": "9bbff5af-73fa-463e-b715-d2f6c01c58cc", "type": "container" }, { "id": "e637137f-ad05-41e3-9a50-045d365d96d9", "type": "container" }, { "id": "8e4f2995-a25a-4c6c-9382-3c02ce1288af", "type": "container" }, { "id": "a5970c64-61c2-4f13-b16d-de46871b77f0", "type": "container" } ] } }, "links": { "self": "/v2/shipments/35285253-d023-4c08-ab12-4ea7ee7793cf" } }, { "id": "c7876dc6-ccad-4219-b0a2-1e9e9845f474", "type": "container", "attributes": { "number": "KOCU4221161", "seal_number": "211962498", "created_at": "2022-10-18T23:20:40Z", "ref_numbers": [ ], "pod_arrived_at": null, "pod_discharged_at": null, "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "dry", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": 20119, "pod_full_out_at": null, "empty_terminated_at": null, "terminal_checked_at": "2022-10-21T20:09:50Z", "fees_at_pod_terminal": [ ], "pickup_lfd": null, "pickup_appointment_at": null, "pod_full_out_chassis_number": null, "location_at_pod_terminal": "On-Board Vessel", "pod_last_tracking_request_at": "2022-10-21T20:09:50Z", "shipment_last_tracking_request_at": null, "availability_known": true, "pod_timezone": "America/Los_Angeles", "final_destination_timezone": null, "empty_terminated_timezone": "America/Los_Angeles" }, "relationships": { "shipment": { "data": { "id": "2bae2c8b-c682-47a6-81c7-f1ac9d4404ef", "type": "shipment" } }, "pod_terminal": { "data": { "id": "43cf9fea-eb03-428a-8e57-6da700f95adc", "type": "terminal" } }, "transport_events": { "data": [ { "id": "e39a063e-da6b-4b97-bb7a-bf27b1b2d96e", "type": "transport_event" }, { "id": "58e026b4-4252-4f40-910e-416b75e3f656", "type": "transport_event" }, { "id": "2032b8a4-150d-40c1-a4d2-9dc89606ba9b", "type": "transport_event" }, { "id": "58114b8d-7aab-491d-97ce-7b32c0c1d198", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "9b382aaf-50b5-49a2-8b72-8e1ebfe687cb", "type": "raw_event" }, { "id": "976bc217-7b24-4a2e-8c4f-4f4e3597348d", "type": "raw_event" }, { "id": "421ab729-8741-4c9b-a493-b8b93ca1ff13", "type": "raw_event" }, { "id": "0eada15a-7830-4e22-8a30-3c994d7e6130", "type": "raw_event" } ] } } }, { "id": "b80acdc3-e2e7-4d09-8c7a-48ce12b4dd38", "type": "port", "attributes": { "id": "b80acdc3-e2e7-4d09-8c7a-48ce12b4dd38", "name": "Ningbo", "code": "CNNGB", "state_abbr": null, "city": null, "country_code": "CN", "latitude": "29.889437243", "longitude": "122.033720842", "time_zone": "Asia/Shanghai" } }, { "id": "c17c5324-008e-4549-9e67-302cff53a56d", "type": "vessel", "attributes": { "name": "NYK THEMIS", "imo": "9356696", "mmsi": "636018225", "latitude": -78.30435842851921, "longitude": 25.471353799804547, "nautical_speed_knots": 100, "navigational_heading_degrees": 18, "position_timestamp": "2023-06-05T19:46:18Z" } }, { "id": "58114b8d-7aab-491d-97ce-7b32c0c1d198", "type": "transport_event", "attributes": { "event": "container.transport.vessel_loaded", "created_at": "2022-10-21T20:16:47Z", "voyage_number": "0082E", "timestamp": "2022-09-22T09:38:00Z", "data_source": "shipping_line", "location_locode": "CNNGB", "timezone": "Asia/Shanghai" }, "relationships": { "shipment": { "data": { "id": "2bae2c8b-c682-47a6-81c7-f1ac9d4404ef", "type": "shipment" } }, "container": { "data": { "id": "c7876dc6-ccad-4219-b0a2-1e9e9845f474", "type": "container" } }, "vessel": { "data": { "id": "c17c5324-008e-4549-9e67-302cff53a56d", "type": "vessel" } }, "location": { "data": { "id": "b80acdc3-e2e7-4d09-8c7a-48ce12b4dd38", "type": "port" } }, "terminal": { "data": null } } } ] } ``` #### container.transport.vessel\_departed Vessel departed the port of lading. ```json expandable theme={null} { "data": { "id": "f65f7fff-2384-4f90-919b-b716c16bc670", "type": "webhook_notification", "attributes": { "id": "f65f7fff-2384-4f90-919b-b716c16bc670", "event": "container.transport.vessel_departed", "delivery_status": "succeeded", "created_at": "2022-10-21T20:16:41Z" }, "relationships": { "reference_object": { "data": { "id": "2b8eb6b3-6dcb-4acc-a234-dfc971408762", "type": "transport_event" } }, "webhook": { "data": { "id": "501aae38-e752-4f15-ab6e-73ea0ede3ca2", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "a65a7f43-0038-4f60-acff-c97a4979d323", "type": "shipment", "attributes": { "created_at": "2022-10-21T20:15:38Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "914595688", "normalized_number": "914595688", "shipping_line_scac": "SEAU", "shipping_line_name": "Sealand Americas", "shipping_line_short_name": "SeaLand Americas", "customer_name": "Lang and Sons", "port_of_lading_locode": "CLARI", "port_of_lading_name": "Arica", "port_of_discharge_locode": "USNYC", "port_of_discharge_name": "New York / New Jersey", "pod_vessel_name": "NORTHERN PRIORITY", "pod_vessel_imo": "9450313", "pod_voyage_number": "242N", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": "2022-09-28T19:51:00Z", "pol_timezone": "America/Santiago", "pod_eta_at": "2022-10-27T12:00:00Z", "pod_original_eta_at": "2022-10-27T12:00:00Z", "pod_ata_at": null, "pod_timezone": "America/New_York", "line_tracking_last_attempted_at": null, "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "cbf247a7-a86c-491f-948e-6a95a41e9199", "type": "port" } }, "port_of_discharge": { "data": { "id": "e06c809e-c437-47ac-92a1-aeb1c6f14480", "type": "port" } }, "pod_terminal": { "data": { "id": "a01b7de4-05a5-4871-8074-c524057216ec", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "56ad45c8-dbd0-4c1e-bee7-7c4376db3924", "type": "container" } ] } }, "links": { "self": "/v2/shipments/2de26519-3fb0-4748-b5ea-fca2b68dcab1" } }, { "id": "56ad45c8-dbd0-4c1e-bee7-7c4376db3924", "type": "container", "attributes": { "number": "MNBU4188482", "seal_number": null, "created_at": "2022-10-21T20:15:38Z", "ref_numbers": [ ], "pod_arrived_at": null, "pod_discharged_at": null, "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "reefer", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": null, "pod_full_out_at": null, "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": null, "availability_known": false, "pod_timezone": "America/New_York", "final_destination_timezone": null, "empty_terminated_timezone": "America/New_York" }, "relationships": { "shipment": { "data": { "id": "a65a7f43-0038-4f60-acff-c97a4979d323", "type": "shipment" } }, "pod_terminal": { "data": { "id": "a01b7de4-05a5-4871-8074-c524057216ec", "type": "terminal" } }, "transport_events": { "data": [ { "id": "875a1706-b8cd-4a27-9b0d-b6bc76b01a91", "type": "transport_event" }, { "id": "571209fb-d43e-4361-8bc6-f17a72005964", "type": "transport_event" }, { "id": "2b8eb6b3-6dcb-4acc-a234-dfc971408762", "type": "transport_event" }, { "id": "7d77608f-647c-45a3-9308-12d988d5be62", "type": "transport_event" }, { "id": "74c00224-82e2-4bb8-aa2a-291aa4f5554d", "type": "transport_event" }, { "id": "1580e710-6e00-4e7c-b0e4-cabed65d09e3", "type": "transport_event" }, { "id": "efa9723b-fb8d-4add-8059-8a95bcb78cba", "type": "transport_event" }, { "id": "3a19cba1-a2e8-40e9-b9cb-8cadbe802ca9", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "f4bf9733-cfef-4e01-ad8c-efc874dd4b83", "type": "raw_event" }, { "id": "29f37e62-08a5-45c5-87dd-eee6a8cddc9c", "type": "raw_event" }, { "id": "c692006a-0755-4f0f-8b6f-e22360de589e", "type": "raw_event" }, { "id": "b41de73f-aa1d-473e-8fed-9bf9ba6f5384", "type": "raw_event" }, { "id": "2c60b856-6120-44ba-8c7f-a8e0a7dc3306", "type": "raw_event" }, { "id": "3d13f46c-b04a-435e-9bc9-7d4c2fb70fe4", "type": "raw_event" }, { "id": "00104149-7bfd-4d11-989a-0bcf3089f446", "type": "raw_event" }, { "id": "5a224d3b-d3b7-489a-a776-09f053e422b1", "type": "raw_event" }, { "id": "85cfe2ce-c88e-4b55-b25f-f36a0c3f5e6a", "type": "raw_event" }, { "id": "b4481a38-511b-4d84-b92c-e36b6e050b5c", "type": "raw_event" }, { "id": "527e2d94-93e2-40f8-9421-a80fd53e2fe8", "type": "raw_event" }, { "id": "86666d95-cb85-4106-b68c-816702e851bf", "type": "raw_event" }, { "id": "afe2b57f-5363-4ad9-ad0e-a83c8928c0d9", "type": "raw_event" }, { "id": "e1240511-7a64-49f7-8dc1-bdf31848f1a7", "type": "raw_event" } ] } } }, { "id": "cbf247a7-a86c-491f-948e-6a95a41e9199", "type": "port", "attributes": { "id": "cbf247a7-a86c-491f-948e-6a95a41e9199", "name": "Arica", "code": "CLARI", "state_abbr": null, "city": null, "country_code": "CL", "latitude": "-18.471872947", "longitude": "-70.327958963", "time_zone": "America/Santiago" } }, { "id": "95016ff7-1084-416a-9c85-4af24e91d883", "type": "vessel", "attributes": { "name": "MERIDIAN", "imo": "7002605", "mmsi": "218415000", "latitude": -78.30435842851921, "longitude": 25.471353799804547, "nautical_speed_knots": 44, "navigational_heading_degrees": 1, "position_timestamp": "2023-06-05T19:46:18Z" } }, { "id": "2b8eb6b3-6dcb-4acc-a234-dfc971408762", "type": "transport_event", "attributes": { "event": "container.transport.vessel_departed", "created_at": "2022-10-21T20:15:38Z", "voyage_number": "239N", "timestamp": "2022-09-28T19:51:00Z", "data_source": "shipping_line", "location_locode": "CLARI", "timezone": "America/Santiago" }, "relationships": { "shipment": { "data": { "id": "a65a7f43-0038-4f60-acff-c97a4979d323", "type": "shipment" } }, "container": { "data": { "id": "56ad45c8-dbd0-4c1e-bee7-7c4376db3924", "type": "container" } }, "vessel": { "data": { "id": "95016ff7-1084-416a-9c85-4af24e91d883", "type": "vessel" } }, "location": { "data": { "id": "cbf247a7-a86c-491f-948e-6a95a41e9199", "type": "port" } }, "terminal": { "data": null } } } ] } ``` ### Transshipment #### container.transport.transshipment\_arrived Container arrived at a transshipment port. ```json expandable theme={null} { "data": { "id": "8711bde5-8172-414b-b418-822c01ac8702", "type": "webhook_notification", "attributes": { "id": "8711bde5-8172-414b-b418-822c01ac8702", "event": "container.transport.transshipment_arrived", "delivery_status": "succeeded", "created_at": "2022-10-21T20:16:41Z" }, "relationships": { "reference_object": { "data": { "id": "a784ad6d-55e5-40ec-83d2-06bc12b4cbe6", "type": "transport_event" } }, "webhook": { "data": { "id": "441a3525-f2d7-4484-a46b-d89727cd3de6", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "9a86d383-af2e-4aaf-84ff-d868c4145de6", "type": "shipment", "attributes": { "created_at": "2022-10-21T20:15:38Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "914595688", "normalized_number": "914595688", "shipping_line_scac": "SEAU", "shipping_line_name": "Sealand Americas", "shipping_line_short_name": "SeaLand Americas", "customer_name": "Runolfsson-Fisher", "port_of_lading_locode": "CLARI", "port_of_lading_name": "Arica", "port_of_discharge_locode": "USNYC", "port_of_discharge_name": "New York / New Jersey", "pod_vessel_name": "NORTHERN PRIORITY", "pod_vessel_imo": "9450313", "pod_voyage_number": "242N", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": "2022-09-28T19:51:00Z", "pol_timezone": "America/Santiago", "pod_eta_at": "2022-10-27T12:00:00Z", "pod_original_eta_at": "2022-10-27T12:00:00Z", "pod_ata_at": null, "pod_timezone": "America/New_York", "line_tracking_last_attempted_at": null, "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "d49cd76a-8ee2-4b1e-90f7-ea75c008bbfb", "type": "port" } }, "port_of_discharge": { "data": { "id": "6dfceea1-86b1-426b-89e5-a20a4f134b58", "type": "port" } }, "pod_terminal": { "data": { "id": "c021339d-7145-429f-b713-9381c6874410", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "e68fc5f3-d058-47dd-935f-3b511b97f963", "type": "container" } ] } }, "links": { "self": "/v2/shipments/2de26519-3fb0-4748-b5ea-fca2b68dcab1" } }, { "id": "e68fc5f3-d058-47dd-935f-3b511b97f963", "type": "container", "attributes": { "number": "MNBU4188482", "seal_number": null, "created_at": "2022-10-21T20:15:38Z", "ref_numbers": [ ], "pod_arrived_at": null, "pod_discharged_at": null, "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "reefer", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": null, "pod_full_out_at": null, "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": null, "availability_known": false, "pod_timezone": "America/New_York", "final_destination_timezone": null, "empty_terminated_timezone": "America/New_York" }, "relationships": { "shipment": { "data": { "id": "9a86d383-af2e-4aaf-84ff-d868c4145de6", "type": "shipment" } }, "pod_terminal": { "data": { "id": "c021339d-7145-429f-b713-9381c6874410", "type": "terminal" } }, "transport_events": { "data": [ { "id": "48460e5e-109f-441e-85e5-f2dafdc0b580", "type": "transport_event" }, { "id": "a0b4028a-cd0d-4cee-81ec-fbcad0bbd6e4", "type": "transport_event" }, { "id": "cb7e6907-6f19-4993-9537-e5639f012855", "type": "transport_event" }, { "id": "28dce1a7-be9c-420b-9204-cb5ba1a8d9f5", "type": "transport_event" }, { "id": "a784ad6d-55e5-40ec-83d2-06bc12b4cbe6", "type": "transport_event" }, { "id": "8f90ebb0-e7ac-4dc3-8289-8d8d707cb599", "type": "transport_event" }, { "id": "e759a7c1-4698-43fa-8655-9e587cd543a0", "type": "transport_event" }, { "id": "3cc0cafb-1e9b-4cfa-8b36-bac8d0637bbc", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "686817d9-a55a-4ddb-a6c3-dbddc9da2c8d", "type": "raw_event" }, { "id": "a6fad72d-f691-4b39-bec9-1ad74f3ba00c", "type": "raw_event" }, { "id": "1debea05-bdef-40ed-afb1-85df593befb2", "type": "raw_event" }, { "id": "cb838330-54fe-4ae3-94d2-d6494a4553b4", "type": "raw_event" }, { "id": "d87dbb59-9b0c-4232-a04c-0b6b1400f865", "type": "raw_event" }, { "id": "d0d79af0-7bf0-4a87-b14b-219ae8d46a70", "type": "raw_event" }, { "id": "abe79851-1346-45a2-8134-60c2c20c82ad", "type": "raw_event" }, { "id": "eb2e1b88-570b-449b-a4c8-ec90f75221b0", "type": "raw_event" }, { "id": "1bbf2111-7125-4b56-877c-13eca65eec23", "type": "raw_event" }, { "id": "9c729bea-2e19-4bca-b6a4-a9112562ffd6", "type": "raw_event" }, { "id": "b75f1d15-375a-41a6-91cb-0055a7eb681e", "type": "raw_event" }, { "id": "e6fa1c43-f6a4-459a-840f-312e24257832", "type": "raw_event" }, { "id": "c4b19e31-309c-4454-9711-3adf028fca5e", "type": "raw_event" }, { "id": "150a808a-a9c4-4807-b3e6-21f0e74491ef", "type": "raw_event" } ] } } }, { "id": "b8047d9a-62de-4f10-b0d9-abc6ff025b67", "type": "port", "attributes": { "id": "b8047d9a-62de-4f10-b0d9-abc6ff025b67", "name": "Balboa", "code": "PABLB", "state_abbr": null, "city": null, "country_code": "PA", "latitude": "8.958933348", "longitude": "-79.565420224", "time_zone": "America/Panama" } }, { "id": "50ce561a-577f-46d1-af67-b236b847f51d", "type": "vessel", "attributes": { "name": "MERIDIAN", "imo": "7002605", "mmsi": "218415000", "latitude": -78.30435842851921, "longitude": 25.471353799804547, "nautical_speed_knots": 100, "navigational_heading_degrees": 1, "position_timestamp": "2023-06-05T19:46:18Z" } }, { "id": "a784ad6d-55e5-40ec-83d2-06bc12b4cbe6", "type": "transport_event", "attributes": { "event": "container.transport.transshipment_arrived", "created_at": "2022-10-21T20:15:38Z", "voyage_number": "239N", "timestamp": "2022-10-11T13:01:00Z", "data_source": "shipping_line", "location_locode": "PABLB", "timezone": "America/Panama" }, "relationships": { "shipment": { "data": { "id": "9a86d383-af2e-4aaf-84ff-d868c4145de6", "type": "shipment" } }, "container": { "data": { "id": "e68fc5f3-d058-47dd-935f-3b511b97f963", "type": "container" } }, "vessel": { "data": { "id": "50ce561a-577f-46d1-af67-b236b847f51d", "type": "vessel" } }, "location": { "data": { "id": "b8047d9a-62de-4f10-b0d9-abc6ff025b67", "type": "port" } }, "terminal": { "data": null } } } ] } ``` #### container.transport.transshipment\_discharged Container discharged at the transshipment port. ```json expandable theme={null} { "data": { "id": "a50e58c5-60eb-453e-b797-de590914d9c6", "type": "webhook_notification", "attributes": { "id": "a50e58c5-60eb-453e-b797-de590914d9c6", "event": "container.transport.transshipment_discharged", "delivery_status": "succeeded", "created_at": "2022-10-21T20:17:33Z" }, "relationships": { "reference_object": { "data": { "id": "2051adf7-b0d4-4f8c-9bb4-5cee4987d7a1", "type": "transport_event" } }, "webhook": { "data": { "id": "d1b29b83-d7f3-4a4a-be86-b3b2e359a021", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "eb2571f2-b189-4d84-ac49-6a606e1f3ce8", "type": "shipment", "attributes": { "created_at": "2022-10-13T06:24:29Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "6345849250", "normalized_number": "6345849250", "shipping_line_scac": "COSU", "shipping_line_name": "COSCO", "shipping_line_short_name": "COSCO", "customer_name": "Bruen, Orn and Ruecker", "port_of_lading_locode": "NOBVK", "port_of_lading_name": "Brevik", "port_of_discharge_locode": "IDJKT", "port_of_discharge_name": "Jakarta, Java", "pod_vessel_name": "CTP MAKASSAR", "pod_vessel_imo": "9181742", "pod_voyage_number": "446N", "destination_locode": null, "destination_name": "Jakarta,Indonesia", "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": "2022-12-05T14:00:00Z", "pol_etd_at": "2022-10-18T10:00:00Z", "pol_atd_at": "2022-10-18T22:12:00Z", "pol_timezone": "Europe/Oslo", "pod_eta_at": "2022-12-05T12:00:00Z", "pod_original_eta_at": "2022-11-28T12:00:00Z", "pod_ata_at": null, "pod_timezone": "Asia/Jakarta", "line_tracking_last_attempted_at": "2022-10-21T20:17:28Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "d4975962-efc1-47bf-9f94-b32b20645678", "type": "port" } }, "port_of_discharge": { "data": { "id": "c1a4b096-90fb-4c48-b797-1646af7a184d", "type": "port" } }, "pod_terminal": { "data": null }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "50959d84-85d4-47a0-a822-496383968eb5", "type": "container" }, { "id": "79f2bc77-8490-43c6-9938-b6ee1723388e", "type": "container" } ] } }, "links": { "self": "/v2/shipments/743450bd-e1e6-4c7c-8e82-5987a5aa578b" } }, { "id": "50959d84-85d4-47a0-a822-496383968eb5", "type": "container", "attributes": { "number": "OOLU1927772", "seal_number": "0167667", "created_at": "2022-10-13T06:24:29Z", "ref_numbers": [ ], "pod_arrived_at": null, "pod_discharged_at": null, "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "dry", "equipment_length": 20, "equipment_height": "standard", "weight_in_lbs": 59525, "pod_full_out_at": null, "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": null, "availability_known": false, "pod_timezone": "Asia/Jakarta", "final_destination_timezone": null, "empty_terminated_timezone": "Asia/Jakarta" }, "relationships": { "shipment": { "data": { "id": "eb2571f2-b189-4d84-ac49-6a606e1f3ce8", "type": "shipment" } }, "pod_terminal": { "data": null }, "transport_events": { "data": [ { "id": "478eaae8-e93e-45d6-b7ff-868a9eeeae20", "type": "transport_event" }, { "id": "7c471ed0-98b5-4ab3-9e8b-732a5a54c65b", "type": "transport_event" }, { "id": "21c59959-0fcb-44cf-ba77-3a117c119c9c", "type": "transport_event" }, { "id": "55a4bbb5-cf7f-48e1-82fb-faa07be87580", "type": "transport_event" }, { "id": "2051adf7-b0d4-4f8c-9bb4-5cee4987d7a1", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "2dc91aa8-b9ae-43e3-bd7a-6092630d3c2e", "type": "raw_event" }, { "id": "681a2cab-337f-41aa-af4f-52b7c1a11e47", "type": "raw_event" }, { "id": "07452b4d-dead-4db6-973b-51cce89e9528", "type": "raw_event" }, { "id": "a8bca88f-61f6-4747-8e6c-a8e0b832733d", "type": "raw_event" }, { "id": "847e6c3f-bdde-47bd-b0d2-272381fdf327", "type": "raw_event" } ] } } }, { "id": "2a7f2058-e408-4259-baa9-ecf2c60ff275", "type": "port", "attributes": { "id": "2a7f2058-e408-4259-baa9-ecf2c60ff275", "name": "Rotterdam", "code": "NLRTM", "state_abbr": null, "city": null, "country_code": "NL", "latitude": "51.956693922", "longitude": "4.063456434", "time_zone": "Europe/Amsterdam" } }, { "id": "19f128eb-9be0-45de-8008-7d66dc7b0091", "type": "vessel", "attributes": { "name": "ELBSPRING", "imo": "9412529", "mmsi": "305575000", "latitude": -78.30435842851921, "longitude": 25.471353799804547, "nautical_speed_knots": 100, "navigational_heading_degrees": 1, "position_timestamp": "2023-06-05T19:46:18Z" } }, { "id": "2051adf7-b0d4-4f8c-9bb4-5cee4987d7a1", "type": "transport_event", "attributes": { "event": "container.transport.transshipment_discharged", "created_at": "2022-10-21T20:17:33Z", "voyage_number": "50", "timestamp": "2022-10-21T16:00:00Z", "data_source": "shipping_line", "location_locode": "NLRTM", "timezone": "Europe/Amsterdam" }, "relationships": { "shipment": { "data": { "id": "eb2571f2-b189-4d84-ac49-6a606e1f3ce8", "type": "shipment" } }, "container": { "data": { "id": "50959d84-85d4-47a0-a822-496383968eb5", "type": "container" } }, "vessel": { "data": { "id": "19f128eb-9be0-45de-8008-7d66dc7b0091", "type": "vessel" } }, "location": { "data": { "id": "2a7f2058-e408-4259-baa9-ecf2c60ff275", "type": "port" } }, "terminal": { "data": null } } } ] } ``` #### container.transport.transshipment\_loaded Container loaded onto a new vessel at the transshipment port. ```json expandable theme={null} { "data": { "id": "e41c559a-3179-4e17-b739-d2458ff972a3", "type": "webhook_notification", "attributes": { "id": "e41c559a-3179-4e17-b739-d2458ff972a3", "event": "container.transport.transshipment_loaded", "delivery_status": "succeeded", "created_at": "2022-10-21T20:16:41Z" }, "relationships": { "reference_object": { "data": { "id": "abd25313-fc0f-4ad6-a9a0-5afdfe2116e2", "type": "transport_event" } }, "webhook": { "data": { "id": "f6e45855-97de-4c13-ba6c-ae2ee42f9d70", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "1e52ad99-2d0e-41ef-87ad-ea3572e2899e", "type": "shipment", "attributes": { "created_at": "2022-10-21T20:15:38Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "914595688", "normalized_number": "914595688", "shipping_line_scac": "SEAU", "shipping_line_name": "Sealand Americas", "shipping_line_short_name": "SeaLand Americas", "customer_name": "Kozey, Ortiz and Legros", "port_of_lading_locode": "CLARI", "port_of_lading_name": "Arica", "port_of_discharge_locode": "USNYC", "port_of_discharge_name": "New York / New Jersey", "pod_vessel_name": "NORTHERN PRIORITY", "pod_vessel_imo": "9450313", "pod_voyage_number": "242N", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": "2022-09-28T19:51:00Z", "pol_timezone": "America/Santiago", "pod_eta_at": "2022-10-27T12:00:00Z", "pod_original_eta_at": "2022-10-27T12:00:00Z", "pod_ata_at": null, "pod_timezone": "America/New_York", "line_tracking_last_attempted_at": null, "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "5a56e8c3-bac0-47da-99c6-cd83ab428a80", "type": "port" } }, "port_of_discharge": { "data": { "id": "990a5038-5273-4fe8-9a9f-4f1de2bcd418", "type": "port" } }, "pod_terminal": { "data": { "id": "62c6c1d7-757b-4cde-b189-9c6898d69be3", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "60998326-fb38-43c7-af7f-a0cc45825152", "type": "container" } ] } }, "links": { "self": "/v2/shipments/2de26519-3fb0-4748-b5ea-fca2b68dcab1" } }, { "id": "60998326-fb38-43c7-af7f-a0cc45825152", "type": "container", "attributes": { "number": "MNBU4188482", "seal_number": null, "created_at": "2022-10-21T20:15:38Z", "ref_numbers": [ ], "pod_arrived_at": null, "pod_discharged_at": null, "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "reefer", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": null, "pod_full_out_at": null, "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": null, "availability_known": false, "pod_timezone": "America/New_York", "final_destination_timezone": null, "empty_terminated_timezone": "America/New_York" }, "relationships": { "shipment": { "data": { "id": "1e52ad99-2d0e-41ef-87ad-ea3572e2899e", "type": "shipment" } }, "pod_terminal": { "data": { "id": "62c6c1d7-757b-4cde-b189-9c6898d69be3", "type": "terminal" } }, "transport_events": { "data": [ { "id": "0923b9f0-23aa-48b7-ba1c-a9069a6b18fe", "type": "transport_event" }, { "id": "b7926c0c-6d32-4952-8f45-93cf8b7be737", "type": "transport_event" }, { "id": "28c51b2e-22fb-4f92-a065-92cd5ec9a189", "type": "transport_event" }, { "id": "d21d539e-11fd-42c6-a9df-7b2d72f1efbe", "type": "transport_event" }, { "id": "3f8a6bc0-3fda-4a82-941a-5d75bf5f1dea", "type": "transport_event" }, { "id": "c42d173f-cebc-4caf-8c7d-ac11374aa3fc", "type": "transport_event" }, { "id": "abd25313-fc0f-4ad6-a9a0-5afdfe2116e2", "type": "transport_event" }, { "id": "ed8ee17c-276c-428a-a4c5-321ceb735293", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "742004b2-db59-4d68-8e94-35523d02b911", "type": "raw_event" }, { "id": "412d71b3-6314-4eb8-a2f7-9e582c5ec1b9", "type": "raw_event" }, { "id": "a03d1986-0889-4ba1-83a1-473209f026f5", "type": "raw_event" }, { "id": "f721632d-257a-4001-b8ab-9a66fff06bb8", "type": "raw_event" }, { "id": "fc0b6306-dcce-497f-b5e8-d4b445bc9ce7", "type": "raw_event" }, { "id": "de6dd640-9f81-4b8c-ba1a-95da3da1d415", "type": "raw_event" }, { "id": "7b60fd0f-d53e-47fd-a8df-d206df12ae30", "type": "raw_event" }, { "id": "1858d515-fa61-4a6c-979d-718b511304ff", "type": "raw_event" }, { "id": "28e25e0a-d57c-4294-8a96-b5f8e41777fa", "type": "raw_event" }, { "id": "4c003cd5-c1c6-4dbd-9a6a-8c7b98b0c176", "type": "raw_event" }, { "id": "b77b40c6-6df3-4324-ba58-6f11cfa430a7", "type": "raw_event" }, { "id": "6aa41832-c607-40fb-924b-8f7017cbbb1b", "type": "raw_event" }, { "id": "da1617cf-1557-4766-bd1c-6c60bc32db87", "type": "raw_event" }, { "id": "26dcb356-a6e4-4255-bc29-a1b074cfb8f0", "type": "raw_event" } ] } } }, { "id": "928f91f5-74d0-4a65-b5cf-995a4a026770", "type": "port", "attributes": { "id": "928f91f5-74d0-4a65-b5cf-995a4a026770", "name": "Manzanillo", "code": "PAMIT", "state_abbr": null, "city": null, "country_code": "PA", "latitude": "9.362360956", "longitude": "-79.882591837", "time_zone": "America/Panama" } }, { "id": "2327b55c-4abb-410d-b97f-dcddf4734f89", "type": "vessel", "attributes": { "name": "NORTHERN PRIORITY", "imo": "9450313", "mmsi": "636091832", "latitude": -78.30435842851921, "longitude": 25.471353799804547, "nautical_speed_knots": 100, "navigational_heading_degrees": 1, "position_timestamp": "2023-06-05T19:46:18Z" } }, { "id": "abd25313-fc0f-4ad6-a9a0-5afdfe2116e2", "type": "transport_event", "attributes": { "event": "container.transport.transshipment_loaded", "created_at": "2022-10-21T20:15:38Z", "voyage_number": "242N", "timestamp": "2022-10-19T18:09:00Z", "data_source": "shipping_line", "location_locode": "PAMIT", "timezone": "America/Panama" }, "relationships": { "shipment": { "data": { "id": "1e52ad99-2d0e-41ef-87ad-ea3572e2899e", "type": "shipment" } }, "container": { "data": { "id": "60998326-fb38-43c7-af7f-a0cc45825152", "type": "container" } }, "vessel": { "data": { "id": "2327b55c-4abb-410d-b97f-dcddf4734f89", "type": "vessel" } }, "location": { "data": { "id": "928f91f5-74d0-4a65-b5cf-995a4a026770", "type": "port" } }, "terminal": { "data": null } } } ] } ``` #### container.transport.transshipment\_departed Vessel departed the transshipment port. ```json expandable theme={null} { "data": { "id": "93006fa9-7ff4-49c3-ba69-4266aac3b54b", "type": "webhook_notification", "attributes": { "id": "93006fa9-7ff4-49c3-ba69-4266aac3b54b", "event": "container.transport.transshipment_departed", "delivery_status": "succeeded", "created_at": "2022-10-21T20:16:41Z" }, "relationships": { "reference_object": { "data": { "id": "e82e8530-1cf5-4680-9120-a737dde69083", "type": "transport_event" } }, "webhook": { "data": { "id": "1e6cd9f5-cb7e-43be-8a7e-99ebcd08106c", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "ccdf4809-0965-49c1-9dfb-9f4b250f3afd", "type": "shipment", "attributes": { "created_at": "2022-10-21T20:15:38Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "914595688", "normalized_number": "914595688", "shipping_line_scac": "SEAU", "shipping_line_name": "Sealand Americas", "shipping_line_short_name": "SeaLand Americas", "customer_name": "Quigley, Romaguera and McDermott", "port_of_lading_locode": "CLARI", "port_of_lading_name": "Arica", "port_of_discharge_locode": "USNYC", "port_of_discharge_name": "New York / New Jersey", "pod_vessel_name": "NORTHERN PRIORITY", "pod_vessel_imo": "9450313", "pod_voyage_number": "242N", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": "2022-09-28T19:51:00Z", "pol_timezone": "America/Santiago", "pod_eta_at": "2022-10-27T12:00:00Z", "pod_original_eta_at": "2022-10-27T12:00:00Z", "pod_ata_at": null, "pod_timezone": "America/New_York", "line_tracking_last_attempted_at": null, "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "6c07b6aa-7f5c-469a-8122-9509251e87c3", "type": "port" } }, "port_of_discharge": { "data": { "id": "9f951b96-5871-442d-a002-5e7b2b1bbb08", "type": "port" } }, "pod_terminal": { "data": { "id": "04f1344e-c5a2-43f1-9a20-7bf94258720b", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "a7a9790d-ec68-4afd-9063-a9cba0ec0cd9", "type": "container" } ] } }, "links": { "self": "/v2/shipments/2de26519-3fb0-4748-b5ea-fca2b68dcab1" } }, { "id": "a7a9790d-ec68-4afd-9063-a9cba0ec0cd9", "type": "container", "attributes": { "number": "MNBU4188482", "seal_number": null, "created_at": "2022-10-21T20:15:38Z", "ref_numbers": [ ], "pod_arrived_at": null, "pod_discharged_at": null, "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "reefer", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": null, "pod_full_out_at": null, "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": null, "availability_known": false, "pod_timezone": "America/New_York", "final_destination_timezone": null, "empty_terminated_timezone": "America/New_York" }, "relationships": { "shipment": { "data": { "id": "ccdf4809-0965-49c1-9dfb-9f4b250f3afd", "type": "shipment" } }, "pod_terminal": { "data": { "id": "04f1344e-c5a2-43f1-9a20-7bf94258720b", "type": "terminal" } }, "transport_events": { "data": [ { "id": "ca941257-104d-4556-881b-59a03ff27fa4", "type": "transport_event" }, { "id": "d17dd416-ee98-4a9b-9946-e053a246b79a", "type": "transport_event" }, { "id": "57aee521-0be4-4d8a-9b59-e90773371e46", "type": "transport_event" }, { "id": "8a3e98a1-ce78-4782-9173-55c2a457101e", "type": "transport_event" }, { "id": "4307eb90-b617-4b8e-8c5c-e9fa9f3b94d1", "type": "transport_event" }, { "id": "f4b2c3d6-1d6b-465a-ba59-c2b228683850", "type": "transport_event" }, { "id": "bdd4624d-5e59-4207-a03f-ee8ae00522aa", "type": "transport_event" }, { "id": "e82e8530-1cf5-4680-9120-a737dde69083", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "d87a8883-6ebc-402a-bf58-75837200b24e", "type": "raw_event" }, { "id": "08637455-91e6-4cb4-b68c-da7da5ce9eb0", "type": "raw_event" }, { "id": "bf9ea8d3-982e-4be6-aaaf-91a96c5081e2", "type": "raw_event" }, { "id": "b33f0ce7-0b01-4760-a61d-8792d1a5075d", "type": "raw_event" }, { "id": "dacc8b9e-95d9-4894-8ab2-a3172726b711", "type": "raw_event" }, { "id": "d1875e34-e862-44bf-8011-8f8e25d40222", "type": "raw_event" }, { "id": "9e5bf153-aa86-461e-bb7f-f75a44a4375c", "type": "raw_event" }, { "id": "c6fc7390-c634-44f2-b2a7-02c1397146f4", "type": "raw_event" }, { "id": "67cbee01-9731-4df4-b763-15ef0996abd8", "type": "raw_event" }, { "id": "dcc6745d-cb84-4575-9ecb-ea8017017289", "type": "raw_event" }, { "id": "798201b5-62f4-4d0b-951c-20d51960029b", "type": "raw_event" }, { "id": "a54fdf85-e96c-4bad-a993-f7ee0e857b9e", "type": "raw_event" }, { "id": "92b43e47-7af6-4317-bd1c-ee1dad0b6ab1", "type": "raw_event" }, { "id": "21c8fa3d-5702-4240-b9af-277d3853b441", "type": "raw_event" } ] } } }, { "id": "083870c7-a84c-4258-a033-73550322a336", "type": "port", "attributes": { "id": "083870c7-a84c-4258-a033-73550322a336", "name": "Manzanillo", "code": "PAMIT", "state_abbr": null, "city": null, "country_code": "PA", "latitude": "9.362360956", "longitude": "-79.882591837", "time_zone": "America/Panama" } }, { "id": "80750832-9ecf-4dae-b290-3263460fcbb1", "type": "vessel", "attributes": { "name": "NORTHERN PRIORITY", "imo": "9450313", "mmsi": "636091832", "latitude": -78.30435842851921, "longitude": 25.471353799804547, "nautical_speed_knots": 100, "navigational_heading_degrees": 1, "position_timestamp": "2023-06-05T19:46:18Z" } }, { "id": "e82e8530-1cf5-4680-9120-a737dde69083", "type": "transport_event", "attributes": { "event": "container.transport.transshipment_departed", "created_at": "2022-10-21T20:15:39Z", "voyage_number": "242N", "timestamp": "2022-10-20T06:01:00Z", "data_source": "shipping_line", "location_locode": "PAMIT", "timezone": "America/Panama" }, "relationships": { "shipment": { "data": { "id": "ccdf4809-0965-49c1-9dfb-9f4b250f3afd", "type": "shipment" } }, "container": { "data": { "id": "a7a9790d-ec68-4afd-9063-a9cba0ec0cd9", "type": "container" } }, "vessel": { "data": { "id": "80750832-9ecf-4dae-b290-3263460fcbb1", "type": "vessel" } }, "location": { "data": { "id": "083870c7-a84c-4258-a033-73550322a336", "type": "port" } }, "terminal": { "data": null } } } ] } ``` ### Destination #### container.transport.vessel\_arrived Vessel arrived at the port of discharge. ```json expandable theme={null} { "data": { "id": "be283e7f-6d95-4d87-b8cc-e4a88a0738ac", "type": "webhook_notification", "attributes": { "id": "be283e7f-6d95-4d87-b8cc-e4a88a0738ac", "event": "container.transport.vessel_arrived", "delivery_status": "succeeded", "created_at": "2022-10-21T20:15:39Z" }, "relationships": { "reference_object": { "data": { "id": "3e814da7-53fe-4397-9776-c97e248eda91", "type": "transport_event" } }, "webhook": { "data": { "id": "361830fa-b087-47f2-94b0-06af1ac5d2a8", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "e55680a0-3378-428e-9783-7d72f9c3fc7f", "type": "shipment", "attributes": { "created_at": "2022-09-09T12:05:57Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "ZIMULEH9024455", "normalized_number": "ZIMULEH9024455", "shipping_line_scac": "ZIMU", "shipping_line_name": "Zim American Integrated Shipping Services", "shipping_line_short_name": "Zim Line", "customer_name": "Denesik, Senger and Feil", "port_of_lading_locode": "FRLEH", "port_of_lading_name": "Le Havre", "port_of_discharge_locode": "INNSA", "port_of_discharge_name": "Nhava Sheva", "pod_vessel_name": "TONGALA", "pod_vessel_imo": "9278105", "pod_voyage_number": "132/E", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": "2022-09-28T22:00:00Z", "pol_atd_at": "2022-09-28T13:40:00Z", "pol_timezone": "Europe/Paris", "pod_eta_at": "2022-10-20T18:30:00Z", "pod_original_eta_at": "2022-10-13T18:30:00Z", "pod_ata_at": "2022-10-21T15:53:00Z", "pod_timezone": "Asia/Calcutta", "line_tracking_last_attempted_at": "2022-10-21T19:32:09Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "b9e91f35-d472-48a8-912d-dbb1f85fe38e", "type": "port" } }, "port_of_discharge": { "data": { "id": "be2b403c-ebaa-4125-aebd-3471ec765edf", "type": "port" } }, "pod_terminal": { "data": null }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "5c3e90ed-5c8c-4c01-a0ce-ea544a2a9f6d", "type": "container" } ] } }, "links": { "self": "/v2/shipments/ae598754-a193-4a1f-9a4e-483d78b48d8c" } }, { "id": "5c3e90ed-5c8c-4c01-a0ce-ea544a2a9f6d", "type": "container", "attributes": { "number": "CAIU3758064", "seal_number": null, "created_at": "2022-09-09T12:05:58Z", "ref_numbers": [ ], "pod_arrived_at": "2022-10-21T15:53:00Z", "pod_discharged_at": null, "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "dry", "equipment_length": 20, "equipment_height": "standard", "weight_in_lbs": null, "pod_full_out_at": null, "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": null, "availability_known": false, "pod_timezone": "Asia/Calcutta", "final_destination_timezone": null, "empty_terminated_timezone": "Asia/Calcutta" }, "relationships": { "shipment": { "data": { "id": "e55680a0-3378-428e-9783-7d72f9c3fc7f", "type": "shipment" } }, "pod_terminal": { "data": null }, "transport_events": { "data": [ { "id": "2f50d744-8c94-4d2c-9a84-857b7871010f", "type": "transport_event" }, { "id": "58fa4da4-e265-45f2-aa47-63596834d094", "type": "transport_event" }, { "id": "3e814da7-53fe-4397-9776-c97e248eda91", "type": "transport_event" }, { "id": "218d96b8-01e8-4349-852a-b03e05182d56", "type": "transport_event" }, { "id": "1a74d4d0-d24c-47c9-8818-59ae75562c8f", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "e96fa9f3-b83b-4340-9853-ca7f793c0ee9", "type": "raw_event" }, { "id": "9d652d24-2753-4b36-b267-c456dee0ce7f", "type": "raw_event" }, { "id": "b06b1ad9-c029-435c-99a0-e2dc6e7121c3", "type": "raw_event" }, { "id": "89aa1a6d-3bb4-477e-a4e3-b8ee9be02b35", "type": "raw_event" }, { "id": "c4b65cc1-7def-46f7-b9e4-35a22d1a58a5", "type": "raw_event" } ] } } }, { "id": "be2b403c-ebaa-4125-aebd-3471ec765edf", "type": "port", "attributes": { "id": "be2b403c-ebaa-4125-aebd-3471ec765edf", "name": "Nhava Sheva", "code": "INNSA", "state_abbr": null, "city": null, "country_code": "IN", "latitude": "18.95580615", "longitude": "72.951204698", "time_zone": "Asia/Calcutta" } }, { "id": "e55637e2-3fd7-405e-b987-401c1c880905", "type": "vessel", "attributes": { "name": "TONGALA", "imo": "9278105", "mmsi": "636013644", "latitude": -78.30435842851921, "longitude": 25.471353799804547, "nautical_speed_knots": 100, "navigational_heading_degrees": 1, "position_timestamp": "2023-06-05T19:46:18Z" } }, { "id": "3e814da7-53fe-4397-9776-c97e248eda91", "type": "transport_event", "attributes": { "event": "container.transport.vessel_arrived", "created_at": "2022-10-21T20:15:39Z", "voyage_number": "132/E", "timestamp": "2022-10-21T15:53:00Z", "data_source": "shipping_line", "location_locode": "INNSA", "timezone": "Asia/Calcutta" }, "relationships": { "shipment": { "data": { "id": "e55680a0-3378-428e-9783-7d72f9c3fc7f", "type": "shipment" } }, "container": { "data": { "id": "5c3e90ed-5c8c-4c01-a0ce-ea544a2a9f6d", "type": "container" } }, "vessel": { "data": { "id": "e55637e2-3fd7-405e-b987-401c1c880905", "type": "vessel" } }, "location": { "data": { "id": "be2b403c-ebaa-4125-aebd-3471ec765edf", "type": "port" } }, "terminal": { "data": null } } } ] } ``` #### container.transport.vessel\_berthed Vessel berthed at the port of discharge. ```json expandable theme={null} { "data": { "id": "4c2bee7b-5929-4a8d-baa4-b8f8580597be", "type": "webhook_notification", "attributes": { "id": "4c2bee7b-5929-4a8d-baa4-b8f8580597be", "event": "container.transport.vessel_berthed", "delivery_status": "succeeded", "created_at": "2022-10-21T18:52:26Z" }, "relationships": { "reference_object": { "data": { "id": "6242e7d4-eeea-40ec-af56-fc0a71e2a36b", "type": "transport_event" } }, "webhook": { "data": { "id": "9ed8cc42-5b0e-4ffc-96a6-335e96cdfcba", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "f028a4c5-4c0b-4894-b4b2-4919d266737e", "type": "shipment", "attributes": { "created_at": "2022-09-22T08:25:55Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "HDMUXMNM78424800", "normalized_number": "XMNM78424800", "shipping_line_scac": "HDMU", "shipping_line_name": "Hyundai Merchant Marine", "shipping_line_short_name": "Hyundai", "customer_name": "Kuvalis-Paucek", "port_of_lading_locode": "CNXMN", "port_of_lading_name": "Xiamen", "port_of_discharge_locode": "USLAX", "port_of_discharge_name": "Los Angeles", "pod_vessel_name": "YM UNANIMITY", "pod_vessel_imo": "9462718", "pod_voyage_number": "0063E", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": "2022-09-27T22:00:00Z", "pol_atd_at": "2022-09-28T02:30:00Z", "pol_timezone": "Asia/Shanghai", "pod_eta_at": "2022-10-17T13:00:00Z", "pod_original_eta_at": "2022-10-15T15:00:00Z", "pod_ata_at": "2022-10-18T17:35:17Z", "pod_timezone": "America/Los_Angeles", "line_tracking_last_attempted_at": "2022-10-21T18:52:23Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "5d392f9e-e3af-41d2-b7cf-7c8e48b5277d", "type": "port" } }, "port_of_discharge": { "data": { "id": "214edbe5-f438-4492-bb18-3cd053c92cc7", "type": "port" } }, "pod_terminal": { "data": { "id": "b76896e1-fb93-4ebe-bbb2-d9e7eefc7553", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "82052525-07c1-4c45-9a09-e7633cbab373", "type": "container" } ] } }, "links": { "self": "/v2/shipments/a065301b-0205-496a-a317-ef0abd2ae2e3" } }, { "id": "82052525-07c1-4c45-9a09-e7633cbab373", "type": "container", "attributes": { "number": "GAOU6337366", "seal_number": "", "created_at": "2022-09-22T08:25:55Z", "ref_numbers": [ ], "pod_arrived_at": "2022-10-17T13:00:00Z", "pod_discharged_at": "2022-10-18T17:21:00Z", "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": true, "equipment_type": "dry", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": 0, "pod_full_out_at": null, "empty_terminated_at": null, "terminal_checked_at": "2022-10-21T19:44:09Z", "fees_at_pod_terminal": [ ], "pickup_lfd": "2022-10-24T07:00:00Z", "pickup_appointment_at": null, "pod_full_out_chassis_number": null, "location_at_pod_terminal": "In Yard
(Decked)", "pod_last_tracking_request_at": "2022-10-21T19:44:09Z", "shipment_last_tracking_request_at": null, "availability_known": true, "pod_timezone": "America/Los_Angeles", "final_destination_timezone": null, "empty_terminated_timezone": "America/Los_Angeles" }, "relationships": { "shipment": { "data": { "id": "f028a4c5-4c0b-4894-b4b2-4919d266737e", "type": "shipment" } }, "pod_terminal": { "data": { "id": "b76896e1-fb93-4ebe-bbb2-d9e7eefc7553", "type": "terminal" } }, "transport_events": { "data": [ { "id": "82abbf20-17da-43ce-b62e-a94f0dbc51ad", "type": "transport_event" }, { "id": "75641949-c9b1-4726-b9dd-58577f3c0709", "type": "transport_event" }, { "id": "89deda0c-ff71-4e87-988e-cdb96f1af4b2", "type": "transport_event" }, { "id": "2553a3b5-e41f-4e22-9798-1aab7f275d35", "type": "transport_event" }, { "id": "6242e7d4-eeea-40ec-af56-fc0a71e2a36b", "type": "transport_event" }, { "id": "04fb0c48-fa3a-42e2-af35-0bec1d09f141", "type": "transport_event" }, { "id": "c45fe70b-83bf-46dd-ba5a-db03002ba349", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "ccb3e789-3902-469f-bbab-96a1ede50dd8", "type": "raw_event" }, { "id": "231b3fbe-6d33-4386-8258-1356ee9b9518", "type": "raw_event" }, { "id": "5d83c231-c711-42a0-abfc-6fedb259381f", "type": "raw_event" }, { "id": "a23c7de8-2ae0-4ca6-9c54-97d905d11a58", "type": "raw_event" }, { "id": "9e6700a1-15cd-4010-b8fc-799de9e3b1a5", "type": "raw_event" }, { "id": "8e50b041-1f9c-40bf-ab78-588e69bc4311", "type": "raw_event" }, { "id": "8027c3fc-c55c-4cb0-98de-ecfb2c8aa0ad", "type": "raw_event" } ] } } }, { "id": "214edbe5-f438-4492-bb18-3cd053c92cc7", "type": "port", "attributes": { "id": "214edbe5-f438-4492-bb18-3cd053c92cc7", "name": "Los Angeles", "code": "USLAX", "state_abbr": "CA", "city": "Los Angeles", "country_code": "US", "latitude": "33.728193631", "longitude": "-118.255820307", "time_zone": "America/Los_Angeles" } }, { "id": "b76896e1-fb93-4ebe-bbb2-d9e7eefc7553", "type": "terminal", "attributes": { "id": "b76896e1-fb93-4ebe-bbb2-d9e7eefc7553", "nickname": "WBCT", "name": "West Basin Container Terminal", "firms_code": "Y773", "smdg_code": null, "bic_facility_code": null, "provided_data": { "pickup_lfd": false, "pickup_lfd_notes": "", "available_for_pickup": false, "fees_at_pod_terminal": false, "holds_at_pod_terminal": false, "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": false, "location_at_pod_terminal_notes": "", "pod_full_out_chassis_number_notes": "" }, "street": "701 New Dock Street Berths 212-225", "city": "Terminal Island", "state": "California", "state_abbr": "CA", "zip": "90731", "country": "United States" }, "relationships": { "port": { "data": { "id": "214edbe5-f438-4492-bb18-3cd053c92cc7", "type": "port" } } } }, { "id": "f800df08-186c-40b9-8d3c-3fcc0838cbe6", "type": "vessel", "attributes": { "name": "YM UNANIMITY", "imo": "9462718", "mmsi": "416466000", "latitude": -78.30435842851921, "longitude": 25.471353799804547, "nautical_speed_knots": 25, "navigational_heading_degrees": 1, "position_timestamp": "2023-06-05T19:46:18Z" } }, { "id": "6242e7d4-eeea-40ec-af56-fc0a71e2a36b", "type": "transport_event", "attributes": { "event": "container.transport.vessel_berthed", "created_at": "2022-10-21T18:52:26Z", "voyage_number": "0063E", "timestamp": "2022-10-17T13:00:00Z", "data_source": "shipping_line", "location_locode": "USLAX", "timezone": "America/Los_Angeles" }, "relationships": { "shipment": { "data": { "id": "f028a4c5-4c0b-4894-b4b2-4919d266737e", "type": "shipment" } }, "container": { "data": { "id": "82052525-07c1-4c45-9a09-e7633cbab373", "type": "container" } }, "vessel": { "data": { "id": "f800df08-186c-40b9-8d3c-3fcc0838cbe6", "type": "vessel" } }, "location": { "data": { "id": "214edbe5-f438-4492-bb18-3cd053c92cc7", "type": "port" } }, "terminal": { "data": { "id": "b76896e1-fb93-4ebe-bbb2-d9e7eefc7553", "type": "terminal" } } } } ] } ``` #### container.transport.vessel\_discharged Container discharged from the vessel at the port of discharge. ```json expandable theme={null} { "data": { "id": "5c048ec8-afb0-48ca-8657-fa262dc9bbd7", "type": "webhook_notification", "attributes": { "id": "5c048ec8-afb0-48ca-8657-fa262dc9bbd7", "event": "container.transport.vessel_discharged", "delivery_status": "succeeded", "created_at": "2022-10-21T20:14:17Z" }, "relationships": { "reference_object": { "data": { "id": "d637c864-ef35-4df1-9563-ba8bbb179af5", "type": "transport_event" } }, "webhook": { "data": { "id": "851d3a8d-4f95-4296-be6d-0510ece0376d", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "56c8b2c7-a5e0-4c51-9793-f7dadc1fa52c", "type": "shipment", "attributes": { "created_at": "2022-09-01T15:22:22Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "TA2PTC560500", "normalized_number": "TA2PTC560500", "shipping_line_scac": "ONEY", "shipping_line_name": "Ocean Network Express", "shipping_line_short_name": "ONE", "customer_name": "Schaden and Sons", "port_of_lading_locode": "CNQIN", "port_of_lading_name": "Qingdao", "port_of_discharge_locode": "USNYC", "port_of_discharge_name": "New York / New Jersey", "pod_vessel_name": "ESSEN EXPRESS", "pod_vessel_imo": "9501370", "pod_voyage_number": "042E", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": "2022-07-30T12:24:00Z", "pol_timezone": "Asia/Shanghai", "pod_eta_at": "2022-09-22T15:00:00Z", "pod_original_eta_at": "2022-09-23T10:00:00Z", "pod_ata_at": "2022-10-19T19:55:00Z", "pod_timezone": "America/New_York", "line_tracking_last_attempted_at": "2022-10-21T20:14:12Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "25a0e8eb-a957-4973-a8fd-e984552baafa", "type": "port" } }, "port_of_discharge": { "data": { "id": "27f76ae6-d6d9-4938-878b-7113cd628159", "type": "port" } }, "pod_terminal": { "data": { "id": "ad02bb7c-a8a3-4ec5-bbb9-220f3dbf797d", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "7e9be7ac-2845-4130-b032-fb447d3c3126", "type": "container" } ] } }, "links": { "self": "/v2/shipments/44ddc574-6636-44f0-be9f-6dd6a7e5f0fb" } }, { "id": "7e9be7ac-2845-4130-b032-fb447d3c3126", "type": "container", "attributes": { "number": "NYKU0800893", "seal_number": "CND674488", "created_at": "2022-09-01T15:22:23Z", "ref_numbers": [ ], "pod_arrived_at": "2022-10-19T19:55:00Z", "pod_discharged_at": "2022-10-21T15:19:00Z", "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": true, "equipment_type": "dry", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": 18928, "pod_full_out_at": null, "empty_terminated_at": null, "terminal_checked_at": "2022-10-21T19:45:07Z", "fees_at_pod_terminal": [ ], "pickup_lfd": "2022-10-27T04:00:00Z", "pickup_appointment_at": null, "pod_full_out_chassis_number": null, "location_at_pod_terminal": "In Yard", "pod_last_tracking_request_at": "2022-10-21T19:45:07Z", "shipment_last_tracking_request_at": null, "availability_known": true, "pod_timezone": "America/New_York", "final_destination_timezone": null, "empty_terminated_timezone": "America/New_York" }, "relationships": { "shipment": { "data": { "id": "56c8b2c7-a5e0-4c51-9793-f7dadc1fa52c", "type": "shipment" } }, "pod_terminal": { "data": { "id": "ad02bb7c-a8a3-4ec5-bbb9-220f3dbf797d", "type": "terminal" } }, "transport_events": { "data": [ { "id": "81879bc8-3b1e-44e7-8b82-18747c5a55c1", "type": "transport_event" }, { "id": "400895bd-70b7-4ef7-8fef-8edd63eb1450", "type": "transport_event" }, { "id": "b2d26b74-3189-410e-a4e6-344e2a5ecbc9", "type": "transport_event" }, { "id": "90e32553-9d25-4426-b625-128d26f9f9c5", "type": "transport_event" }, { "id": "72375da8-6916-4db4-80a5-903602035b91", "type": "transport_event" }, { "id": "a0718da3-3185-4038-9b12-fc1886895933", "type": "transport_event" }, { "id": "eb452da3-ad22-457b-b3b5-6ef7d691efef", "type": "transport_event" }, { "id": "e474a621-fdad-413b-8a86-80ca59444d2e", "type": "transport_event" }, { "id": "9f274f93-4763-4af3-a513-47be9de6db74", "type": "transport_event" }, { "id": "c9a093b5-8962-4e67-9c65-cc82811352ae", "type": "transport_event" }, { "id": "d637c864-ef35-4df1-9563-ba8bbb179af5", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "4d66e330-b0b0-47bc-bd67-1385b6da145f", "type": "raw_event" }, { "id": "7db280b7-5728-4d5c-a3f0-80086e726144", "type": "raw_event" }, { "id": "93cbaa5e-67c8-4aa0-b264-aed51f0a25eb", "type": "raw_event" }, { "id": "88a6cf44-d652-4b0b-b58e-2e2d5ead3cf0", "type": "raw_event" }, { "id": "9583e9f1-70c0-426b-801a-99f9193cb7a4", "type": "raw_event" }, { "id": "e6663d02-6b90-4d4f-a697-1980eca44789", "type": "raw_event" }, { "id": "d6ad787d-6ad1-479c-87f6-2e104d1275ef", "type": "raw_event" }, { "id": "6a227c8f-865f-47be-8551-f043be9b9a8b", "type": "raw_event" }, { "id": "0b6ea128-508c-4951-9c08-a33ba5134447", "type": "raw_event" }, { "id": "614b23d9-0b23-4800-b5fe-ac2d06178eb3", "type": "raw_event" }, { "id": "d83e119a-9214-4e07-ae13-024c68b3d231", "type": "raw_event" }, { "id": "7ea41e5c-b6dd-4972-a42b-e630cbaaa6da", "type": "raw_event" }, { "id": "12c1202e-5a6b-4551-a78e-670c52dde93a", "type": "raw_event" }, { "id": "fbff6533-b387-4af5-912b-4040ba3d5a34", "type": "raw_event" } ] } } }, { "id": "27f76ae6-d6d9-4938-878b-7113cd628159", "type": "port", "attributes": { "id": "27f76ae6-d6d9-4938-878b-7113cd628159", "name": "New York / New Jersey", "code": "USNYC", "state_abbr": "NY", "city": "New York", "country_code": "US", "latitude": "40.684996498", "longitude": "-74.151115685", "time_zone": "America/New_York" } }, { "id": "ad02bb7c-a8a3-4ec5-bbb9-220f3dbf797d", "type": "terminal", "attributes": { "id": "ad02bb7c-a8a3-4ec5-bbb9-220f3dbf797d", "nickname": "GCTB", "name": "GCT Bayonne", "firms_code": "E364", "smdg_code": null, "bic_facility_code": null, "provided_data": { "pickup_lfd": false, "pickup_lfd_notes": "", "available_for_pickup": false, "fees_at_pod_terminal": false, "holds_at_pod_terminal": false, "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": false, "location_at_pod_terminal_notes": "", "pod_full_out_chassis_number_notes": "" }, "street": "701 New Dock Street Berths 212-225", "city": "Terminal Island", "state": "California", "state_abbr": "CA", "zip": "90731", "country": "United States" }, "relationships": { "port": { "data": { "id": "27f76ae6-d6d9-4938-878b-7113cd628159", "type": "port" } } } }, { "id": "043bbbbe-c26f-44fb-a21c-4ee80c8fe319", "type": "vessel", "attributes": { "name": "ESSEN EXPRESS", "imo": "9501370", "mmsi": "218474000", "latitude": -78.30435842851921, "longitude": 25.471353799804547, "nautical_speed_knots": 13, "navigational_heading_degrees": 99, "position_timestamp": "2023-06-05T19:46:18Z" } }, { "id": "d637c864-ef35-4df1-9563-ba8bbb179af5", "type": "transport_event", "attributes": { "event": "container.transport.vessel_discharged", "created_at": "2022-10-21T20:14:17Z", "voyage_number": "042E", "timestamp": "2022-10-21T15:19:00Z", "data_source": "shipping_line", "location_locode": "USNYC", "timezone": "America/New_York" }, "relationships": { "shipment": { "data": { "id": "56c8b2c7-a5e0-4c51-9793-f7dadc1fa52c", "type": "shipment" } }, "container": { "data": { "id": "7e9be7ac-2845-4130-b032-fb447d3c3126", "type": "container" } }, "vessel": { "data": { "id": "043bbbbe-c26f-44fb-a21c-4ee80c8fe319", "type": "vessel" } }, "location": { "data": { "id": "27f76ae6-d6d9-4938-878b-7113cd628159", "type": "port" } }, "terminal": { "data": { "id": "ad02bb7c-a8a3-4ec5-bbb9-220f3dbf797d", "type": "terminal" } } } } ] } ``` #### container.transport.available Container is available for pickup at the destination. ```json expandable theme={null} { "data": { "id": "fb111726-d489-4ef7-bac9-d39f2cbe66ba", "type": "webhook_notification", "attributes": { "id": "fb111726-d489-4ef7-bac9-d39f2cbe66ba", "event": "container.transport.available", "delivery_status": "succeeded", "created_at": "2025-02-26T12:51:52Z" }, "relationships": { "reference_object": { "data": { "id": "7adced6b-0ae4-4554-8c51-f85e58e57eb7", "type": "transport_event" } }, "webhook": { "data": { "id": "91357e6c-43f9-49c2-b052-a2941a003751", "type": "webhook" } }, "webhook_notification_logs": { "data": [] } } }, "links": { "self": "https://api.terminal49.com/v2/webhook_notifications/fb111726-d489-4ef7-bac9-d39f2cbe66ba" } } ``` #### container.transport.not\_available Container is no longer available for pickup at the destination. ```json expandable theme={null} { "data": { "id": "ed98a9fe-e704-4f8e-9211-0e2a319d30a0", "type": "webhook_notification", "attributes": { "id": "ed98a9fe-e704-4f8e-9211-0e2a319d30a0", "event": "container.transport.not_available", "delivery_status": "succeeded", "created_at": "2025-02-26T13:10:52Z" }, "relationships": { "reference_object": { "data": { "id": "a449fc53-5fe0-4275-bc17-51780877df5a", "type": "transport_event" } }, "webhook": { "data": { "id": "91357e6c-43f9-49c2-b052-a2941a003751", "type": "webhook" } }, "webhook_notification_logs": { "data": [] } } }, "links": { "self": "https://api.terminal49.com/v2/webhook_notifications/examples?event=container.transport.not_available" } } ``` #### container.transport.full\_out Container picked up (gated out) at the port of discharge. ```json expandable theme={null} { "data": { "id": "bef3aef4-6e81-4824-8bf6-44e1cffa41a7", "type": "webhook_notification", "attributes": { "id": "bef3aef4-6e81-4824-8bf6-44e1cffa41a7", "event": "container.transport.full_out", "delivery_status": "succeeded", "created_at": "2022-10-21T20:19:06Z" }, "relationships": { "reference_object": { "data": { "id": "65f4a065-a9f3-4f2e-b060-0e3d857ed67f", "type": "transport_event" } }, "webhook": { "data": { "id": "715b8e22-2671-45a8-972c-76784feca537", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "e0afd8d9-a942-480b-8902-03aec602808d", "type": "shipment", "attributes": { "created_at": "2022-09-12T02:12:39Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "MAEUGAP001939", "normalized_number": "GAP001939", "shipping_line_scac": "MAEU", "shipping_line_name": "Maersk", "shipping_line_short_name": "Maersk", "customer_name": "Shields, Pollich and Stoltenberg", "port_of_lading_locode": "CNYTN", "port_of_lading_name": "Yantian", "port_of_discharge_locode": "USSAV", "port_of_discharge_name": "Savannah", "pod_vessel_name": "GLEN CANYON", "pod_vessel_imo": "9302097", "pod_voyage_number": "003E", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": "2022-09-10T01:08:00Z", "pol_timezone": "Asia/Shanghai", "pod_eta_at": "2022-10-19T10:00:00Z", "pod_original_eta_at": "2022-10-18T10:00:00Z", "pod_ata_at": "2022-10-19T10:00:00Z", "pod_timezone": "America/New_York", "line_tracking_last_attempted_at": "2022-10-21T20:19:02Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": "2022-10-21T20:19:06Z", "line_tracking_stopped_reason": "all_containers_terminated" }, "relationships": { "port_of_lading": { "data": { "id": "cdf4a74f-5c13-48f0-92e7-4a7704d2030f", "type": "port" } }, "port_of_discharge": { "data": { "id": "f15826a5-d826-4845-8aaa-9f295b36397b", "type": "port" } }, "pod_terminal": { "data": { "id": "db19e898-22b3-44a9-ba61-3a4dbf4018e6", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "2ca3f310-4d0c-4b4f-8dcd-8b8d19f60fb8", "type": "container" } ] } }, "links": { "self": "/v2/shipments/4a97efa3-2383-41b8-87f3-3ae1fe81d429" } }, { "id": "2ca3f310-4d0c-4b4f-8dcd-8b8d19f60fb8", "type": "container", "attributes": { "number": "MSKU8532556", "seal_number": null, "created_at": "2022-09-12T02:12:39Z", "ref_numbers": [ ], "pod_arrived_at": "2022-10-19T10:00:00Z", "pod_discharged_at": "2022-10-20T06:01:00Z", "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "dry", "equipment_length": 40, "equipment_height": "standard", "weight_in_lbs": null, "pod_full_out_at": "2022-10-21T15:05:00Z", "empty_terminated_at": null, "terminal_checked_at": "2022-10-21T03:31:42Z", "fees_at_pod_terminal": [ ], "pickup_lfd": null, "pickup_appointment_at": null, "pod_full_out_chassis_number": null, "location_at_pod_terminal": "Yard", "pod_last_tracking_request_at": "2022-10-21T03:31:29Z", "shipment_last_tracking_request_at": null, "availability_known": true, "pod_timezone": "America/New_York", "final_destination_timezone": null, "empty_terminated_timezone": "America/New_York" }, "relationships": { "shipment": { "data": { "id": "e0afd8d9-a942-480b-8902-03aec602808d", "type": "shipment" } }, "pod_terminal": { "data": { "id": "db19e898-22b3-44a9-ba61-3a4dbf4018e6", "type": "terminal" } }, "transport_events": { "data": [ { "id": "b9c685dc-0556-4b68-a9d5-f4747fcb9611", "type": "transport_event" }, { "id": "cab0d40f-ce6f-4170-bf08-d573a484944e", "type": "transport_event" }, { "id": "aa4ef47b-77b2-42c7-b81f-3dc2b1ead4a8", "type": "transport_event" }, { "id": "5f75e4b6-f139-4314-930c-e79efdd7b254", "type": "transport_event" }, { "id": "693b3062-6520-4390-a827-2dd390dbbc44", "type": "transport_event" }, { "id": "41270e3a-ee02-41c1-9135-3fb174bac54b", "type": "transport_event" }, { "id": "65f4a065-a9f3-4f2e-b060-0e3d857ed67f", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "a74dbeaf-8a2f-4409-8580-d049f131c7ae", "type": "raw_event" }, { "id": "0b500fb5-1613-4066-8b42-33d9d07e021f", "type": "raw_event" }, { "id": "3072849e-3031-427d-9e96-aa5ec6b8ca4f", "type": "raw_event" }, { "id": "c6e6023b-1b50-4ea5-aaf1-b6b9f1c184ef", "type": "raw_event" }, { "id": "7bccb807-404c-48c9-93df-3f3dcc754eb2", "type": "raw_event" }, { "id": "8226d324-42b0-480e-bc15-0d34bc73fde5", "type": "raw_event" }, { "id": "fdf27063-1a8c-48e7-afae-dc64d48346fb", "type": "raw_event" } ] } } }, { "id": "f15826a5-d826-4845-8aaa-9f295b36397b", "type": "port", "attributes": { "id": "f15826a5-d826-4845-8aaa-9f295b36397b", "name": "Savannah", "code": "USSAV", "state_abbr": "GA", "city": "Savannah", "country_code": "US", "latitude": "32.128923976", "longitude": "-81.140998396", "time_zone": "America/New_York" } }, { "id": "db19e898-22b3-44a9-ba61-3a4dbf4018e6", "type": "terminal", "attributes": { "id": "db19e898-22b3-44a9-ba61-3a4dbf4018e6", "nickname": "GCT", "name": "Garden City Terminals", "firms_code": "L737", "smdg_code": null, "bic_facility_code": null, "provided_data": { "pickup_lfd": false, "pickup_lfd_notes": "", "available_for_pickup": false, "fees_at_pod_terminal": false, "holds_at_pod_terminal": false, "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": false, "location_at_pod_terminal_notes": "", "pod_full_out_chassis_number_notes": "" }, "street": "701 New Dock Street Berths 212-225", "city": "Terminal Island", "state": "California", "state_abbr": "CA", "zip": "90731", "country": "United States" }, "relationships": { "port": { "data": { "id": "f15826a5-d826-4845-8aaa-9f295b36397b", "type": "port" } } } }, { "id": "65f4a065-a9f3-4f2e-b060-0e3d857ed67f", "type": "transport_event", "attributes": { "event": "container.transport.full_out", "created_at": "2022-10-21T20:19:06Z", "voyage_number": null, "timestamp": "2022-10-21T15:05:00Z", "data_source": "shipping_line", "location_locode": "USSAV", "timezone": "America/New_York" }, "relationships": { "shipment": { "data": { "id": "e0afd8d9-a942-480b-8902-03aec602808d", "type": "shipment" } }, "container": { "data": { "id": "2ca3f310-4d0c-4b4f-8dcd-8b8d19f60fb8", "type": "container" } }, "vessel": { "data": null }, "location": { "data": { "id": "f15826a5-d826-4845-8aaa-9f295b36397b", "type": "port" } }, "terminal": { "data": { "id": "db19e898-22b3-44a9-ba61-3a4dbf4018e6", "type": "terminal" } } } } ] } ``` #### container.transport.delivered Container was manually marked as delivered. ```json expandable theme={null} { "data": { "id": "a548e0d3-bcc6-44bf-8f9a-b97e545fb98d", "type": "webhook_notification", "attributes": { "id": "a548e0d3-bcc6-44bf-8f9a-b97e545fb98d", "event": "container.transport.delivered", "delivery_status": "succeeded", "created_at": "2025-02-26T14:20:52Z" }, "relationships": { "reference_object": { "data": { "id": "ba2a76ce-d026-45b6-9b63-54c0d1714688", "type": "transport_event" } }, "webhook": { "data": { "id": "91357e6c-43f9-49c2-b052-a2941a003751", "type": "webhook" } }, "webhook_notification_logs": { "data": [] } } }, "links": { "self": "https://api.terminal49.com/v2/webhook_notifications/examples?event=container.transport.delivered" } } ``` #### container.transport.empty\_in Empty container returned at the destination. ```json expandable theme={null} { "data": { "id": "7e4e8acf-de36-401d-b3b9-55a5b16adbde", "type": "webhook_notification", "attributes": { "id": "7e4e8acf-de36-401d-b3b9-55a5b16adbde", "event": "container.transport.empty_in", "delivery_status": "succeeded", "created_at": "2022-10-21T20:18:58Z" }, "relationships": { "reference_object": { "data": { "id": "b9936ca0-7e63-48db-8cad-e7d55d756530", "type": "transport_event" } }, "webhook": { "data": { "id": "b485aa7f-042b-49f8-8d81-31fa2c3c79eb", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "f7837cfa-2dc9-4f29-8562-1d1c8882eccd", "type": "shipment", "attributes": { "created_at": "2022-09-23T16:35:47Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "LQ692823", "normalized_number": "MEDULQ692823", "shipping_line_scac": "MSCU", "shipping_line_name": "Mediterranean Shipping Company", "shipping_line_short_name": "MSC", "customer_name": "Zulauf and Sons", "port_of_lading_locode": "ITNAP", "port_of_lading_name": "Naples", "port_of_discharge_locode": "USNYC", "port_of_discharge_name": "New York / New Jersey", "pod_vessel_name": "MSC TIANJIN", "pod_vessel_imo": "9285471", "pod_voyage_number": "237W", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": "2022-09-23T06:30:00Z", "pol_timezone": "Europe/Rome", "pod_eta_at": "2022-10-14T04:00:00Z", "pod_original_eta_at": "2022-10-14T04:00:00Z", "pod_ata_at": "2022-10-14T13:54:01Z", "pod_timezone": "America/New_York", "line_tracking_last_attempted_at": "2022-10-21T20:18:48Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "bd523255-d320-489e-8710-1ec48ada8e45", "type": "port" } }, "port_of_discharge": { "data": { "id": "74e47232-22a9-4cd5-aef6-30e21d826261", "type": "port" } }, "pod_terminal": { "data": { "id": "774573f6-beb7-4024-9bc4-a29f1d6eaf90", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "fe7e686c-1e34-4181-9333-9ed09c79b159", "type": "container" }, { "id": "4652f270-ce0c-4d89-89e1-bdd0993eac35", "type": "container" }, { "id": "8d460f4d-bf05-41fc-9daf-6afa1917a644", "type": "container" } ] } }, "links": { "self": "/v2/shipments/de8fcacc-0aed-4049-b324-aa65c9c2a765" } }, { "id": "8d460f4d-bf05-41fc-9daf-6afa1917a644", "type": "container", "attributes": { "number": "FSCU8883322", "seal_number": null, "created_at": "2022-09-23T16:35:47Z", "ref_numbers": [ ], "pod_arrived_at": "2022-10-14T13:54:01Z", "pod_discharged_at": "2022-10-14T04:00:00Z", "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "dry", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": null, "pod_full_out_at": "2022-10-19T17:52:00Z", "empty_terminated_at": "2022-10-21T04:00:00Z", "terminal_checked_at": "2022-10-19T19:24:05Z", "fees_at_pod_terminal": [ ], "pickup_lfd": "2022-10-20T04:00:00Z", "pickup_appointment_at": "2022-10-19T16:00:00Z", "pod_full_out_chassis_number": "OWNCHASSIS", "location_at_pod_terminal": "COMMUNITY - OUT", "pod_last_tracking_request_at": "2022-10-19T19:24:04Z", "shipment_last_tracking_request_at": null, "availability_known": true, "pod_timezone": "America/New_York", "final_destination_timezone": null, "empty_terminated_timezone": "America/New_York" }, "relationships": { "shipment": { "data": { "id": "f7837cfa-2dc9-4f29-8562-1d1c8882eccd", "type": "shipment" } }, "pod_terminal": { "data": { "id": "774573f6-beb7-4024-9bc4-a29f1d6eaf90", "type": "terminal" } }, "transport_events": { "data": [ { "id": "91646567-40a8-42cf-93e1-6016b1274568", "type": "transport_event" }, { "id": "1313d847-66c0-425a-b797-4796275a8c41", "type": "transport_event" }, { "id": "61b832bf-031c-4296-8ca4-8b8f256a9fe1", "type": "transport_event" }, { "id": "747d7cc1-82b7-4183-bbb7-356b6d5e025d", "type": "transport_event" }, { "id": "0d2b94be-dea6-4d63-97c1-f21b7d5e767b", "type": "transport_event" }, { "id": "f53f143e-9f85-4fa2-a0df-6e25c4bfbc87", "type": "transport_event" }, { "id": "38cb8320-de6a-4385-a626-71d59473d5ff", "type": "transport_event" }, { "id": "b9936ca0-7e63-48db-8cad-e7d55d756530", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "e0e35bdd-fd6d-41cb-b39c-3d4f7c8ce758", "type": "raw_event" }, { "id": "5def6df9-2878-46cb-8648-69b157bd0993", "type": "raw_event" }, { "id": "20decd2c-3e2f-463d-a270-d249fd0bdbdb", "type": "raw_event" }, { "id": "260e6094-dfbb-40c1-ad90-37c1627b778d", "type": "raw_event" }, { "id": "264f4d56-7c1b-4550-830f-c26b1448cce1", "type": "raw_event" }, { "id": "1d20551a-dfe9-4584-baae-d0288f7342e8", "type": "raw_event" }, { "id": "c5baa1c6-0255-444f-afe6-65db202c33fb", "type": "raw_event" }, { "id": "09554878-a1b5-4c0f-972e-8250163a6be4", "type": "raw_event" }, { "id": "59974058-689b-4052-8598-592aa2c999fa", "type": "raw_event" }, { "id": "00081755-7c93-421a-9a0b-5adf01f1051e", "type": "raw_event" } ] } } }, { "id": "74e47232-22a9-4cd5-aef6-30e21d826261", "type": "port", "attributes": { "id": "74e47232-22a9-4cd5-aef6-30e21d826261", "name": "New York / New Jersey", "code": "USNYC", "state_abbr": "NY", "city": "New York", "country_code": "US", "latitude": "40.684996498", "longitude": "-74.151115685", "time_zone": "America/New_York" } }, { "id": "b9936ca0-7e63-48db-8cad-e7d55d756530", "type": "transport_event", "attributes": { "event": "container.transport.empty_in", "created_at": "2022-10-21T20:18:58Z", "voyage_number": null, "timestamp": "2022-10-21T04:00:00Z", "data_source": "shipping_line", "location_locode": "USNYC", "timezone": "America/New_York" }, "relationships": { "shipment": { "data": { "id": "f7837cfa-2dc9-4f29-8562-1d1c8882eccd", "type": "shipment" } }, "container": { "data": { "id": "8d460f4d-bf05-41fc-9daf-6afa1917a644", "type": "container" } }, "vessel": { "data": null }, "location": { "data": { "id": "74e47232-22a9-4cd5-aef6-30e21d826261", "type": "port" } }, "terminal": { "data": null } } } ] } ``` ### Rail (inland moves) #### container.transport.rail\_loaded Container loaded onto a rail car. ```json expandable theme={null} { "data": { "id": "dc507a07-9749-40b5-8481-fa4c539df722", "type": "webhook_notification", "attributes": { "id": "dc507a07-9749-40b5-8481-fa4c539df722", "event": "container.transport.rail_loaded", "delivery_status": "succeeded", "created_at": "2022-10-21T19:29:49Z" }, "relationships": { "reference_object": { "data": { "id": "4c6c85eb-79bc-4f7c-ad66-962a6ab3a506", "type": "transport_event" } }, "webhook": { "data": { "id": "22cd79b4-3d37-4f34-a990-3f378760dd89", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "93921adf-af96-4096-9797-2abea7e95e79", "type": "shipment", "attributes": { "created_at": "2022-10-04T22:00:30Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "6344045750", "normalized_number": "6344045750", "shipping_line_scac": "COSU", "shipping_line_name": "COSCO", "shipping_line_short_name": "COSCO", "customer_name": "Hilll, Boyle and Hagenes", "port_of_lading_locode": "CNSGH", "port_of_lading_name": "Shanghai", "port_of_discharge_locode": "CAVAN", "port_of_discharge_name": "Vancouver", "pod_vessel_name": "APL COLUMBUS", "pod_vessel_imo": "9597525", "pod_voyage_number": "0TN7VS1MA", "destination_locode": "USCHI", "destination_name": "Chicago", "destination_timezone": "America/Chicago", "destination_ata_at": null, "destination_eta_at": "2022-10-30T02:50:00Z", "pol_etd_at": null, "pol_atd_at": "2022-09-23T00:17:00Z", "pol_timezone": "Asia/Shanghai", "pod_eta_at": "2022-10-19T13:00:00Z", "pod_original_eta_at": "2022-10-18T12:00:00Z", "pod_ata_at": "2022-10-19T13:49:00Z", "pod_timezone": "America/Los_Angeles", "line_tracking_last_attempted_at": "2022-10-21T19:29:46Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "6ec66cea-7a35-4522-98cd-52246198502b", "type": "port" } }, "port_of_discharge": { "data": { "id": "a1edd1e9-61b0-429c-a05a-31ac8d9d2b8b", "type": "port" } }, "pod_terminal": { "data": { "id": "c989c2e6-c933-4ca9-98a5-a0418254b218", "type": "terminal" } }, "destination": { "data": { "id": "83b8c829-08d7-4b02-be09-a515b33f4237", "type": "metro_area" } }, "destination_terminal": { "data": { "id": "79fdf215-b32b-4a7b-99c5-1e44df0dcd76", "type": "rail_terminal" } }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "3be28cd9-165f-4af1-827a-902bef0147d0", "type": "container" } ] } }, "links": { "self": "/v2/shipments/5d990a0a-a854-4bf2-acd4-abc96d98da29" } }, { "id": "3be28cd9-165f-4af1-827a-902bef0147d0", "type": "container", "attributes": { "number": "FFAU3144344", "seal_number": "22626037", "created_at": "2022-10-04T22:00:30Z", "ref_numbers": [ ], "pod_arrived_at": "2022-10-19T13:49:00Z", "pod_discharged_at": "2022-10-20T20:25:00Z", "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "dry", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": 35598, "pod_full_out_at": "2022-10-21T18:30:00Z", "empty_terminated_at": null, "terminal_checked_at": "2022-10-21T18:24:07Z", "fees_at_pod_terminal": [ ], "pickup_lfd": null, "pickup_appointment_at": null, "pod_full_out_chassis_number": null, "location_at_pod_terminal": "RAIL", "pod_last_tracking_request_at": "2022-10-21T18:24:07Z", "shipment_last_tracking_request_at": null, "availability_known": true, "pod_timezone": "America/Los_Angeles", "final_destination_timezone": "America/Chicago", "empty_terminated_timezone": "America/Chicago" }, "relationships": { "shipment": { "data": { "id": "93921adf-af96-4096-9797-2abea7e95e79", "type": "shipment" } }, "pod_terminal": { "data": { "id": "c989c2e6-c933-4ca9-98a5-a0418254b218", "type": "terminal" } }, "transport_events": { "data": [ { "id": "462c6724-d4fc-4b3f-ae7e-1bb2f4ed9321", "type": "transport_event" }, { "id": "afa7595d-e6ea-4e99-8af9-a8d6d3269adb", "type": "transport_event" }, { "id": "5e36ae0c-bf1d-4993-8f58-cf2b3efc440a", "type": "transport_event" }, { "id": "6e4b9563-5b52-4d45-b3c6-a5111c14564d", "type": "transport_event" }, { "id": "ad1a0236-d071-41ac-bad4-dacecc245fc1", "type": "transport_event" }, { "id": "6195bd49-f61d-4f7e-9587-0ee03541b1d7", "type": "transport_event" }, { "id": "4353904b-a40b-494a-a13b-ad85141082d5", "type": "transport_event" }, { "id": "4c6c85eb-79bc-4f7c-ad66-962a6ab3a506", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "45f8bc1e-9a29-4505-b534-f6832a62c516", "type": "raw_event" }, { "id": "c72961bd-22b2-4940-bca2-5c17e97faee1", "type": "raw_event" }, { "id": "3b955f74-7f88-4b00-8aa1-e952bb914172", "type": "raw_event" }, { "id": "702ddb53-81d3-426a-b582-ac1f6cc40135", "type": "raw_event" }, { "id": "26b396c1-b220-40d4-af89-9ae4f69c359d", "type": "raw_event" }, { "id": "b1f05846-e074-4bd1-8e7b-e2eee8683f00", "type": "raw_event" } ] } } }, { "id": "a1edd1e9-61b0-429c-a05a-31ac8d9d2b8b", "type": "port", "attributes": { "id": "a1edd1e9-61b0-429c-a05a-31ac8d9d2b8b", "name": "Vancouver", "code": "CAVAN", "state_abbr": "BC", "city": "Vancouver", "country_code": "CA", "latitude": "49.287489751", "longitude": "-123.094867064", "time_zone": "America/Los_Angeles" } }, { "id": "4c6c85eb-79bc-4f7c-ad66-962a6ab3a506", "type": "transport_event", "attributes": { "event": "container.transport.rail_loaded", "created_at": "2022-10-21T19:29:49Z", "voyage_number": null, "timestamp": "2022-10-21T18:11:00Z", "data_source": "shipping_line", "location_locode": "CAVAN", "timezone": "America/Los_Angeles" }, "relationships": { "shipment": { "data": { "id": "93921adf-af96-4096-9797-2abea7e95e79", "type": "shipment" } }, "container": { "data": { "id": "3be28cd9-165f-4af1-827a-902bef0147d0", "type": "container" } }, "vessel": { "data": null }, "location": { "data": { "id": "a1edd1e9-61b0-429c-a05a-31ac8d9d2b8b", "type": "port" } }, "terminal": { "data": null } } } ] } ``` #### container.transport.rail\_departed Rail car departed. ```json expandable theme={null} { "data": { "id": "176364d2-7f63-4382-8fba-da24e3c14057", "type": "webhook_notification", "attributes": { "id": "176364d2-7f63-4382-8fba-da24e3c14057", "event": "container.transport.rail_departed", "delivery_status": "succeeded", "created_at": "2022-10-21T20:15:29Z" }, "relationships": { "reference_object": { "data": { "id": "7ee8aae1-14da-491a-81fc-6c98ed89775f", "type": "transport_event" } }, "webhook": { "data": { "id": "90c5b9c3-366f-49fb-bbf5-df024f1848d4", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "015c921e-ecdf-4491-982e-89152288f3ae", "type": "shipment", "attributes": { "created_at": "2022-09-20T08:00:59Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "2706772870", "normalized_number": "2706772870", "shipping_line_scac": "OOLU", "shipping_line_name": "Orient Overseas Container Line", "shipping_line_short_name": "OOCL", "customer_name": "Brekke Inc", "port_of_lading_locode": "CNYTN", "port_of_lading_name": "Yantian", "port_of_discharge_locode": "USLGB", "port_of_discharge_name": "Long Beach", "pod_vessel_name": "COSCO ENGLAND", "pod_vessel_imo": "9516428", "pod_voyage_number": "054E", "destination_locode": "USEWI", "destination_name": "Elwood", "destination_timezone": "America/Chicago", "destination_ata_at": null, "destination_eta_at": "2022-10-25T00:50:00Z", "pol_etd_at": "2022-09-25T10:00:00Z", "pol_atd_at": "2022-09-25T10:41:00Z", "pol_timezone": "Asia/Shanghai", "pod_eta_at": "2022-10-12T14:00:00Z", "pod_original_eta_at": "2022-10-09T15:00:00Z", "pod_ata_at": "2022-10-12T13:26:00Z", "pod_timezone": "America/Los_Angeles", "line_tracking_last_attempted_at": "2022-10-21T20:15:13Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "fcc475e5-9625-4632-815e-bd84db28ed4e", "type": "port" } }, "port_of_discharge": { "data": { "id": "39425403-9982-47f1-9988-6b73f400b9ba", "type": "port" } }, "pod_terminal": { "data": { "id": "b0765575-ee97-45e4-a2c8-e9a11c969b37", "type": "terminal" } }, "destination": { "data": { "id": "239ca895-c67b-4563-b496-821833d03272", "type": "metro_area" } }, "destination_terminal": { "data": { "id": "e8e91a82-44ac-4690-baed-2f0998c2d303", "type": "rail_terminal" } }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "600785bd-04b3-48be-b2a5-37264ba9fc74", "type": "container" } ] } }, "links": { "self": "/v2/shipments/0372dba4-f153-44e1-a2d0-e6f444658b60" } }, { "id": "600785bd-04b3-48be-b2a5-37264ba9fc74", "type": "container", "attributes": { "number": "OOCU7853330", "seal_number": null, "created_at": "2022-09-20T08:00:59Z", "ref_numbers": [ ], "pod_arrived_at": "2022-10-12T13:26:00Z", "pod_discharged_at": "2022-10-14T02:56:00Z", "final_destination_full_out_at": null, "holds_at_pod_terminal": [ { "status": "hold", "name": "other", "description": "ONDOCK" }, { "status": "hold", "name": "other", "description": "CTF_CONTAINER_HOLD" }, { "status": "hold", "name": "freight", "description": "FREIGHT_BL_HOLD" } ], "available_for_pickup": false, "equipment_type": "dry", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": 45101, "pod_full_out_at": "2022-10-20T20:41:00Z", "empty_terminated_at": null, "terminal_checked_at": "2022-10-21T00:19:37Z", "fees_at_pod_terminal": [ ], "pickup_lfd": null, "pickup_appointment_at": null, "pod_full_out_chassis_number": null, "location_at_pod_terminal": "GROUNDED", "pod_last_tracking_request_at": "2022-10-21T00:19:36Z", "shipment_last_tracking_request_at": null, "availability_known": true, "pod_timezone": "America/Los_Angeles", "final_destination_timezone": "America/Chicago", "empty_terminated_timezone": "America/Chicago" }, "relationships": { "shipment": { "data": { "id": "015c921e-ecdf-4491-982e-89152288f3ae", "type": "shipment" } }, "pod_terminal": { "data": { "id": "b0765575-ee97-45e4-a2c8-e9a11c969b37", "type": "terminal" } }, "transport_events": { "data": [ { "id": "521f72b5-aee1-4857-859d-a03c6a1d79f1", "type": "transport_event" }, { "id": "ea94574e-dab4-4751-90f7-32be994c15a2", "type": "transport_event" }, { "id": "95dd1b3c-04e2-4587-85c3-9458684792e8", "type": "transport_event" }, { "id": "6791ad37-3316-4f27-b3f4-78be3411e791", "type": "transport_event" }, { "id": "b87c94b6-237b-4140-a3a0-56b10f402a2a", "type": "transport_event" }, { "id": "fa4d115d-6cfc-4584-8658-1175bf7cb31e", "type": "transport_event" }, { "id": "278a247c-6561-4b9a-a251-27024ffef12b", "type": "transport_event" }, { "id": "24dca556-e1be-4963-84e0-dec6e4419dd8", "type": "transport_event" }, { "id": "7ee8aae1-14da-491a-81fc-6c98ed89775f", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "f8ce10b7-eed3-46c5-9ee9-91a23dbb70b1", "type": "raw_event" }, { "id": "c84461fe-2920-4d06-8868-4f2609b8a773", "type": "raw_event" }, { "id": "0d387d25-6fc1-4e29-a0df-2cd0b09a46e6", "type": "raw_event" }, { "id": "ac549216-841e-4d24-ab39-605b01ed8843", "type": "raw_event" }, { "id": "e12200c1-a077-460f-bac7-907683198b8b", "type": "raw_event" }, { "id": "3b8d212c-973a-42b6-b773-aa0ce4716e89", "type": "raw_event" }, { "id": "fd40180b-9aff-4d17-b508-4bfa6d205f6d", "type": "raw_event" }, { "id": "797a3eb1-d8cd-4011-ae63-bcd4437ea402", "type": "raw_event" }, { "id": "864089e6-61dd-4533-9d27-dc639bda8eee", "type": "raw_event" }, { "id": "1cbd85c2-177a-4ac1-bc25-dab6970da97c", "type": "raw_event" }, { "id": "cb5e6908-5add-4e99-b386-eb45a4bcf1f4", "type": "raw_event" }, { "id": "8fbe4b5a-dd3c-425e-96fd-d796b016792a", "type": "raw_event" }, { "id": "1dcaaa69-4aa0-4a30-849a-8e1e4b16c885", "type": "raw_event" } ] } } }, { "id": "9b98ee7b-6e2a-4eaa-9d23-2e0b8e911e92", "type": "port", "attributes": { "id": "9b98ee7b-6e2a-4eaa-9d23-2e0b8e911e92", "name": "Los Angeles", "code": "USLAX", "state_abbr": "CA", "city": "Los Angeles", "country_code": "US", "latitude": "33.728193631", "longitude": "-118.255820307", "time_zone": "America/Los_Angeles" } }, { "id": "7ee8aae1-14da-491a-81fc-6c98ed89775f", "type": "transport_event", "attributes": { "event": "container.transport.rail_departed", "created_at": "2022-10-21T20:15:29Z", "voyage_number": null, "timestamp": "2022-10-21T19:03:00Z", "data_source": "shipping_line", "location_locode": "USLAX", "timezone": "America/Los_Angeles" }, "relationships": { "shipment": { "data": { "id": "015c921e-ecdf-4491-982e-89152288f3ae", "type": "shipment" } }, "container": { "data": { "id": "600785bd-04b3-48be-b2a5-37264ba9fc74", "type": "container" } }, "vessel": { "data": null }, "location": { "data": { "id": "9b98ee7b-6e2a-4eaa-9d23-2e0b8e911e92", "type": "port" } }, "terminal": { "data": null } } } ] } ``` #### container.transport.rail\_arrived Rail car arrived. ```json expandable theme={null} { "data": { "id": "83cc76e6-64c9-4a47-ab7a-b1a796016041", "type": "webhook_notification", "attributes": { "id": "83cc76e6-64c9-4a47-ab7a-b1a796016041", "event": "container.transport.rail_arrived", "delivery_status": "pending", "created_at": "2022-10-21T20:18:00Z" }, "relationships": { "reference_object": { "data": { "id": "7120723d-7bbd-43a8-bbd2-69d742ba76ae", "type": "transport_event" } }, "webhook": { "data": { "id": "72e27eda-f3ff-47f4-9ebd-04e4c82e411f", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "a8a97ac7-648b-42fa-9629-ecbd323a2cd6", "type": "shipment", "attributes": { "created_at": "2022-09-28T14:19:08Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "OOLU2706578920", "normalized_number": "2706578920", "shipping_line_scac": "OOLU", "shipping_line_name": "Orient Overseas Container Line", "shipping_line_short_name": "OOCL", "customer_name": "Heller, Hansen and Schumm", "port_of_lading_locode": "TWKHH", "port_of_lading_name": "Kaohsiung", "port_of_discharge_locode": "USLGB", "port_of_discharge_name": "Long Beach", "pod_vessel_name": "COSCO ENGLAND", "pod_vessel_imo": "9516428", "pod_voyage_number": "054E", "destination_locode": "USEWI", "destination_name": "Elwood", "destination_timezone": "America/Chicago", "destination_ata_at": "2022-10-21T17:43:00Z", "destination_eta_at": "2022-10-18T09:36:00Z", "pol_etd_at": null, "pol_atd_at": "2022-09-27T02:05:00Z", "pol_timezone": "Asia/Taipei", "pod_eta_at": "2022-10-12T14:00:00Z", "pod_original_eta_at": "2022-10-10T15:00:00Z", "pod_ata_at": "2022-10-12T13:26:00Z", "pod_timezone": "America/Los_Angeles", "line_tracking_last_attempted_at": "2022-10-21T20:17:48Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "0233b87d-833e-45bb-ae74-83fd69200d81", "type": "port" } }, "port_of_discharge": { "data": { "id": "f8361d18-09c2-4aff-a933-ca2c22919532", "type": "port" } }, "pod_terminal": { "data": { "id": "d9bf35cd-3bb6-4235-96ca-b57665173c11", "type": "terminal" } }, "destination": { "data": { "id": "ad3c66a7-2580-4757-90f4-5f28c2892468", "type": "metro_area" } }, "destination_terminal": { "data": { "id": "cac87536-25db-4d4f-bb15-3a674d67d31f", "type": "rail_terminal" } }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "e25ff651-f58f-4165-8e97-aa2ad73027be", "type": "container" } ] } }, "links": { "self": "/v2/shipments/7ffaff8a-8004-4c02-8acf-3d744708e0b4" } }, { "id": "e25ff651-f58f-4165-8e97-aa2ad73027be", "type": "container", "attributes": { "number": "OOLU6213464", "seal_number": null, "created_at": "2022-09-28T14:19:08Z", "ref_numbers": [ ], "pod_arrived_at": "2022-10-12T13:26:00Z", "pod_discharged_at": "2022-10-12T22:27:00Z", "final_destination_full_out_at": null, "holds_at_pod_terminal": [ { "status": "hold", "name": "other", "description": "ONDOCK" }, { "status": "hold", "name": "other", "description": "CTF_CONTAINER_HOLD" }, { "status": "hold", "name": "freight", "description": "FREIGHT_BL_HOLD" } ], "available_for_pickup": false, "equipment_type": "reefer", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": 56879, "pod_full_out_at": "2022-10-14T16:21:00Z", "empty_terminated_at": null, "terminal_checked_at": "2022-10-14T20:50:59Z", "fees_at_pod_terminal": [ ], "pickup_lfd": null, "pickup_appointment_at": null, "pod_full_out_chassis_number": null, "location_at_pod_terminal": "GROUNDED", "pod_last_tracking_request_at": "2022-10-14T20:50:59Z", "shipment_last_tracking_request_at": null, "availability_known": true, "pod_timezone": "America/Los_Angeles", "final_destination_timezone": "America/Chicago", "empty_terminated_timezone": "America/Chicago" }, "relationships": { "shipment": { "data": { "id": "a8a97ac7-648b-42fa-9629-ecbd323a2cd6", "type": "shipment" } }, "pod_terminal": { "data": { "id": "d9bf35cd-3bb6-4235-96ca-b57665173c11", "type": "terminal" } }, "transport_events": { "data": [ { "id": "78941fc1-91e0-440a-84d2-b87106a854ba", "type": "transport_event" }, { "id": "60898751-f616-45b1-942e-a65aa7c9ee96", "type": "transport_event" }, { "id": "7bc2b53a-7638-47d8-9820-0cfbf1ef5300", "type": "transport_event" }, { "id": "8cb669fb-9dde-4f2c-b35e-47c460f3e650", "type": "transport_event" }, { "id": "f33ccf00-c8e8-4712-a323-2f43245e5fe6", "type": "transport_event" }, { "id": "20a936fb-b0e2-49ff-a2a7-7ea3c3ae91a6", "type": "transport_event" }, { "id": "f5392045-0e24-46cb-8984-72642844d373", "type": "transport_event" }, { "id": "a3bf7950-1cf6-43fb-859d-ef35610b7d26", "type": "transport_event" }, { "id": "cb8f07dd-8be4-4743-95d9-704da3d788de", "type": "transport_event" }, { "id": "02f73f8c-26ad-48e3-b4a4-e127ded9dfc6", "type": "transport_event" }, { "id": "ce57263a-d817-46fd-9f26-b9290d555b47", "type": "transport_event" }, { "id": "d5274b77-2829-4b03-b466-d6bb3685811a", "type": "transport_event" }, { "id": "8bb29b9a-f49a-42a3-9381-7bd9fe1245b9", "type": "transport_event" }, { "id": "7120723d-7bbd-43a8-bbd2-69d742ba76ae", "type": "transport_event" }, { "id": "7dc68b35-48f4-4941-b7a4-3b6c5ee63871", "type": "transport_event" }, { "id": "7233a3af-1d0b-4725-a15c-1e303f5b5e49", "type": "transport_event" }, { "id": "57687dd8-b5f1-4fd8-bf77-e9e801991084", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "a0fd66f6-6787-45c4-8b98-b1534f8b0598", "type": "raw_event" }, { "id": "5caa8987-0847-4453-a9fd-df68d963785f", "type": "raw_event" }, { "id": "b5efac1c-c773-46c7-ae41-cc7e7bc3059c", "type": "raw_event" }, { "id": "35074992-a17b-4a85-97f2-9ffe9620bc5f", "type": "raw_event" }, { "id": "209702ed-0fad-43d9-a092-20b5a46b5ad6", "type": "raw_event" }, { "id": "9fe50c8e-5584-4491-a8d6-c30ef8cffa11", "type": "raw_event" }, { "id": "cd36808d-3dc7-486f-b0a8-082b879f115c", "type": "raw_event" }, { "id": "a7c6ead4-e104-4b51-aa28-fc4b7cc95494", "type": "raw_event" }, { "id": "61c4afd6-d5f5-4eec-b8fd-e3fd2bcab381", "type": "raw_event" }, { "id": "fa6d887d-77ed-49b3-8ce4-4b7856d55a70", "type": "raw_event" }, { "id": "59f40a9b-cf7a-494e-a251-d47ae1bc28c0", "type": "raw_event" }, { "id": "0eb7e500-94cc-4a50-ac16-6d207edd2a48", "type": "raw_event" }, { "id": "c71761fc-2330-455e-b4b2-20b5b4e35800", "type": "raw_event" }, { "id": "e1e33733-23f3-44a7-a683-c79f7e1c36a6", "type": "raw_event" }, { "id": "6ed36430-f0a6-49eb-96dc-b67bebd58ffa", "type": "raw_event" }, { "id": "d0c88cb3-ca75-4216-bfb6-a3305156bb29", "type": "raw_event" }, { "id": "c3d046cc-56ae-487a-9ac3-5d3b00edb5a4", "type": "raw_event" }, { "id": "ec3f15e1-b342-454f-8d85-8d7495ca3767", "type": "raw_event" }, { "id": "311a1b33-2341-4342-b5e7-6b42bdb4d5d1", "type": "raw_event" }, { "id": "c6a819c6-97db-4894-ab70-6be34f1cf2a3", "type": "raw_event" }, { "id": "1a57d88a-789a-44ad-a1ec-f97cf8561090", "type": "raw_event" }, { "id": "36d04b27-9f8d-493c-8df6-d435f0f50293", "type": "raw_event" }, { "id": "7af8c6df-45a6-4fb4-8bd6-cda6726a428a", "type": "raw_event" }, { "id": "ad3aa77e-4d12-4772-bf01-ca0ca98d91ed", "type": "raw_event" }, { "id": "e86c02d2-577d-49f8-9c86-5632b8ab0fe7", "type": "raw_event" }, { "id": "f159db75-2e5e-45e9-bffc-3879bca1cc5c", "type": "raw_event" }, { "id": "f605cfd9-a956-4051-b209-b83b83c459c7", "type": "raw_event" }, { "id": "e12a406b-a3f6-40e3-a771-31e7596f94b8", "type": "raw_event" } ] } } }, { "id": "ad3c66a7-2580-4757-90f4-5f28c2892468", "type": "metro_area", "attributes": { "id": "ad3c66a7-2580-4757-90f4-5f28c2892468", "name": "Elwood", "state_abbr": "IL", "code": "USEWI", "latitude": "41.4039201", "longitude": "-88.1117242", "country_code": "US", "time_zone": "America/Chicago" } }, { "id": "cac87536-25db-4d4f-bb15-3a674d67d31f", "type": "rail_terminal", "attributes": { "id": "cac87536-25db-4d4f-bb15-3a674d67d31f", "nickname": "BNSF", "name": "BNSF - Logistics Park Chicago (LPC) Intermodal Facility", "city": "Elwood", "firms_code": "H572" }, "relationships": { "metro_area": { "data": { "id": "ad3c66a7-2580-4757-90f4-5f28c2892468", "type": "metro_area" } }, "port": { "data": null } } }, { "id": "7120723d-7bbd-43a8-bbd2-69d742ba76ae", "type": "transport_event", "attributes": { "event": "container.transport.rail_arrived", "created_at": "2022-10-21T20:18:00Z", "voyage_number": null, "timestamp": "2022-10-21T17:43:00Z", "data_source": "shipping_line", "location_locode": "USEWI", "timezone": "America/Chicago" }, "relationships": { "shipment": { "data": { "id": "a8a97ac7-648b-42fa-9629-ecbd323a2cd6", "type": "shipment" } }, "container": { "data": { "id": "e25ff651-f58f-4165-8e97-aa2ad73027be", "type": "container" } }, "vessel": { "data": null }, "location": { "data": { "id": "ad3c66a7-2580-4757-90f4-5f28c2892468", "type": "metro_area" } }, "terminal": { "data": { "id": "cac87536-25db-4d4f-bb15-3a674d67d31f", "type": "rail_terminal" } } } } ] } ``` #### container.transport.rail\_unloaded Container unloaded from the rail car. ```json expandable theme={null} { "data": { "id": "c64aa704-adad-4a45-a2f1-0173afedc598", "type": "webhook_notification", "attributes": { "id": "c64aa704-adad-4a45-a2f1-0173afedc598", "event": "container.transport.rail_unloaded", "delivery_status": "pending", "created_at": "2022-10-21T20:18:00Z" }, "relationships": { "reference_object": { "data": { "id": "42a1cd6e-9891-401c-aa28-7e2dfcdf5895", "type": "transport_event" } }, "webhook": { "data": { "id": "99cd87c5-5bb2-49ba-96c8-a29f12159d82", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "0ee340ba-0f60-49cb-8ae1-551897887a52", "type": "shipment", "attributes": { "created_at": "2022-09-28T14:19:08Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "OOLU2706578920", "normalized_number": "2706578920", "shipping_line_scac": "OOLU", "shipping_line_name": "Orient Overseas Container Line", "shipping_line_short_name": "OOCL", "customer_name": "Schimmel-Beatty", "port_of_lading_locode": "TWKHH", "port_of_lading_name": "Kaohsiung", "port_of_discharge_locode": "USLGB", "port_of_discharge_name": "Long Beach", "pod_vessel_name": "COSCO ENGLAND", "pod_vessel_imo": "9516428", "pod_voyage_number": "054E", "destination_locode": "USEWI", "destination_name": "Elwood", "destination_timezone": "America/Chicago", "destination_ata_at": "2022-10-21T17:43:00Z", "destination_eta_at": "2022-10-18T09:36:00Z", "pol_etd_at": null, "pol_atd_at": "2022-09-27T02:05:00Z", "pol_timezone": "Asia/Taipei", "pod_eta_at": "2022-10-12T14:00:00Z", "pod_original_eta_at": "2022-10-10T15:00:00Z", "pod_ata_at": "2022-10-12T13:26:00Z", "pod_timezone": "America/Los_Angeles", "line_tracking_last_attempted_at": "2022-10-21T20:17:48Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "dbe43734-a6c0-40da-9dd8-955e3a536bcf", "type": "port" } }, "port_of_discharge": { "data": { "id": "52f2038f-aecd-4329-b368-a339ae81e4b7", "type": "port" } }, "pod_terminal": { "data": { "id": "6a1aa48d-1493-48ff-91c9-48ef6997e63b", "type": "terminal" } }, "destination": { "data": { "id": "43b40e7b-e339-450e-8581-19e49cb32a61", "type": "metro_area" } }, "destination_terminal": { "data": { "id": "9d04a5cb-083b-4a76-86a8-e37fc3dd65b3", "type": "rail_terminal" } }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "7309bbe8-ec63-4740-979a-e4dc3047778a", "type": "container" } ] } }, "links": { "self": "/v2/shipments/7ffaff8a-8004-4c02-8acf-3d744708e0b4" } }, { "id": "7309bbe8-ec63-4740-979a-e4dc3047778a", "type": "container", "attributes": { "number": "OOLU6213464", "seal_number": null, "created_at": "2022-09-28T14:19:08Z", "ref_numbers": [ ], "pod_arrived_at": "2022-10-12T13:26:00Z", "pod_discharged_at": "2022-10-12T22:27:00Z", "final_destination_full_out_at": null, "holds_at_pod_terminal": [ { "status": "hold", "name": "other", "description": "ONDOCK" }, { "status": "hold", "name": "other", "description": "CTF_CONTAINER_HOLD" }, { "status": "hold", "name": "freight", "description": "FREIGHT_BL_HOLD" } ], "available_for_pickup": false, "equipment_type": "reefer", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": 56879, "pod_full_out_at": "2022-10-14T16:21:00Z", "empty_terminated_at": null, "terminal_checked_at": "2022-10-14T20:50:59Z", "fees_at_pod_terminal": [ ], "pickup_lfd": null, "pickup_appointment_at": null, "pod_full_out_chassis_number": null, "location_at_pod_terminal": "GROUNDED", "pod_last_tracking_request_at": "2022-10-14T20:50:59Z", "shipment_last_tracking_request_at": null, "availability_known": true, "pod_timezone": "America/Los_Angeles", "final_destination_timezone": "America/Chicago", "empty_terminated_timezone": "America/Chicago" }, "relationships": { "shipment": { "data": { "id": "0ee340ba-0f60-49cb-8ae1-551897887a52", "type": "shipment" } }, "pod_terminal": { "data": { "id": "6a1aa48d-1493-48ff-91c9-48ef6997e63b", "type": "terminal" } }, "transport_events": { "data": [ { "id": "9ae17caa-9c9c-47a2-8444-949b25a4edc5", "type": "transport_event" }, { "id": "3313e5fd-f7f1-4bab-b1ca-4eefe26fa533", "type": "transport_event" }, { "id": "bd5a09a1-f77f-4b8a-9f0e-38a2ad30a26c", "type": "transport_event" }, { "id": "969da0e3-eb68-4da2-96e8-3765fc22a03f", "type": "transport_event" }, { "id": "707b092c-7662-4dc1-93c8-e8893c797da2", "type": "transport_event" }, { "id": "0de0c1d0-9502-4474-ad6f-acd138f7775d", "type": "transport_event" }, { "id": "09d00e28-8e4a-4362-aade-63ed7cb70968", "type": "transport_event" }, { "id": "b23ff291-e087-45e0-8dbf-141062a47d20", "type": "transport_event" }, { "id": "095aa809-3e1e-424d-9cd7-92d49a8f0f43", "type": "transport_event" }, { "id": "ba55a78a-7391-46bd-ad85-558b9c1fcbb5", "type": "transport_event" }, { "id": "6ca9d38f-1a8f-411a-87d8-af3f174999a1", "type": "transport_event" }, { "id": "3cf739f9-198a-409e-a9a8-89724eeb4821", "type": "transport_event" }, { "id": "18294c8a-a619-4416-a112-83dfde0e757c", "type": "transport_event" }, { "id": "d56f9846-95b5-4acb-87de-3c93fda143f1", "type": "transport_event" }, { "id": "42a1cd6e-9891-401c-aa28-7e2dfcdf5895", "type": "transport_event" }, { "id": "dd9edd7b-1798-4663-88bc-a5ce07700534", "type": "transport_event" }, { "id": "66643a7e-3e34-4693-8a6f-1d85126f93d1", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "4853f3bf-2660-430e-89d0-f1516dcd48f7", "type": "raw_event" }, { "id": "a7ade087-4e66-4555-bd3e-140dfb08785a", "type": "raw_event" }, { "id": "bdd42309-84db-4337-a80a-2ff3e417e95e", "type": "raw_event" }, { "id": "8e6203e7-f431-4cfa-af04-5a7a5018e745", "type": "raw_event" }, { "id": "a42b14f8-5a08-41ff-a864-8f92bbad4bb0", "type": "raw_event" }, { "id": "e92b5727-669e-4490-bab8-66d6edbd1f31", "type": "raw_event" }, { "id": "a98b1ae3-26be-4585-b8f2-c439c726b723", "type": "raw_event" }, { "id": "6d4cb7b5-32d4-4051-937b-5e5ad1dbbbad", "type": "raw_event" }, { "id": "489961d2-2791-45e8-baf3-d975a1dc0a01", "type": "raw_event" }, { "id": "16e55312-dd6d-4def-b4c2-39fb0eeaa3ac", "type": "raw_event" }, { "id": "596312ac-becf-4ed4-bc69-074d889385dc", "type": "raw_event" }, { "id": "1e314cb2-b9e7-4be5-a63a-d185588c3b41", "type": "raw_event" }, { "id": "150fc217-0bd5-427e-b09c-b123ec97ce2c", "type": "raw_event" }, { "id": "7feacaf8-62b8-47f4-abb4-d5ee8fdda43b", "type": "raw_event" }, { "id": "9435c083-5af6-47f3-90cf-8a554f995e10", "type": "raw_event" }, { "id": "2f451b22-9770-456f-b7fc-b050e42e37ce", "type": "raw_event" }, { "id": "c34c12c2-6bc3-44eb-a321-56b0a07233e4", "type": "raw_event" }, { "id": "ffa3d8b7-153d-4e2d-95c8-608cebfa8e6e", "type": "raw_event" }, { "id": "ae0f633f-c7f7-426b-8f9e-6112efe54b54", "type": "raw_event" }, { "id": "a7adde60-cf9e-41b9-a372-0410a3cfb437", "type": "raw_event" }, { "id": "67976eb0-e900-4c67-8dc5-327651625db4", "type": "raw_event" }, { "id": "84658720-b52f-43c6-8d8a-02c3c848b67f", "type": "raw_event" }, { "id": "a376f1f4-016b-40ba-83b6-514cd27a7c4b", "type": "raw_event" }, { "id": "7995ed60-cc48-44f2-b733-46094ca9e1d4", "type": "raw_event" }, { "id": "e1b66db7-d5a4-4b6b-8386-b4fc3dd56154", "type": "raw_event" }, { "id": "8950c6cf-6150-43cd-bf80-64bf776493de", "type": "raw_event" }, { "id": "aec9df81-c580-4c60-b1f8-15ff2cc2a293", "type": "raw_event" }, { "id": "25135e6c-3417-4de3-a0ba-9ca30aa768ed", "type": "raw_event" } ] } } }, { "id": "43b40e7b-e339-450e-8581-19e49cb32a61", "type": "metro_area", "attributes": { "id": "43b40e7b-e339-450e-8581-19e49cb32a61", "name": "Elwood", "state_abbr": "IL", "code": "USEWI", "latitude": "41.4039201", "longitude": "-88.1117242", "country_code": "US", "time_zone": "America/Chicago" } }, { "id": "9d04a5cb-083b-4a76-86a8-e37fc3dd65b3", "type": "rail_terminal", "attributes": { "id": "9d04a5cb-083b-4a76-86a8-e37fc3dd65b3", "nickname": "BNSF", "name": "BNSF - Logistics Park Chicago (LPC) Intermodal Facility", "city": "Elwood", "firms_code": "H572" }, "relationships": { "metro_area": { "data": { "id": "43b40e7b-e339-450e-8581-19e49cb32a61", "type": "metro_area" } }, "port": { "data": null } } }, { "id": "42a1cd6e-9891-401c-aa28-7e2dfcdf5895", "type": "transport_event", "attributes": { "event": "container.transport.rail_unloaded", "created_at": "2022-10-21T20:18:00Z", "voyage_number": null, "timestamp": "2022-10-21T18:32:00Z", "data_source": "shipping_line", "location_locode": "USEWI", "timezone": "America/Chicago" }, "relationships": { "shipment": { "data": { "id": "0ee340ba-0f60-49cb-8ae1-551897887a52", "type": "shipment" } }, "container": { "data": { "id": "7309bbe8-ec63-4740-979a-e4dc3047778a", "type": "container" } }, "vessel": { "data": null }, "location": { "data": { "id": "43b40e7b-e339-450e-8581-19e49cb32a61", "type": "metro_area" } }, "terminal": { "data": { "id": "9d04a5cb-083b-4a76-86a8-e37fc3dd65b3", "type": "rail_terminal" } } } } ] } ``` #### container.transport.arrived\_at\_inland\_destination Container arrived at the final inland destination. ```json expandable theme={null} { "data": { "id": "51cc2480-760e-4ce6-af36-a69669292cad", "type": "webhook_notification", "attributes": { "id": "51cc2480-760e-4ce6-af36-a69669292cad", "event": "container.transport.arrived_at_inland_destination", "delivery_status": "pending", "created_at": "2024-06-26T21:21:53Z" }, "relationships": { "reference_object": { "data": { "id": "1a61d6af-64ee-4f45-846d-59e0b8b257d0", "type": "transport_event" } }, "webhook": { "data": { "id": "9fc3b3b6-5551-4b76-b0c7-d9bb1e86ed26", "type": "webhook" } }, "webhook_notification_logs": { "data": [] } } }, "links": { "self": "https://api.terminal49.com/v2/webhook_notifications/examples?event=container.transport.arrived_at_inland_destination" }, "included": [ { "id": "e7a78724-6ddd-48a2-85c9-e88def7a8406", "type": "shipment", "attributes": { "created_at": "2024-06-26T21:21:52Z", "ref_numbers": [ "REF-4DB6E7", "REF-045A0E" ], "tags": [], "bill_of_lading_number": "TE49BB993AAD", "normalized_number": "TE49BB993AAD", "shipping_line_scac": "MSCU", "shipping_line_name": "Mediterranean Shipping Company", "shipping_line_short_name": "MSC", "customer_name": "Rempel-Becker", "port_of_lading_locode": "MXZLO", "port_of_lading_name": "Manzanillo", "port_of_discharge_locode": "USOAK", "port_of_discharge_name": "Port of Oakland", "pod_vessel_name": "MSC CHANNE", "pod_vessel_imo": "9710438", "pod_voyage_number": "098N", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": "2024-06-13T21:21:52Z", "pol_timezone": "America/Mexico_City", "pod_eta_at": "2024-07-03T21:21:52Z", "pod_original_eta_at": "2024-07-03T21:21:52Z", "pod_ata_at": "2024-07-03T22:21:52Z", "pod_timezone": "America/Los_Angeles", "line_tracking_last_attempted_at": "2024-06-26T21:21:52Z", "line_tracking_last_succeeded_at": "2024-06-26T21:21:52Z", "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "links": { "self": "/v2/shipments/e7a78724-6ddd-48a2-85c9-e88def7a8406" }, "relationships": { "port_of_lading": { "data": { "id": "dd85723f-17a6-4b7a-bd98-c6f0d94fe4e6", "type": "port" } }, "port_of_discharge": { "data": { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "type": "port" } }, "pod_terminal": { "data": { "id": "3e550f0e-ac2a-48fb-b242-5be45ecf2c78", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": { "id": "a88159cc-9c76-492f-a145-003352ef8e92", "type": "terminal" } }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "30c56cea-b155-4618-a365-a1d77d5bdac5", "type": "container" } ] } } }, { "id": "30c56cea-b155-4618-a365-a1d77d5bdac5", "type": "container", "attributes": { "number": "OERU4412200", "seal_number": "f6ab033e15ec49fd", "created_at": "2024-06-26T21:21:53Z", "ref_numbers": [ "REF-ED3A41" ], "pod_arrived_at": "2024-06-26T21:21:52Z", "pod_discharged_at": "2024-06-26T21:21:52Z", "final_destination_full_out_at": "2024-06-26T21:21:52Z", "holds_at_pod_terminal": [], "available_for_pickup": false, "equipment_type": "dry", "equipment_length": 40, "equipment_height": "standard", "weight_in_lbs": 53443, "pod_full_out_at": null, "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": "2024-06-26T21:21:52Z", "availability_known": true, "pod_timezone": "America/Los_Angeles", "final_destination_timezone": null, "empty_terminated_timezone": "America/Los_Angeles" }, "relationships": { "shipment": { "data": { "id": "e7a78724-6ddd-48a2-85c9-e88def7a8406", "type": "shipment" } }, "pickup_facility": { "data": { "id": "a88159cc-9c76-492f-a145-003352ef8e92", "type": "terminal" } }, "pod_terminal": { "data": null }, "transport_events": { "data": [ { "id": "1a61d6af-64ee-4f45-846d-59e0b8b257d0", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "807f7867-9f4b-467b-b5be-c297eba40533", "type": "raw_event" }, { "id": "3a03c7a3-8aec-4f33-8603-3050b974cbef", "type": "raw_event" }, { "id": "45df4091-4b77-4526-b7ec-b3619c14f1fd", "type": "raw_event" }, { "id": "6ff26f49-ca58-4a3f-92e1-db0f295b6a5c", "type": "raw_event" }, { "id": "141ad992-befb-4951-8421-bf84dfed1fb8", "type": "raw_event" }, { "id": "1621ff9e-e45d-4151-a9ee-6f242fa61d0d", "type": "raw_event" }, { "id": "06f3f0c3-8ab8-4cf4-b442-0de4e7147dbf", "type": "raw_event" }, { "id": "f6681a35-b760-45f6-a69f-66fa81be5ea8", "type": "raw_event" }, { "id": "423481e2-4e8f-45c0-8707-92742dc7ba60", "type": "raw_event" }, { "id": "09bef6df-0dcd-4a84-99da-038a5f64261b", "type": "raw_event" }, { "id": "c3b334ed-9ade-45f5-bfd1-66d26a28351a", "type": "raw_event" } ] } } }, { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "type": "port", "attributes": { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "name": "Port of Oakland", "code": "USOAK", "state_abbr": "CA", "city": "Oakland", "country_code": "US", "latitude": "37.8044", "longitude": "-122.2712", "time_zone": "America/Los_Angeles" } }, { "id": "3e550f0e-ac2a-48fb-b242-5be45ecf2c78", "type": "terminal", "attributes": { "id": "3e550f0e-ac2a-48fb-b242-5be45ecf2c78", "nickname": "SSA", "name": "SSA Terminal", "firms_code": "Z985", "smdg_code": "B58", "bic_facility_code": "USOAKTYJE", "provided_data": { "pickup_lfd": true, "pod_full_out_at": true, "pickup_lfd_notes": "", "available_for_pickup": true, "fees_at_pod_terminal": true, "holds_at_pod_terminal": true, "pickup_appointment_at": false, "location_at_pod_terminal": true, "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": "880 Koepp Manors", "city": "South Dustin", "state": "West Virginia", "state_abbr": "AR", "zip": "71794", "country": "Lithuania", "facility_type": "ocean_terminal" }, "relationships": { "port": { "data": { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "type": "port" } } } }, { "id": "1a61d6af-64ee-4f45-846d-59e0b8b257d0", "type": "transport_event", "attributes": { "event": "container.transport.arrived_at_inland_destination", "created_at": "2024-06-26T21:21:52Z", "voyage_number": null, "timestamp": "2024-06-26T21:21:52Z", "data_source": "shipping_line", "location_locode": "USOAK", "timezone": "America/Los_Angeles" }, "relationships": { "shipment": { "data": { "id": "e7a78724-6ddd-48a2-85c9-e88def7a8406", "type": "shipment" } }, "container": { "data": { "id": "30c56cea-b155-4618-a365-a1d77d5bdac5", "type": "container" } }, "vessel": { "data": { "id": "daf64780-ecdd-4d46-ae4a-eb70968069ed", "type": "vessel" } }, "location": { "data": { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "type": "port" } }, "terminal": { "data": { "id": "3e550f0e-ac2a-48fb-b242-5be45ecf2c78", "type": "terminal" } } } } ] } ``` ## Estimated events These events fire when an estimated departure or arrival time changes. ### container.transport.estimated.vessel\_departed ETA changed for vessel departure at the port of lading. ```json expandable theme={null} { "data": { "id": "85eb9ac3-8bbb-41c2-9230-b8434c8af5cd", "type": "webhook_notification", "attributes": { "id": "85eb9ac3-8bbb-41c2-9230-b8434c8af5cd", "event": "container.transport.estimated.vessel_departed", "delivery_status": "succeeded", "created_at": "2024-06-26T21:22:21Z" }, "relationships": { "reference_object": { "data": { "id": "3e1f544c-72a4-45d3-9cf0-14c892a63f34", "type": "transport_event" } }, "webhook": { "data": { "id": "188e6629-d9e0-446e-8dd8-078090eba7b3", "type": "webhook" } }, "webhook_notification_logs": { "data": [] } } }, "links": { "self": "https://api.terminal49.com/v2/webhook_notifications/examples?event=container.transport.estimated.vessel_departed" }, "included": [ { "id": "3e1f544c-72a4-45d3-9cf0-14c892a63f34", "type": "transport_event", "attributes": { "event": "container.transport.estimated.vessel_departed", "created_at": "2024-06-26T21:22:21Z", "voyage_number": "098N", "timestamp": "2024-06-28T21:22:21Z", "data_source": "shipping_line", "location_locode": "MXZLO", "timezone": "America/Mexico_City" } } ] } ``` ### shipment.estimated.arrival ETA changed for the port of discharge (shipment level). ```json expandable theme={null} { "data": { "id": "0a6b1c26-25c1-4309-b190-ff7cb50f75e3", "type": "webhook_notification", "attributes": { "id": "0a6b1c26-25c1-4309-b190-ff7cb50f75e3", "event": "shipment.estimated.arrival", "delivery_status": "succeeded", "created_at": "2022-10-21T20:19:13Z" }, "relationships": { "reference_object": { "data": { "id": "0db4d1a2-ec3e-4123-9d80-1431c81733e6", "type": "estimated_event" } }, "webhook": { "data": { "id": "5e58fc0c-0686-4156-96cf-410f673d54cb", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "fd0b571a-d6bc-4059-93ed-fc5b00a83b15", "type": "shipment", "attributes": { "created_at": "2022-09-20T02:55:25Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "OOLU2136060630", "normalized_number": "2136060630", "shipping_line_scac": "OOLU", "shipping_line_name": "Orient Overseas Container Line", "shipping_line_short_name": "OOCL", "customer_name": "Casper, Abshire and Dibbert", "port_of_lading_locode": "MYPEN", "port_of_lading_name": "Penang", "port_of_discharge_locode": "USLAX", "port_of_discharge_name": "Los Angeles", "pod_vessel_name": "CMA CGM NORMA", "pod_vessel_imo": "9299812", "pod_voyage_number": "0TUPFE1MA", "destination_locode": "USEWI", "destination_name": "Elwood", "destination_timezone": "America/Chicago", "destination_ata_at": null, "destination_eta_at": "2022-11-07T17:00:00Z", "pol_etd_at": "2022-09-24T18:30:00Z", "pol_atd_at": "2022-09-24T23:35:00Z", "pol_timezone": "Asia/Kuala_Lumpur", "pod_eta_at": "2022-11-01T14:00:00Z", "pod_original_eta_at": "2022-11-03T01:00:00Z", "pod_ata_at": null, "pod_timezone": "America/Los_Angeles", "line_tracking_last_attempted_at": "2022-10-21T20:18:58Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "7d0a06e4-f894-46ee-96f5-407d7cc7db1b", "type": "port" } }, "port_of_discharge": { "data": { "id": "56adff5b-0ec1-4b81-985f-3380bca38b0b", "type": "port" } }, "pod_terminal": { "data": { "id": "e37931e1-d68e-497f-bd7c-6bb4ebf00520", "type": "terminal" } }, "destination": { "data": { "id": "fc2e0c64-0491-400c-afe8-a5e8dca77c7c", "type": "metro_area" } }, "destination_terminal": { "data": { "id": "ed03e950-a528-4b3d-bf6b-15141e397dc5", "type": "rail_terminal" } }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "300f6e6f-04ba-4f49-b9bd-7302fc382c4d", "type": "container" } ] } }, "links": { "self": "/v2/shipments/4f363cf9-8d55-4709-bf64-77f4bd7ed824" } }, { "id": "56adff5b-0ec1-4b81-985f-3380bca38b0b", "type": "port", "attributes": { "id": "56adff5b-0ec1-4b81-985f-3380bca38b0b", "name": "Los Angeles", "code": "USLAX", "state_abbr": "CA", "city": "Los Angeles", "country_code": "US", "latitude": "33.728193631", "longitude": "-118.255820307", "time_zone": "America/Los_Angeles" } }, { "id": "0db4d1a2-ec3e-4123-9d80-1431c81733e6", "type": "estimated_event", "attributes": { "created_at": "2022-10-21T20:19:13Z", "estimated_timestamp": "2022-11-01T14:00:00Z", "voyage_number": "0TUPFE1MA", "event": "shipment.estimated.arrival", "location_locode": "USLAX", "data_source": "shipping_line", "timezone": "America/Los_Angeles" }, "relationships": { "shipment": { "data": { "id": "fd0b571a-d6bc-4059-93ed-fc5b00a83b15", "type": "shipment" } }, "port": { "data": { "id": "56adff5b-0ec1-4b81-985f-3380bca38b0b", "type": "port" } }, "vessel": { "data": { "id": "3f97ddb7-2e53-4ed1-ae22-04b82b340136", "type": "vessel" } } } } ] } ``` ### container.transport.estimated.vessel\_arrived ETA changed for vessel arrival at the port of discharge (container level). ```json expandable theme={null} { "data": { "id": "1ababdd7-3d93-436d-8fc7-e8029bb4b466", "type": "webhook_notification", "attributes": { "id": "1ababdd7-3d93-436d-8fc7-e8029bb4b466", "event": "container.transport.estimated.vessel_arrived", "delivery_status": "succeeded", "created_at": "2020-05-11T15:09:58Z" }, "relationships": { "reference_object": { "data": { "id": "e400b938-19d9-4d78-888f-351af48a915e", "type": "transport_event" } }, "webhook": { "data": { "id": "a3ea832c-179c-4fd9-891d-5765a77af9d4", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "links": { "self": "https://api.terminal49.com/v2/webhook_notifications/examples?event=container.transport.estimated.vessel_arrived" }, "included": [ { "id": "e400b938-19d9-4d78-888f-351af48a915e", "type": "transport_event", "attributes": { "event": "container.transport.estimated.vessel_arrived", "created_at": "2020-05-11T15:09:58Z", "voyage_number": "0TUPFE1MA", "timestamp": "2020-05-18T14:00:00Z", "data_source": "shipping_line", "location_locode": "USLAX", "timezone": "America/Los_Angeles" } } ] } ``` ### container.transport.estimated.arrived\_at\_inland\_destination ETA changed for the inland destination. ```json expandable theme={null} { "data": { "id": "2ff96102-8016-41d8-a313-1fcbf4cba2cc", "type": "webhook_notification", "attributes": { "id": "2ff96102-8016-41d8-a313-1fcbf4cba2cc", "event": "container.transport.estimated.arrived_at_inland_destination", "delivery_status": "pending", "created_at": "2024-06-26T21:22:22Z" }, "relationships": { "reference_object": { "data": { "id": "ce3376bf-ed14-43ea-b0cd-9ebd5105be7b", "type": "transport_event" } }, "webhook": { "data": { "id": "188e6629-d9e0-446e-8dd8-078090eba7b3", "type": "webhook" } }, "webhook_notification_logs": { "data": [] } } }, "links": { "self": "https://api.terminal49.com/v2/webhook_notifications/examples?event=container.transport.estimated.arrived_at_inland_destination" }, "included": [ { "id": "a8d9896f-a483-4548-9bae-3107e6adca3b", "type": "shipment", "attributes": { "created_at": "2024-06-26T21:22:21Z", "ref_numbers": [ "REF-7B250E" ], "tags": [], "bill_of_lading_number": "TE492FCF3119", "normalized_number": "TE492FCF3119", "shipping_line_scac": "MSCU", "shipping_line_name": "Mediterranean Shipping Company", "shipping_line_short_name": "MSC", "customer_name": "Gerlach, Hettinger and Mitchell", "port_of_lading_locode": "MXZLO", "port_of_lading_name": "Manzanillo", "port_of_discharge_locode": "USOAK", "port_of_discharge_name": "Port of Oakland", "pod_vessel_name": "MSC CHANNE", "pod_vessel_imo": "9710438", "pod_voyage_number": "098N", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": "2024-06-13T21:22:21Z", "pol_timezone": "America/Mexico_City", "pod_eta_at": "2024-07-03T21:22:21Z", "pod_original_eta_at": "2024-07-03T21:22:21Z", "pod_ata_at": "2024-07-03T22:22:21Z", "pod_timezone": "America/Los_Angeles", "line_tracking_last_attempted_at": "2024-06-26T21:22:21Z", "line_tracking_last_succeeded_at": "2024-06-26T21:22:21Z", "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "links": { "self": "/v2/shipments/a8d9896f-a483-4548-9bae-3107e6adca3b" }, "relationships": { "port_of_lading": { "data": { "id": "dd85723f-17a6-4b7a-bd98-c6f0d94fe4e6", "type": "port" } }, "port_of_discharge": { "data": { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "type": "port" } }, "pod_terminal": { "data": { "id": "3e550f0e-ac2a-48fb-b242-5be45ecf2c78", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": { "id": "99a517a6-b7a1-49aa-9012-2d03d7aff720", "type": "terminal" } }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "eb48ec6e-3057-44b6-8aee-db1fbd8705e3", "type": "container" } ] } } }, { "id": "eb48ec6e-3057-44b6-8aee-db1fbd8705e3", "type": "container", "attributes": { "number": "GLDU9577709", "seal_number": "fcbe2b0c3fa3e367", "created_at": "2024-06-26T21:22:22Z", "ref_numbers": [ "REF-2A857F", "REF-ADB003" ], "pod_arrived_at": "2024-06-26T21:22:21Z", "pod_discharged_at": "2024-06-26T21:22:21Z", "final_destination_full_out_at": "2024-06-26T21:22:21Z", "holds_at_pod_terminal": [], "available_for_pickup": false, "equipment_type": "dry", "equipment_length": 40, "equipment_height": "standard", "weight_in_lbs": 54472, "pod_full_out_at": null, "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": "2024-06-26T21:22:21Z", "availability_known": true, "pod_timezone": "America/Los_Angeles", "final_destination_timezone": null, "empty_terminated_timezone": "America/Los_Angeles" }, "relationships": { "shipment": { "data": { "id": "a8d9896f-a483-4548-9bae-3107e6adca3b", "type": "shipment" } }, "pickup_facility": { "data": { "id": "99a517a6-b7a1-49aa-9012-2d03d7aff720", "type": "terminal" } }, "pod_terminal": { "data": null }, "transport_events": { "data": [ { "id": "ce3376bf-ed14-43ea-b0cd-9ebd5105be7b", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "9da5b426-9e08-4d82-913e-fc3e0b1b5b69", "type": "raw_event" }, { "id": "5f8809ef-4d80-4ffa-8e92-faba48a3909e", "type": "raw_event" }, { "id": "13651871-42bb-4c97-b3e2-9cc3b5737da7", "type": "raw_event" }, { "id": "dda7f006-c741-43c4-9ab3-019f391dae13", "type": "raw_event" }, { "id": "c7ec67f1-bfb0-47ca-9515-7f4f1449dffc", "type": "raw_event" }, { "id": "44325279-6fbe-46c8-9138-52732d5128b6", "type": "raw_event" }, { "id": "51539ed3-10c7-4f22-955f-07c27321fbb7", "type": "raw_event" }, { "id": "ea9f5732-404b-4c7d-a98c-ad960bd0632c", "type": "raw_event" }, { "id": "3361e110-9cc3-4023-a162-3301adb342f1", "type": "raw_event" }, { "id": "d6b4cb90-f93d-4e0e-a1c9-1f6593e60e2c", "type": "raw_event" }, { "id": "5404e2a6-945a-4c2f-923c-15efab1aadf6", "type": "raw_event" } ] } } }, { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "type": "port", "attributes": { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "name": "Port of Oakland", "code": "USOAK", "state_abbr": "CA", "city": "Oakland", "country_code": "US", "latitude": "37.8044", "longitude": "-122.2712", "time_zone": "America/Los_Angeles" } }, { "id": "3e550f0e-ac2a-48fb-b242-5be45ecf2c78", "type": "terminal", "attributes": { "id": "3e550f0e-ac2a-48fb-b242-5be45ecf2c78", "nickname": "SSA", "name": "SSA Terminal", "firms_code": "Z985", "smdg_code": "B58", "bic_facility_code": "USOAKTYJE", "provided_data": { "pickup_lfd": true, "pod_full_out_at": true, "pickup_lfd_notes": "", "available_for_pickup": true, "fees_at_pod_terminal": true, "holds_at_pod_terminal": true, "pickup_appointment_at": false, "location_at_pod_terminal": true, "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": "806 Lillia Forks", "city": "South Edison", "state": "Colorado", "state_abbr": "KY", "zip": "47421", "country": "Mauritius", "facility_type": "ocean_terminal" }, "relationships": { "port": { "data": { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "type": "port" } } } }, { "id": "ce3376bf-ed14-43ea-b0cd-9ebd5105be7b", "type": "transport_event", "attributes": { "event": "container.transport.estimated.arrived_at_inland_destination", "created_at": "2024-06-26T21:22:21Z", "voyage_number": null, "timestamp": "2024-06-26T21:22:21Z", "data_source": "shipping_line", "location_locode": "USOAK", "timezone": "America/Los_Angeles" }, "relationships": { "shipment": { "data": { "id": "a8d9896f-a483-4548-9bae-3107e6adca3b", "type": "shipment" } }, "container": { "data": { "id": "eb48ec6e-3057-44b6-8aee-db1fbd8705e3", "type": "container" } }, "vessel": { "data": { "id": "daf64780-ecdd-4d46-ae4a-eb70968069ed", "type": "vessel" } }, "location": { "data": { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "type": "port" } }, "terminal": { "data": { "id": "3e550f0e-ac2a-48fb-b242-5be45ecf2c78", "type": "terminal" } } } } ] } ``` ## Container update events These events fire when container attributes change. ### container.created A new container was added to a shipment. Common for bookings where containers are assigned after sailing. ```json expandable theme={null} { "data": { "id": "c6e6af71-f75d-49e3-9e79-50b719d8376e", "type": "webhook_notification", "attributes": { "id": "c6e6af71-f75d-49e3-9e79-50b719d8376e", "event": "container.created", "delivery_status": "succeeded", "created_at": "2022-10-21T20:18:43Z" }, "relationships": { "reference_object": { "data": { "id": "8d86b03a-0ff7-4efe-b893-4feaf7d0bddc", "type": "container_created_event" } }, "webhook": { "data": { "id": "f1c5487c-ac3c-4ddc-ad77-5d1f32f75669", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "0b315c62-71f2-4c04-b252-88096d7f226f", "type": "shipment", "attributes": { "created_at": "2022-10-21T20:18:36Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "MAEU221876618", "normalized_number": "221876618", "shipping_line_scac": "MAEU", "shipping_line_name": "Maersk", "shipping_line_short_name": "Maersk", "customer_name": "Nienow LLC", "port_of_lading_locode": "CNNGB", "port_of_lading_name": "Ningbo", "port_of_discharge_locode": null, "port_of_discharge_name": null, "pod_vessel_name": null, "pod_vessel_imo": null, "pod_voyage_number": null, "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": null, "pol_timezone": "Asia/Shanghai", "pod_eta_at": "2022-11-25T08:00:00Z", "pod_original_eta_at": "2022-11-25T08:00:00Z", "pod_ata_at": null, "pod_timezone": null, "line_tracking_last_attempted_at": null, "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "9b8a6dcc-2f14-4d2d-a91b-5a154ee6fbf8", "type": "port" } }, "port_of_discharge": { "data": null }, "pod_terminal": { "data": null }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "ede7ebb0-19e6-4bad-afcd-824bb8ca3cd7", "type": "container" } ] } }, "links": { "self": "/v2/shipments/e5a39855-f438-467a-9c18-ae91cd46cfaf" } }, { "id": "ede7ebb0-19e6-4bad-afcd-824bb8ca3cd7", "type": "container", "attributes": { "number": "MRKU3700927", "seal_number": null, "created_at": "2022-10-21T20:18:36Z", "ref_numbers": [ ], "pod_arrived_at": null, "pod_discharged_at": null, "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "dry", "equipment_length": 40, "equipment_height": "standard", "weight_in_lbs": null, "pod_full_out_at": null, "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": null, "availability_known": false, "pod_timezone": null, "final_destination_timezone": null, "empty_terminated_timezone": null }, "relationships": { "shipment": { "data": { "id": "0b315c62-71f2-4c04-b252-88096d7f226f", "type": "shipment" } }, "pod_terminal": { "data": null }, "transport_events": { "data": [ ] }, "raw_events": { "data": [ ] } } }, { "id": "8d86b03a-0ff7-4efe-b893-4feaf7d0bddc", "type": "container_created_event", "attributes": { "timestamp": "2022-10-21T20:18:36Z", "timezone": "Etc/UTC" }, "relationships": { "container": { "data": { "id": "ede7ebb0-19e6-4bad-afcd-824bb8ca3cd7", "type": "container" } }, "shipment": { "data": { "id": "0b315c62-71f2-4c04-b252-88096d7f226f", "type": "shipment" } } } } ] } ``` ### container.updated Container attributes changed at the terminal — fees, holds, LFD, pickup appointment, availability, or POD terminal. The payload includes a `changeset` of the updated fields. ```json expandable theme={null} { "data": { "id": "aee69c9e-66e5-4ead-82ee-668dafc242ee", "type": "webhook_notification", "attributes": { "id": "aee69c9e-66e5-4ead-82ee-668dafc242ee", "event": "container.updated", "delivery_status": "succeeded", "created_at": "2022-10-21T20:19:13Z" }, "relationships": { "reference_object": { "data": { "id": "638dd40b-6d1a-48a5-af2f-c68463059149", "type": "container_updated_event" } }, "webhook": { "data": { "id": "fdd1cf95-7569-4bf9-965b-825a55fa6303", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "c0633538-4e53-4c33-bde5-055a5bdbfa29", "type": "shipment", "attributes": { "created_at": "2022-09-06T08:29:58Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "MAEUJAK015053", "normalized_number": "JAK015053", "shipping_line_scac": "MAEU", "shipping_line_name": "Maersk", "shipping_line_short_name": "Maersk", "customer_name": "Lakin and Sons", "port_of_lading_locode": "IDJKT", "port_of_lading_name": "Jakarta, Java", "port_of_discharge_locode": "USNYC", "port_of_discharge_name": "New York / New Jersey", "pod_vessel_name": "MAERSK SYDNEY", "pod_vessel_imo": "9289958", "pod_voyage_number": "235W", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": "2022-09-04T16:54:00Z", "pol_timezone": "Asia/Jakarta", "pod_eta_at": "2022-10-10T22:00:00Z", "pod_original_eta_at": "2022-10-14T07:00:00Z", "pod_ata_at": "2022-10-10T22:00:00Z", "pod_timezone": "America/New_York", "line_tracking_last_attempted_at": "2022-10-21T18:11:45Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "82bf3631-280e-4d73-81d8-fc16753d08a7", "type": "port" } }, "port_of_discharge": { "data": { "id": "9e9e8a64-bc96-417f-87d2-189ebccd0123", "type": "port" } }, "pod_terminal": { "data": { "id": "fb4666e2-f159-429e-9d6c-4a09ced32262", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "5f3d40ad-2e2f-4778-8973-8a3a70bef56d", "type": "container" } ] } }, "links": { "self": "/v2/shipments/7297af4f-e047-4a8e-9eda-d1013ce2ab16" } }, { "id": "9e9e8a64-bc96-417f-87d2-189ebccd0123", "type": "port", "attributes": { "id": "9e9e8a64-bc96-417f-87d2-189ebccd0123", "name": "New York / New Jersey", "code": "USNYC", "state_abbr": "NY", "city": "New York", "country_code": "US", "latitude": "40.684996498", "longitude": "-74.151115685", "time_zone": "America/New_York" } }, { "id": "fb4666e2-f159-429e-9d6c-4a09ced32262", "type": "terminal", "attributes": { "id": "fb4666e2-f159-429e-9d6c-4a09ced32262", "nickname": "APM Terminals", "name": "Port Elizabeth", "firms_code": "E425", "smdg_code": null, "bic_facility_code": null, "provided_data": { "pickup_lfd": false, "pickup_lfd_notes": "", "available_for_pickup": false, "fees_at_pod_terminal": false, "holds_at_pod_terminal": false, "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": false, "location_at_pod_terminal_notes": "", "pod_full_out_chassis_number_notes": "" }, "street": "701 New Dock Street Berths 212-225", "city": "Terminal Island", "state": "California", "state_abbr": "CA", "zip": "90731", "country": "United States" }, "relationships": { "port": { "data": { "id": "9e9e8a64-bc96-417f-87d2-189ebccd0123", "type": "port" } } } }, { "id": "5f3d40ad-2e2f-4778-8973-8a3a70bef56d", "type": "container", "attributes": { "number": "MSKU4807969", "seal_number": null, "created_at": "2022-09-06T08:29:58Z", "ref_numbers": [ ], "pod_arrived_at": "2022-10-10T22:00:00Z", "pod_discharged_at": "2022-10-11T21:32:00Z", "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": true, "equipment_type": "dry", "equipment_length": 45, "equipment_height": "high_cube", "weight_in_lbs": null, "pod_full_out_at": null, "empty_terminated_at": null, "terminal_checked_at": "2022-10-21T20:19:12Z", "fees_at_pod_terminal": [ ], "pickup_lfd": "2022-10-24T04:00:00Z", "pickup_appointment_at": "2022-10-18T16:00:00Z", "pod_full_out_chassis_number": null, "location_at_pod_terminal": "Yard Grounded (G90402B1)", "pod_last_tracking_request_at": "2022-10-21T20:19:12Z", "shipment_last_tracking_request_at": null, "availability_known": true, "pod_timezone": "America/New_York", "final_destination_timezone": null, "empty_terminated_timezone": "America/New_York" }, "relationships": { "shipment": { "data": { "id": "c0633538-4e53-4c33-bde5-055a5bdbfa29", "type": "shipment" } }, "pod_terminal": { "data": { "id": "fb4666e2-f159-429e-9d6c-4a09ced32262", "type": "terminal" } }, "transport_events": { "data": [ { "id": "e9b1b49a-b84e-469e-ac72-b0026cd0a17a", "type": "transport_event" }, { "id": "dbf8a2b9-d19e-42c1-8bdf-216f075028c8", "type": "transport_event" }, { "id": "4a10db53-cfd1-4a1e-98e3-6dcea72b34b5", "type": "transport_event" }, { "id": "f32b184d-aea7-4a61-98c3-a2e5e7b78775", "type": "transport_event" }, { "id": "87245720-cc9f-4e17-b2df-c76a969b6294", "type": "transport_event" }, { "id": "74d08ccb-0e8c-4715-9d48-d083441764c3", "type": "transport_event" }, { "id": "f700c2f0-edbe-406f-943c-8575e4656af8", "type": "transport_event" }, { "id": "8242486b-0410-40e5-9566-c57cda4a2948", "type": "transport_event" }, { "id": "b6e33165-7848-48b0-a621-2012e6392d6e", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "e6ecd493-4e6b-4d92-809a-ffd73d401d67", "type": "raw_event" }, { "id": "abff03e4-d246-4d2a-b02b-5a5f81d493b5", "type": "raw_event" }, { "id": "603d631b-6a81-4389-94f5-7fe21e71a43e", "type": "raw_event" }, { "id": "3fcc6702-859c-4088-81ea-407f37d83481", "type": "raw_event" }, { "id": "108f5bab-7a33-4fef-bbfc-fc1b12f8742d", "type": "raw_event" }, { "id": "e588e237-7dd1-4982-8fc8-6ce82447cd92", "type": "raw_event" }, { "id": "c796106f-39ce-402e-8296-5a02cafe6da7", "type": "raw_event" }, { "id": "2fb9cdf8-8f9b-4ff2-9a79-e79031f745c6", "type": "raw_event" }, { "id": "aa56172c-c0c4-4fe8-a718-ff95826469ad", "type": "raw_event" } ] } } }, { "id": "638dd40b-6d1a-48a5-af2f-c68463059149", "type": "container_updated_event", "attributes": { "changeset": { "available_for_pickup": [ false, true ], "holds_at_pod_terminal": [ [ { "name": "other", "status": "hold", "description": "DOWN - Broken Machine Over Pile" }, { "name": "other", "status": "hold", "description": "MACHINE - " } ], [ ] ] }, "timestamp": "2022-10-21T20:19:12Z", "data_source": "terminal", "timezone": "America/New_York" }, "relationships": { "container": { "data": { "id": "5f3d40ad-2e2f-4778-8973-8a3a70bef56d", "type": "container" } }, "terminal": { "data": { "id": "fb4666e2-f159-429e-9d6c-4a09ced32262", "type": "terminal" } }, "shipment": { "data": { "id": "c0633538-4e53-4c33-bde5-055a5bdbfa29", "type": "shipment" } } } } ] } ``` ### container.pod\_terminal\_changed The port of discharge terminal assignment changed for the container. ```json expandable theme={null} { "data": { "id": "262c2b9c-92f9-46ce-a3f7-e5cb14b1e9b3", "type": "webhook_notification", "attributes": { "id": "262c2b9c-92f9-46ce-a3f7-e5cb14b1e9b3", "event": "container.pod_terminal_changed", "delivery_status": "succeeded", "created_at": "2022-10-21T20:18:14Z" }, "relationships": { "reference_object": { "data": { "id": "9df173e3-96b1-4b41-b0b2-a8459190ffc1", "type": "container_pod_terminal_changed_event" } }, "webhook": { "data": { "id": "33a10002-3bba-486d-b397-1361c4dd4858", "type": "webhook" } }, "webhook_notification_logs": { "data": [ ] } } }, "included": [ { "id": "ecab2629-f537-4a38-9099-cd78a3577fdc", "type": "shipment", "attributes": { "created_at": "2022-10-20T17:02:14Z", "ref_numbers": [ ], "tags": [ ], "bill_of_lading_number": "CMDUSHZ5223740", "normalized_number": "SHZ5223740", "shipping_line_scac": "CMDU", "shipping_line_name": "CMA CGM", "shipping_line_short_name": "CMA CGM", "customer_name": "Muller, Parisian and Bauch", "port_of_lading_locode": "CNSHK", "port_of_lading_name": "Shekou", "port_of_discharge_locode": "USMIA", "port_of_discharge_name": "Miami Seaport", "pod_vessel_name": "CMA CGM OTELLO", "pod_vessel_imo": "9299628", "pod_voyage_number": "0PGDNE1MA", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": "2022-10-23T05:30:00Z", "pol_atd_at": null, "pol_timezone": "Asia/Shanghai", "pod_eta_at": "2022-12-16T12:00:00Z", "pod_original_eta_at": "2022-12-16T12:00:00Z", "pod_ata_at": null, "pod_timezone": "America/New_York", "line_tracking_last_attempted_at": "2022-10-21T20:18:06Z", "line_tracking_last_succeeded_at": null, "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "relationships": { "port_of_lading": { "data": { "id": "7cbb8ba7-66ca-4c6e-84e7-8cfa2686ae3b", "type": "port" } }, "port_of_discharge": { "data": { "id": "ba9cc715-9b4c-4f78-a250-d68e26b23a5a", "type": "port" } }, "pod_terminal": { "data": { "id": "7db4d154-86c1-41e9-aa89-612eeb909f95", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": null }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "5820ed38-4b8b-4034-aefa-d5b5dbeb45e9", "type": "container" } ] } }, "links": { "self": "/v2/shipments/d08ffcbf-43c6-4f68-85c4-7f2199211723" } }, { "id": "5820ed38-4b8b-4034-aefa-d5b5dbeb45e9", "type": "container", "attributes": { "number": "TGSU5023798", "seal_number": null, "created_at": "2022-10-20T17:02:14Z", "ref_numbers": [ ], "pod_arrived_at": null, "pod_discharged_at": null, "final_destination_full_out_at": null, "holds_at_pod_terminal": [ ], "available_for_pickup": false, "equipment_type": "dry", "equipment_length": 40, "equipment_height": "high_cube", "weight_in_lbs": null, "pod_full_out_at": null, "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": null, "availability_known": false, "pod_timezone": "America/New_York", "final_destination_timezone": null, "empty_terminated_timezone": "America/New_York" }, "relationships": { "shipment": { "data": { "id": "ecab2629-f537-4a38-9099-cd78a3577fdc", "type": "shipment" } }, "pod_terminal": { "data": { "id": "7db4d154-86c1-41e9-aa89-612eeb909f95", "type": "terminal" } }, "transport_events": { "data": [ { "id": "7ef9704f-1b7f-4eb5-b8c3-931fa68d2151", "type": "transport_event" }, { "id": "a5af8967-877e-4078-a5cd-200423ddcba2", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "9338ac85-2509-4d04-a0a4-5d4a572ea172", "type": "raw_event" }, { "id": "c10317c1-11b9-4d1b-b973-42d961da6340", "type": "raw_event" }, { "id": "daa39bfd-042f-487c-9f56-3d18fc7edb32", "type": "raw_event" }, { "id": "0c33d121-291f-4a5d-81d9-6a01f67c67bb", "type": "raw_event" } ] } } }, { "id": "7db4d154-86c1-41e9-aa89-612eeb909f95", "type": "terminal", "attributes": { "id": "7db4d154-86c1-41e9-aa89-612eeb909f95", "nickname": "SFCT", "name": "South Florida Container Terminal", "firms_code": "N775", "smdg_code": null, "bic_facility_code": null, "provided_data": { "pickup_lfd": false, "pickup_lfd_notes": "", "available_for_pickup": false, "fees_at_pod_terminal": false, "holds_at_pod_terminal": false, "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": false, "location_at_pod_terminal_notes": "", "pod_full_out_chassis_number_notes": "" }, "street": "302 Port Jersey Boulevard", "city": "Jersey City", "state": "New Jersey", "state_abbr": "NJ", "zip": "07305", "country": "United States" }, "relationships": { "port": { "data": { "id": "ba9cc715-9b4c-4f78-a250-d68e26b23a5a", "type": "port" } } } }, { "id": "9df173e3-96b1-4b41-b0b2-a8459190ffc1", "type": "container_pod_terminal_changed_event", "attributes": { "timestamp": "2022-10-21T20:18:14Z", "data_source": "shipping_line" }, "relationships": { "container": { "data": { "id": "5820ed38-4b8b-4034-aefa-d5b5dbeb45e9", "type": "container" } }, "terminal": { "data": { "id": "7db4d154-86c1-41e9-aa89-612eeb909f95", "type": "terminal" } }, "shipment": { "data": { "id": "ecab2629-f537-4a38-9099-cd78a3577fdc", "type": "shipment" } } } } ] } ``` ### container.pickup\_lfd.changed The coalesced `pickup_lfd` attribute changed. This field combines the `import_deadlines` values, preferring the shipping line LFD when available. ```json expandable theme={null} { "data": { "id": "4f95eaca-ebd1-414d-b50f-84e113a01b37", "type": "webhook_notification", "attributes": { "id": "4f95eaca-ebd1-414d-b50f-84e113a01b37", "event": "container.pickup_lfd.changed", "delivery_status": "pending", "created_at": "2024-06-26T21:22:47Z" }, "relationships": { "reference_object": { "data": { "id": "82099f21-0a1d-40bd-b56c-30461f7db1cc", "type": "transport_event" } }, "webhook": { "data": { "id": "046cf6b8-ae02-47e2-90d4-d6379319bb71", "type": "webhook" } }, "webhook_notification_logs": { "data": [] } } }, "links": { "self": "https://api.terminal49.com/v2/webhook_notifications/examples?event=container.pickup_lfd.changed" }, "included": [ { "id": "29a59f1d-cd71-4c0c-9be8-bc453f945d27", "type": "shipment", "attributes": { "created_at": "2024-06-26T21:22:46Z", "ref_numbers": [ "REF-D79122" ], "tags": [], "bill_of_lading_number": "TE491A648538", "normalized_number": "TE491A648538", "shipping_line_scac": "MSCU", "shipping_line_name": "Mediterranean Shipping Company", "shipping_line_short_name": "MSC", "customer_name": "Muller-Hauck", "port_of_lading_locode": "MXZLO", "port_of_lading_name": "Manzanillo", "port_of_discharge_locode": "USOAK", "port_of_discharge_name": "Port of Oakland", "pod_vessel_name": "MSC CHANNE", "pod_vessel_imo": "9710438", "pod_voyage_number": "098N", "destination_locode": null, "destination_name": null, "destination_timezone": null, "destination_ata_at": null, "destination_eta_at": null, "pol_etd_at": null, "pol_atd_at": "2024-06-13T21:22:46Z", "pol_timezone": "America/Mexico_City", "pod_eta_at": "2024-07-03T21:22:46Z", "pod_original_eta_at": "2024-07-03T21:22:46Z", "pod_ata_at": "2024-07-03T22:22:46Z", "pod_timezone": "America/Los_Angeles", "line_tracking_last_attempted_at": "2024-06-26T21:22:46Z", "line_tracking_last_succeeded_at": "2024-06-26T21:22:46Z", "line_tracking_stopped_at": null, "line_tracking_stopped_reason": null }, "links": { "self": "/v2/shipments/29a59f1d-cd71-4c0c-9be8-bc453f945d27" }, "relationships": { "port_of_lading": { "data": { "id": "dd85723f-17a6-4b7a-bd98-c6f0d94fe4e6", "type": "port" } }, "port_of_discharge": { "data": { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "type": "port" } }, "pod_terminal": { "data": { "id": "3e550f0e-ac2a-48fb-b242-5be45ecf2c78", "type": "terminal" } }, "destination": { "data": null }, "destination_terminal": { "data": { "id": "a40d96ef-3f53-4289-b371-8d26a2aaeff8", "type": "terminal" } }, "line_tracking_stopped_by_user": { "data": null }, "containers": { "data": [ { "id": "3d2ce91a-6ac8-4053-a6fb-7dac1c47bf71", "type": "container" } ] } } }, { "id": "3d2ce91a-6ac8-4053-a6fb-7dac1c47bf71", "type": "container", "attributes": { "number": "OERU6438708", "seal_number": "16082b290c25f0c5", "created_at": "2024-06-26T21:22:47Z", "ref_numbers": [ "REF-2E50D1" ], "pod_arrived_at": "2024-06-26T21:22:46Z", "pod_discharged_at": "2024-06-26T21:22:46Z", "final_destination_full_out_at": "2024-06-26T21:22:46Z", "holds_at_pod_terminal": [], "available_for_pickup": false, "equipment_type": "dry", "equipment_length": 40, "equipment_height": "standard", "weight_in_lbs": 60753, "pod_full_out_at": null, "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": "2024-06-26T21:22:46Z", "availability_known": true, "pod_timezone": "America/Los_Angeles", "final_destination_timezone": null, "empty_terminated_timezone": "America/Los_Angeles" }, "relationships": { "shipment": { "data": { "id": "29a59f1d-cd71-4c0c-9be8-bc453f945d27", "type": "shipment" } }, "pickup_facility": { "data": { "id": "a40d96ef-3f53-4289-b371-8d26a2aaeff8", "type": "terminal" } }, "pod_terminal": { "data": null }, "transport_events": { "data": [ { "id": "82099f21-0a1d-40bd-b56c-30461f7db1cc", "type": "transport_event" } ] }, "raw_events": { "data": [ { "id": "4327e350-4a73-4960-9842-e2a228477e8a", "type": "raw_event" }, { "id": "5e51cdbb-3dd9-42d5-9d1e-30aca590b874", "type": "raw_event" }, { "id": "f2b4c0e4-2f53-4653-82d4-31c5d68a9886", "type": "raw_event" }, { "id": "350e6c50-8594-48c7-845d-c657f841f46c", "type": "raw_event" }, { "id": "57794561-bae5-4f9b-9d14-24b35da518eb", "type": "raw_event" }, { "id": "8c50848d-15b4-4155-a504-e8df4c9ae7e4", "type": "raw_event" }, { "id": "c3abbd18-198c-441e-8c46-9033ee5bdfbc", "type": "raw_event" }, { "id": "9160cf78-5414-4eb1-853a-519a03e83879", "type": "raw_event" }, { "id": "69f5b333-53a0-40ee-a759-375f6cae04df", "type": "raw_event" }, { "id": "1f7c900c-2940-4c6b-a132-397a9ca075aa", "type": "raw_event" }, { "id": "2c9e5f2f-df5d-4992-b33e-088f37e123f8", "type": "raw_event" } ] } } }, { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "type": "port", "attributes": { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "name": "Port of Oakland", "code": "USOAK", "state_abbr": "CA", "city": "Oakland", "country_code": "US", "latitude": "37.8044", "longitude": "-122.2712", "time_zone": "America/Los_Angeles" } }, { "id": "3e550f0e-ac2a-48fb-b242-5be45ecf2c78", "type": "terminal", "attributes": { "id": "3e550f0e-ac2a-48fb-b242-5be45ecf2c78", "nickname": "SSA", "name": "SSA Terminal", "firms_code": "Z985", "smdg_code": "B58", "bic_facility_code": "USOAKTYJE", "provided_data": { "pickup_lfd": true, "pod_full_out_at": true, "pickup_lfd_notes": "", "available_for_pickup": true, "fees_at_pod_terminal": true, "holds_at_pod_terminal": true, "pickup_appointment_at": false, "location_at_pod_terminal": true, "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": "636 Volkman Valleys", "city": "Lake Jame", "state": "Indiana", "state_abbr": "TN", "zip": "34546-1736", "country": "Saint Lucia", "facility_type": "ocean_terminal" }, "relationships": { "port": { "data": { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "type": "port" } } } }, { "id": "82099f21-0a1d-40bd-b56c-30461f7db1cc", "type": "transport_event", "attributes": { "event": "container.pickup_lfd.changed", "created_at": "2024-06-26T21:22:46Z", "voyage_number": null, "timestamp": "2024-06-26T21:22:46Z", "data_source": "shipping_line", "location_locode": "USOAK", "timezone": "America/Los_Angeles" }, "relationships": { "shipment": { "data": { "id": "29a59f1d-cd71-4c0c-9be8-bc453f945d27", "type": "shipment" } }, "container": { "data": { "id": "3d2ce91a-6ac8-4053-a6fb-7dac1c47bf71", "type": "container" } }, "vessel": { "data": { "id": "daf64780-ecdd-4d46-ae4a-eb70968069ed", "type": "vessel" } }, "location": { "data": { "id": "42d1ba3a-f4b8-431d-a6fe-49fd748a59e7", "type": "port" } }, "terminal": { "data": { "id": "3e550f0e-ac2a-48fb-b242-5be45ecf2c78", "type": "terminal" } } } } ] } ``` ### container.pickup\_lfd\_terminal.changed The terminal-reported Last Free Day (`import_deadlines.pickup_lfd_terminal`) at the destination terminal changed. ```json expandable theme={null} { "data": { "id": "4f421249-8429-4383-9377-075ce3e2c3d8", "type": "webhook_notification", "attributes": { "id": "4f421249-8429-4383-9377-075ce3e2c3d8", "event": "container.pickup_lfd_terminal.changed", "delivery_status": "succeeded", "created_at": "2024-06-26T21:22:47Z" }, "relationships": { "reference_object": { "data": { "id": "3b0adf2f-25b7-4dc8-bb98-8e43f804c3c7", "type": "transport_event" } }, "webhook": { "data": { "id": "046cf6b8-ae02-47e2-90d4-d6379319bb71", "type": "webhook" } }, "webhook_notification_logs": { "data": [] } } }, "links": { "self": "https://api.terminal49.com/v2/webhook_notifications/examples?event=container.pickup_lfd_terminal.changed" }, "included": [ { "id": "3b0adf2f-25b7-4dc8-bb98-8e43f804c3c7", "type": "transport_event", "attributes": { "event": "container.pickup_lfd_terminal.changed", "created_at": "2024-06-26T21:22:47Z", "timestamp": "2024-07-01T07:00:00Z", "value": "2024-07-01T07:00:00Z", "data_source": "terminal", "location_locode": "USOAK", "timezone": "America/Los_Angeles" } } ] } ``` ### container.pickup\_lfd\_rail.changed The rail-carrier-reported Last Free Day (`import_deadlines.pickup_lfd_rail`) at the inland rail destination changed. Rail Plan only — see [Entitlements](/docs/api-docs/useful-info/entitlements). ```json expandable theme={null} { "data": { "id": "89a12e03-46f6-4b99-b705-703800eb68c4", "type": "webhook_notification", "attributes": { "id": "89a12e03-46f6-4b99-b705-703800eb68c4", "event": "container.pickup_lfd_rail.changed", "delivery_status": "succeeded", "created_at": "2024-06-26T21:22:47Z" }, "relationships": { "reference_object": { "data": { "id": "5a62d9de-7ab6-49a5-87a8-b829286a2c5e", "type": "transport_event" } }, "webhook": { "data": { "id": "046cf6b8-ae02-47e2-90d4-d6379319bb71", "type": "webhook" } }, "webhook_notification_logs": { "data": [] } } }, "links": { "self": "https://api.terminal49.com/v2/webhook_notifications/examples?event=container.pickup_lfd_rail.changed" }, "included": [ { "id": "5a62d9de-7ab6-49a5-87a8-b829286a2c5e", "type": "transport_event", "attributes": { "event": "container.pickup_lfd_rail.changed", "created_at": "2024-06-26T21:22:47Z", "timestamp": "2024-07-02T07:00:00Z", "value": "2024-07-02T07:00:00Z", "data_source": "terminal", "location_locode": "USCHI", "timezone": "America/Chicago" } } ] } ``` ### container.pickup\_appointment.changed The pickup appointment changed. ```json expandable theme={null} { "data": { "id": "a20a9088-6af0-4e40-b77c-9f1e347618a3", "type": "webhook_notification", "attributes": { "id": "a20a9088-6af0-4e40-b77c-9f1e347618a3", "event": "container.pickup_appointment.changed", "delivery_status": "succeeded", "created_at": "2024-06-26T21:22:47Z" }, "relationships": { "reference_object": { "data": { "id": "6b9fc846-c244-4791-9ee4-f63d95620ec9", "type": "transport_event" } }, "webhook": { "data": { "id": "046cf6b8-ae02-47e2-90d4-d6379319bb71", "type": "webhook" } }, "webhook_notification_logs": { "data": [] } } }, "links": { "self": "https://api.terminal49.com/v2/webhook_notifications/examples?event=container.pickup_appointment.changed" }, "included": [ { "id": "6b9fc846-c244-4791-9ee4-f63d95620ec9", "type": "transport_event", "attributes": { "event": "container.pickup_appointment.changed", "created_at": "2024-06-26T21:22:47Z", "timestamp": "2024-06-28T15:00:00Z", "value": "2024-06-28T15:00:00Z", "data_source": "terminal", "location_locode": "USOAK", "timezone": "America/Los_Angeles" } } ] } ``` # Webhook best practices Source: https://terminal49.com/docs/api-docs/webhooks/best-practices Handle retries, ensure idempotency, verify signatures, and build reliable webhook consumers for Terminal49 shipment and container events. Webhooks are the primary way to receive tracking updates from Terminal49. Follow these practices to build a consumer that handles every edge case reliably. ## Acknowledge quickly with a success status Terminal49 expects your endpoint to return a success status (`200`, `201`, `202`, or `204`). Acknowledge quickly — but only after you have durably accepted the event. Persist the raw payload or push it onto a queue first, then return the response and process the event asynchronously. ```javascript theme={null} app.post("/webhooks/terminal49", async (req, res) => { // Durably accept the event first (persist or enqueue the raw payload) await enqueueWebhook(req.body); // Acknowledge; process asynchronously from the queue res.sendStatus(202); }); ``` If you acknowledge before storing the event and your process crashes mid-handling, the notification is lost. Terminal49 sees a success response and does not retry. Any other response — including a timeout — triggers retries. If your endpoint consistently fails, Terminal49 retries up to 12 times before marking the notification as failed. ## Handle retries and duplicate deliveries Terminal49 retries failed deliveries, which means your endpoint may receive the same notification more than once. Design your consumer to be **idempotent**. Every webhook notification has a unique `id` in `data.id`. Use it to deduplicate: ```javascript theme={null} async function processWebhook(payload) { const notificationId = payload.data.id; // Check if already processed const alreadyProcessed = await db.webhookLog.findOne({ where: { notificationId }, }); if (alreadyProcessed) { return; // Skip duplicate } // Process the event await handleEvent(payload); // Record that we processed it await db.webhookLog.create({ notificationId, processedAt: new Date() }); } ``` If you use a message queue (SQS, RabbitMQ, etc.), enqueue the raw payload and return a success status as soon as the enqueue succeeds. Your queue consumer can handle deduplication and processing at its own pace. ## Verify the webhook source Terminal49 publishes the IP addresses that webhook notifications originate from. Use the [List Webhook IPs](/docs/api-docs/api-reference/webhooks/list-webhook-ips) endpoint to fetch the current list and validate incoming requests: ```javascript theme={null} const ALLOWED_IPS = await fetchTerminal49WebhookIPs(); app.post("/webhooks/terminal49", (req, res) => { const sourceIp = req.ip; if (!ALLOWED_IPS.includes(sourceIp)) { return res.sendStatus(403); } // Process the webhook res.sendStatus(200); }); ``` Cache the IP list and refresh it periodically (e.g., daily). The list rarely changes, but checking the endpoint ensures you stay current. ## Monitor delivery status Use the [Webhook Notifications API](/docs/api-docs/api-reference/webhook-notifications/list-webhook-notifications) to check delivery status and catch any notifications your endpoint may have missed: ```bash theme={null} curl -s "https://api.terminal49.com/v2/webhook_notifications" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Content-Type: application/vnd.api+json" ``` Each notification's `delivery_status` attribute reports the delivery state (`pending`, `succeeded`, or `failed`). Page through recent notifications and review the ones marked `failed` regularly to identify issues with your endpoint before they cause data gaps. ## Test with the Trigger endpoint Use the [Trigger Webhook](/docs/api-docs/api-reference/webhooks/trigger-a-webhook) endpoint to send a one-time sample webhook notification to an HTTPS URL. This is useful when you want to validate your signature verification, parsing, queueing, and event routing before subscribing a production webhook. ```bash theme={null} curl -s -X POST "https://api.terminal49.com/v2/webhooks/trigger" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/terminal49", "event": "tracking_request.succeeded", "secret": "optional-test-secret" }' ``` The Trigger endpoint sends an example payload for the event you choose. If you include `secret`, Terminal49 signs the test request with the same `X-T49-Webhook-Signature` header used by real webhook deliveries. ## Handle downtime gracefully If your endpoint goes down, Terminal49 retries failed deliveries. When your endpoint recovers: 1. Use the [Webhook Notifications API](/docs/api-docs/api-reference/webhook-notifications/list-webhook-notifications) to list recent notifications and identify the ones with `delivery_status: failed`. 2. Re-fetch the current state of the affected shipments and containers from the REST API. Webhook notifications cannot be replayed. The [Trigger Webhook](/docs/api-docs/api-reference/webhooks/trigger-a-webhook) endpoint only sends a one-time sample payload for testing. 3. For longer outages, list recent shipments via the API to catch up on any missed state changes. ### Recover events you were never subscribed to If you add a new event to a webhook subscription after it has already fired for a shipment or container (for example, subscribing to `shipment.estimated.arrival` after the ETA has already changed several times), Terminal49 will **not** send those historical notifications retroactively. To recover state you missed: * Re-fetch the affected shipments and containers via the [Shipments](/docs/api-docs/api-reference/shipments/list-shipments) and [Containers](/docs/api-docs/api-reference/containers/list-containers) endpoints. This returns the **current** value of fields such as `pod_eta_at`, `pod_ata_at`, and the `import_deadlines` object. * You will only see the latest value, not the history of intermediate changes that occurred before you subscribed. If auditing every change matters to your workflow, subscribe to the event before you start ingesting the shipment. * The [Trigger Webhook](/docs/api-docs/api-reference/webhooks/trigger-a-webhook) endpoint sends a **sample** payload only. It is intended for testing your handler and does not replay real historical events for your shipments. ## Keep your webhook active Terminal49 may deactivate a webhook after repeated delivery failures. Check your webhook's `active` status periodically: ```bash theme={null} curl -s "https://api.terminal49.com/v2/webhooks" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Content-Type: application/vnd.api+json" ``` If your webhook has been deactivated, fix the endpoint issue and [update the webhook](/docs/api-docs/api-reference/webhooks/edit-a-webhook) to set `active: true`. ## Summary checklist * [ ] Return a success status (`200`, `201`, `202`, or `204`) after durably accepting the event, then process asynchronously * [ ] Deduplicate using the notification `id` * [ ] Validate the source IP against the webhook IPs list * [ ] Monitor failed notifications * [ ] Subscribe only to the events you need * [ ] Log raw payloads for debugging ## Related * [Setting up webhooks](/docs/api-docs/in-depth-guides/webhooks) — create and configure endpoints * [Event catalog](/docs/api-docs/webhooks/event-catalog) — all available events * [Webhook payloads](/docs/api-docs/webhooks/payloads) — notification envelope and included resources * [Webhook API Reference](/docs/api-docs/api-reference/webhooks/create-a-webhook) — CRUD operations for webhooks # Webhook event catalog Source: https://terminal49.com/docs/api-docs/webhooks/event-catalog Browse every Terminal49 webhook event by category — tracking request status changes, transport milestones, container updates, ETA changes, and more. Terminal49 sends webhook notifications for over 30 events across the container lifecycle. Each event represents a specific change to a tracking request, shipment, or container. Subscribe to individual events when [creating a webhook](/docs/api-docs/in-depth-guides/webhooks), or subscribe to all events and filter in your handler. Use [List Webhook Events](/docs/api-docs/api-reference/webhooks/list-webhook-events) to fetch the event categories available to your account. Some events depend on account features. ## Tracking request events These events fire when a tracking request changes status. | Event | Description | | ------------------------------------ | ------------------------------------------------------------------------------------------------ | | `tracking_request.succeeded` | Shipment created and linked to the tracking request. Your tracking is active. | | `tracking_request.failed` | The tracking request failed. The carrier could not find the shipment. | | `tracking_request.awaiting_manifest` | The carrier has not yet manifested this shipment. Terminal49 will retry automatically. | | `tracking_request.tracking_stopped` | Terminal49 is no longer updating this tracking request (shipment delivered or manually stopped). | ## Transport milestone events These events map to physical milestones in a container's journey. They fire in roughly chronological order as a container moves from origin to destination. ### Origin | Event | Description | | ------------------------------------- | --------------------------------------------------- | | `container.transport.empty_out` | Empty container picked up at port of lading. | | `container.transport.full_in` | Full container returned to port of lading. | | `container.transport.vessel_loaded` | Container loaded onto the vessel at port of lading. | | `container.transport.vessel_departed` | Vessel departed the port of lading. | ### Transshipment | Event | Description | | ---------------------------------------------- | ------------------------------------------------------------- | | `container.transport.transshipment_arrived` | Container arrived at a transshipment port. | | `container.transport.transshipment_discharged` | Container discharged at the transshipment port. | | `container.transport.transshipment_loaded` | Container loaded onto a new vessel at the transshipment port. | | `container.transport.transshipment_departed` | Vessel departed the transshipment port. | ### Feeder vessel | Event | Description | | --------------------------------------- | ----------------------------------------------------- | | `container.transport.feeder_arrived` | Container arrived on a feeder vessel or barge. | | `container.transport.feeder_discharged` | Container discharged from the feeder vessel or barge. | | `container.transport.feeder_loaded` | Container loaded onto a feeder vessel or barge. | | `container.transport.feeder_departed` | Feeder vessel or barge departed. | ### Destination | Event | Description | | --------------------------------------- | --------------------------------------------------------------- | | `container.transport.vessel_arrived` | Vessel arrived at the port of discharge. | | `container.transport.vessel_berthed` | Vessel berthed at the port of discharge. | | `container.transport.vessel_discharged` | Container discharged from the vessel at the port of discharge. | | `container.transport.available` | Container is available for pickup at the destination. | | `container.transport.not_available` | Container is no longer available for pickup at the destination. | | `container.transport.full_out` | Container picked up (gated out) at the port of discharge. | | `container.transport.delivered` | Container was manually marked as delivered. | | `container.transport.empty_in` | Empty container returned at the destination. | ### Rail (inland moves) | Event | Description | | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `container.transport.rail_loaded` | Container loaded onto a rail car. | | `container.transport.rail_departed` | Rail car departed. | | `container.transport.rail_arrived` | Rail car arrived. | | `container.transport.rail_unloaded` | Container unloaded from the rail car. | | `container.transport.arrived_at_inland_destination` | Container arrived at the final inland destination. Fires only for shipments with an inland rail leg. See the [Rail integration guide](/docs/api-docs/in-depth-guides/rail-integration-guide). | ## Estimated events These events fire when an estimated departure or arrival time changes. | Event | Description | | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `container.transport.estimated.vessel_departed` | ETA changed for vessel departure at the port of lading. | | `shipment.estimated.arrival` | ETA changed for the port of discharge (shipment level). | | `container.transport.estimated.vessel_arrived` | ETA changed for vessel arrival at the port of discharge (container level). | | `container.transport.estimated.arrived_at_inland_destination` | ETA changed for the inland destination. Fires only for shipments with an inland rail leg. See the [Rail integration guide](/docs/api-docs/in-depth-guides/rail-integration-guide). | These are the only estimated event types. There are no estimated equivalents for feeder, rail, or transshipment events. If you need estimated timestamps for those milestones, use the deprecated [raw events endpoint](/docs/api-docs/api-reference/containers/get-a-containers-raw-events), which flags estimates with an `attributes.estimated` boolean on any event type. ## Container update events These events fire when container attributes change. | Event | Description | | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `container.created` | A new container was added to a shipment. Common for bookings where containers are assigned after sailing. | | `container.updated` | Container attributes changed at the terminal — fees, holds, LFD, pickup appointment, availability, or POD terminal. The payload includes a `changeset` of the updated fields. | | `container.pod_terminal_changed` | The port of discharge terminal assignment changed for the container. | | `container.pickup_lfd.changed` | The coalesced `pickup_lfd` attribute changed. It follows a fixed source priority: `pickup_lfd_line`, then `pickup_lfd_terminal`, then `pickup_lfd_rail`. It does not pick the earliest date — see [LFD & Availability Alerts](/docs/api-docs/webhooks/use-cases/lfd-alerts#how-pickup_lfd-is-chosen). | | `container.pickup_lfd_line.changed` | The shipping line's Last Free Day (`import_deadlines.pickup_lfd_line`) changed. | | `container.pickup_lfd_terminal.changed` | The terminal-reported Last Free Day (`import_deadlines.pickup_lfd_terminal`) at the destination terminal changed. | | `container.pickup_lfd_rail.changed` | The rail-carrier-reported Last Free Day (`import_deadlines.pickup_lfd_rail`) at the inland rail destination changed. Rail Plan only — see [Entitlements](/docs/api-docs/useful-info/entitlements). | | `container.pickup_appointment.changed` | The pickup appointment changed. | There are no field-specific events for holds, fees, or availability, such as `container.holds_at_pod_terminal.changed`. Changes to `holds_at_pod_terminal`, `fees_at_pod_terminal`, and `available_for_pickup` are delivered through `container.updated`. Read the `changeset` in the payload to detect which fields changed. See [Container Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). ## Changes that do not fire webhooks Some updates you can make through the API do not have a corresponding webhook event. Poll the affected resource or reconcile in your handler when you need to react to them. | Change | How to observe | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Editing a tracking request's `ref_numbers` (reference numbers) through [Edit a tracking request](/docs/api-docs/api-reference/tracking-requests/edit-a-tracking-request) | Fetch the tracking request with [Get a tracking request](/docs/api-docs/api-reference/tracking-requests/get-a-single-tracking-request) or search with the `q` query parameter on [List tracking requests](/docs/api-docs/api-reference/tracking-requests/list-tracking-requests). | | Creating, updating, or deleting custom field values on tracking requests, shipments, or containers | Read them back with [List tracking request custom fields](/docs/api-docs/api-reference/tracking-requests/list-tracking-request-custom-fields), [List shipment custom fields](/docs/api-docs/api-reference/shipments/list-shipment-custom-fields), or [List container custom fields](/docs/api-docs/api-reference/containers/list-container-custom-fields). | Custom field values are also not embedded in webhook notification payloads. The `included` array carries the shipment, container, tracking request, and transport event records described in [Webhook Payloads](/docs/api-docs/webhooks/payloads), but not their custom fields — fetch custom fields separately when you need them in your handler. ## Document processing events These events fire when document extraction completes for accounts using document processing. | Event | Description | | ---------------------------- | -------------------------------------------------------------- | | `document.extracted` | Document classification and extraction completed successfully. | | `document.extraction_failed` | Document extraction did not complete successfully. | See [Document Processing Workflows](/docs/api-docs/in-depth-guides/document-processing-workflows) for payload structure and related document resources. ## Payload structure Every webhook notification follows the same structure: a `data` object containing the event metadata, and an `included` array with the related shipment, container, and event objects. See [Webhook Payloads](/docs/api-docs/webhooks/payloads) for the common envelope and included-resource patterns. See [Payload Examples](/docs/api-docs/useful-info/webhook-events-examples) for complete JSON samples. ## Related * [Setting up webhooks](/docs/api-docs/in-depth-guides/webhooks) — create and configure webhook endpoints * [Webhook Payloads](/docs/api-docs/webhooks/payloads) — notification envelope and included resources * [Best practices](/docs/api-docs/webhooks/best-practices) — retry handling, idempotency, and reliability * [Webhook API Reference](/docs/api-docs/api-reference/webhooks/create-a-webhook) — programmatic webhook management # Why Webhooks Source: https://terminal49.com/docs/api-docs/webhooks/overview Terminal49 is event-driven. Use webhooks to receive real-time shipment and container tracking updates instead of polling the REST API for changes. Terminal49 tracking is fully event-driven. When a carrier reports a status change, Terminal49 processes it and pushes a webhook notification to your system within minutes. You never need to poll. ## How it works ```mermaid theme={null} sequenceDiagram participant Carrier participant Terminal49 participant Your System Carrier->>Terminal49: Reports status change Terminal49->>Terminal49: Processes and normalizes data Terminal49->>Your System: Sends webhook notification Your System->>Terminal49: Returns a success status ``` 1. You [create a tracking request](/docs/api-docs/getting-started/tracking-shipments-and-containers) with a Bill of Lading (BOL) or container number. 2. Terminal49 monitors the carrier and destination terminal for changes. 3. When something changes — an ETA update, a milestone event, a container hold — Terminal49 sends a `POST` request to your webhook URL with the full event payload. 4. Your system accepts the event and returns a success status (`200`, `201`, `202`, or `204`). ## Why webhooks instead of polling | | Webhooks | Polling | | --------------- | ----------------------------- | ----------------------------------- | | **Latency** | Minutes after carrier reports | Depends on your poll interval | | **Efficiency** | Only fires when data changes | Most requests return unchanged data | | **Rate limits** | No API calls consumed | Each poll consumes a request | | **Complexity** | Handle incoming POST requests | Build and maintain a polling loop | | **Recommended** | Yes | Only for on-demand data retrieval | Terminal49 exposes over 30 distinct events covering the full container lifecycle — from empty-out at origin to empty-return at destination. Webhooks let you react to each of these in real time. Polling the API to check for updates is discouraged. It consumes your rate limit, adds latency, and misses the event context that webhooks provide (such as which specific field changed on a `container.updated` event). Use the [List](/docs/api-docs/api-reference/shipments/list-shipments) and [Get](/docs/api-docs/api-reference/shipments/get-a-shipment) endpoints for on-demand lookups, not status monitoring. Use the [Trigger Webhook](/docs/api-docs/api-reference/webhooks/trigger-a-webhook) endpoint to send a sample event payload to your HTTPS endpoint while you build your integration. ## What you can do with webhooks Webhooks power most Terminal49 integrations. Common patterns include: * **[ETA monitoring](/docs/api-docs/webhooks/use-cases/eta-monitoring)** — alert your team when a shipment's arrival estimate changes * **[LFD and availability alerts](/docs/api-docs/webhooks/use-cases/lfd-alerts)** — trigger dispatch when a container clears holds and is ready for pickup * **[Milestone tracking](/docs/api-docs/webhooks/use-cases/milestone-tracking)** — build a complete timeline of a container's journey from origin to destination * **Database and TMS updates** — push every update into your database, ERP, or TMS as it happens. For managed table delivery into a data warehouse, database, or spreadsheet, see [DataSync](/docs/datasync/home) * **Customer notifications** — surface tracking updates to your end customers in real time ## Secure your endpoint Before using webhooks in production: * Verify the `X-T49-Webhook-Signature` HMAC signature against the raw request body. * Allowlist Terminal49 webhook IPs with the [List Webhook IPs](/docs/api-docs/api-reference/webhooks/list-webhook-ips) endpoint. * Make handlers idempotent because retries can deliver the same notification more than once. * Return a success status (`200`, `201`, `202`, or `204`) only after you have safely accepted the event. See [Setting up webhooks](/docs/api-docs/in-depth-guides/webhooks) for signature examples and [Webhook Best Practices](/docs/api-docs/webhooks/best-practices) for retry handling. ## Next steps Create your first webhook and subscribe to events. Browse all available webhook events by category. Send a sample webhook payload to your endpoint. Review the notification envelope and included resources. # Webhook Payloads Source: https://terminal49.com/docs/api-docs/webhooks/payloads Reference for the Terminal49 webhook notification envelope, JSON:API included resource patterns, and links to complete event payload examples. Every Terminal49 webhook delivery is a JSON:API document with a `webhook_notification` primary resource and related records in `included`. Use this page as the payload reference. Use [Payload Examples](/docs/api-docs/useful-info/webhook-events-examples) when you need complete sample JSON. ## One notification per container Container-scoped events fire **once per container**, not once per shipment or once per vessel move. If ten containers on the same vessel are discharged, you receive ten separate `container.transport.vessel_discharged` notifications — one for each container. Each payload references a single container through its `reference_object` and (when included) the `container` resource in `included`. Use `data.id` on the notification as the idempotency key when deduplicating retries, and the container `id` or `number` to route each notification to the right record on your side. ## Notification envelope ```json theme={null} { "data": { "id": "87d4f5e3-df7b-4725-85a3-b80acc572e5d", "type": "webhook_notification", "attributes": { "event": "container.updated", "delivery_status": "pending", "created_at": "2026-05-11T18:30:00Z" }, "relationships": { "webhook": { "data": { "id": "8a5ffa8f-3dc1-48de-a0ea-09fc4f2cd96f", "type": "webhook" } }, "reference_object": { "data": { "id": "e8f1976c-0089-4b98-96ae-90aa87fbdfee", "type": "container_updated_event" } } } }, "included": [] } ``` ## Top-level fields | Field | Type | Description | | ------------------------------------- | ------------ | ------------------------------------------------------------------------------------------ | | `data.id` | UUID | Unique webhook notification ID. Use this as the idempotency key. | | `data.type` | string | Always `webhook_notification`. | | `data.attributes.event` | string | Event name, such as `tracking_request.succeeded` or `container.updated`. | | `data.attributes.delivery_status` | string | Delivery state for this notification. Values include `pending`, `succeeded`, and `failed`. | | `data.attributes.created_at` | timestamp | Time Terminal49 created the notification. | | `data.relationships.webhook` | relationship | Webhook endpoint that received the notification. | | `data.relationships.reference_object` | relationship | Event-specific object that caused the notification. | | `included` | array | Related records included for convenience. Contents vary by event. | ## Reference object types | Event family | Common `reference_object.type` | Notes | | ------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `tracking_request.*` | `tracking_request` | Includes the tracking request and, when available, shipment/container records. | | `container.transport.*` | `transport_event` | Includes the transport event, container, shipment, and location records that are available. | | `shipment.estimated.arrival` | `estimated_event` | Includes the estimate event and related shipment records. | | `container.updated` | `container_updated_event` | Includes a `changeset` describing changed container attributes. | | `container.created` | `container_created_event` | Includes the new container and related shipment. | | `container.pickup_lfd.changed` and the `pickup_lfd_line` / `pickup_lfd_terminal` / `pickup_lfd_rail` variants | `transport_event` | The transport event's `value` attribute carries the new Last Free Day. The container with updated LFD fields may also be included, as in the `container.pickup_lfd.changed` example. | | `document.*` | document-related event resource | Available for accounts using document processing. See [Document Processing Workflows](/docs/api-docs/in-depth-guides/document-processing-workflows). | ## Included resources Webhook payloads may include: | Resource type | When included | | ------------------------- | ------------------------------------------------------------------------------------------- | | `tracking_request` | Included for tracking request lifecycle events and related shipment updates. | | `shipment` | Included when the event relates to a shipment or one of its containers. | | `container` | Included for container lifecycle, status, availability, and milestone events. | | `transport_event` | Included for `container.transport.*` milestone events and LFD or appointment change events. | | `estimated_event` | Included for ETA change events. | | `container_updated_event` | Included for `container.updated` events. | | `port` | Included when a payload references a port record. | | `terminal` | Included when a payload references a terminal record. | Other resource types — such as `vessel`, `rail_terminal`, and `metro_area` — can appear for specific events. The webhook endpoint that received the delivery is referenced through the `webhook` relationship on the notification, not serialized in `included`. Do not require every resource to be present. Carrier, terminal, and event data can arrive at different times. ### Events with a minimal `included` array A few events ship with only the reference object in `included` (or with an empty `included` array). The `shipment` and `container` records are **not** embedded in the payload: * `container.transport.available` * `container.transport.not_available` * `container.transport.estimated.vessel_departed` * `container.transport.estimated.vessel_arrived` * `container.transport.estimated.arrived_at_inland_destination` * `container.pickup_appointment.changed` To resolve the container number or bill of lading number for these events, follow the `reference_object` relationship and fetch the related transport event, container, or shipment through the API — for example, [Get a container](/docs/api-docs/api-reference/containers/get-a-container) using the container ID from the transport event's relationships, or [Get a shipment](/docs/api-docs/api-reference/shipments/get-a-shipment) using the shipment ID. ### Extracting common fields from `included` For events that do include the related records, the fields most integrations pull are consistent across events: | Field | Location in `included` | | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Event timestamp | Object with `type: "transport_event"` → `attributes.timestamp` | | Estimated timestamp (ETA events) | Object with `type: "estimated_event"` or `type: "transport_event"` → `attributes.timestamp` (or `attributes.estimated_timestamp` on legacy `estimated_event` payloads) | | Container ID | Object with `type: "container"` → `id` | | Container number | Object with `type: "container"` → `attributes.number` | | `pod_full_out_at`, `pod_arrived_at`, `pod_discharged_at`, `empty_terminated_at`, `pickup_lfd`, `available_for_pickup` | Object with `type: "container"` → `attributes.` | | Shipment ID | Object with `type: "shipment"` → `id` | | Bill of lading number | Object with `type: "shipment"` → `attributes.bill_of_lading_number` | | Reference numbers (`ref_numbers`) | Object with `type: "shipment"` → `attributes.ref_numbers` | | `pod_eta_at`, `pod_original_eta_at`, `destination_eta_at`, `pod_ata_at` | Object with `type: "shipment"` → `attributes.` | | Voyage number | Object with `type: "transport_event"` → `attributes.voyage_number` | | Location UN/LOCODE | Object with `type: "transport_event"` → `attributes.location_locode` | ETA fields (`pod_eta_at`, `pod_original_eta_at`, `destination_eta_at`) and identifiers like `bill_of_lading_number` live on the **shipment**, not the container. If you only see container fields in a payload, look for the object with `type: "shipment"` in `included`. For a full mapping, see [Which object holds which field?](/docs/api-docs/getting-started/list-shipments-and-containers#which-object-holds-which-field). ## Container update changesets For `container.updated` events, the event resource includes a `changeset` object. Each key is a changed field. The value is a two-item array: `[previous_value, current_value]`. ```json theme={null} { "changeset": { "pickup_lfd": [null, "2026-05-14T07:00:00Z"], "available_for_pickup": [false, true] } } ``` Common changed fields include: * `fees_at_pod_terminal` * `holds_at_pod_terminal` * `pickup_lfd` * `pickup_lfd_line` * `pickup_lfd_rail` * `pickup_appointment_at` * `available_for_pickup` * `pod_terminal` The event's `timestamp` attribute tells you when Terminal49 picked up the changes from the terminal. For `pod_terminal`, the changeset values are terminal record IDs, not names: ```json theme={null} { "changeset": { "pod_terminal": ["0ef5519f-3bde-4d1f-a327-f4f2d833dc7b", "08831e36-676a-4bd4-9c26-c8ab7dbfe73e"] } } ``` Resolve the IDs through the `terminal` resources serialized in `included`. The `container_updated_event` also has a `terminal` relationship that indicates where the data came from. Currently this is always the POD terminal; in the future it may be the final destination terminal or an off-dock location. ### Milestone fields reverting to null Container milestone timestamps such as `pod_full_out_at` are derived from transport events. Terminal49 vets incoming events and can mark an event as invalid after publishing it, for example when a carrier or terminal retracts or corrects the data. When the source event is invalidated, the derived container field reverts, often back to `null`, and the change appears in a `container.updated` changeset as `[previous_value, null]`. To investigate why a field reverted, DataSync customers can query the [`transport_events` table](/docs/datasync/table-properties/transport-events), which records `invalidated_at`, `invalidation_reason`, and `previous_version_id` for each event. These invalidation fields are not exposed on the API's transport events endpoint. If the invalidation reason is unclear, contact Terminal49 support with the affected container numbers. ## Related * [Event Catalog](/docs/api-docs/webhooks/event-catalog) — canonical event names * [Payload Examples](/docs/api-docs/useful-info/webhook-events-examples) — complete sample JSON * [Set Up Webhooks](/docs/api-docs/in-depth-guides/webhooks) — create and secure webhook endpoints * [Webhook Notifications API](/docs/api-docs/api-reference/webhook-notifications/list-webhook-notifications) — inspect notification delivery status # Monitor ETA Changes with Webhooks Source: https://terminal49.com/docs/api-docs/webhooks/use-cases/eta-monitoring Subscribe to Terminal49 ETA change webhook events and alert your team automatically when a shipment's estimated arrival date or port of discharge shifts. Carriers frequently revise arrival estimates as vessels encounter weather, port congestion, or schedule changes. The `shipment.estimated.arrival` event fires every time Terminal49 detects an ETA change so you can react immediately. ## Events to subscribe to | Event | When it fires | | ------------------------------------------------------------- | ----------------------------------------------------------------------- | | `shipment.estimated.arrival` | Shipment-level ETA changes for the port of discharge | | `container.transport.estimated.vessel_arrived` | Container-level ETA changes for vessel arrival at the port of discharge | | `container.transport.estimated.arrived_at_inland_destination` | Container-level ETA changes for the inland destination (rail moves) | ## What the payload includes When a `shipment.estimated.arrival` event fires, the `included` array contains: * An `estimated_event` object with the new `estimated_timestamp` * The full `shipment` object with updated `pod_eta_at` and related fields * The `port` object for the port of discharge * The `vessel` object when Terminal49 can identify the vessel ## Handle the webhook Parse the incoming notification, extract the new ETA, and compare it to your stored value: ```javascript theme={null} app.post("/webhooks/terminal49", async (req, res) => { const { data, included } = req.body; if (data.attributes.event !== "shipment.estimated.arrival") { return res.sendStatus(200); } const shipment = included.find((obj) => obj.type === "shipment"); const estimatedEvent = included.find((obj) => obj.type === "estimated_event"); const newEta = estimatedEvent.attributes.estimated_timestamp; const bolNumber = shipment.attributes.bill_of_lading_number; const portOfDischarge = shipment.attributes.port_of_discharge_name; // Compare against your stored ETA const previousEta = await getStoredEta(bolNumber); const changeInHours = Math.abs( (new Date(newEta) - new Date(previousEta)) / (1000 * 60 * 60) ); if (changeInHours > 12) { await notifyTeam({ message: `ETA for ${bolNumber} at ${portOfDischarge} shifted by ${Math.round(changeInHours)} hours`, newEta, previousEta, }); } await updateStoredEta(bolNumber, newEta); res.sendStatus(200); }); ``` ETA events can fire multiple times per day as carriers update their schedules. Filter for significant changes (e.g., more than 12 hours) to avoid alert fatigue. ## Inland destination ETAs For shipments with an inland rail move, subscribe to `container.transport.estimated.arrived_at_inland_destination` as well. This event fires when the estimated arrival at the rail ramp or inland depot changes. This is a container transport event, so the payload includes a `transport_event` object rather than an `estimated_event` object. Look for the `transport_event` in `included`, then read `attributes.timestamp` for the estimated arrival time. ## Container-level vessel arrival ETAs Subscribe to `container.transport.estimated.vessel_arrived` when you need ETA changes at the container level. This event uses the same transport event payload shape as other `container.transport.*` events, with the estimated vessel arrival stored on the included `transport_event` object. ## Common patterns * **Threshold alerts** — only notify when the ETA shifts by more than N hours * **Direction tracking** — distinguish delays (ETA moved later) from early arrivals (ETA moved earlier) * **Customer notifications** — forward ETA changes to your customers with a human-readable message * **Planning updates** — adjust warehouse receiving schedules or drayage bookings automatically ## Related * [Event catalog](/docs/api-docs/webhooks/event-catalog) — full list of available events * [Payload examples](/docs/api-docs/useful-info/webhook-events-examples) — complete JSON payloads * [Event timestamps](/docs/api-docs/in-depth-guides/event-timestamps) — how Terminal49 stores and returns timestamps in UTC # Alert on LFD Changes and Container Availability Source: https://terminal49.com/docs/api-docs/webhooks/use-cases/lfd-alerts Use Terminal49 webhooks to monitor Last Free Day changes and container availability so you can dispatch pickups before demurrage charges begin. Last Free Day (LFD) is the deadline to pick up a container before demurrage charges start. Terminals and shipping lines can change the LFD at any time. Terminal49 sends webhook events whenever the LFD changes or a container becomes available for pickup so you can act before costs accrue. ## Events to subscribe to | Event | When it fires | | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `container.pickup_lfd.changed` | The coalesced `pickup_lfd` field changed. See [How `pickup_lfd` is chosen](#how-pickup_lfd-is-chosen) for the source priority | | `container.pickup_lfd_line.changed` | The shipping line's Last Free Day changed | | `container.pickup_lfd_terminal.changed` | The destination terminal's Last Free Day changed | | `container.pickup_lfd_rail.changed` | The rail terminal's Last Free Day changed for an inland rail move ([Rail Plan](/docs/api-docs/useful-info/entitlements) only) | | `container.transport.available` | The container is available for pickup at the destination | | `container.updated` | Container attributes changed (may include holds or availability updates) | ## How `pickup_lfd` is chosen The coalesced `pickup_lfd` field follows a fixed source priority. It does not pick the earliest date, and it does not follow the most recently updated source. 1. `import_deadlines.pickup_lfd_line` (shipping line) — used when present. 2. `import_deadlines.pickup_lfd_terminal` (POD terminal) — used when there is no line LFD. 3. `import_deadlines.pickup_lfd_rail` (rail carrier at the inland destination) — used when neither of the above is present. Rail Plan only. The line LFD wins even when the terminal LFD is sooner. If your demurrage exposure is driven by whichever LFD lands first, do not rely on `pickup_lfd` alone. Subscribe to `container.pickup_lfd_line.changed` and `container.pickup_lfd_terminal.changed` (and `container.pickup_lfd_rail.changed` for rail moves), read the individual `import_deadlines.*` fields from the included container, and compare them yourself to pick the earliest date. ## Handle LFD change events When a `container.pickup_lfd.changed` event fires, the `included` array contains the full container object with the updated `pickup_lfd` field: ```javascript theme={null} app.post("/webhooks/terminal49", async (req, res) => { const { data, included } = req.body; const event = data.attributes.event; if (event === "container.pickup_lfd.changed") { const container = included.find((obj) => obj.type === "container"); const shipment = included.find((obj) => obj.type === "shipment"); const containerNumber = container.attributes.number; const newLfd = container.attributes.pickup_lfd; const bolNumber = shipment.attributes.bill_of_lading_number; const daysUntilLfd = Math.ceil( (new Date(newLfd) - new Date()) / (1000 * 60 * 60 * 24) ); if (daysUntilLfd <= 2) { try { await alertDispatch({ priority: "urgent", message: `Container ${containerNumber} (${bolNumber}) — LFD is ${newLfd}, ${daysUntilLfd} days away`, }); } catch (err) { console.error("Failed to dispatch LFD alert", err); } } } res.sendStatus(200); }); ``` ## Determine pickup readiness A container is ready for pickup when `available_for_pickup` is `true` **and** there are no active holds. Combine the `container.transport.available` event with hold checking: ```javascript theme={null} function isReadyForPickup(container) { const { available_for_pickup, holds_at_pod_terminal } = container.attributes; const hasActiveHolds = holds_at_pod_terminal?.some( (h) => h.status === "hold" ); return available_for_pickup === true && !hasActiveHolds; } ``` For a deeper explanation of holds, fees, and the `available_for_pickup` field, see [Container Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). Subscribe to both `container.transport.available` and `container.updated` events. The `available` event fires when the terminal first marks the container as released, but subsequent hold changes arrive as `container.updated` events. ## Common patterns * **Demurrage prevention** — alert when LFD is within 48 hours and the container has not been dispatched * **Automated dispatch** — trigger a drayage order as soon as the container clears holds * **LFD comparison** — for demurrage planning, compare `import_deadlines.pickup_lfd_line`, `import_deadlines.pickup_lfd_terminal`, and (for rail) `import_deadlines.pickup_lfd_rail` yourself and act on the earliest date, since the coalesced `pickup_lfd` field follows a fixed priority instead (see [How `pickup_lfd` is chosen](#how-pickup_lfd-is-chosen)) * **Hold monitoring** — watch for `container.updated` events where `holds_at_pod_terminal` changes ## Related * [Container Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees) — full guide to hold and fee fields * [Event catalog](/docs/api-docs/webhooks/event-catalog) — all available events * [Payload examples](/docs/api-docs/useful-info/webhook-events-examples) — complete JSON payloads # Track container milestones with webhooks Source: https://terminal49.com/docs/api-docs/webhooks/use-cases/milestone-tracking Build a complete container journey timeline by subscribing to Terminal49 transport milestone webhook events from origin through to final destination. A container's journey from origin to destination passes through a predictable sequence of milestones. Terminal49 sends a webhook event for each milestone so you can build a real-time timeline without polling. ## The container journey ```mermaid theme={null} graph LR A[Empty Out] --> B[Full In] B --> C[Vessel Loaded] C --> D[Vessel Departed] D --> E[Vessel Arrived] E --> F[Discharged] F --> G[Available] G --> H[Full Out] H --> I[Empty In] ``` For containers with transshipments, feeder vessels, or inland rail moves, additional milestone events fire at each intermediate point. ## Events in journey order ### Origin to vessel | Event | Milestone | | ------------------------------------- | ----------------------------------------- | | `container.transport.empty_out` | Empty container picked up at origin | | `container.transport.full_in` | Full container gated in at port of lading | | `container.transport.vessel_loaded` | Loaded onto vessel | | `container.transport.vessel_departed` | Vessel departed port of lading | ### Transshipment (if applicable) | Event | Milestone | | ---------------------------------------------- | -------------------------------- | | `container.transport.transshipment_arrived` | Arrived at transshipment port | | `container.transport.transshipment_discharged` | Discharged at transshipment port | | `container.transport.transshipment_loaded` | Loaded onto next vessel | | `container.transport.transshipment_departed` | Departed transshipment port | ### Destination | Event | Milestone | | --------------------------------------- | ----------------------------------- | | `container.transport.vessel_arrived` | Vessel arrived at port of discharge | | `container.transport.vessel_berthed` | Vessel berthed at port of discharge | | `container.transport.vessel_discharged` | Container discharged from vessel | | `container.transport.available` | Available for pickup | | `container.transport.full_out` | Picked up from terminal | | `container.transport.empty_in` | Empty returned | ### Rail (if applicable) | Event | Milestone | | --------------------------------------------------- | ----------------------------------- | | `container.transport.rail_loaded` | Loaded onto rail | | `container.transport.rail_departed` | Rail departed | | `container.transport.rail_arrived` | Rail arrived at inland ramp | | `container.transport.rail_unloaded` | Unloaded from rail | | `container.transport.arrived_at_inland_destination` | Arrived at final inland destination | ## Build a milestone timeline Each transport event webhook includes a `transport_event` object in the `included` array with the event type, timestamp, and location: ```javascript theme={null} app.post("/webhooks/terminal49", async (req, res) => { const { data, included } = req.body; const event = data.attributes.event; if (!event.startsWith("container.transport.")) { return res.sendStatus(200); } const transportEvent = included.find((obj) => obj.type === "transport_event"); const container = included.find((obj) => obj.type === "container"); const shipment = included.find((obj) => obj.type === "shipment"); const milestone = { containerNumber: container.attributes.number, bolNumber: shipment.attributes.bill_of_lading_number, event: event, timestamp: transportEvent.attributes.timestamp, timezone: transportEvent.attributes.timezone, location: transportEvent.attributes.location_locode, voyageNumber: transportEvent.attributes.voyage_number, }; await saveMilestone(milestone); res.sendStatus(200); }); ``` Transport event timestamps are stored in UTC. Use the `timezone` field to convert to local time. See [Event Timestamps](/docs/api-docs/in-depth-guides/event-timestamps) for details. ## Common patterns * **Customer portal** — display a visual timeline showing where each container is in its journey * **Dwell time tracking** — measure time between `vessel_arrived` and `full_out` to identify port delays * **Transit time analysis** — compare `vessel_departed` to `vessel_arrived` across carriers and routes * **Exception detection** — alert when a container has been at a milestone for longer than expected * **Export visibility** — track `empty_out` through `vessel_departed` for outbound shipments ## Related * [Event catalog](/docs/api-docs/webhooks/event-catalog) — full list of events with descriptions * [Container statuses](/docs/api-docs/in-depth-guides/container-statuses) — how Terminal49 derives container status from milestones * [Rail integration](/docs/api-docs/in-depth-guides/rail-integration-guide) — details on rail-specific tracking data * [Routing data](/docs/api-docs/in-depth-guides/routing) — vessel and container route information # Coverage changelog Source: https://terminal49.com/docs/coverage/changelog Changes to Terminal49 data coverage: new ocean carriers, ports and terminals, and rail carriers, plus anything removed from the live set. Coverage changes as carriers and terminals come online. This page records what was added to or removed from the live set, so you can audit coverage for your lanes over time. For product and API changes, see the [API changelog](/docs/updates/home). The public terminal table now lists 136 direct integrations, up from 102 on August 14. * 12 Australian terminal integrations are public defaults across Adelaide, Brisbane, Fremantle, Melbourne, and Sydney. * 26 global terminal integrations have shipped. They add 24 terminals to the public table because the two Jeddah integrations are additional sources for terminals that were already listed. * PortConnect integrations for Lyttelton and Timaru shipped but remain disabled and non-default pending commercial entitlement and production credentials. They are not included in the public terminal table. No API endpoint or schema changes are required. First publication of the live coverage list, generated directly from production integrations. **Ocean carriers** * 36 shipping lines in the public default set, trackable by bill of lading, booking, or container number * Account-enabled carriers listed separately: Seaboard Marine (`SMLU`) and Trailer Bridge (`TRBR`) * Newly documented lines that were missing from the previous list: KMTC (`KMTU`), World Direct Shipping (`WDSB`), and SeaLead (`SQQY`) **Ports & terminals** * 102 container terminals live, searchable by port, UN/LOCODE, and FIRMS code * Newly documented coverage that was missing from the previous list, including Montreal, Mexico, Felixstowe, Wilmington (NC), Hong Kong, Jebel Ali, Hamburg, Livorno, Dammam, Jeddah, Salalah, Barcelona, Naples, Chittagong, Gdynia, Guayaquil, and Umm Qasr * Removed from the supported list: Boston (Conley), Saint John, Fraser Surrey, and Florida International Terminal (Port Everglades Terminal remains supported) **Rail carriers** * All six North American Class I railroads confirmed live: BNSF, CN, CP, CSX, Norfolk Southern, and Union Pacific # Field availability Source: https://terminal49.com/docs/coverage/fields Reference for which shipment, container, and milestone fields the Terminal49 API returns, and whether each field is always present or source-dependent. The tables below describe the fields you can retrieve, and whether a field is always present, carrier-dependent, terminal-dependent, or journey-dependent (for example inland rail). ## Shipment data Shipment data comes from the carrier. It contains the bill of lading details and references the related containers. | Data | Availability | More details | Notes | | ---------------------------------------------- | ------------------------------------ | ------------------------------------------- | -------------------------------------------------------- | | Port of Lading | Always | Port of Lading name, UN/LOCODE, timezone | | | Port of Discharge | Always | Port of Discharge name, UN/LOCODE, timezone | | | Final Destination beyond Port of Discharge | Carrier dependent, journey dependent | Destination name, UN/LOCODE, timezone | Only for inland moves provided or booked by the carrier. | | Listing of container numbers | Always | Container numbers with the attributes below | | | Bill of Lading Number | Always (you supply it) | BOL | | | Shipping Line Details | Always | SCAC, shipping line name | | | Voyage Details | Milestone-based | Vessel name, vessel IMO, voyage number | | | Estimated Time of Departure | Carrier dependent | Timestamp | | | Actual Time of Departure | Always | Timestamp | After departure | | Estimated Time of Arrival at Port of Discharge | Carrier dependent | Timestamp | | | Actual Time of Arrival at Port of Discharge | Always | Timestamp | After arrival | | Estimated Time of Arrival at Final Destination | Carrier dependent, journey dependent | Timestamp | Inland moves only | ## Container data Container data is combined from every source into one view. Some fields appear only after a milestone has passed. | Data | Availability | More details | Notes | | -------------------------------------- | ------------------------------------ | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | | Container Number | Always | number | | | Seal Number | Carrier dependent | number | | | Equipment Type | Always | Dry, reefer, open top, flat rack, tank, hard top | | | Equipment length | Always | 20, 40, 45, 50 | | | Equipment height | Always | Standard, high cube | | | Weight | Carrier dependent | Number | | | Terminal Availability | Always | Availability known, available for pickup | | | Holds | Terminal dependent | Array of statuses | Hold name (customs, freight, TMF, other, USDA) and status (pending, hold), plus any extra description | | Fees | Terminal dependent | Array of statuses | Fee type (demurrage, exam, other) and amount | | Last Free Day | Terminal dependent | Date | | | Arrived at Port of Discharge | Always | Once arrived | | | Discharged at Port of Discharge | Always | Once discharged | | | Full Out at Port of Discharge | Always | | | | Full out at final destination | Journey dependent | Non-port final destination only | | | Rail Loaded At Port of Discharge | Journey dependent | Non-port final destination only | | | Rail Departed At Port of Discharge | Journey dependent | Non-port final destination only | | | Rail Carrier SCAC at Port of Discharge | Journey dependent | Non-port final destination only | | | ETA for final destination | Carrier dependent, journey dependent | Non-port final destination only | | | ATA for final destination | Journey dependent | Non-port final destination only | | | LFD at final destination | Carrier dependent, journey dependent | Non-port final destination only | | ## Milestone event data When a milestone passes, Terminal49 can send a webhook. Container, shipment, vessel, location, and terminal objects use the fields above. See the [webhook event catalog](/docs/api-docs/webhooks/event-catalog) for payload examples. | Milestone data | Description | | -------------- | ----------------------------------------------------------- | | Event Name | Event name, for example `container.transport.vessel_loaded` | | Created At | When Terminal49 created the event | | Timestamp | When the event occurred | | Timezone | Timezone of the event | | Voyage Number | Voyage number of the vessel | | Container | Link to container data | | Shipment | Link to shipment data | | Vessel | Vessel the event occurred on | | Location | Where the event occurred | | Terminal | Terminal the event occurred at | ## Milestone events supported | Milestone | Event name | | --------------------------------------- | -------------------------------------------------------------- | | Vessel Loaded | container.transport.vessel\_loaded | | Vessel Departed | container.transport.vessel\_departed | | Vessel Arrived | container.transport.vessel\_arrived | | Vessel Berthed | container.transport.vessel\_berthed | | Vessel Discharged | container.transport.vessel\_discharged | | Empty Out | container.transport.empty\_out | | Full In | container.transport.full\_in | | Full Out | container.transport.full\_out | | Empty In | container.transport.empty\_in | | Rail Departed | container.transport.rail\_departed | | Rail Arrived | container.transport.rail\_arrived | | Rail Loaded | container.transport.rail\_loaded | | Rail Unloaded | container.transport.rail\_unloaded | | Transshipment Arrived | container.transport.transshipment\_arrived | | Transshipment Discharged | container.transport.transshipment\_discharged | | Transshipment Loaded | container.transport.transshipment\_loaded | | Transshipment Departed | container.transport.transshipment\_departed | | Feeder Arrived | container.transport.feeder\_arrived | | Feeder Discharged | container.transport.feeder\_discharged | | Feeder Loaded | container.transport.feeder\_loaded | | Feeder Departed | container.transport.feeder\_departed | | Arrived at inland destination | container.transport.arrived\_at\_inland\_destination | | Estimated Arrived at inland destination | container.transport.estimated.arrived\_at\_inland\_destination | | Pickup LFD changed | container.pickup\_lfd.changed | | Available at Destination | container.transport.available | # Data coverage Source: https://terminal49.com/docs/coverage/home Overview of the ocean carriers, ports and terminals, rail carriers, and vessels Terminal49 tracks, and what data each source returns to the API. Terminal49 is a single Automated Container Tracking API for bills of lading, bookings, and container numbers. Behind it are direct integrations with ocean carriers, port terminal operators, and North American rail carriers, plus AIS vessel data — normalized into one data model so you integrate once and get every source. This section lists exactly what is live in production, so you can verify coverage for your lanes before you write a line of code. Terminal coverage reflects production as of 15 September 2026; the [coverage changelog](/docs/coverage/changelog) records additions and removals. Use [`GET /shipping_lines`](/docs/api-docs/api-reference/shipping-lines/shipping-lines) for the current carrier list on your account. Direct integrations with 36+ shipping lines (VOCCs). Filter by name, SCAC, or track-by type. Global port and terminal mapping on every route, plus 136 direct terminal integrations. All six North American Class I railroads that move containers, with inland milestones and LFD. Vessel identity on every voyage, plus AIS position and projected route with the Routing Data entitlement. New carriers and terminals as they come online, and anything removed from the live set. ## Getting the coverage list programmatically Coverage is documented on the pages linked above. There is no downloadable spreadsheet, Google Sheet, or Excel export of the carrier or terminal matrix. If you need a machine-readable list, query it from the API: * [`GET /shipping_lines`](/docs/api-docs/api-reference/shipping-lines/shipping-lines) returns every ocean carrier enabled on your account, with SCAC and supported tracking number types. * [`GET /metro_areas/{unlocode_or_id}`](/docs/api-docs/api-reference/metro-areas/get-a-metro-area-using-the-unlocode-or-the-id) returns terminal details for a given port. ## Data sources * **Ocean carriers (shipping lines / VOCCs):** bill of lading and booking details, vessel ETAs, containers, and transport milestones. See [ocean carriers](/docs/coverage/ocean-carriers). * **Ports & terminals:** global mapping of origin, destination, transshipment, and inland locations on every route, plus availability, last free day (LFD), holds, and fees straight from the terminal operator. See [ports & terminals](/docs/coverage/terminals). * **Rail carriers:** inland rail milestones across North America. See [rail carriers](/docs/coverage/rail). * **Vessels:** identity on the voyage, plus AIS position and projected route. See [vessels](/docs/coverage/vessels). ## Supported ocean carriers Terminal49 integrates directly with each supported shipping line and normalizes their data into a single API. Filter by name, Standard Carrier Alpha Code (SCAC), or the number types you can send on a [tracking request](/docs/api-docs/getting-started/tracking-shipments-and-containers). 36 public lines with support for bill of lading, booking, or container tracking, depending on the carrier. Includes account-enabled carriers and known field gaps. ### Account-enabled carriers Some carriers are live in production but not in the public default set. Contact Terminal49 to enable them on your account. See [account-enabled carriers](/docs/coverage/ocean-carriers#account-enabled-carriers) for Seaboard Marine (`SMLU`) and Trailer Bridge (`TRBR`). ## Supported terminals Terminal49 maps origin, destination, transshipment, and inland ports and terminals globally on every route, and attributes carrier events — arrival, departure, loaded, discharged, transshipment — to the location where they occur. Direct terminal integrations add the operational answers on top: availability, holds, fees, and last free day, straight from the terminal operator. Global port and terminal mapping, plus 136 directly integrated terminals with UN/LOCODE and FIRMS codes. ## Rail carriers Tracking continues inland: North American rail milestones, rail ETA, and rail LFD arrive on the same container objects, covering all six Class I railroads — BNSF, CN, CP, CSX, Norfolk Southern, and Union Pacific. Carrier SCACs, inland events, rail LFD, and how rail data appears on the container. ## Known issues (ocean) Shipment data comes from the carrier, and some carriers do not publish every field. The gaps we see most often — missing seal numbers, weights, or equipment types — are listed on the [ocean carriers](/docs/coverage/ocean-carriers#known-issues-ocean) page. ## Data fields and availability The tables on [field availability](/docs/coverage/fields) describe the fields you can retrieve, and whether a field is always present, carrier-dependent, terminal-dependent, or journey-dependent (for example inland rail). ### Shipment data Shipment data comes from the carrier. It contains the bill of lading details and references the related containers. See [shipment fields](/docs/coverage/fields#shipment-data). ### Container data Container data is combined from every source into one view. Some fields appear only after a milestone has passed. See [container fields](/docs/coverage/fields#container-data). ### Milestone event data When a milestone passes, Terminal49 can send a webhook. See [milestone fields](/docs/coverage/fields#milestone-event-data) and the [webhook event catalog](/docs/api-docs/webhooks/event-catalog). ### Milestone events supported The full milestone list — vessel, terminal, rail, transshipment, and inland — is on [field availability](/docs/coverage/fields#milestone-events-supported). # Ocean carriers Source: https://terminal49.com/docs/coverage/ocean-carriers Ocean carriers (shipping lines / VOCCs) Terminal49 integrates with directly, including SCAC, track-by type, account-enabled lines, and known field gaps. Ocean carriers — also called shipping lines or vessel-operating common carriers (VOCCs) — are the primary source of shipment data. Terminal49 integrates directly with each carrier below and normalizes bill of lading details, bookings, containers, vessel ETAs, and transport milestones into a single API. Track with a bill of lading number, booking number, or container number, depending on what the carrier supports. Filter the list by name, Standard Carrier Alpha Code (SCAC), or track-by type. Carrier coverage is account-specific and can change. Use [`GET /shipping_lines`](/docs/api-docs/api-reference/shipping-lines/shipping-lines) for the current list on your account. Have a tracking number but not the SCAC? The [Auto-Detect Carrier](/docs/api-docs/in-depth-guides/auto-detect-carrier) API identifies the carrier for you. Westwood shipments are tracked through Swire (`SSBF`); `WWSU` is an alternative SCAC on that line. ## Account-enabled carriers These carriers are live in production but are not part of the public default set. Contact Terminal49 to enable them on your account before sending tracking requests. ## Known issues (ocean) Shipment data comes from the carrier, and some carriers do not publish every field. These are the gaps we see most often: No container weight. No container seal number. Shipment departure and arrival events are not always available, depending on when the bill of lading is entered. No container seal number. No container seal number. No container weight. No container seal number. All dates are provided as dates, not datetimes. Terminal49 stores them as midnight at the event location when the location is available, otherwise midnight UTC. Only dry, reefer, and flatpack equipment types are mapped. No departure or arrival events. Departure and arrival timestamps can still be present. No container seal number. Only dry and reefer equipment types are mapped. When a bill of lading has multiple containers, the returned weight is the shipment average (gross weight divided by container count). No container type. No container weight. No container seal number. No container weight. Only dry equipment types are mapped. # Rail carriers Source: https://terminal49.com/docs/coverage/rail List of North American Class I and short-line rail carriers Terminal49 tracks for inland container milestones, rail last free day, and destination ETA. Terminal49 keeps tracking after the container leaves the ocean terminal. For inland moves on North American rail, the same container and shipment objects pick up rail milestones, the rail carrier's ETA, and the rail last free day — no separate tracking request needed. Rail last free day data (`import_deadlines.pickup_lfd_rail` and the `container.pickup_lfd_rail.changed` webhook) requires the Rail Plan on your account. See [entitlements](/docs/api-docs/useful-info/entitlements). ## North American Class I rail Terminal49 covers all six Class I railroads that move intermodal containers in North America: | Carrier | SCAC | | ------------------------- | ---- | | BNSF Railway | BNSF | | Canadian National Railway | CNRU | | Canadian Pacific Railway | CPRS | | CSX Transportation | CSXT | | Norfolk Southern | NSRR | | Union Pacific | UPRR | ## What you get * Rail loaded, departed, arrived, and unloaded events * Arrival at the inland destination, with the rail carrier's ETA and ATA * Rail last free day (LFD) at the inland facility — requires the [Rail Plan](/docs/api-docs/useful-info/entitlements) * Pickup availability after unload * The rail carrier SCAC at port of discharge and at the inland destination (they can differ) See the [rail integration guide](/docs/api-docs/in-depth-guides/rail-integration-guide) for webhook names, container attributes, and API vs DataSync setup. ## Rail events ```mermaid theme={null} graph LR A[Rail Loaded] --> B[Rail Departed] B --> C[Arrived at Inland Destination] C --> D[Rail Unloaded] D --> G[Available for Pickup] G --> E[Full Out] E --> F[Empty Return] ``` | Event | Webhook | | ----------------------------- | ------------------------------------------------------------- | | Rail loaded | `container.transport.rail_loaded` | | Rail departed | `container.transport.rail_departed` | | Rail arrived | `container.transport.rail_arrived` | | Arrived at inland destination | `container.transport.arrived_at_inland_destination` | | Rail unloaded | `container.transport.rail_unloaded` | | Rail LFD changed | `container.pickup_lfd_rail.changed` | | Inland ETA changed | `container.transport.estimated.arrived_at_inland_destination` | Some railroads do not share every event. The events above are the ones you should plan around. ## Inland ETA: rail vs shipping line For an inland move, two arrival views can differ: | Field | Lives on | Source | | ------------------------------------------- | --------- | ------------- | | `ind_eta_at` / `ind_ata_at` | container | Rail carrier | | `destination_eta_at` / `destination_ata_at` | shipment | Ocean carrier | Use the rail fields when you want the inland railroad's view. Use the shipment fields when you want the ocean carrier's view of the same destination. ## Rail last free day `pickup_lfd` on the container is coalesced from `import_deadlines`, in this order: 1. `pickup_lfd_line` — shipping line 2. `pickup_lfd_terminal` — port of discharge terminal 3. `pickup_lfd_rail` — rail carrier at the inland destination Subscribe to `container.pickup_lfd_rail.changed` when you need the inland railroad's LFD specifically. Rail LFD data requires the [Rail Plan](/docs/api-docs/useful-info/entitlements); without it, `pickup_lfd_rail` values and the webhook are not delivered. # Ports & terminals Source: https://terminal49.com/docs/coverage/terminals Ports and terminals Terminal49 maps on container routes, with direct terminal operator connections for availability, last free day, holds, and fees. Terminal49 covers ports and terminals at two levels: a global port and terminal dataset that maps every container's route, and direct terminal integrations that add the operational data you need to execute imports and exports. ## Global port and terminal mapping Every tracked container's route is mapped end to end against Terminal49's global port and terminal dataset — origin, destination, transshipment, and inland ports and terminals worldwide, each identified by UN/LOCODE. Transport events reported by the ocean carrier — loaded, departed, arrived, berthed, discharged, and transshipment moves — are attributed to the port and terminal where they occur. This mapping is global and applies to every shipment. You always know where a container is, which locations it moves through, and what happened at each one, on any lane the ocean carriers serve. ## Import and export execution data At the terminals below, Terminal49 also connects directly to the terminal operator. That connection adds the attributes that drive import and export execution at the gate: * **Pickup availability** — whether the container is ready to be picked up * **Last free day (LFD)** — when demurrage starts * **Holds** — customs, freight, TMF, USDA, and other holds with status * **Fees** — demurrage, exam, and other charges with amounts A port that is not in this table still appears on routes with carrier-reported events; the table below is where the terminal-level operational data comes from. Search by terminal, port, UN/LOCODE, or FIRMS code, or filter by region. Holds, fees, LFD, and pickup availability vary by terminal. A terminal on this list does not mean every field is present for every container — see [field availability](/docs/coverage/fields) for what each field depends on. Coverage reflects production as of 15 September 2026. The table contains 136 publicly available direct terminal integrations. ## Direct terminal integrations Boston (Conley), Saint John, and Fraser Surrey are not currently supported. At Port Everglades, Port Everglades Terminal is supported; Florida International Terminal is not. PortConnect integrations for Lyttelton and Timaru have shipped but are not publicly available. Both remain disabled and non-default pending commercial entitlement and production credentials, so neither terminal appears in this table. For how holds, fees, and LFD appear on a container, see [holds and fees](/docs/api-docs/in-depth-guides/holds-and-fees). # Vessels Source: https://terminal49.com/docs/coverage/vessels Vessel identity, AIS position, voyage schedules, and projected route data available on Terminal49 shipments and through the Vessels API endpoints. Every tracked shipment includes the vessel on the current voyage: name, IMO number, and voyage number. That identity comes from the ocean carrier and is available as soon as the carrier publishes the voyage. AIS position and projected route are available through the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id) with the Routing Data / vessel positions entitlement — a separate account enablement, not included in every paid plan. Without it, the position endpoints return `403 Forbidden`. See [entitlements](/docs/api-docs/useful-info/entitlements). ## What you get | Data | Source | Availability | | ---------------------------- | -------------- | ------------------------------------------------------------------------------------ | | Vessel name | Ocean carrier | On the shipment and transport events once the voyage is known | | IMO number | Ocean carrier | Same as vessel name | | Voyage number | Ocean carrier | Same as vessel name | | MMSI | AIS | Vessels API, Routing Data entitlement | | Current latitude / longitude | AIS | Vessels API, Routing Data entitlement | | Speed and heading | AIS | Vessels API, Routing Data entitlement | | Position timestamp | AIS | Vessels API, Routing Data entitlement | | Position history | AIS | Vessels API when `show_positions` is true | | Projected route | AIS + schedule | [Future positions](/docs/api-docs/api-reference/vessels/get-vessel-future-positions) | ## How to look up a vessel * [`GET /vessels/{id}`](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id) — Terminal49 vessel ID from a shipment or event * [`GET /vessels/{imo}`](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo) — when you already have the IMO number * [`GET /vessels/{id}/future_positions`](/docs/api-docs/api-reference/vessels/get-vessel-future-positions) — estimated positions between ports, one minute apart Use the [map](/docs/api-docs/in-depth-guides/terminal49-map) and [routing](/docs/api-docs/in-depth-guides/routing) guides when you want to plot the voyage. ## What vessel data is not AIS coverage is not a substitute for carrier milestones. A position update does not mean the container is loaded, discharged, or available. Treat AIS as the vessel's location; treat ocean, terminal, and rail events as the container's status. Terminal49 does not offer sailing schedule search. You cannot query vessels or departures by port, carrier, or date range. Vessel identity, voyage number, and schedule data are available per shipment, once you track a bill of lading, booking, or container number. # Terminal49 DataSync Documentation Source: https://terminal49.com/docs/datasync/home Learn how Terminal49 DataSync delivers shipment, container, and transport event data into your data warehouse, database, or spreadsheet automatically. Terminal49 offers two ways to track your shipments from origin to destination. 1. [Terminal49 DataSync](/docs/datasync/overview). Get tables full of fresh information delivered into your current data system. Easy to set up, and perfect for complementing your current data. 2. [Terminal49 API](/docs/api-docs/getting-started/start-here). Connect directly with the API, pull data for specific shipments and containers, and get updates via webhooks. If you already have a data store that feeds the rest of your system, DataSync is probably what you want. ## What can you use Terminal49 data for? Here are just a few of the data points Terminal49 returns and possible use-cases. | DATA | EXAMPLE USE CASE | | -------------------------------------- | ------------------------------------------------------------------------ | | Destination ETA | Surface ETA changes to your relevant teams as they're reported | | Last Free Day and terminal status¹ | Track containers approaching LFD and prioritize dispatching | | Fees and holds at destination terminal | Clear your cargo to keep you containers moving | | Actual departure and arrival times | Report journey times by route to compare your ocean carriers performance | *1. At container ports in the US* ## How it works All you need to provide are your BOL numbers and SCACs. Terminal49 looks up the shipment with the carrier and populates shipment details including containers. Once the shipment is set up, Terminal49 periodically checks with the carrier and the destination terminal. If any of the details of your shipment or containers change (for example, if the ETA changes), Terminal49 ensures you're always kept up to date. * If you're using DataSync, Terminal49 updates the data in your system * If you're using the API, Terminal49 posts the shipment to the webhook you provide 👈🏽 Select API Docs or DataSync on the left to get started. # DataSync Overview Source: https://terminal49.com/docs/datasync/overview See how Terminal49 DataSync syncs shipment, container, and tracking request data into your existing data warehouse, database, or spreadsheet systems. DataSync is the easiest way to get fresh, up-to-date container and shipment data into your system. DataSync will create 3 tables in your system, in the schema / dataset / folder / spreadsheet of your choice: [containers](/docs/datasync/table-properties/containers_rail), [shipments](/docs/datasync/table-properties/shipments), and [tracking\_requests](/docs/datasync/table-properties/tracking-requests). In addition to these 3 tables, DataSync also creates a technical table named [\_transfer\_status](/docs/datasync/table-properties/transfer-status), which tells you when each table was last refreshed. We can send the data to almost any database, data warehouse, or object store, as well as to Google Sheets. See the [full list of supported systems](/docs/datasync/supported-destinations). ## How often does the data update? DataSync will keep the data tables updated with a refresh every hour. Each refresh reloads only the rows that have been changed since the previous refresh, so you won't have excess writes to your system. To check when a table was last updated, check the [\_transfer\_status](/docs/datasync/table-properties/transfer-status) table. Each row in that table has a unique table key and the time when the latest sync occurred for that table. ## How to use the data You can use the container and shipment tracking data any way you like, but here are a couple ideas: * Send data directly to your visualization/analytics/reports software like PowerBI or Tableau * Send data directly to your TMS or ERP * Join data with one of your own tables * Use a Database View or Pivot Table to narrow down what you're looking at, or rename columns * Use Database Triggers to respond to updates ## The setup process If you're already tracking shipments with Terminal49, setup is a 3-step process that takes less than 2 hours on average. Simpler setups can be done in 20 minutes. See below for ways to get data into the system. 1. **Connect data systems**. This could mean doing role-based auth or sharing credentials for a single-purpose user. See the [security FAQ](https://help.terminal49.com/en/articles/7988732-security-considerations-for-terminal49-datasync) for more on how Terminal49 keeps your data secure. 2. **1-hour configuration call**. Terminal49 makes sure you're getting data the way you want, configuring it to fit how you store all your current data. 3. **Start querying the data**. And then you're ready to go! Nothing new to learn. Use the tools you already know, now with more data. [Schedule a call with the Terminal49 Customer Success team](https://meetings.hubspot.com/kyle-blount) to get started. ## How to start tracking shipments There are many ways you can start tracking shipments with Terminal49. They all require that you have the Booking Number or Master Bill of Lading number for the shipments you want to track. * [Send an email with a CSV to track@terminal49.com](https://help.terminal49.com/en/articles/5506959-how-to-add-shipments-via-email) * Upload a CSV through [the Terminal49 dashboard](https://app.terminal49.com/shipments/imports/new) * Input shipments directly through [the Terminal49 dashboard](https://app.terminal49.com/shipments/imports/new) * [Use the Terminal49 API to create TrackingRequests](/docs/api-docs/api-reference/tracking-requests/create-a-tracking-request) ## Getting started Schedule your call now! For current Terminal49 customers, [schedule a call with the Terminal49 Customer Support team](https://meetings.hubspot.com/kyle-blount) to get set up. If you're not yet a customer, [schedule a demo with the Terminal49 sales team](https://www.terminal49.com/contact) — they'll help you find the solution that's best for you. # DataSync Supported Destinations Source: https://terminal49.com/docs/datasync/supported-destinations See which databases, data warehouses, object stores, and spreadsheets Terminal49 DataSync supports as sync destinations for your tracking data. Terminal49 DataSync directly supports over a dozen different destinations out of the box. Tools like **Excel**, **Power BI**, and **many TMS and ERP systems** can read data from a database or data warehouse. We can feed data into your system and indirectly power those tools. Don’t see your supported database or tool? [Reach out](https://www.terminal49.com/contact). ## Spreadsheets * Google Sheets ## Databases * MariaDB * Microsoft SQL Server * MySQL * Postgres * SingleStore ## Data warehouses * Amazon Athena * Amazon Redshift * Clickhouse * Databricks * Firebolt * Google BigQuery * Snowflake ## Object store * Amazon S3 * Azure Blob Store * Cloudflare R2 * Google Cloud Storage ## Other systems If you have something like **Excel**, **Power BI/Tableau**, or a **TMS** or **ERP** system, contact your IT team to see what database, data warehouse, or object store is powering them. We can [securely](https://help.terminal49.com/en/articles/7988732-security-considerations-for-terminal49-datasync) feed data into most systems. # Deprecated DataSync Containers Table Source: https://terminal49.com/docs/datasync/table-properties/containers Reference the deprecated Terminal49 DataSync containers table schema. Migrate to the current rail-aware containers table for new and existing integrations. *This is a deprecated version of the `containers` table, used by DataSync customers before September 2024.* The `containers` table contains 1 row per container (`container_id` is the unique key). Each container is part of 1 shipment (`shipment_id`). This is a large table with denormalized columns to make it easy to use for reporting purposes. | COLUMN NAME | DESCRIPTION | TYPE | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | `container_id` | Container ID. This is the unique key of the table. | `text` | | `container_number` | Container number | `text` | | `shipment_id` | Shipment ID associated to the container | `text` | | `shipment_bill_of_lading_number` | Shipment number exactly as submitted in the tracking request. May differ from `shipment_normalized_number` when the carrier expects a different format. | `text` | | `shipment_normalized_number` | The normalized version of the shipment number that Terminal49 uses when querying the carrier. Some carriers require a specific format (for example a stripped prefix or padded digits), so this value may differ from `shipment_bill_of_lading_number`. | `text` | | `shipment_reference_numbers` | Reference numbers of the shipment, concatenated | `text` | | `container_reference_numbers` | Reference numbers of the container, concatenated | `text` | | `shipment_tags` | Tags added to the shipment, sorted by alphabetical order, concatenated and separated by a comma | `text` | | `customer_id` | Account ID of the customer | `text` | | `customer_name` | Name of the customer | `text` | | `shipping_line_scac` | Standard carrier alpha numeric code of the shipping line | `text` | | `shipping_line_name` | Name of the shipping line | `text` | | `origin_country_code` | Origin country code, populated only if the Empty Out event happens at a different location from the POL | `text` | | `origin_locode` | Origin UN/LOCODE, populated only if the Empty Out event happens at a different location from the POL | `text` | | `origin_city` | Origin city, populated only if the Empty Out event happens at a different location from the POL | `text` | | `origin_timezone` | Origin time zone, populated only if the Empty Out event happens at a different location from the POL | `text` | | `pol_country_code` | Port of Lading country code | `text` | | `pol_locode` | Port of Lading UN/LOCODE | `text` | | `pol_city` | Port of Lading city | `text` | | `pol_timezone` | Port of Lading time zone | `text` | | `pod_country_code` | Port of Discharge country code | `text` | | `pod_locode` | Port of Discharge UN/LOCODE | `text` | | `pod_city` | Port of Discharge city | `text` | | `pod_timezone` | Port of Discharge time zone | `text` | | `pod_terminal_firms_code` | Port of Discharge terminal firms code | `text` | | `pod_terminal_nickname` | Port of Discharge terminal nickname | `text` | | `pod_terminal_name` | Port of Discharge terminal name | `text` | | `destination_country_code` | Destination country code | `text` | | `destination_locode` | Destination UN/LOCODE | `text` | | `destination_city` | Destination city | `text` | | `destination_timezone` | Destination time zone | `text` | | `destination_terminal_firms_code` | Destination terminal firms code | `text` | | `destination_terminal_nickname` | Destination terminal nickname | `text` | | `destination_terminal_name` | Destination terminal name | `text` | | `pol_empty_out_at` | Empty Out, as a UTC timestamp | `timestamp` | | `pol_empty_out_at_local` | Empty Out, as a string in the POL local time zone | `text` | | `pol_full_in_at` | Full In event, as a UTC timestamp | `timestamp` | | `pol_full_in_at_local` | Full In event, as a string in the POL local time zone | `text` | | `origin_rail_loaded_at` | Origin Rail Loaded, as a UTC timestamp | `timestamp` | | `origin_rail_loaded_at_local` | Origin Rail Loaded, as a string in the origin local time zone | `text` | | `origin_rail_departed_at` | Origin Rail Departed, as a UTC timestamp | `timestamp` | | `origin_rail_departed_at_local` | Origin Rail Departed, as a string in the origin local time zone | `text` | | `pol_rail_arrived_at` | Port of Lading Rail Arrived, as a UTC timestamp | `timestamp` | | `pol_rail_arrived_at_local` | Port of Lading Rail Arrived, as a string in the origin local time zone | `text` | | `pol_rail_unloaded_at` | Port of Lading Rail Unloaded, as a UTC timestamp | `timestamp` | | `pol_rail_unloaded_at_local` | Port of Lading Rail Unloaded, as a string in the origin local time zone | `text` | | `pol_loaded_at` | Port of Lading Loaded event, as a UTC timestamp | `timestamp` | | `pol_loaded_at_local` | Port of Lading Loaded event, as a string in the POL local time zone | `text` | | `pol_etd_at` | Port of Lading Estimated Time of Departure, as a UTC timestamp | `timestamp` | | `pol_etd_at_local` | Port of Lading Estimated Time of Departure, as a string in the POL local time zone | `text` | | `pol_atd_at` | Port of Lading Actual Time of Departure, as a UTC timestamp | `timestamp` | | `pol_atd_at_local` | Port of Lading Actual Time of Departure, as a string in the POL local time zone | `text` | | `pod_eta_at` | Port of Discharge Estimated Time of Arrival, as a UTC timestamp | `timestamp` | | `pod_eta_at_local` | Port of Discharge Estimated Time of Arrival, as a string in the POD local time zone | `text` | | `pod_arrived_at` | Port of Discharge Actual Time of Arrival, as a UTC timestamp | `timestamp` | | `pod_arrived_at_local` | Port of Discharge Actual Time of Arrival, as a string in the POD local time zone | `text` | | `pod_berthed_at` | Port of Discharge Berthed event, as a UTC timestamp | `timestamp` | | `pod_berthed_at_local` | Port of Discharge Berthed event, as a string in the POD local time zone | `text` | | `pod_discharged_at` | Port of Discharge Discharged event, as a UTC timestamp | `timestamp` | | `pod_discharged_at_local` | Port of Discharge Discharged event, as a string in the POD local time zone | `text` | | `pod_last_free_day_on` | Current Last Free Day from the POD terminal, as a UTC timestamp | `timestamp` | | `pod_last_free_day_on_local` | Current Last Free Day from the POD terminal, as a string in the POD local time zone | `text` | | `ssl_last_free_day_on` | Current Last Free Day from the shipping line, as a UTC timestamp | `timestamp` | | `ssl_last_free_day_on_local` | Current Last Free Day from the shipping line, as a string in the Destination or POD local time zone | `text` | | `pod_pickup_appointment_at` | Port of Discharge Pickup Appointment, as a UTC timestamp | `timestamp` | | `pod_pickup_appointment_at_local` | Port of Discharge Pickup Appointment, as a string in the POD local time zone | `text` | | `pod_full_out_at` | Port of Discharge Full Out event, as a UTC timestamp | `timestamp` | | `pod_full_out_at_local` | Port of Discharge Full Out event, as a string in the POD local time zone | `text` | | `rail_loaded_at` | First rail loaded after the POD discharge, as a UTC timestamp | `timestamp` | | `rail_loaded_at_local` | First rail loaded after the POD discharge, as a string in the POD local time zone | `text` | | `rail_departed_at` | First rail departure after the POD discharge, as a UTC timestamp | `timestamp` | | `rail_departed_at_local` | First rail departure after the POD discharge, as a string in the POD local time zone | `text` | | `destination_eta_at` | Destination Estimated Time of Arrival, as a UTC timestamp | `timestamp` | | `destination_eta_at_local` | Destination Estimated Time of Arrival, as a string in the Destination local time zone | `text` | | `destination_arrived_at` | Destination Actual Time of Arrival, as a UTC timestamp | `timestamp` | | `destination_arrived_at_local` | Destination Actual Time of Arrival, as a string in the Destination local time zone | `text` | | `rail_unloaded_at` | Destination Rail Unloaded, as a UTC timestamp | `timestamp` | | `rail_unloaded_at_local` | Destination Rail Unloaded, as a string in the Destination local time zone | `text` | | `destination_full_out_at` | Destination Full Out event, as a UTC timestamp | `timestamp` | | `destination_full_out_at_local` | Destination Full Out event, as a string in the Destination local time zone | `text` | | `empty_terminated_at` | Container Empty Returned event, as a UTC timestamp | `timestamp` | | `empty_terminated_at_local` | Container Empty Returned event, as a string in the Destination or POD local time zone | `text` | | `fees_at_pod_terminal` | Current fee amounts, in JSON format. See [hold and fee types](/docs/api-docs/in-depth-guides/holds-and-fees). | `text` | | `demurrage_at_pod_terminal` | Current demurrage amount owed. See [fee types](/docs/api-docs/in-depth-guides/holds-and-fees#fee-types-at-a-glance). | `text` | | `holds_at_pod_terminal` | Current terminal hold statuses, in JSON format. See [hold types](/docs/api-docs/in-depth-guides/holds-and-fees#hold-types-at-a-glance). | `text` | | `freight_hold_at_pod_terminal` | Current freight hold, value is either "Hold", "Pending", or blank | `text` | | `customs_hold_at_pod_terminal` | Current customs hold, value is either "Hold", "Pending", or blank | `text` | | `usda_hold_at_pod_terminal` | Current USDA hold, value is either "Hold", "Pending", or blank | `text` | | `tmf_hold_at_pod_terminal` | Current Traffic Mitigation Fee hold, value is either "Hold", "Pending", or blank | `text` | | `other_hold_at_pod_terminal` | Any other current hold, value is either "Hold", "Pending", or blank | `text` | | `location_at_pod_terminal` | Location at port of discharge terminal | `text` | | `availability_known` | Yes if Terminal49 is receiving availability status from the terminal, No otherwise. | `text` | | `available_for_pickup` | If availability\_known is Yes, then Yes if the container is available to be picked up at terminal, No otherwise | `text` | | `equipment_length` | Length of the container | `integer` | | `equipment_type` | Container type: Dry, Flat Rack, Open Top, Reefer, Tank, unknown | `text` | | `equipment_height` | Container height: High Cube, Standard, unknown | `text` | | `equipment` | Concatenation of the equipment\_length, equipment\_type, and equipment\_height | `text` | | `weight_in_lbs` | Weight of the containre in lbs | `integer` | | `seal_number` | Seal number of the container | `text` | | `pod_full_out_chassis_number` | The chassis number used when container was picked up at POD, if available | `text` | | `pol_voyage_number` | Voyage number of the vessel that departed or will depart from the POL | `text` | | `pol_vessel_name` | Name of the vessel that departed or will depart from the POL | `text` | | `pol_vessel_imo` | IMO of the vessel that departed or will depart from the POL | `text` | | `pod_voyage_number` | Voyage number of the vessel that arrived or will arrive at the POD | `text` | | `pod_vessel_name` | Name of the vessel that arrived or will arrive at the POD | `text` | | `pod_vessel_imo` | IMO of the vessel that arrived or will arrive at the POD | `text` | | `terminal_checked_at` | When the terminal was last checked, as a UTC timestamp | `timestamp` | | `line_tracking_last_succeeded_at` | When the shipment information was last refreshed from the shipping line, as a UTC timestamp | `timestamp` | | `line_tracking_stopped_at` | When the tracking of the container stopped, as a UTC timestamp | `timestamp` | | `line_tracking_stopped_reason` | The reason Terminal49 stopped the tracking | `text` | | `created_at` | When the container was added, as a UTC timestamp | `timestamp` | | `updated_at` | When the container was last updated, as a UTC timestamp | `timestamp` | # Containers Table Source: https://terminal49.com/docs/datasync/table-properties/containers_rail Reference the Terminal49 DataSync containers table schema, including rail-aware tracking fields, column definitions, data types, and relationships. The `containers` table contains 1 row per container (`container_id` is the unique key). Each container is part of 1 shipment (`shipment_id`). This is a large table with denormalized columns to make it easy to use for reporting purposes. For each **event timestamp** there are 2 columns : * a `timestamp` type column in the UTC time zone (Universal Time Coordinated), e.g., `pol_loaded_at`. * a `text` type column in the local time zone of where the event happened, e.g., `pol_loaded_at_local`. The format of the text is : `YYYY-MM-DD HH:MI:SS`. For example `2024-09-24 17:25:00` for 5:25 PM on September 24, 2024. Depending on the event, the time zone applied can be the one from the Port of Lading (`pol_timezone`), the Port of Discharge (`pod_timezone`), or the Inland Destination (`ind_timezone`). *Columns marked with \* are only included with the Intermodal Rail product.* | COLUMN NAME | DESCRIPTION | TYPE | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | `container_id` | Container ID. This is the unique key of the table. | `text` | | `container_number` | Container number | `text` | | `shipment_id` | Shipment ID associated to the container | `text` | | `shipment_bill_of_lading_number` | Shipment number exactly as submitted in the tracking request. May differ from `shipment_normalized_number` when the carrier expects a different format. | `text` | | `shipment_normalized_number` | The normalized version of the shipment number that Terminal49 uses when querying the carrier. Some carriers require a specific format (for example a stripped prefix or padded digits), so this value may differ from `shipment_bill_of_lading_number`. | `text` | | `shipment_reference_numbers` | Reference numbers of the shipment, concatenated | `text` | | `container_reference_numbers` | Reference numbers of the container, concatenated | `text` | | `shipment_tags` | Tags added to the shipment, sorted by alphabetical order, concatenated and separated by a comma | `text` | | `customer_id` | Account ID of the customer | `text` | | `customer_name` | Name of the customer | `text` | | `shipping_line_scac` | Standard carrier alpha numeric code of the shipping line | `text` | | `shipping_line_name` | Name of the shipping line | `text` | | `pol_country_code` | Port of Lading country code | `text` | | `pol_locode` | Port of Lading UN/LOCODE | `text` | | `pol_city` | Port of Lading city | `text` | | `pol_timezone` | Port of Lading time zone | `text` | | `pod_country_code` | Port of Discharge country code | `text` | | `pod_locode` | Port of Discharge UN/LOCODE | `text` | | `pod_city` | Port of Discharge city | `text` | | `pod_timezone` | Port of Discharge time zone | `text` | | `pod_terminal_firms_code` | Port of Discharge terminal firms code | `text` | | `pod_terminal_nickname` | Port of Discharge terminal nickname | `text` | | `pod_terminal_name` | Port of Discharge terminal name | `text` | | `ind_country_code` | Inland Destination country code | `text` | | `ind_locode` | Inland Destination UN/LOCODE | `text` | | `ind_city` | Inland Destination city | `text` | | `ind_timezone` | Inland Destination time zone | `text` | | `ind_terminal_firms_code` | Inland Destination terminal firms code | `text` | | `ind_terminal_nickname` | Inland Destination terminal nickname | `text` | | `ind_terminal_name` | Inland Destination terminal name | `text` | | `empty_out_at` | Empty Out, as a UTC timestamp | `timestamp` | | `empty_out_at_local` | Empty Out, as a string in the POL local time zone | `text` | | `full_in_at` | Full In event, as a UTC timestamp | `timestamp` | | `full_in_at_local` | Full In event, as a string in the POL local time zone | `text` | | `pol_loaded_at` | Port of Lading Loaded event, as a UTC timestamp | `timestamp` | | `pol_loaded_at_local` | Port of Lading Loaded event, as a string in the POL local time zone | `text` | | `pol_etd_at` | Port of Lading Estimated Time of Departure, as a UTC timestamp | `timestamp` | | `pol_etd_at_local` | Port of Lading Estimated Time of Departure, as a string in the POL local time zone | `text` | | `pol_atd_at` | Port of Lading Actual Time of Departure, as a UTC timestamp | `timestamp` | | `pol_atd_at_local` | Port of Lading Actual Time of Departure, as a string in the POL local time zone | `text` | | `pod_eta_at` | Port of Discharge Estimated Time of Arrival, as a UTC timestamp | `timestamp` | | `pod_eta_at_local` | Port of Discharge Estimated Time of Arrival, as a string in the POD local time zone | `text` | | `pod_arrived_at` | Port of Discharge Actual Time of Arrival, as a UTC timestamp | `timestamp` | | `pod_arrived_at_local` | Port of Discharge Actual Time of Arrival, as a string in the POD local time zone | `text` | | `pod_berthed_at` | Port of Discharge Berthed event, as a UTC timestamp | `timestamp` | | `pod_berthed_at_local` | Port of Discharge Berthed event, as a string in the POD local time zone | `text` | | `pod_discharged_at` | Port of Discharge Discharged event, as a UTC timestamp | `timestamp` | | `pod_discharged_at_local` | Port of Discharge Discharged event, as a string in the POD local time zone | `text` | | `pod_last_free_day_on` | Current Last Free Day from the POD terminal, as a UTC timestamp

Named `pickup_lfd` in the API | `timestamp` | | `pod_last_free_day_on_local` | Current Last Free Day from the POD terminal, as a string in the POD local time zone | `text` | | `ssl_last_free_day_on` | Current Last Free Day from the shipping line, as a UTC timestamp | `timestamp` | | `ssl_last_free_day_on_local` | Current Last Free Day from the shipping line, as a string in the Destination or POD local time zone | `text` | | `pod_pickup_appointment_at` | Port of Discharge Pickup Appointment, as a UTC timestamp

Named `pickup_appointment_at` in the API | `timestamp` | | `pod_pickup_appointment_at_local` | Port of Discharge Pickup Appointment, as a string in the POD local time zone | `text` | | `pod_full_out_at` | Port of Discharge Full Out event, as a UTC timestamp | `timestamp` | | `pod_full_out_at_local` | Port of Discharge Full Out event, as a string in the POD local time zone | `text` | | `pod_rail_carrier_scac`\* | SCAC of the rail carrier at the POD | `text` | | `pod_rail_loaded_at`\* | First rail loaded after the POD discharge, as a UTC timestamp | `timestamp` | | `pod_rail_loaded_at_local`\* | First rail loaded after the POD discharge, as a string in the POD local time zone | `text` | | `pod_rail_departed_at`\* | First rail departure after the POD discharge, as a UTC timestamp | `timestamp` | | `pod_rail_departed_at_local`\* | First rail departure after the POD discharge, as a string in the POD local time zone | `text` | | `ind_rail_carrier_scac`\* | SCAC of the rail carrier at the inland destination | `text` | | `ind_eta_at`\* | Inland Destination Estimated Time of Arrival, as a UTC timestamp | `timestamp` | | `ind_eta_at_local`\* | Inland Destination Estimated Time of Arrival, as a string in the Inland Destination local time zone | `text` | | `ind_arrived_at`\* | Inland Destination Actual Time of Arrival, as a UTC timestamp

Named `ind_ata_at` in the API | `timestamp` | | `ind_arrived_at_local`\* | Inland Destination Actual Time of Arrival, as a string in the Inland Destination local time zone | `text` | | `ind_rail_unloaded_at`\* | Inland Destination Rail Unloaded, as a UTC timestamp | `timestamp` | | `ind_rail_unloaded_at_local`\* | Inland Destination Rail Unloaded, as a string in the Inland Destination local time zone | `text` | | `ind_last_free_day_on`\* | Last Free Day at the inland destination facility from the rail carrier, as a UTC timestamp

Named `ind_facility_lfd_on` in the API | `timestamp` | | `ind_last_free_day_on_local`\* | Last Free Day at the inland destination facility from the rail carrier, as a string in the inland estination local time zone | `text` | | `ind_full_out_at` | Inland Destination Full Out event, as a UTC timestamp

Named `final_destination_full_out_at` in the API | `timestamp` | | `ind_full_out_at_local` | Inland Destination Full Out event, as a string in the Inland Destination local time zone | `text` | | `empty_terminated_at` | Container Empty Returned event, as a UTC timestamp | `timestamp` | | `empty_terminated_at_local` | Container Empty Returned event, as a string in the Destination or POD local time zone | `text` | | `fees_at_pod_terminal` | Current fee amounts at the POD terminal, in JSON format. See [hold and fee types](/docs/api-docs/in-depth-guides/holds-and-fees). | `text` | | `demurrage_at_pod_terminal` | Current demurrage amount owed at the POD terminal. See [fee types](/docs/api-docs/in-depth-guides/holds-and-fees#fee-types-at-a-glance). | `text` | | `holds_at_pod_terminal` | Current terminal hold statuses at the POD, in JSON format. See [hold types](/docs/api-docs/in-depth-guides/holds-and-fees#hold-types-at-a-glance). | `text` | | `freight_hold_at_pod_terminal` | Current freight hold at the POD terminal, value is either "Hold", "Pending", or blank | `text` | | `customs_hold_at_pod_terminal` | Current customs hold at the POD terminal, value is either "Hold", "Pending", or blank | `text` | | `usda_hold_at_pod_terminal` | Current USDA hold at the POD terminal, value is either "Hold", "Pending", or blank | `text` | | `tmf_hold_at_pod_terminal` | Current Traffic Mitigation Fee hold at the POD terminal, value is either "Hold", "Pending", or blank | `text` | | `other_hold_at_pod_terminal` | Any other current hold at the POD terminal, value is either "Hold", "Pending", or blank | `text` | | `location_at_pod_terminal` | Location at the port of discharge terminal | `text` | | `availability_known` | Yes if Terminal49 is receiving availability status from the POD terminal, No otherwise. | `text` | | `available_for_pickup` | If availability\_known is Yes, then Yes if the container is available to be picked up at the POD terminal, No otherwise | `text` | | `equipment_length` | Length of the container | `integer` | | `equipment_type` | Container type: Dry, Flat Rack, Open Top, Reefer, Tank, unknown | `text` | | `equipment_height` | Container height: High Cube, Standard, unknown | `text` | | `equipment` | Concatenation of the equipment\_length, equipment\_type, and equipment\_height | `text` | | `weight_in_lbs` | Weight of the containre in lbs | `integer` | | `seal_number` | Seal number of the container | `text` | | `pod_full_out_chassis_number` | The chassis number used when container was picked up at POD, if available | `text` | | `pod_voyage_number` | Voyage number of the vessel that arrived or will arrive at the POD | `text` | | `pod_vessel_name` | Name of the vessel that arrived or will arrive at the POD | `text` | | `pod_vessel_imo` | IMO of the vessel that arrived or will arrive at the POD | `text` | | `terminal_checked_at` | When the POD terminal was last checked, as a UTC timestamp | `timestamp` | | `line_tracking_last_succeeded_at` | When the shipment information was last refreshed from the shipping line, as a UTC timestamp | `timestamp` | | `line_tracking_stopped_at` | When the tracking of the container stopped, as a UTC timestamp | `timestamp` | | `line_tracking_stopped_reason` | The reason Terminal49 stopped the tracking | `text` | | `created_at` | When the container was added, as a UTC timestamp | `timestamp` | | `updated_at` | When the container was last updated, as a UTC timestamp | `timestamp` | # Shipments Table Source: https://terminal49.com/docs/datasync/table-properties/shipments Reference the Terminal49 DataSync shipments table schema, including column definitions, relationships to containers, and shipment-level tracking fields. The `shipments` table contains 1 row per shipment (`shipment_id` is the unique key). A shipment contains 1 or more containers. | COLUMN NAME | DESCRIPTION | TYPE | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | `shipment_id` | Shipment ID. This is the unique key of the table. | `text` | | `shipping_line_scac` | Standard carrier alpha numeric code of the shipping line | `text` | | `shipping_line_name` | Name of the shipping line | `text` | | `bill_of_lading_number` | Shipment number exactly as submitted in the tracking request. May differ from `normalized_number` when the carrier expects a different format. | `text` | | `normalized_number` | The normalized version of the shipment number that Terminal49 uses when querying the carrier. Some carriers require a specific format (for example a stripped prefix or padded digits), so this value may differ from `bill_of_lading_number`. | `text` | | `reference_numbers` | Reference numbers of the shipment, contatenated | `text` | | `tags` | Tags added to the shipment, sorted by alphabetical order, concatenated and separated by a comma | `text` | | `customer_id` | Account ID of the customer | `text` | | `customer_name` | Name of the customer | `text` | | `pol_locode` | Port of Lading UN/LOCODE | `text` | | `pod_locode` | Port of Discharge UN/LOCODE | `text` | | `pod_terminal_firms_code` | Port of Discharge terminal firms code | `text` | | `destination_locode` | Destination UN/LOCODE | `text` | | `destination_terminal_firms_code` | Destination terminal firms code | `text` | | `pol_atd_at` | Port of Lading Actual Time of Departure, as a UTC timestamp | `timestamp` | | `pol_etd_at` | Port of Lading Estimated Time of Departure, as a UTC timestamp | `timestamp` | | `pod_eta_at` | Port of Discharge Estimated Time of Arrival, as a UTC timestamp | `timestamp` | | `pod_arrived_at` | Port of Discharge Actual Time of Arrival, as a UTC timestamp | `timestamp` | | `pod_voyage_number` | Voyage number of the vessel that arrived or will arrive at the POD | `text` | | `pod_vessel_name` | Name of the vessel that arrived or will arrive at the POD | `text` | | `pod_vessel_imo` | IMO of the vessel that arrived or will arrive at the POD | `text` | | `line_tracking_last_succeeded_at` | When the shipment information was last refreshed from the shipping line, as a UTC timestamp | `timestamp` | | `line_tracking_stopped_at` | When the tracking of the shipment stopped, as a UTC timestamp | `timestamp` | | `line_tracking_stopped_reason` | Reason why the tracking of the shipment stopped | `text` | | `created_at` | When the shipment was added, as a UTC timestamp | `timestamp` | | `updated_at` | When the shipment was last updated, as a UTC timestamp | `timestamp` | # Tracking requests table Source: https://terminal49.com/docs/datasync/table-properties/tracking-requests Reference the Terminal49 DataSync tracking_requests table schema, including request status fields, retry-related columns, and carrier information. The `tracking_requests` table contains 1 row per tracking request (`tracking_request_id`is the unique key). A tracking request can fail or succeed (`status` column). A successful tracking request will lead to the creation of a shipment (`shipment_id`). There can be multiple tracking requests for the same requested number (possibly failing before finally succeeding). | COLUMN NAME | DESCRIPTION | TYPE | | --------------------- | ----------------------------------------------------------------------------------------------- | ----------- | | `tracking_request_id` | Tracking request ID. This is the unique key of the table. | `text` | | `request_number` | Number requested to be tracked | `text` | | `reference_numbers` | Reference numbers associated to the tracking request, concatenated | `text` | | `shipment_tags` | Tags added to the request, concatenated and separated by a comma | `text` | | `status` | Status of the tracking request: created, pending, awaiting\_manifest, failed, tracking\_stopped | `text` | | `failed_reason` | For tracking requests that failed, a description of the error | `text` | | `request_type` | Type of tracking request: bill\_of\_lading, booking\_number, or container | `text` | | `scac` | Standard carrier alpha numeric code of the shipping line | `text` | | `shipment_id` | If the tracking request succeeded, this is the ID of the shipment that was created | `text` | | `created_at` | When the tracking was requested, as a UTC timestamp | `timestamp` | | `updated_at` | When the tracking request was last updated, as a UTC timestamp | `timestamp` | # Transfer status table Source: https://terminal49.com/docs/datasync/table-properties/transfer-status Reference the Terminal49 DataSync _transfer_status table to monitor data refresh timing, last sync timestamps, and per-table sync health status. The `_transfer_status` is an additional technical table that identifies when each table was last updated by DataSync. | COLUMN NAME | DESCRIPTION | TYPE | | -------------------------- | ------------------------------------------------- | ----------- | | `data_model_name` | Name of the table | `text` | | `transfer_last_updated_at` | When the latest sync happened, as a UTC timestamp | `timestamp` | # Transport events table Source: https://terminal49.com/docs/datasync/table-properties/transport-events Reference the Terminal49 DataSync transport_events table schema for shipment milestone timestamps, event type codes, and port or location data. The `transport_events` table contains 1 row per event (`id`is the unique key). An event is associated to a specific container (`container_id` is the foreign key). An event is a specific milestone in the container lifecycle: for example, when the container was loaded at the Port of Lading, or when the vessel arrived at the Port of Discharge. These events are provided as columns in the `containers` DataSync table, and as rows here in the `transport_events` table. You can use one or the other based on what is most practical for you. The `transport_events` table includes the transshipment events, which are not part of the `containers` table columns. This table does not provide any estimated future events. *The `transport_events` table is currently only provided to DataSync customers who request it.* *Rail events from the POD to the inland destination are only provided in the Intermodal Rail product : rail\_loaded, rail\_departed, rail\_arrived, arrived\_at\_inland\_destination, rail\_unloaded, pickup\_lfd.changed.* | COLUMN NAME | DESCRIPTION | TYPE | | ------------------------- | ------------------------------------------------------------------------------------------------- | ----------- | | `id` | Transport Event ID. This is the unique key of the table. | `text` | | `event` | Name of the transport event. For example: container.transport.vessel\_departed | `text` | | `event_timestamp` | When the event happened, as a UTC timestamp | `timestamp` | | `event_timestamp_local` | When the event happened, as a string in the local time zone | `text` | | `container_id` | ID of the container the event is associated to | `text` | | `container_number` | Number of the container the event is associated to | `text` | | `shipment_id` | ID of the shipment the event is associated to | `text` | | `shipment_number` | Number of the shipment the event is associated to | `text` | | `port_metro_id` | ID of the location where the event happened | `text` | | `port_metro_locode` | Locode of the location where the event happened | `text` | | `port_metro_country_code` | Country code of the location where the event happened | `text` | | `port_metro_city` | Name of the location where the event happened | `text` | | `port_metro_time_zone` | Name of the time zone where the event happened | `text` | | `facility_id` | ID of the facility (terminal) where the event happened | `text` | | `facility_firms_code` | Firms code of the facility (terminal) where the event happened | `text` | | `facility_nickname` | Nickname of the facility (terminal) where the event happened | `text` | | `facility_name` | Name of the facility (terminal) where the event happened | `text` | | `vessel_id` | ID of the vessel associated to the event | `text` | | `vessel_name` | Name of the vessel associated to the event | `text` | | `vessel_imo` | IMO of the vessel associated to the event | `text` | | `vessel_mmsi` | MMSI of the vessel associated to the event | `text` | | `voyage_number` | Voyage number associated to the event | `text` | | `data_source_label` | Data source of the event: shipping\_line, terminal, ais, rail, t49\_operations\_team, user\_input | `text` | | `invalidated_at` | When the event was marked as invalid, as a UTC timestamp | `timestamp` | | `invalidation_reason` | Reason why the event was marked as invalid | `text` | | `previous_version_id` | If the event replaces an invalidated event, this is the ID of the invalidated event | `text` | | `created_at` | When the event was originally added, as a UTC timestamp | `timestamp` | | `updated_at` | When the event was updated, as a UTC timestamp | `timestamp` | # Terminal49 Developer Documentation Source: https://terminal49.com/docs/home Developer documentation for the Terminal49 container tracking API, DataSync data pipelines, TypeScript SDK, and MCP server for AI integrations.
Terminal49 Developer Docs

Automated container tracking from empty-out at origin to empty-return at destination across ocean carrier, terminal, rail, and vessel data sources.

Build with Terminal49

Track shipments by Bill of Lading, container number, or booking. Integrate data from ocean carriers, rail terminals, port terminals, and vessel positions into a single stream.

Data sources & capabilities

Integrated data from ocean carriers, port terminals, rail providers, and vessel positions — normalized into a single stream for every container.

# Terminal49 MCP Server Source: https://terminal49.com/docs/mcp/home Connect Claude, ChatGPT, Cursor, Copilot, or any MCP client to the Terminal49 MCP server to query live shipment tracking data with OAuth — no API key required. Use the Terminal49 MCP server to let Claude, ChatGPT, Cursor, Microsoft Copilot, or any MCP client answer questions with live container and shipment data—without writing custom glue code. ## TL;DR – get started in 5 minutes Follow the setup guide for your tool: * [Claude](/docs/mcp/setup/claude) (claude.ai, Claude Desktop — [available in the Claude Directory](https://claude.ai/directory/connectors/terminal49)) * [Claude Code](/docs/mcp/setup/claude-code) * [ChatGPT](/docs/mcp/setup/chatgpt) * [Cursor](/docs/mcp/setup/cursor) * [Microsoft Copilot](/docs/mcp/setup/microsoft-copilot) (Copilot Studio) * [VS Code](/docs/mcp/setup/vs-code) (GitHub Copilot) * [Agent plugins](/docs/mcp/setup/agent-plugins) (Claude Code, Cursor, Codex, GitHub Copilot CLI) * [Other MCP clients](/docs/mcp/setup/other-clients) For Claude, add Terminal49 from the [Claude Directory](https://claude.ai/directory/connectors/terminal49). For other clients, point the client at `https://mcp.terminal49.com`. No API key needed. The server supports OAuth 2.1: your client opens a browser window, you sign in with your Terminal49 credentials and approve access. That's it. > "Using the Terminal49 MCP server, search for container CAIU1234567 and summarize its status." > "List the tools available in the Terminal49 MCP server and what they're for." Need test container numbers? See [Test Numbers](/docs/api-docs/useful-info/test-numbers) for containers you can use during development. For the full walkthrough (including local stdio dev, deployment, and SDK examples), see [MCP Server Quickstart](/docs/api-docs/in-depth-guides/mcp). *** ## Transports | Transport | Endpoint | Best For | | ----------------- | --------------------------------- | -------------------------------- | | HTTP (streamable) | `POST https://mcp.terminal49.com` | Serverless, short-lived requests | **Authentication**: * **OAuth 2.1 (recommended)** – no API key needed. Add the connector URL, sign in with your Terminal49 credentials in the browser window your client opens, and approve access. Clients discover the authorization server (`https://auth.terminal49.com`) automatically and use authorization code with PKCE and Dynamic Client Registration. * **API key (for clients without OAuth support)** – create a key in the [developer portal](https://app.terminal49.com/developers/api-keys) and pass `Authorization: Token YOUR_API_KEY`. Use the `Token` scheme for API keys; the `Bearer` scheme is used for OAuth access tokens, which OAuth clients obtain automatically. * The [local stdio server](/docs/api-docs/in-depth-guides/mcp#local-stdio-development) reads the `T49_API_TOKEN` environment variable instead. Connector URL: `https://mcp.terminal49.com`. It is the canonical OAuth resource identifier, so OAuth clients (ChatGPT, Claude connectors) bind to the correct token audience. ## Setup guides Add Terminal49 from the Claude Directory One `claude mcp add` command from your terminal Install from the ChatGPT Plugins Directory `mcp.json` or Cursor Settings → MCP Copilot Studio agent tools GitHub Copilot agent mode Terminal49 plugin for Claude Code, Cursor, Codex, and Copilot CLI Generic OAuth or API-key configuration ## Any MCP client Use the same hosted Streamable HTTP endpoint in any MCP-compatible client. With an OAuth-capable client, no credentials are needed — just the URL: ```json theme={null} { "url": "https://mcp.terminal49.com" } ``` If your client can't run a browser OAuth flow, pass an API key instead: ```json theme={null} { "url": "https://mcp.terminal49.com", "headers": { "Authorization": "Token YOUR_API_KEY" } } ``` See [Other MCP clients](/docs/mcp/setup/other-clients) for details on OAuth discovery and verification. The same [rate limits](/docs/api-docs/in-depth-guides/rate-limiting) apply to MCP endpoints as the REST API. *** ## Monitoring Self-hosted deployments can enable [Sentry MCP Monitoring](https://docs.sentry.io/ai/monitoring/mcp/) by setting `SENTRY_DSN`. This captures MCP tool calls, resource reads, prompt usage, performance spans, and errors in Sentry. Input and output recording is disabled by default. Leave `SENTRY_MCP_RECORD_INPUTS=false` and `SENTRY_MCP_RECORD_OUTPUTS=false` unless your Sentry project is approved to store shipment identifiers, references, and customer data. *** ## Tools reference Every tool is read-only except `track_container`, which creates a tracking request. If a container for the number is already in your account, `track_container` returns it instead of creating a new request; otherwise it creates a tracking request, so repeated calls before a container links can create additional requests. ### `search_container` Find containers by container number, BL, booking, or your own reference. This is the fastest way to locate containers. **Parameters** * `query` *(string, required)* – container number, BL, booking, or reference ```json theme={null} { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "search_container", "arguments": { "query": "CAIU1234567" } } } ``` ```json theme={null} { "containers": [ { "id": "abc-123-uuid", "containerNumber": "CAIU1234567", "status": "in_transit", "shippingLine": "Maersk", "podTerminal": "APM Terminals", "destination": "Los Angeles" } ], "shipments": [], "totalResults": 1 } ``` **Good for** * "Find this container and tell me where it is" * "Show all containers with reference PO-12345" **REST equivalent**: [GET /containers](/docs/api-docs/api-reference/containers/list-containers) with filters *** ### `track_container` Start tracking a new container. Creates a tracking request and returns container details. **Parameters** * `number` *(string, required)* – container number, BL, or booking number * `numberType` *(string, optional)* – override inference (`container`, `bill_of_lading`, `booking_number`) * `scac` *(string, optional)* – shipping line code, e.g., `MAEU` for Maersk * `refNumbers` *(string\[], optional)* – your reference numbers ```json theme={null} { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "track_container", "arguments": { "number": "CAIU1234567", "scac": "MAEU" } } } ``` **Good for** * "Track container CAIU1234567 with Maersk" * "Start tracking this new shipment" **REST equivalent**: [POST /tracking\_requests](/docs/api-docs/api-reference/tracking-requests/create-a-tracking-request) *** ### `get_container` Get detailed container information with flexible data loading. Choose what to include based on your question. **Parameters** * `id` *(uuid, required)* – Terminal49 container UUID * `include` *(string\[], optional)* – what to load: * `shipment` – routing, BOL, line, ref numbers (lightweight) * `pod_terminal` – terminal name, location (lightweight) * `transport_events` – adds `events.count`, `events.rail_events_count`, and `events.latest_event`; use `get_container_transport_events` for the full timeline The response may also include `_metadata` with factual details such as `includes_loaded`. ```json theme={null} { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_container", "arguments": { "id": "abc-123-uuid", "include": ["shipment", "pod_terminal"] } } } ``` ```json theme={null} { "id": "abc-123-uuid", "number": "CAIU1234567", "status": "available_for_pickup", "equipment": { "type": "40HC", "length": "40", "height": "high_cube" }, "location": { "currentLocation": "APM Terminals", "availableForPickup": true, "podArrivedAt": "2025-01-15T08:30:00Z", "podDischargedAt": "2025-01-16T14:20:00Z" }, "demurrage": { "pickupLfd": "2025-01-22", "holds": [], "fees": [] }, "shipment": { "id": "shipment-uuid", "billOfLading": "MAEU123456789", "shippingLineScac": "MAEU" } } ``` **Good for** * "What's the status of this container?" * "Is it available for pickup? Any holds?" * "When does demurrage start?" **REST equivalent**: [GET /containers/](/docs/api-docs/api-reference/containers/get-a-container) *** ### `get_container_transport_events` Get the full event timeline for a container's journey. **Parameters** * `id` *(uuid, required)* – Terminal49 container UUID ```json theme={null} { "totalEvents": 47, "eventCategories": { "vesselEvents": 8, "railEvents": 12, "terminalEvents": 18 }, "milestones": { "vesselLoadedAt": "2024-12-08T10:30:00Z", "vesselDepartedAt": "2024-12-09T14:00:00Z", "vesselArrivedAt": "2024-12-22T08:30:00Z", "dischargedAt": "2024-12-23T11:15:00Z" }, "timeline": [ { "event": "container.transport.vessel_loaded", "timestamp": "2024-12-08T10:30:00Z", "location": { "name": "Shanghai", "locode": "CNSHA" } } ] } ``` **Good for** * "Show me the journey timeline" * "What happened to this container?" * "How long was the rail portion?" **REST equivalent**: [GET /containers//transport\_events](/docs/api-docs/api-reference/containers/get-a-containers-transport-events) *** ### `get_shipment_details` Get shipment-level information including routing, BOL, and all containers. **Parameters** * `id` *(uuid, required)* – Terminal49 shipment UUID * `include_containers` *(boolean, optional)* – include container list (default: true) **Good for** * "Tell me about this shipment" * "What containers are on this BL?" * "Show me the routing" **REST equivalent**: [GET /shipments/](/docs/api-docs/api-reference/shipments/get-a-shipment) *** ### `get_supported_shipping_lines` List carriers supported by Terminal49 with their SCAC codes. **Parameters** * `search` *(string, optional)* – filter by name or SCAC **Good for** * "What carriers do you support?" * "What's the SCAC code for CMA CGM?" **REST equivalent**: [GET /shipping\_lines](/docs/api-docs/api-reference/shipping-lines/shipping-lines) *** ### `get_container_route` Get detailed multi-leg routing with vessel itinerary. This is a **paid feature**. If not enabled for your account, use `get_container_transport_events` for historical movement data instead. See [Entitlements and Paid Features](/docs/api-docs/useful-info/entitlements) for the related Routing Data entitlement. **Parameters** * `id` *(uuid, required)* – Terminal49 container UUID **Good for** * "What's the routing for this container?" * "Which transshipment ports?" * "What vessel is it on?" **REST equivalent**: [GET /containers/](/docs/api-docs/api-reference/containers/get-a-container) (route data is included with the container) *** ### `list_shipments` List shipments with optional filters and pagination. **Parameters** * `number` *(string, optional)* – shipment, booking, or Bill of Lading identifier * `tracking_stopped` *(boolean, optional)* – filter by whether shipping-line tracking has stopped * `include_containers` *(boolean, optional)* – include container relationships (default: false) * `page` *(number, optional)* – page number, starting at 1 * `page_size` *(number, optional)* – results per page (default: 25; maximum: 25) **Good for** * "List recent shipments" * "Find a shipment by booking or Bill of Lading number" **REST equivalent**: [GET /shipments](/docs/api-docs/api-reference/shipments/list-shipments) *** ### `list_containers` List containers with optional filters and pagination. **Parameters** * `include` *(string\[], optional)* – include `shipment`, `pod_terminal`, or both * `page` *(number, optional)* – page number, starting at 1 * `page_size` *(number, optional)* – results per page (default: 25; maximum: 25) **Good for** * "List containers in my account" * "List containers with their shipment and POD terminal details" **REST equivalent**: [GET /containers](/docs/api-docs/api-reference/containers/list-containers) *** ### `list_tracking_requests` List tracking requests with optional filters and pagination. **Parameters** * `request_number` *(string, optional)* – tracking request identifier * `status` *(string, optional)* – `created`, `pending`, `succeeded`, or `failed` * `scac` *(string, optional)* – four-letter shipping line SCAC * `page` *(number, optional)* – page number, starting at 1 * `page_size` *(number, optional)* – results per page (default: 25; maximum: 25) **Good for** * "Show failed tracking requests" * "List latest tracking activity" **REST equivalent**: [GET /tracking\_requests](/docs/api-docs/api-reference/tracking-requests/list-tracking-requests) *** ## Prompts reference Prompts are pre-built workflows that guide the AI through multi-step analysis. ### `track-shipment` Quick container tracking with optional carrier specification. **Arguments** * `container_number` *(string, required)* – e.g., `CAIU1234567` * `carrier` *(string, optional)* – SCAC code, e.g., `MAEU` **Try this in Claude:** > "Using Terminal49, track container CAIU1234567 and show me its current status, location, and ETA." *** ### `check-demurrage` Analyze demurrage/detention risk for a container. **Arguments** * `container_id` *(uuid, required)* – from `search_container` or `get_container` **Try this in Claude:** > "Using Terminal49, check demurrage risk for container CAIU1234567 and explain which fees apply and when." *** ### `analyze-delays` Identify delays and root causes in a container's journey. **Arguments** * `container_id` *(uuid, required)* – Container UUID **Try this in Claude:** > "Using Terminal49, analyze delays for container CAIU1234567 and tell me what caused them." *** ## Resources reference Resources provide static or dynamic data that AI clients can read. | Resource URI | Description | | -------------------------------------- | --------------------------------------- | | `terminal49://container/{id}` | Container data in markdown format | | `terminal49://docs/milestone-glossary` | Event/milestone reference documentation | ```json theme={null} { "jsonrpc": "2.0", "id": 1, "method": "resources/read", "params": { "uri": "terminal49://docs/milestone-glossary" } } ``` *** ## Not yet supported These Terminal49 API capabilities are available via the [SDK](/docs/api-docs/in-depth-guides/mcp#sdk-usage) but not yet exposed as MCP tools: | API | Description | Workaround | | ------------------- | -------------------------------- | ----------------------------------------------------------------------------------- | | `update_shipment` | Update shipment ref numbers/tags | Use [REST API](/docs/api-docs/api-reference/shipments/edit-a-shipment) | | `stop_tracking` | Stop tracking a shipment | Use [REST API](/docs/api-docs/api-reference/shipments/stop-tracking-shipment) | | `resume_tracking` | Resume tracking a shipment | Use [REST API](/docs/api-docs/api-reference/shipments/resume-tracking-shipment) | | `raw_events` | Get raw EDI event data | Use [REST API](/docs/api-docs/api-reference/containers/get-a-containers-raw-events) | | `refresh_container` | Force refresh container data | Use [REST API](/docs/api-docs/api-reference/containers/refresh-container) | | Webhooks | Real-time event notifications | Configure via the [webhooks guide](/docs/api-docs/in-depth-guides/webhooks) | Shipment/container list operations are available via MCP. Update/stop/resume tracking operations still require REST API or direct SDK usage. *** ## Related guides * [MCP Server Quickstart](/docs/api-docs/in-depth-guides/mcp) – Full setup, local dev, deployment * [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) – Same limits apply to MCP * [Test Numbers](/docs/api-docs/useful-info/test-numbers) – Containers for testing * [Webhooks](/docs/api-docs/in-depth-guides/webhooks) – Real-time updates (use with MCP for best results) # Install the Terminal49 Agent Plugin Source: https://terminal49.com/docs/mcp/setup/agent-plugins Install the Terminal49 plugin in Claude Code, Cursor, Codex, or GitHub Copilot CLI — MCP connection plus a container-tracking skill, with OAuth and no API key. Install the **Terminal49** plugin from the [Terminal49/agent-plugins](https://github.com/Terminal49/agent-plugins) marketplace to set up your coding agent in one step. The plugin configures the Terminal49 MCP server connection (`https://mcp.terminal49.com`) and adds a shared `container-tracking` skill, so you don't have to add the server manually. **No API key required.** The plugin contains no API keys or customer data. Authentication uses the same OAuth 2.1 browser flow as a direct MCP connection. Sign in with your Terminal49 credentials when your agent first connects. ## What the plugin provides * OAuth connection to `https://mcp.terminal49.com` — the same hosted MCP server the [other setup guides](/docs/mcp/home#setup-guides) configure directly * A shared `container-tracking` skill that teaches agents how to choose and sequence the Terminal49 tools when searching, tracking, and investigating container shipments * Workflow guidance for status, pickup readiness, holds, ETAs, routes, delays, and demurrage-risk questions * Guardrails around tracking-request creation, credentials, dates, and missing data ## Install ```sh theme={null} claude plugin marketplace add Terminal49/agent-plugins claude plugin install terminal49@terminal49 ``` The first time the Terminal49 tools are used, sign in through the browser OAuth flow. If you aren't prompted, run `/mcp`, select **terminal49**, then select **Authenticate**. Prefer adding the MCP server directly? See the [Claude Code guide](/docs/mcp/setup/claude-code). In Cursor chat, run: ```text theme={null} /add-plugin https://github.com/Terminal49/agent-plugins ``` Then select and install **Terminal49**. Cursor prompts you to connect the MCP server (the browser OAuth sign-in) when the plugin first needs it. Prefer adding the MCP server directly? See the [Cursor guide](/docs/mcp/setup/cursor). ```sh theme={null} codex plugin marketplace add Terminal49/agent-plugins codex plugin add terminal49@terminal49 ``` Copilot CLI reads the Claude-compatible marketplace included in the repository: ```sh theme={null} copilot plugin marketplace add Terminal49/agent-plugins copilot plugin install terminal49@terminal49 ``` ## Test the plugin After connecting your Terminal49 account, ask your agent: > "Where is container CAIU1234567?" > "Is this container ready for pickup, and are there any holds?" It should use tools such as `search_container`, `track_container`, and `get_container`. See the [tools reference](/docs/mcp/home#tools-reference) for the full list, and [Test Numbers](/docs/api-docs/useful-info/test-numbers) for containers you can use during development. ## Troubleshooting | Symptom | How to fix | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | Marketplace add fails | Confirm the marketplace reference is exactly `Terminal49/agent-plugins` (in Cursor, the full URL `https://github.com/Terminal49/agent-plugins`) | | Plugin installed but tools missing | Restart the agent session so it reloads the plugin's MCP configuration | | Sign-in window never opens | Pop-up blocked or no default browser — allow pop-ups and retry the authenticate action (in Claude Code, run `/mcp` and select **Authenticate**) | | `401 Unauthorized` after connecting | Re-authenticate from your client's MCP settings to refresh the OAuth token | For plugin sources, the shared skill, and contribution guidelines, see the [repository README](https://github.com/Terminal49/agent-plugins#readme). ## Related guides * [Claude Code](/docs/mcp/setup/claude-code) – Add the MCP server directly with `claude mcp add` * [Cursor](/docs/mcp/setup/cursor) – Add the MCP server directly via `mcp.json` * [MCP Overview](/docs/mcp/home) – Tools, prompts, and resources reference * [MCP Server Quickstart](/docs/api-docs/in-depth-guides/mcp) – Full setup, including API-key and local stdio options * [Other MCP clients](/docs/mcp/setup/other-clients) – Generic configuration for any client # Connect ChatGPT to Terminal49 Source: https://terminal49.com/docs/mcp/setup/chatgpt Install the Terminal49 plugin from the ChatGPT Plugins Directory to query live shipment and container tracking data — no API key needed. Connect ChatGPT to the Terminal49 MCP server so it can answer questions with live container and shipment tracking data. **No API key required.** The Terminal49 MCP server supports OAuth 2.1. When you connect, ChatGPT opens a browser sign-in page. Log in with your Terminal49 credentials to finish. ## Install Terminal49 from the Plugins Directory Terminal49 is an approved plugin in the [ChatGPT Plugins Directory](https://chatgpt.com/plugins/plugin_asdk_app_69f5795de4a48191a35dc5c448520676). Installing it from the directory needs no Developer mode and no manual server URL. Go to the [Terminal49 plugin in ChatGPT](https://chatgpt.com/plugins/plugin_asdk_app_69f5795de4a48191a35dc5c448520676), or open **Plugins** in the ChatGPT sidebar and search for `Terminal49`. Select **Install plugin**. On Business, Enterprise, and Edu workspaces, an admin may need to enable the plugin for the workspace first. When prompted to connect, ChatGPT opens the Terminal49 sign-in page in your browser. Log in and approve access to the account you want to use. In a new chat, select **Terminal49** from the tools menu or type `@Terminal49`, then ask: > "Using Terminal49, search for container CAIU1234567 and summarize its status." ## Use a custom connector instead If your workspace blocks directory plugins, add the server yourself with Developer mode. This requires a paid plan (Plus, Pro, Business, Enterprise, or Edu); free accounts cannot add custom connectors. In ChatGPT, open **Settings**, find the connectors section (labeled **Connectors** or **Apps**, depending on your version), open **Advanced settings**, and turn on **Developer mode**. In the same connectors section, select **Create** (or **Add custom connector**). Then enter: | Field | Value | | -------------- | ---------------------------- | | Name | `Terminal49` | | MCP server URL | `https://mcp.terminal49.com` | | Authentication | `OAuth` | Save the connector. ChatGPT opens the Terminal49 sign-in page in your browser. Log in with your Terminal49 account and approve access. OpenAI has moved the Developer mode toggle between settings sections over time. If you don't see it under the connectors section, check **Settings → Security** (or search the [OpenAI Help Center](https://help.openai.com) for "developer mode"). ## Test your connection Ask ChatGPT: > "List the tools available in the Terminal49 MCP server and what they're for." It should list tools such as `search_container`, `track_container`, and `get_container`. See the [tools reference](/docs/mcp/home#tools-reference) for the full list, and [Test Numbers](/docs/api-docs/useful-info/test-numbers) for containers you can use during development. ## Troubleshooting | Symptom | How to fix | | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | Plugin not found in the directory | Search for `Terminal49` in **Plugins**; if your workspace hides it, ask your admin to enable it | | **Install plugin** is greyed out or says disabled by admin | Your workspace admin must enable the plugin in **Workspace settings → Plugins** | | Connector fails to create | Confirm the URL is exactly `https://mcp.terminal49.com` (no path) | | Sign-in window never opens | Pop-up blocked — allow pop-ups for chatgpt.com and retry | | `401 Unauthorized` after connecting | Disconnect and reconnect Terminal49 to refresh the OAuth token | ## Related guides * [MCP Overview](/docs/mcp/home) – Tools, prompts, and resources reference * [MCP Server Quickstart](/docs/api-docs/in-depth-guides/mcp) – Full setup, including API-key and local stdio options * [Other MCP clients](/docs/mcp/setup/other-clients) – Generic configuration for any client # Connect Claude to Terminal49 Source: https://terminal49.com/docs/mcp/setup/claude Install the Terminal49 connector from the Claude Directory to query live shipment, container, and ETA tracking data in Claude with OAuth sign-in. Connect Claude to the Terminal49 MCP server so it can answer questions with live container and shipment tracking data. **No API key required.** The Terminal49 MCP server supports OAuth 2.1. When you connect, Claude opens a browser sign-in page. Log in with your Terminal49 credentials to finish. Using Claude Code? See the [Claude Code guide](/docs/mcp/setup/claude-code) — it's one command from your terminal. ## Add Terminal49 from the Claude Directory Terminal49 is available in the [Claude Directory](https://claude.ai/directory/connectors/terminal49). Open the listing in Claude, then select **Add to Claude**. Go to the [Terminal49 connector in the Claude Directory](https://claude.ai/directory/connectors/terminal49). Select **Add to Claude**. Claude opens the Terminal49 sign-in page; log in and approve access to the account you want to use. In a new chat, open the tools menu and make sure **Terminal49** is enabled. Then ask: > "Using Terminal49, search for container CAIU1234567 and summarize its status." ## Use a custom connector instead If your organization requires a custom connector, use `https://mcp.terminal49.com` as the URL. In Claude, go to **Settings → Connectors → Add custom connector**, enter `Terminal49` as the name, and leave the advanced OAuth fields empty. Claude registers itself automatically. ## Test your connection Ask Claude: > "List the tools available in the Terminal49 MCP server and what they're for." Claude should list tools such as `search_container`, `track_container`, and `get_container`. See the [tools reference](/docs/mcp/home#tools-reference) for the full list, and [Test Numbers](/docs/api-docs/useful-info/test-numbers) for containers you can use during development. ## Troubleshooting | Symptom | How to fix | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Connector fails to add | Open the [Terminal49 Claude Directory listing](https://claude.ai/directory/connectors/terminal49) and try again; for a custom connector, confirm the URL is exactly `https://mcp.terminal49.com` (no path) | | Sign-in window never opens | Pop-up blocked — allow pop-ups for claude.ai and retry | | `401 Unauthorized` after connecting | Disconnect and reconnect the connector to refresh the OAuth token | | Connector not selectable in chat | Enable it in the chat's tools menu; on Team/Enterprise ask an Owner to enable it for the organization | ## Related guides * [Claude Code](/docs/mcp/setup/claude-code) – Add the server from your terminal * [MCP Overview](/docs/mcp/home) – Tools, prompts, and resources reference * [MCP Server Quickstart](/docs/api-docs/in-depth-guides/mcp) – Full setup, including API-key and local stdio options * [Other MCP clients](/docs/mcp/setup/other-clients) – Generic configuration for any client # Connect Claude Code to Terminal49 Source: https://terminal49.com/docs/mcp/setup/claude-code Add Terminal49 to Claude Code with `claude mcp add` and sign in through your browser — no API key needed to query live container and shipment tracking data. Connect Claude Code to the Terminal49 MCP server so it can answer questions with live container and shipment tracking data while you work in your terminal. **No API key required.** The Terminal49 MCP server supports OAuth 2.1. When you authenticate, Claude Code opens a browser sign-in page. Log in with your Terminal49 credentials to finish. Using claude.ai or Claude Desktop instead? See the [Claude guide](/docs/mcp/setup/claude). ## Add the server ```bash theme={null} claude mcp add --transport http terminal49 https://mcp.terminal49.com ``` Inside a Claude Code session, run `/mcp`, select **terminal49**, then select **Authenticate**. Your browser opens the Terminal49 sign-in page; after you log in and approve access, the server shows as connected. Ask Claude Code: > "Using Terminal49, search for container CAIU1234567 and summarize its status." ## Alternative: install the Terminal49 plugin Instead of adding the MCP server directly, you can install the **Terminal49** plugin, which configures the same MCP connection and adds a `container-tracking` skill that teaches Claude Code how to choose and sequence the Terminal49 tools. See the [Agent plugins guide](/docs/mcp/setup/agent-plugins) for install commands and details. ## Test your connection Ask Claude Code: > "List the tools available in the Terminal49 MCP server and what they're for." It should list tools such as `search_container`, `track_container`, and `get_container`. See the [tools reference](/docs/mcp/home#tools-reference) for the full list, and [Test Numbers](/docs/api-docs/useful-info/test-numbers) for containers you can use during development. ## Troubleshooting | Symptom | How to fix | | ----------------------------------- | --------------------------------------------------------------------------------------------- | | Server missing from `/mcp` | Confirm it was added with `claude mcp list`; re-run the `claude mcp add` command if needed | | `claude mcp add` fails | Confirm the URL is exactly `https://mcp.terminal49.com` (no path) and the transport is `http` | | Sign-in window never opens | Pop-up blocked or no default browser — allow pop-ups and retry **Authenticate** from `/mcp` | | `401 Unauthorized` after connecting | Run `/mcp`, select **terminal49**, and re-authenticate to refresh the OAuth token | ## Related guides * [Claude](/docs/mcp/setup/claude) – claude.ai and Claude Desktop connectors * [Agent plugins](/docs/mcp/setup/agent-plugins) – Terminal49 plugin with the `container-tracking` skill * [MCP Overview](/docs/mcp/home) – Tools, prompts, and resources reference * [MCP Server Quickstart](/docs/api-docs/in-depth-guides/mcp) – Full setup, including API-key and local stdio options * [Other MCP clients](/docs/mcp/setup/other-clients) – Generic configuration for any client # Connect Cursor to Terminal49 Source: https://terminal49.com/docs/mcp/setup/cursor Install the Terminal49 MCP server in Cursor with a one-click deep link or `.cursor/mcp.json`, then sign in via OAuth — no API key needed for container tracking. Connect Cursor to the Terminal49 MCP server so its AI agent can answer questions with live container and shipment tracking data. **No API key required.** The Terminal49 MCP server supports OAuth 2.1. When you connect, Cursor opens a browser sign-in page. Log in with your Terminal49 credentials to finish. ## Add the server The fastest way is the one-click install button. It opens Cursor with the Terminal49 server configuration pre-filled; confirm to install: [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=terminal49\&config=eyJ1cmwiOiJodHRwczovL21jcC50ZXJtaW5hbDQ5LmNvbSJ9) Alternatively, add the server manually to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` to make it available in every project): ```json theme={null} { "mcpServers": { "terminal49": { "url": "https://mcp.terminal49.com" } } } ``` You can also add it from the UI: **Cursor Settings → MCP → Add new MCP server**, using the same URL. Cursor detects that the server requires authentication and shows a **Needs login** / **Connect** action next to it in **Cursor Settings → MCP**. Select it. Your browser opens the Terminal49 sign-in page. Log in and approve access. Cursor stores and refreshes the tokens automatically. Open the agent chat and ask: > "Using Terminal49, search for container CAIU1234567 and summarize its status." ## Alternative: install the Terminal49 plugin Instead of adding the MCP server directly, you can install the **Terminal49** plugin, which configures the same MCP connection and adds a `container-tracking` skill that teaches Cursor's agent how to choose and sequence the Terminal49 tools. See the [Agent plugins guide](/docs/mcp/setup/agent-plugins) for install steps and details. ## API key alternative If you prefer a fixed credential (for example on a shared machine where the browser flow isn't practical), create an API key in the [developer portal](https://app.terminal49.com/developers/api-keys) and pass it as a header instead: ```json theme={null} { "mcpServers": { "terminal49": { "url": "https://mcp.terminal49.com", "headers": { "Authorization": "Token YOUR_API_KEY" } } } } ``` ## Test your connection Ask the Cursor agent: > "List the tools available in the Terminal49 MCP server and what they're for." It should list tools such as `search_container`, `track_container`, and `get_container`. See the [tools reference](/docs/mcp/home#tools-reference) for the full list, and [Test Numbers](/docs/api-docs/useful-info/test-numbers) for containers you can use during development. ## Troubleshooting | Symptom | How to fix | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | Server shows as disconnected | Confirm the URL is exactly `https://mcp.terminal49.com` (no path), then toggle the server off and on in **Cursor Settings → MCP** | | Sign-in window never opens | Update Cursor to the latest version, then retry the **Connect** action | | `401 Unauthorized` | Re-run the login from **Cursor Settings → MCP**, or check the `Authorization: Token YOUR_API_KEY` header if you use an API key | | Tools list is empty | Restart Cursor so it reloads `mcp.json` | ## Related guides * [Agent plugins](/docs/mcp/setup/agent-plugins) – Terminal49 plugin with the `container-tracking` skill * [MCP Overview](/docs/mcp/home) – Tools, prompts, and resources reference * [MCP Server Quickstart](/docs/api-docs/in-depth-guides/mcp) – Full setup, including API-key and local stdio options * [Other MCP clients](/docs/mcp/setup/other-clients) – Generic configuration for any client # Connect Microsoft Copilot to Terminal49 Source: https://terminal49.com/docs/mcp/setup/microsoft-copilot Add the Terminal49 MCP server as a tool in Microsoft Copilot Studio agents using OAuth 2.0 with dynamic discovery — no API key required. Add the Terminal49 MCP server as a tool in a Microsoft Copilot Studio agent so Copilot can answer questions with live container and shipment tracking data. Agents built in Copilot Studio can then be published to Microsoft 365 Copilot, Teams, and other channels. **No API key required.** The Terminal49 MCP server supports OAuth 2.1 with Dynamic Client Registration, which maps to Copilot Studio's **OAuth 2.0 → Dynamic discovery** authentication option. Users sign in with their Terminal49 credentials in the browser. ## Prerequisites * Access to [Microsoft Copilot Studio](https://copilotstudio.microsoft.com) with permission to edit an agent and add tools. * The MCP tool configuration wizard enabled in your environment (Microsoft is rolling it out by region and tenant; if selecting the MCP option only opens documentation, the wizard isn't enabled for your tenant yet). ## Add the server to an agent In Copilot Studio, open your agent and go to the **Tools** page, then select **+ Add a tool**. Select **+ New tool → Model Context Protocol**, then enter: | Field | Value | | ------------------ | ----------------------------------------------------------- | | Server name | `Terminal49` | | Server description | `Live container and shipment tracking data from Terminal49` | | Server URL | `https://mcp.terminal49.com` | Under **Authentication**, select **OAuth 2.0**, then select **Dynamic discovery**. Copilot Studio discovers Terminal49's authorization server and registers itself automatically. You don't need to enter client credentials. Create the tool, then select **Connect** (or open the connection manager) and sign in with your Terminal49 account in the browser window that opens. Make sure the tool is enabled for the agent, then ask in the test pane: > "Using Terminal49, search for container CAIU1234567 and summarize its status." ## Test your connection Ask the agent: > "List the tools available in the Terminal49 MCP server and what they're for." It should list tools such as `search_container`, `track_container`, and `get_container`. See the [tools reference](/docs/mcp/home#tools-reference) for the full list, and [Test Numbers](/docs/api-docs/useful-info/test-numbers) for containers you can use during development. ## Troubleshooting | Symptom | How to fix | | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | | MCP option only opens a documentation page | The MCP config wizard isn't enabled for your region or tenant yet — check with your Microsoft admin | | Connection fails during discovery | Confirm the URL is exactly `https://mcp.terminal49.com` (no path) and that OAuth 2.0 → Dynamic discovery is selected | | `401 Unauthorized` | Re-authenticate the connection from the agent's tool settings | | End users are asked to sign in | Expected — each user authorizes with their own Terminal49 account on first use | ## Related guides * [MCP Overview](/docs/mcp/home) – Tools, prompts, and resources reference * [MCP Server Quickstart](/docs/api-docs/in-depth-guides/mcp) – Full setup, including API-key and local stdio options * [Other MCP clients](/docs/mcp/setup/other-clients) – Generic configuration for any client # Connect Any MCP Client to Terminal49 Source: https://terminal49.com/docs/mcp/setup/other-clients Reference config for any MCP-compatible client connecting to Terminal49 — OAuth 2.1 with dynamic client registration or an API-key header fallback. Any client that supports MCP over streamable HTTP can connect to the Terminal49 MCP server. This page gives the generic settings; for step-by-step guides see [Claude](/docs/mcp/setup/claude), [Claude Code](/docs/mcp/setup/claude-code), [ChatGPT](/docs/mcp/setup/chatgpt), [Cursor](/docs/mcp/setup/cursor), [Microsoft Copilot](/docs/mcp/setup/microsoft-copilot), [VS Code](/docs/mcp/setup/vs-code), and [Agent plugins](/docs/mcp/setup/agent-plugins) (Claude Code, Cursor, Codex, GitHub Copilot CLI). ## Server details | Setting | Value | | -------------- | ----------------------------------------- | | Server URL | `https://mcp.terminal49.com` | | Transport | HTTP (streamable) | | Authentication | OAuth 2.1 (recommended) or API key header | Always use the root origin `https://mcp.terminal49.com` — it is the canonical OAuth resource identifier, so OAuth clients bind to the correct token audience. ## OAuth 2.1 (recommended — no API key) Point your client at `https://mcp.terminal49.com` with no credentials. Clients that implement MCP authorization discover everything automatically: * Protected resource metadata is served at `https://mcp.terminal49.com/.well-known/oauth-protected-resource`, which points to the authorization server at `https://auth.terminal49.com`. * Dynamic Client Registration is supported, so clients register themselves — no pre-configured client ID or secret is needed. * The flow is authorization code with PKCE: the client opens your browser, you sign in with your Terminal49 credentials and approve access, and the client stores and refreshes tokens automatically. ```json theme={null} { "url": "https://mcp.terminal49.com" } ``` ## API key (for clients without OAuth support) If your client can't run a browser OAuth flow (for example, a headless integration), create an API key in the [developer portal](https://app.terminal49.com/developers/api-keys) and send it in the `Authorization` header with the `Token` scheme: ```json theme={null} { "url": "https://mcp.terminal49.com", "headers": { "Authorization": "Token YOUR_API_KEY" } } ``` You can verify connectivity with `curl`: ```bash theme={null} curl -X POST https://mcp.terminal49.com \ -H "Authorization: Token $T49_API_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' ``` Use the `Token` scheme for API keys. The `Bearer` scheme is used for OAuth access tokens, which OAuth-capable clients obtain automatically during sign-in. The same [rate limits](/docs/api-docs/in-depth-guides/rate-limiting) apply to MCP endpoints as the REST API. ## Related guides * [MCP Overview](/docs/mcp/home) – Tools, prompts, and resources reference * [MCP Server Quickstart](/docs/api-docs/in-depth-guides/mcp) – Full setup, local stdio development, deployment * [Test Numbers](/docs/api-docs/useful-info/test-numbers) – Containers for testing # Connect VS Code to Terminal49 Source: https://terminal49.com/docs/mcp/setup/vs-code Add Terminal49 to VS Code Copilot agent mode via `.vscode/mcp.json` and sign in with OAuth — no API key needed to query live shipment and container tracking. Connect VS Code to the Terminal49 MCP server so GitHub Copilot's agent mode can answer questions with live container and shipment tracking data. **No API key required.** The Terminal49 MCP server supports OAuth 2.1. When you connect, VS Code opens a browser sign-in page. Log in with your Terminal49 credentials to finish. ## Prerequisites * VS Code 1.101 or later (required for remote MCP servers with OAuth). * GitHub Copilot enabled in VS Code. ## Add the server Add the server to `.vscode/mcp.json` in your workspace: ```json theme={null} { "servers": { "terminal49": { "type": "http", "url": "https://mcp.terminal49.com" } } } ``` Alternatively, run **MCP: Add Server** from the Command Palette, select **HTTP**, and enter the same URL. Save the file, then use the **Start** CodeLens above the server entry in `mcp.json` (or **MCP: List Servers** from the Command Palette). When VS Code prompts you to authenticate, allow it — your browser opens the Terminal49 sign-in page. Log in and approve access. Open Copilot Chat in **agent mode**, confirm the Terminal49 tools are enabled in the tools picker, and ask: > "Using Terminal49, search for container CAIU1234567 and summarize its status." ## Test your connection Ask Copilot in agent mode: > "List the tools available in the Terminal49 MCP server and what they're for." It should list tools such as `search_container`, `track_container`, and `get_container`. See the [tools reference](/docs/mcp/home#tools-reference) for the full list, and [Test Numbers](/docs/api-docs/useful-info/test-numbers) for containers you can use during development. ## Troubleshooting | Symptom | How to fix | | ------------------------- | ----------------------------------------------------------------------------------------------- | | Server won't start | Confirm the URL is exactly `https://mcp.terminal49.com` (no path) and VS Code is 1.101+ | | Never prompted to sign in | Use the **Auth** action from the CodeLens above the server in `mcp.json`, or restart the server | | `401 Unauthorized` | Re-authenticate via the server's CodeLens actions | | Tools missing in chat | Switch Copilot Chat to agent mode and enable the Terminal49 tools in the tools picker | ## Related guides * [MCP Overview](/docs/mcp/home) – Tools, prompts, and resources reference * [MCP Server Quickstart](/docs/api-docs/in-depth-guides/mcp) – Full setup, including API-key and local stdio options * [Other MCP clients](/docs/mcp/setup/other-clients) – Generic configuration for any client # Migrating from Beacon Source: https://terminal49.com/docs/migrate/beacon Map Beacon tracking API fields, parameters, and errors to their Terminal49 equivalents. Includes webhook setup, carrier coverage, and a migration checklist. Beacon is a supply chain visibility platform that also tracks air waybills and offers order management. If you use only the ocean tracking API, moving to Terminal49 gives you direct terminal integrations (holds, fees, LFD) and 30+ webhook events. This guide covers ocean container and BOL tracking only, not Beacon's air tracking or order management. There is no compatibility shim. You will change your request code and your response parsing. For most integrations that is an afternoon. ## Start in sixty seconds Signing up and getting a key is self-serve. Sign up at [app.terminal49.com](https://app.terminal49.com). The free plan tracks up to 10 active containers. Create a key at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). ```bash theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "tracking_request", "attributes": { "request_number": "YOUR_BOL_NUMBER", "request_type": "bill_of_lading", "scac": "MAEU" } } }' ``` The full API key is shown once, right after you create it. Copy it before you navigate away. After that it is masked and cannot be revealed. If you miss it, create a new key and delete the old one. The free plan tracks up to 10 active containers. Creating tracking requests through the API works right away; reading tracking data back through the API requires a free 7-day API trial (same 10-container limit) — contact us via in-app chat or [support@terminal49.com](mailto:support@terminal49.com) and we enable it. If you want to test without a live shipment, use the [test tracking numbers](/docs/api-docs/useful-info/test-numbers), which simulate success and failure outcomes. **Migration offer:** sign up now and the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id) — vessel schedules, AIS positions, and projected routes — is free for your first month. ## The architectural shift Beacon gives you a container-centric API. You register a container number, poll for current state, and own the schedule, the cache, and the deduplication. Terminal49 splits this in two. You register a tracking request once. We keep it updated and push changes to your webhook. Cron every 2+ hours, call Beacon's container endpoint, diff against your cache, dedupe events, then write to your database. Every successful call spends contracted usage. Freshness is capped by your polling interval. `POST /tracking_requests` once. Terminal49 polls carriers, terminals, and rail, then POSTs to your endpoint as things change. No cache layer, no dedupe logic. You can keep polling if you prefer. Point your existing scheduler at `GET /v2/shipments` or `GET /v2/containers`. But webhooks are the reason the API is shaped this way, and terminal data (holds, fees, last free day) changes on a cadence that polling tends to miss. ## Quick comparison | | Beacon | Terminal49 | | ----------------------- | --------------------------------------------------------- | ------------------------------------- | | Tracking model | Register containers, then poll (max every 2h recommended) | Register once, then push or poll | | Authentication | Username/password login → short-lived Bearer token | `Authorization: Token` header | | Base URL | `https://api.beacon.com/v1` | `https://api.terminal49.com/v2` | | Content type | `application/json` | `application/vnd.api+json` | | Response format | JSON API schema | JSON:API | | Webhooks | Available via support | 30+ events, HMAC-signed | | Carrier identification | 4-character SCAC (`carrier_code`) | `scac`, or `auto_detect_vocc_scac` | | Terminal holds and fees | No direct equivalent | Included on the container object | | Last free day | No direct equivalent | Included, with per-source breakdown | | Rail milestones | Limited | North American Class I and short-line | | Getting an API key | Granted per user by your customer success manager | Self-serve | ## Authentication Beacon issues a short-lived Bearer token from a login call and expects you to refresh it before it expires. Terminal49 uses a single static API key, so this entire flow goes away. ```bash Beacon theme={null} # Step 1: log in to get an access token (max 20 requests/minute) curl -X POST https://api.beacon.com/v1/login \ -H "Content-Type: application/json" \ -d '{"username": "YOUR_BEACON_USERNAME", "password": "YOUR_BEACON_PASSWORD"}' # Returns: { "access_token", "refresh_token", "token_type": "Bearer", "expires_in": 300 } # Step 2: call the API with the access token (expires after 300 seconds) curl -X GET "https://api.beacon.com/v1/containers/MRKU9465770" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" # Step 3: refresh before it expires curl -X POST https://api.beacon.com/v1/login/token \ -H "Content-Type: application/json" \ -d '{"refresh_token": "YOUR_REFRESH_TOKEN"}' ``` ```bash Terminal49 theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_T49_KEY" \ -d '{"data":{"type":"tracking_request","attributes":{ "request_number":"MRKU9465770", "request_type":"container", "scac":"MAEU"}}}' ``` Note the `Token` prefix. It is not `Bearer`. Terminal49's API key does not expire. Once you switch, delete the login call, the token cache, the 300-second expiry timer, and the refresh logic entirely — there is nothing to replace them with. Beacon also requires your customer success manager to grant API access per user before you can call the API at all. Terminal49 keys are self-serve from the dashboard. ## Request parameter mapping Beacon's `POST /v1/containers` takes an array of container objects in one call. Terminal49's `POST /tracking_requests` takes one identifier per request, but that identifier can be a container number, a BOL, or a booking number. | Beacon field | Terminal49 equivalent | Notes | | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `container_number` (required) | `request_number` + `request_type: "container"` | Terminal49 also accepts BOL (`request_type: "bill_of_lading"`) and booking numbers (`request_type: "booking_number"`) as the identifier, which Beacon's container endpoint does not | | `carrier_code` (optional, 4-char SCAC) | `scac` | Same SCAC format | | Omit `carrier_code` | Set `auto_detect_vocc_scac: true`, or call [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) first | Auto-detection runs asynchronously; a failed inference fails the tracking request with `scac_auto_detect_failed` | | `destination_warehouse_name` (optional) | No equivalent field | Terminal49 does not model an inland warehouse name on the tracking request; the container's `relationships.destination` covers inland rail/port destinations | | `custom_fields` (optional, up to 10 `{name, value}` pairs) | `ref_numbers` (array of strings) on the tracking request, per the [OpenAPI spec](/docs/api-docs/api-reference/introduction) | Terminal49 stores these as a flat list of reference strings, not named key/value pairs — if you rely on the field name to route data downstream, you'll need to encode that in the string or handle it in your own mapping layer | | Refresh parameter | `PATCH /v2/containers/{id}/refresh` | Forces an immediate pull from all sources. Paid feature — requires account enablement, limited to 10 requests per minute | | Route data | `GET /v2/containers/{id}/map_geojson` — requires the Routing Data entitlement | See [Routing](/docs/api-docs/in-depth-guides/routing) | | API key header | `Authorization` header | `Token` prefix, not `Bearer` | Terminal49 takes one identifier per tracking request. Track by BOL and we return every container on that bill of lading as related container resources. Container-number tracking requests are currently in beta. Beacon consumes your contracted usage whenever a request succeeds and returns tracking data — even for containers you've stopped actively watching. Terminal49's free plan tracks up to 10 active containers at no cost; beyond that, usage is based on active tracking requests, not per-call lookups. ## Response field mapping Terminal49 is JSON:API compliant, so relationships between shipments, containers, ports, and terminals are explicit rather than something you reassemble from ID references. Use the [`include` parameter](/docs/api-docs/in-depth-guides/including-resources) to sideload related resources in one call instead of chasing IDs. Beacon's `GET /v1/containers/{containerNumber}` response is container-centric — it does not return a separate shipment object. Below, the Beacon column reflects the real field names from that response. ### Shipment level | Beacon field | Terminal49 | | ------------------------------------- | -------------------------------------------- | | `carrier.code` | `shipment.attributes.shipping_line_scac` | | `carrier.name` | `shipment.attributes.shipping_line_name` | | `status` (enum) | Derived from container status and milestones | | `port_of_loading` | `shipment.relationships.port_of_lading` | | `port_of_discharge` | `shipment.relationships.port_of_discharge` | | `vessel_arrival_dates.estimated_date` | `shipment.attributes.pod_eta_at` | | No polling/refresh cache field | No equivalent. Refresh is managed for you. | ### Container level | Beacon field | Terminal49 | | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `container_number` | `container.attributes.number` | | `status` (enum: `GATED_OUT_EMPTY`, `GATED_IN_FULL`, `LOADED_AT_POL`, `IN_TRANSIT`, `ARRIVED_AT_POD`, `DISCHARGED_AT_POD`, `GATED_OUT_FULL`, `PROCESSING`) | `container.attributes.current_status` | | `gated_out_empty_dates` … `gated_out_full_dates` (each with `estimated_date` and/or `actual_date`) | Container timestamps, [transport events](/docs/api-docs/api-reference/containers/get-a-containers-transport-events), and webhook events — see the milestone mapping below | | `custom_fields` | Set at tracking-request time as `ref_numbers`; not returned on the container object | | `purchase_orders` | No equivalent. Terminal49's tracking API does not do order management | Beacon's documented container response does not include an equipment type or size field. If your integration reads container equipment (dry, reefer, size) from Beacon today, confirm with Beacon support where that comes from before you map it — Terminal49 returns it as `container.attributes.equipment_type`, `equipment_length` (10, 20, 40, 45), and `equipment_height` (standard, high cube). ### Locations, facilities, and vessels | Beacon field | Terminal49 | | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `port_of_loading.name` | `port.attributes.name` | | `port_of_loading.un_location_code` | `port.attributes.code` | | `port_of_discharge.name` / `.un_location_code` | Same fields, on the discharge port resource | | Port coordinates | Not returned by Beacon. Terminal49: `port.attributes.latitude` / `.longitude` | | Port timezone | Not returned by Beacon. Terminal49: `port.attributes.time_zone` | | Port country | Not returned by Beacon. Terminal49: `port.attributes.country_code` | | Terminal name/code | Not returned by Beacon. Terminal49: `terminal.attributes.name`, `terminal.attributes.smdg_code` or `bic_facility_code` | | `vessel.name` | `shipment.attributes.pod_vessel_name` | | `vessel.imo_number` | `shipment.attributes.pod_vessel_imo` | | Vessel MMSI, position, extra fields | Not returned by Beacon. Available on Terminal49 via the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo) | ## Milestone and event mapping Beacon returns each milestone as its own date object with `estimated_date` and/or `actual_date`, rather than a single flat events array. Terminal49 exposes the same milestones as normalized transport events and pushes each one to your webhook. | Beacon date object | Terminal49 event | | ------------------------ | --------------------------------------- | | `gated_out_empty_dates` | `container.transport.empty_out` | | `gated_in_full_dates` | `container.transport.full_in` | | `loaded_dates` | `container.transport.vessel_loaded` | | `vessel_departure_dates` | `container.transport.vessel_departed` | | `vessel_arrival_dates` | `container.transport.vessel_arrived` | | `discharged_dates` | `container.transport.vessel_discharged` | | `gated_out_full_dates` | `container.transport.full_out` | | No equivalent | `container.transport.empty_in` | Terminal49 also emits milestones Beacon has no equivalent for: * **Vessel berthed:** `container.transport.vessel_berthed` * **Available for pickup:** `container.transport.available` and `.not_available` * **Transshipment:** arrived, discharged, loaded, departed * **Feeder vessel and barge:** arrived, discharged, loaded, departed * **Rail:** loaded, departed, arrived, unloaded, plus `arrived_at_inland_destination` See the full [event catalog](/docs/api-docs/webhooks/event-catalog). ### Registering a webhook ```bash theme={null} curl -X POST https://api.terminal49.com/v2/webhooks \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "webhook", "attributes": { "url": "https://your-endpoint.example.com/t49", "active": true, "events": [ "container.transport.vessel_discharged", "container.transport.available", "container.pickup_lfd.changed" ] } } }' ``` Payloads are HMAC-signed. See [webhook setup](/docs/api-docs/in-depth-guides/webhooks) for signature verification, and [List webhook IPs](/docs/api-docs/api-reference/webhooks/list-webhook-ips) if your firewall restricts inbound traffic. ## What you gain This is the part worth reading even if the rest is mechanical. Terminal49 integrates with terminals directly, not only carriers, so the container object carries operational data that has no Beacon equivalent. ### Holds `holds_at_pod_terminal` is an array of active holds blocking pickup: ```json theme={null} { "holds_at_pod_terminal": [ { "name": "customs", "status": "hold", "description": "CBP HOLD" }, { "name": "freight", "status": "hold", "description": null } ] } ``` Hold names are `freight`, `customs`, `USDA`, `VACIS`, `TMF`, and `other`. Status is `hold` or `pending`. When a hold clears, the object is removed from the array. There is no released state. Hold names are case-sensitive. `USDA`, `VACIS`, and `TMF` are uppercase; `freight`, `customs`, and `other` are lowercase. Match exactly. ### Fees `fees_at_pod_terminal` carries type, amount, and currency: ```json theme={null} { "fees_at_pod_terminal": [ { "type": "demurrage", "amount": 850.00, "currency_code": "USD" }, { "type": "exam", "amount": 450.00, "currency_code": "USD" } ] } ``` Fee types are `demurrage`, `extended_dwell_time`, `exam`, `total`, and `other`. Some terminals report a `total` line item alongside individual fees. Filter it out before summing or you will double-count. ### Last free day `pickup_lfd` is a coalesced value that follows a fixed source priority: shipping line, then terminal, then rail. It does not pick the earliest date. The individual sources are available separately on `import_deadlines`: * `pickup_lfd_line`: the shipping line's LFD (per diem deadline) * `pickup_lfd_terminal`: the terminal's LFD (demurrage deadline) * `pickup_lfd_rail`: the rail carrier's LFD at the inland destination Each has its own webhook event, so you can alert on whichever source your operation cares about. ### Release readiness Two fields answer "can I pick this up?" `available_for_pickup` and the holds array: ```javascript theme={null} function isReadyForPickup(container) { const { available_for_pickup, holds_at_pod_terminal } = container.attributes; const hasActiveHolds = holds_at_pod_terminal.some(h => h.status === 'hold'); return available_for_pickup === true && !hasActiveHolds; } ``` Full detail in [Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). Holds, fees, LFD, and availability come back on the container object wherever the terminal is a supported source. They are not a paid add-on and they do not require a sales conversation. See [Entitlements](/docs/api-docs/useful-info/entitlements) for the features that do require account enablement. Routing Data (container map and vessel positions), rail LFD, container refresh, and the embeddable map and widget are the gated ones. ## Gotchas that will bite you Beacon also tracks air waybills (AWBs) and offers order management (creating and updating orders, linking an order to a shipment). Terminal49 does not do either. If your integration uses Beacon for air freight or order management, plan to keep Beacon for those or replace them separately — this guide covers ocean container and BOL tracking only. Beacon's access token expires every 300 seconds, so a real integration ends up with a refresh timer, a token cache, and retry logic around `401`s from an expired token. Terminal49's API key is static — delete all of that. There is no login call, no refresh endpoint, and no expiry to track. Beacon consumes your contracted usage every time a request succeeds and returns tracking data, even for containers you're no longer actively watching — so idle containers you forgot to stop polling still cost you. Terminal49's free plan tracks up to 10 active containers; usage is based on active tracking requests, not per-call lookups, so stopping a tracking request stops it counting. `POST /tracking_requests` returns immediately with a pending status. The shipment appears once the carrier responds. Subscribe to `tracking_request.succeeded` and `tracking_request.failed` rather than expecting shipment data in the creation response. A request may also land in `awaiting_manifest` if the carrier has not manifested the shipment yet, and we retry automatically. See [Tracking Request Lifecycle](/docs/api-docs/in-depth-guides/tracking-request-lifecycle). Terminal49 returns JSON:API, not flat JSON. Relationships are ID references into an `included` array. Use a JSON:API client library, or use `include` to sideload exactly what you need. Parsing raw JSON works but you will write more code than you expect. Beacon's date fields carry a local UTC offset for the event's location, but fall back to a plain `...Z` UTC timestamp if no zone information is available, or a date with no time at all if no time information is available — so parsing has to branch on which shape you got. Terminal49 always stores event timestamps in UTC and returns the matching IANA timezone alongside as a separate field. Convert for display rather than assuming local time. See [Event Timestamps](/docs/api-docs/in-depth-guides/event-timestamps). Beacon recommends polling `GET /v1/containers/{containerNumber}` no more than once every 2 hours. If your Beacon integration is polling on a cron, you can either keep the same cadence against `GET /v2/containers`, or drop polling entirely and let Terminal49 push webhook events as they happen — freshness is no longer capped by how often you ask. `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` mean no active holds or fees. This is the common case. Do not treat it as missing data. Terminal changes (fees, holds, LFD, appointment, availability) arrive on `container.updated` with a `changeset` showing old value first, new value second. Use it instead of diffing state yourself. ## Error handling Beacon returns HTTP status codes with an error body containing `timestamp`, `error`, and `sub_error` fields. Terminal49 also uses standard HTTP status codes. Replace any Beacon-specific error parsing with status-code checks. | Status | Meaning | | ------ | ------------------------------------------------------------------------------- | | 400 | Malformed request or failed validation | | 401 | Missing or invalid API key | | 403 | Key lacks permission, or the feature is not enabled on your plan | | 404 | Resource does not exist | | 422 | Valid syntax, rejected content. For example, a malformed container number | | 429 | Rate limited. See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) | | 5xx | Terminal49 or an upstream carrier or terminal is unavailable | Rough equivalence for the Beacon status codes you are handling today: | Beacon status | Terminal49 | | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | 400 Bad request | HTTP 400 or 422 | | 401 Unauthorised (missing or expired token) | HTTP 401 — but there is no token to expire, since the key is static | | 403 Forbidden | HTTP 403 | | 429 Rate limit exceeded (120/min non-login, 20/min login) | HTTP 429. See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) for Terminal49's limits | | No tracking data found | `tracking_request.failed`, or `tracking_request.awaiting_manifest` if not yet manifested | | Carrier unavailable | Not surfaced. We retry internally. | The [TypeScript SDK](/docs/sdk/introduction) maps these to typed errors (`AuthenticationError`, `ValidationError`, `RateLimitError`, `UpstreamError`, `FeatureNotEnabledError`, `AuthorizationError`, `NotFoundError`) and retries rate-limit and server errors automatically with exponential backoff. ## Where we are narrower than Beacon Worth knowing before you commit. **Carrier count.** Terminal49 integrates directly with 36 ocean carriers, plus 2 more enabled on request, as of 14 August 2026. Beacon advertises coverage across 160+ ocean and air carriers combined. Ours are direct integrations covering the lines that move volume into North America, and each is normalized into one schema. Check your carrier mix against the [ocean carrier list](/docs/coverage/ocean-carriers) before cutover, and read the known issues section there. We publish the per-carrier field gaps. **Terminal data is North America.** Holds, fees, LFD, and availability come from direct terminal integrations concentrated in the US and Canada, with European ports expanding. Ocean milestones work globally; terminal-level operational data does not yet. **No air tracking.** Beacon also tracks air waybills (AWBs). Terminal49 tracks ocean only. If your integration covers air freight, keep it on Beacon or move it elsewhere — this migration handles only the ocean portion. **No order management.** Beacon lets you create and update orders and link an order to a shipment. Terminal49 does not model orders — it tracks shipments and containers. If your Beacon integration uses order management, keep Beacon for that or replace it separately. **Some fields are source-dependent.** Seal number, container weight, and departure or arrival events vary by carrier. The [field availability reference](/docs/coverage/fields) says which fields are always present and which depend on the carrier, terminal, or journey. ## Migration checklist Everyone does the base path. Then pick a branch. ### Base path Self-serve at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). Copy it immediately, it is shown once. Move the key to `Authorization: Token`. Note the `Token` prefix. Set `Content-Type: application/vnd.api+json`. Compare your Beacon carrier values against the [carrier list](/docs/coverage/ocean-carriers). Flag anything missing before you cut over. One `POST /tracking_requests` per BOL, booking, or container, replacing the per-request lookup. Tracking requests start pending. Handle `succeeded`, `failed`, and `awaiting_manifest` rather than expecting data on creation. JSON:API structure, split equipment fields, UTC timestamps with a separate timezone. Replace Beacon-specific error parsing with HTTP status codes. Submit tracking requests for everything currently in transit. Send us the list if it is large and we will load it. Holds, fees, and LFD are the reason to do this properly rather than porting like for like. ### Then pick one Accept our POST payloads at a public URL. Subscribe only to events you act on. Reject any payload whose signature does not match. Only needed if your firewall restricts inbound traffic. Confirm end-to-end before going live. Remove your dedupe layer along with it. See [webhook best practices](/docs/api-docs/webhooks/best-practices) for retries and idempotency. The creation response is pending and carries no shipment yet. Keep the tracking request ID, then fetch the tracking request again (or handle `tracking_request.succeeded`) to get the shipment ID once the carrier responds. Point it at `GET /v2/shipments` or `GET /v2/containers`. No change to how often you poll. Skipped: endpoint setup, signature verification, IP whitelisting, delivery testing. Terminal data changes on a cadence polling tends to miss. If you only adopt webhooks for one thing, make it `container.updated` and `container.pickup_lfd.changed`. ## Migrate with an AI coding agent If you use Cursor, Claude Code, Windsurf, Copilot, or another AI coding assistant, hand it the prompt below. It is written to run a **side-by-side migration**: the agent stands up a Terminal49 client next to your existing Beacon code, shadows every Beacon call with a Terminal49 call, diffs the responses, and only cuts over once parity is proven. Point your agent at this page as context (paste the URL or add it as a doc source). The prompt references the mappings above, so the more of this page the agent can see, the better it does. 1. Open your repo in your AI coding tool. 2. Add this page as a documentation source, or paste its URL into the chat. 3. Copy the prompt below into a new chat and send it. 4. Answer the agent's discovery questions (Beacon client location, env var names, carrier mix). 5. Review each PR the agent opens. It should ship in small, reviewable steps: client, shadow, parity harness, cutover, cleanup. * A `Terminal49Client` alongside your existing `BeaconClient`, sharing the same interface where possible. * A shadow-mode wrapper that calls both providers and logs response diffs without changing behavior. * A parity report per shipment: matched fields, diverged fields, and Terminal49-only fields (holds, fees, LFD). * A feature-flagged cutover: route reads to Terminal49, keep Beacon as fallback until you flip the flag off. * A webhook receiver with HMAC verification, or a polling scheduler, depending on which path you pick. * A cleanup PR that removes Beacon code, its login/token-refresh flow, env vars, dependencies, and dedupe logic. ### The prompt Copy this into your agent. Replace the bracketed placeholders in the **Repo context** block before sending. ```markdown Terminal49 migration agent prompt expandable icon=robot wrap theme={null} You are migrating this codebase from the Beacon tracking API to the Terminal49 API, side by side. Terminal49's authoritative migration guide is at https://terminal49.com/docs/migrate/beacon. Use it as the source of truth for field mappings, event names, error codes, and behavior differences. # Repo context (fill this in before running) - Beacon client lives at: [path/to/beacon/client.ts] - Beacon is called from: [list the call sites or "find them"] - Beacon auth env vars in use today: [e.g. BEACON_USERNAME, BEACON_PASSWORD — Beacon logs in via POST /v1/login and refreshes a 300-second access token via POST /v1/login/token; note anywhere that refresh timer or token cache lives] - Language and framework: [e.g. TypeScript + Node, Python + FastAPI, Ruby on Rails] - Storage for tracking state: [e.g. Postgres table `shipments`] - Current polling schedule: [e.g. cron every 2h via BullMQ, matching Beacon's recommended minimum interval] - Carrier mix (SCACs we track most): [e.g. MAEU, MSCU, CMDU, HLCU, ONEY] - Deployment target: [e.g. AWS ECS, Vercel, Fly.io] - Secret store: [e.g. AWS Secrets Manager, `.env`, Doppler] # Rules 1. Do not delete Beacon code until the cleanup step. Migration is side by side. 2. Ship in small PRs. Each PR must build, pass tests, and be independently revertable. 3. Never invent Terminal49 fields, endpoints, or event names. If the guide does not confirm a mapping, ask me. Do not guess. 4. Terminal49 uses `Authorization: Token ` (not `Bearer`) and content type `application/vnd.api+json`. Get this right on the first request. 5. Terminal49 is asynchronous. `POST /tracking_requests` returns pending. Data arrives via `tracking_request.succeeded`, `tracking_request.failed`, or `tracking_request.awaiting_manifest` webhooks, or by polling `GET /v2/shipments`. Do not expect shipment data on creation. 6. Timestamps are UTC with a separate IANA `time_zone` field. Do not assume local time. 7. `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` are the normal state, not missing data. A fee `amount` of 0 is valid. 8. On `container.updated`, prefer the `changeset` over diffing state yourself. # Plan (execute in order, one PR per step) ## PR 1 — Discovery and interface - Grep the repo for every Beacon call site. List them in the PR description. - Extract the Beacon client's public surface into an interface (`TrackingProvider` with methods like `track(number, type, carrier)`, `getShipment(id)`, `refresh(id)`). - Make the existing Beacon client implement it. No behavior change. ## PR 2 — Terminal49 client - Add a `Terminal49Client` implementing the same `TrackingProvider` interface. - Auth via `T49_API_KEY` env var, header `Authorization: Token ${key}`. - Base URL `https://api.terminal49.com/v2`, content type `application/vnd.api+json`. - Implement: - `createTrackingRequest({ request_number, request_type, scac })` returning the tracking request ID. - `getShipment(id, { include: 'containers,port_of_lading,port_of_discharge,...' })`. - `getContainer(id)`. - `refreshContainer(id)` mapping to `PATCH /v2/containers/{id}/refresh`. - Normalize responses to the same shape Beacon callers expect today, using the field mapping from the guide. Split equipment codes into `equipment_type`, `equipment_length`, `equipment_height`. Convert timestamps to UTC + `time_zone`. - Map errors to typed classes: `AuthenticationError` (401), `AuthorizationError` (403), `ValidationError` (400/422), `RateLimitError` (429), `UpstreamError` (5xx), `FeatureNotEnabledError` (403 + feature flag response). - Add unit tests using recorded fixtures. Do not hit the live API in tests. ## PR 3 — Shadow mode - Add a `ShadowProvider` that wraps both clients. On every read: - Call Beacon as the primary. Return its response. - Fire-and-forget a Terminal49 call for the same identifier. - Log a structured diff: matched fields, diverged fields, T49-only fields. - Gate with env var `TRACKING_SHADOW_MODE=true`. Off by default. - Add a parity report script that aggregates shadow logs by SCAC and field. - Do NOT change what callers see. This step is observation only. ## PR 4 — Backfill script - Write a one-shot script that reads all active shipments from our database and calls `POST /tracking_requests` for each (one per BOL, booking, or container). - Store the returned `tracking_request.id` and eventual `shipment.id` on our records. - Rate-limit to respect Terminal49's limits (handle 429 with exponential backoff). - Idempotent: safe to re-run. Skip rows already backfilled. ## PR 5 — Webhook receiver (only if we picked the webhook path) - Add `POST /webhooks/terminal49` endpoint. - Verify HMAC signature using the shared secret from `T49_WEBHOOK_SECRET`. Reject on mismatch with 401. - Handle these events at minimum: - `tracking_request.succeeded`, `tracking_request.failed`, `tracking_request.awaiting_manifest` - `container.transport.vessel_discharged`, `container.transport.available`, `container.transport.full_out` - `container.updated` (use the `changeset`, do not diff) - `container.pickup_lfd.changed` - Persist events idempotently keyed by event ID. - Register the webhook via `POST /v2/webhooks` from a bootstrap script, subscribing only to events we handle. - If our firewall restricts inbound traffic, whitelist Terminal49 IPs from `GET /v2/webhooks/ips`. ## PR 6 — Cutover behind a flag - Add feature flag `TRACKING_PROVIDER` with values `beacon` (default) and `terminal49`. - Route all reads through the flag. Beacon stays available as a fallback for one release cycle. - Flip staging to `terminal49`, verify parity report is clean, then flip production. ## PR 7 — Cleanup - Delete `BeaconClient`, its tests, its dedupe cache, and any equipment-code parsing helpers Terminal49 makes redundant. - Delete the Beacon login flow entirely: the `POST /v1/login` call, the `POST /v1/login/token` refresh call, the token cache, and any refresh timer or scheduled job tied to the 300-second expiry. Terminal49's key does not expire, so none of this has a replacement — it just goes away. - Remove `BEACON_USERNAME`, `BEACON_PASSWORD`, and any Beacon token env vars from the secret store and deployment config. - Remove the shadow provider and the feature flag. - Update README and any runbooks. Note the new webhook endpoint if applicable. # Definition of done - All Beacon call sites now go through Terminal49. - Webhook (or polling) is live in production. - Parity report shows no unexplained divergences for our top 10 SCACs. - Holds, fees, and LFD are exposed to whichever downstream system needs them (dashboard, alerts, customer emails). Do not migrate without wiring these up. They are the reason to do this properly. - Beacon dependency, env vars, and dead code are gone from the repo. # Ask me before you - Choose between the webhook path and the polling path. Default to webhooks unless our infra makes an inbound HTTPS endpoint hard. - Change the shape of any function Beacon callers use today. Prefer a normalization layer inside the Terminal49 client. - Add a new dependency. Prefer stdlib and what is already in the repo. - Touch anything outside the tracking integration. Start with PR 1. Post the list of Beacon call sites and the proposed `TrackingProvider` interface, and wait for my review before writing PR 2. ``` The prompt is deliberately opinionated on side-by-side migration and small PRs. If your team prefers a big-bang cutover or a different branching model, edit the **Plan** section before sending it to your agent. ## Getting help Send us your list of active container and bill of lading numbers and we will load them rather than making you script the backfill. If something in this mapping is wrong or incomplete, tell us. We would rather fix the page than have you work around it. Every endpoint, with request and response schemas Typed client with retries and pagination built in Carriers, terminals, rail, and field availability Simulate success and failure outcomes # Migrating from FourKites Source: https://terminal49.com/docs/migrate/fourkites Map FourKites ocean tracking fields, webhooks, and errors to Terminal49 equivalents. Includes carrier coverage and a migration checklist. This guide covers the ocean tracking portion only. FourKites road, rail, and LTL tracking are out of scope here; keep them in FourKites unless you are replacing them with another provider. If you have FourKites ocean tracking in production, this page maps it onto Terminal49 field by field, so you can cut over without reverse-engineering our schema. Terminal49 is a direct-integration ocean and North American terminal API, self-serve, with holds and fees out of the box. FourKites is multi-modal, enterprise-priced, and centered on their broader visibility platform. This guide addresses only the ocean container portion. There is no compatibility shim. You will change your request code and your response parsing. For most integrations that is an afternoon. ## Start in sixty seconds Signing up and getting a key is self-serve. [Create an account](https://app.terminal49.com) — the free plan tracks up to 10 active containers. Generate a key at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). ```bash theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "tracking_request", "attributes": { "request_number": "YOUR_BOL_NUMBER", "request_type": "bill_of_lading", "scac": "MAEU" } } }' ``` The full API key is shown once, right after you create it. Copy it before you navigate away — after that it is masked and cannot be revealed. If you miss it, create a new key and delete the old one. The free plan tracks up to 10 active containers. Creating tracking requests through the API works right away; reading tracking data back through the API requires a free 7-day API trial (same 10-container limit) — contact us via in-app chat or [support@terminal49.com](mailto:support@terminal49.com) and we enable it. If you want to test without a live shipment, use the [test tracking numbers](/docs/api-docs/useful-info/test-numbers), which simulate success and failure outcomes. **Migration offer:** sign up now and the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id) — vessel schedules, AIS positions, and projected routes — is free for your first month. ## The architectural shift FourKites tracks shipments across modes in a single enterprise platform. Ocean is one product among many, organized around loads and shipments with nested segments. Terminal49 splits ocean tracking in two. You register a tracking request once. We keep it updated and push changes to your webhook. Query the FourKites platform for shipment or load state, often across ocean, road, and rail segments. Extract ocean data from the multi-modal shipment model. Polling, enterprise contract, and broad visibility scope. `POST /tracking_requests` once → Terminal49 polls carriers, terminals, and rail → we POST to your endpoint as things change → write to your database. No cache layer, no dedupe logic. Ocean only, direct terminal data. You can keep polling if you prefer — point your existing scheduler at `GET /v2/shipments` or `GET /v2/containers`. But webhooks are the reason the API is shaped this way, and terminal data (holds, fees, last free day) changes on a cadence that polling tends to miss. ## Quick comparison | | FourKites | Terminal49 | | ----------------------- | ---------------------------------------- | --------------------------------------- | | Tracking model | Poll via enterprise platform | Register once, then push or poll | | Authentication | Platform credentials (varies by product) | `Authorization: Token` header | | Base URL | Your FourKites API endpoint | `https://api.terminal49.com/v2` | | Content type | `application/json` | `application/vnd.api+json` | | Response format | Custom JSON | JSON:API | | Webhooks | Available | 30+ events, HMAC-signed | | Carrier identification | SCAC or internal mapping | `scac`, or `auto_detect_vocc_scac` | | Terminal holds and fees | No direct equivalent | Included on the container object | | Last free day | No direct equivalent | Included, with per-source breakdown | | Rail milestones | Available | North American Class I and short-line | | Multi-modal scope | Ocean, road, rail, freight | Ocean and North American terminals only | | Getting an API key | Enterprise contract | Self-serve | ## Authentication Move from your FourKites credentials — whichever scheme your FourKites product uses today — to a single `Authorization: Token` header. ```bash FourKites theme={null} # However your integration authenticates today, for example: curl "https:///shipments" \ -H "Authorization: Bearer YOUR_FOURKITES_TOKEN" ``` ```bash Terminal49 theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_T49_KEY" \ -d '{"data":{"type":"tracking_request","attributes":{ "request_number":"MRKU9465770", "request_type":"container", "scac":"MAEU"}}}' ``` Note the `Token` prefix. It is not `Bearer`. ## Request parameter mapping | FourKites parameter | Terminal49 equivalent | Notes | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Shipment / load identifier | `request_number` | | | Container number | `request_type: "container"` + `request_number` | | | Bill of lading number | `request_type: "bill_of_lading"` | Master or house BOL | | Booking number | `request_type: "booking_number"` | | | SCAC / carrier code | `scac` | Same SCAC values for most carriers | | Carrier auto-detection | Set `auto_detect_vocc_scac: true`, or call [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) first | Auto-detection runs asynchronously; a failed inference fails the tracking request with `scac_auto_detect_failed` | | Force refresh | `PATCH /v2/containers/{id}/refresh` | Forces an immediate pull from all sources. Paid feature — requires account enablement, limited to 10 requests per minute | | Route / journey legs | `GET /v2/containers/{id}/map_geojson` | Requires the Routing Data entitlement. See [Routing](/docs/api-docs/in-depth-guides/routing) | FourKites organizes around shipments and loads that may contain multiple segments across modes. Terminal49 takes one ocean identifier per tracking request. Track by BOL and we return every container on that bill of lading as related container resources. Container-number tracking requests are currently in beta. ## Response field mapping Terminal49 is JSON:API compliant, so relationships between shipments, containers, ports, and terminals are explicit rather than something you reassemble from ID references. Use the [`include` parameter](/docs/api-docs/in-depth-guides/including-resources) to sideload related resources in one call instead of chasing IDs. ### Shipment level | FourKites | Terminal49 | | -------------------------------- | -------------------------------------------------------------------------------------------------------- | | Bill of lading number | `shipment.attributes.bill_of_lading_number` | | Carrier SCAC | `shipment.attributes.shipping_line_scac` | | Carrier name | `shipment.attributes.shipping_line_name` | | Shipment status | Derived from container status and milestones | | Inland origin / place of receipt | No direct equivalent — Terminal49 shipments start at the port of lading | | Port of loading | `shipment.relationships.port_of_lading` (name and LOCODE also on `shipment.attributes.port_of_lading_*`) | | Port of discharge | `shipment.relationships.port_of_discharge` | | Final destination (inland) | `shipment.relationships.destination` | | ETA at discharge port | `shipment.attributes.pod_eta_at` | ### Container level | FourKites | Terminal49 | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Container number | `container.attributes.number` | | Equipment / ISO code | `container.attributes.equipment_type` + `equipment_length` + `equipment_height` | | Container status | `container.attributes.current_status` | | Event list | Container timestamps, [transport events](/docs/api-docs/api-reference/containers/get-a-containers-transport-events), and webhook events | FourKites may return an ISO code string like `45G1`. Terminal49 splits this into three normalized fields: type (dry, reefer, open top, flat rack, bulk, tank), length (10, 20, 40, 45), and height (standard, high cube). If you were parsing ISO codes yourself, you can delete that code. ### Locations, facilities, and vessels | FourKites | Terminal49 | | -------------------------- | ------------------------------------------------------------------------------------------------ | | Port name | `port.attributes.name` | | Port code / UN/LOCODE | `port.attributes.code` | | Port coordinates | `port.attributes.latitude` / `.longitude` | | Port timezone | `port.attributes.time_zone` | | Port country | `port.attributes.country_code` | | Terminal / facility name | `terminal.attributes.name` | | Terminal SMDG code | `terminal.attributes.smdg_code` | | Terminal BIC facility code | `terminal.attributes.bic_facility_code` | | Vessel name | `shipment.attributes.pod_vessel_name` | | Vessel IMO | `shipment.attributes.pod_vessel_imo` | | Vessel MMSI | Available via the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo) | | Vessel call sign / flag | Not returned | ## Milestone and event mapping FourKites returns milestones within your shipment or load resource. Terminal49 exposes the same milestones as normalized transport events and pushes each one to your webhook. Where mappings exist: | FourKites milestone | Terminal49 event | | ---------------------- | --------------------------------------- | | Loaded on vessel | `container.transport.vessel_loaded` | | Vessel departed | `container.transport.vessel_departed` | | Vessel arrived | `container.transport.vessel_arrived` | | Discharged from vessel | `container.transport.vessel_discharged` | | Full out / gated out | `container.transport.full_out` | | Full in / gated in | `container.transport.full_in` | | — | `container.transport.empty_out` | | — | `container.transport.empty_in` | Terminal49 also emits milestones with no FourKites ocean equivalent: * **Vessel berthed** — `container.transport.vessel_berthed` * **Available for pickup** — `container.transport.available` and `.not_available` * **Transshipment** — arrived, discharged, loaded, departed * **Feeder vessel and barge** — arrived, discharged, loaded, departed * **Rail** — loaded, departed, arrived, unloaded, plus `arrived_at_inland_destination` See the full [event catalog](/docs/api-docs/webhooks/event-catalog). ### Registering a webhook ```bash theme={null} curl -X POST https://api.terminal49.com/v2/webhooks \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "webhook", "attributes": { "url": "https://your-endpoint.example.com/t49", "active": true, "events": [ "container.transport.vessel_discharged", "container.transport.available", "container.pickup_lfd.changed" ] } } }' ``` Payloads are HMAC-signed. See [webhook setup](/docs/api-docs/in-depth-guides/webhooks) for signature verification, and [List webhook IPs](/docs/api-docs/api-reference/webhooks/list-webhook-ips) if your firewall restricts inbound traffic. ## What you gain This is the part worth reading even if the rest is mechanical. Terminal49 integrates with terminals directly, not only carriers, so the container object carries operational data that has no FourKites equivalent. ### Holds `holds_at_pod_terminal` is an array of active holds blocking pickup: ```json theme={null} { "holds_at_pod_terminal": [ { "name": "customs", "status": "hold", "description": "CBP HOLD" }, { "name": "freight", "status": "hold", "description": null } ] } ``` Hold names are `freight`, `customs`, `USDA`, `VACIS`, `TMF`, and `other`. Status is `hold` or `pending`. When a hold clears, the object is removed from the array — there is no released state. Hold names are case-sensitive. `USDA`, `VACIS`, and `TMF` are uppercase; `freight`, `customs`, and `other` are lowercase. Match exactly. ### Fees `fees_at_pod_terminal` carries type, amount, and currency: ```json theme={null} { "fees_at_pod_terminal": [ { "type": "demurrage", "amount": 850.00, "currency_code": "USD" }, { "type": "exam", "amount": 450.00, "currency_code": "USD" } ] } ``` Fee types are `demurrage`, `extended_dwell_time`, `exam`, `total`, and `other`. Some terminals report a `total` line item alongside individual fees. Filter it out before summing or you will double-count. ### Last free day `pickup_lfd` is a coalesced value that follows a fixed source priority: shipping line, then terminal, then rail. It does not pick the earliest date. The individual sources are available separately on `import_deadlines`: * `pickup_lfd_line` — the shipping line's LFD (per diem deadline) * `pickup_lfd_terminal` — the terminal's LFD (demurrage deadline) * `pickup_lfd_rail` — the rail carrier's LFD at the inland destination Each has its own webhook event, so you can alert on whichever source your operation cares about. ### Release readiness Two fields answer "can I pick this up?" — `available_for_pickup` and the holds array: ```javascript theme={null} function isReadyForPickup(container) { const { available_for_pickup, holds_at_pod_terminal } = container.attributes; const hasActiveHolds = holds_at_pod_terminal.some(h => h.status === 'hold'); return available_for_pickup === true && !hasActiveHolds; } ``` Full detail in [Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). Holds, fees, LFD, and availability come back on the container object wherever the terminal is a supported source. They are not a paid add-on and they do not require a sales conversation. See [Entitlements](/docs/api-docs/useful-info/entitlements) for the features that do require account enablement — Routing Data (container map and vessel positions), rail LFD, container refresh, and the embeddable map and widget are the gated ones. ## Gotchas that will bite you FourKites is an enterprise platform. Terminal49 is self-serve with a free plan that tracks up to 10 active containers. API reads require a free 7-day API trial we enable on request. If you need volume pricing, it exists, but you do not need a contract to start. FourKites shipments may contain road, rail, and ocean segments in one object. Terminal49 is ocean only. Map only the ocean leg. Keep road and rail in FourKites or replace them separately. FourKites event names and codes are specific to their platform. You will need a mapping table to translate them onto Terminal49's normalized events such as `container.transport.vessel_discharged`. Where exact mappings do not exist, handle the Terminal49 events directly. `POST /tracking_requests` returns immediately with a pending status. The shipment appears once the carrier responds. Subscribe to `tracking_request.succeeded` and `tracking_request.failed` rather than expecting shipment data in the creation response. A request may also land in `awaiting_manifest` if the carrier has not manifested the shipment yet — we retry automatically. See [Tracking Request Lifecycle](/docs/api-docs/in-depth-guides/tracking-request-lifecycle). FourKites may return local time or timestamps with offset. Terminal49 stores event timestamps in UTC and returns the matching IANA timezone alongside. Convert for display rather than assuming local time. See [Event Timestamps](/docs/api-docs/in-depth-guides/event-timestamps). `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` mean no active holds or fees. This is the common case. Do not treat it as missing data. Terminal changes (fees, holds, LFD, appointment, availability) arrive on `container.updated` with a `changeset` showing old value first, new value second. Use it instead of diffing state yourself. ## Error handling FourKites returns errors within the response body of their platform API. Terminal49 uses standard HTTP status codes. Replace body checks and message-string matching with status-code checks. | Status | Meaning | | ------ | ------------------------------------------------------------------------------- | | 400 | Malformed request or failed validation | | 401 | Missing or invalid API key | | 403 | Key lacks permission, or the feature is not enabled on your plan | | 404 | Resource does not exist | | 422 | Valid syntax, rejected content — for example a malformed container number | | 429 | Rate limited. See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) | | 5xx | Terminal49 or an upstream carrier or terminal is unavailable | Rough equivalence for the FourKites errors you are handling today: | FourKites error | Terminal49 | | ---------------------------- | ---------------------------------- | | Authentication failure | HTTP 401 | | Permission / access denied | HTTP 403 | | Rate limit exceeded | HTTP 429 | | Validation failure | HTTP 400 or 422 | | Resource not found | HTTP 404 | | Upstream carrier unavailable | Not surfaced — we retry internally | The [TypeScript SDK](/docs/sdk/introduction) maps these to typed errors (`AuthenticationError`, `ValidationError`, `RateLimitError`, `UpstreamError`, `FeatureNotEnabledError`, `AuthorizationError`, `NotFoundError`) and retries rate-limit and server errors automatically with exponential backoff. ## Where we are narrower than FourKites Worth knowing before you commit. **Carrier count.** Terminal49 integrates directly with 36 ocean carriers, plus 2 more enabled on request — as of 14 August 2026. Check your carrier mix against the [ocean carrier list](/docs/coverage/ocean-carriers) before cutover, and read the known issues section there — we publish the per-carrier field gaps. **Terminal data is North America.** Holds, fees, LFD, and availability come from direct terminal integrations concentrated in the US and Canada, with European ports expanding. Ocean milestones work globally; terminal-level operational data does not yet. **No road, rail, or LTL tracking.** FourKites is multi-modal. Terminal49 is ocean and North American terminals only. This migration handles only the ocean portion. **No freight rates or sailing schedules.** We do not offer a rate calculator, rate index, or schedule search. **Some fields are source-dependent.** Seal number, container weight, and departure or arrival events vary by carrier. The [field availability reference](/docs/coverage/fields) says which fields are always present and which depend on the carrier, terminal, or journey. ## Migration checklist Everyone does the base path. Then pick a branch. ### Base path Self-serve at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). Copy it immediately — it is shown once. Move from OAuth2 bearer or API key to `Authorization: Token`. Note the `Token` prefix. Compare your FourKites carrier values against the [carrier list](/docs/coverage/ocean-carriers). Flag anything missing before you cut over. Identify the ocean leg of each FourKites shipment. Discard road, rail, and LTL for this migration. One `POST /tracking_requests` per BOL, booking, or container, replacing the per-request lookup. Tracking requests start pending. Handle `succeeded`, `failed`, and `awaiting_manifest` rather than expecting data on creation. JSON:API structure, split equipment fields, UTC timestamps with a separate timezone. Replace body checks with HTTP status codes. Submit tracking requests for everything currently in transit. Send us the list if it is large and we will load it. Holds, fees, and LFD are the reason to do this properly rather than porting like for like. ### Then pick one 1. Expose an HTTPS endpoint that accepts our POST payloads. 2. Register a webhook and subscribe only to events you act on. 3. Verify HMAC signatures. 4. Whitelist our IPs if your firewall restricts inbound traffic. 5. Trigger a test delivery before going live. 6. Retire your polling job and your dedupe layer. See [webhook best practices](/docs/api-docs/webhooks/best-practices) for retries and idempotency. 1. Store the tracking request ID from the creation response. The shipment ID arrives later — the creation response is pending with no shipment attached; fetch the tracking request again (or handle `tracking_request.succeeded`) to get it once the carrier responds. 2. Repoint your existing scheduler at `GET /v2/shipments` or `GET /v2/containers`. 3. Keep your existing cadence. Skipped: endpoint setup, signature verification, IP whitelisting, delivery testing. Terminal data changes on a cadence polling tends to miss. If you only adopt webhooks for one thing, make it `container.updated` and `container.pickup_lfd.changed`. ## Migrate with an AI coding agent If you use Claude Code, Cursor, Codex, Windsurf, Copilot, or another AI coding assistant, hand it the prompt below. It is written to run a **side-by-side migration**: the agent stands up a Terminal49 client next to your existing FourKites code, shadows every FourKites call with a Terminal49 call, diffs the responses, and only cuts over once parity is proven. Point your agent at this page as context (paste the URL or add it as a doc source). The prompt references the mappings above, so the more of this page the agent can see, the better it does. 1. Open your repo in your AI coding tool. 2. Add this page as a documentation source, or paste its URL into the chat. 3. Copy the prompt below into a new chat and send it. 4. Answer the agent's discovery questions (FourKites client location, env var names, carrier mix). 5. Review each PR the agent opens. It should ship in small, reviewable steps: client, shadow, parity harness, cutover, cleanup. * A `Terminal49Client` alongside your existing FourKites client, sharing the same interface where possible. * A shadow-mode wrapper that calls both providers and logs response diffs without changing behavior. * A parity report per shipment: matched fields, diverged fields, and Terminal49-only fields (holds, fees, LFD). * A feature-flagged cutover: route reads to Terminal49, keep FourKites as fallback until you flip the flag off. * A webhook receiver with HMAC verification, or a polling scheduler, depending on which path you pick. * A cleanup PR that removes FourKites code, env vars, dependencies, and dedupe logic. ### The prompt Copy this into your agent. Replace the bracketed placeholders in the **Repo context** block before sending. ```markdown Terminal49 migration agent prompt expandable icon=robot wrap theme={null} You are migrating this codebase from the FourKites API to the Terminal49 API, side by side. Terminal49's authoritative migration guide is at https://terminal49.com/docs/migrate/fourkites. Use it as the source of truth for field mappings, event names, error codes, and behavior differences. # Repo context (fill this in before running) - FourKites client lives at: [path/to/client] - FourKites is called from: [list the call sites or "find them"] - Language and framework: [e.g. TypeScript + Node, Python + FastAPI, Ruby on Rails] - Storage for tracking state: [e.g. Postgres table `shipments`] - Current polling schedule: [e.g. cron every 2h] - Carrier mix (SCACs we track most): [e.g. MAEU, MSCU, CMDU, HLCU, ONEY] - Deployment target: [e.g. AWS ECS, Vercel, Fly.io] - Secret store: [e.g. AWS Secrets Manager, `.env`, Doppler] # Rules 1. Do not delete FourKites code until the cleanup step. Migration is side by side. 2. Ship in small PRs. Each PR must build, pass tests, and be independently revertable. 3. Never invent Terminal49 fields, endpoints, or event names. If the guide does not confirm a mapping, ask me. Do not guess. 4. Terminal49 uses `Authorization: Token ` (not `Bearer`) and content type `application/vnd.api+json`. Get this right on the first request. 5. Terminal49 is asynchronous. `POST /tracking_requests` returns pending. Data arrives via `tracking_request.succeeded`, `tracking_request.failed`, or `tracking_request.awaiting_manifest` webhooks, or by polling `GET /v2/shipments`. Do not expect shipment data on creation. 6. Timestamps are UTC with a separate IANA `time_zone` field. Do not assume local time. 7. `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` are the normal state, not missing data. A fee `amount` of 0 is valid. 8. On `container.updated`, prefer the `changeset` over diffing state yourself. 9. Terminal49 covers the ocean leg only. Keep FourKites road, rail, and LTL tracking in place (or leave stubs pointing elsewhere). Extract the ocean segment from each multi-modal FourKites shipment before mapping it. # Plan (execute in order, one PR per step) ## PR 1 — Discovery and interface - Grep the repo for every FourKites call site. List them in the PR description. - Extract the FourKites client's public surface into an interface (`TrackingProvider` with methods like `track(number, type, carrier)`, `getShipment(id)`, `refresh(id)`). - Make the existing FourKites client implement it. No behavior change. ## PR 2 — Terminal49 client - Add a `Terminal49Client` implementing the same `TrackingProvider` interface. - Auth via `T49_API_KEY` env var, header `Authorization: Token ${key}`. - Base URL `https://api.terminal49.com/v2`, content type `application/vnd.api+json`. - Implement: - `createTrackingRequest({ request_number, request_type, scac })` returning the tracking request ID. - `getShipment(id, { include: 'containers,port_of_lading,port_of_discharge' })`. - `getContainer(id)`. - `refreshContainer(id)` mapping to `PATCH /v2/containers/{id}/refresh` (a paid feature — skip it unless our account has it enabled). - Normalize responses to the same shape FourKites callers expect today, using the field mapping from the guide. Split equipment codes into `equipment_type`, `equipment_length`, `equipment_height`. Convert timestamps to UTC + `time_zone`. - Map errors to typed classes: `AuthenticationError` (401), `AuthorizationError` (403), `ValidationError` (400/422), `RateLimitError` (429), `UpstreamError` (5xx), `FeatureNotEnabledError` (403 + feature flag response). - Add unit tests using recorded fixtures. Do not hit the live API in tests. ## PR 3 — Shadow mode - Add a `ShadowProvider` that wraps both clients. On every read: - Call FourKites as the primary. Return its response. - Fire-and-forget a Terminal49 call for the same identifier. - Log a structured diff: matched fields, diverged fields, T49-only fields. - Gate with env var `TRACKING_SHADOW_MODE=true`. Off by default. - Add a parity report script that aggregates shadow logs by SCAC and field. - Do NOT change what callers see. This step is observation only. ## PR 4 — Backfill script - Write a one-shot script that reads all active shipments from our database and calls `POST /tracking_requests` for each (one per BOL, booking, or container). - Store the returned `tracking_request.id` and eventual `shipment.id` on our records. - Rate-limit to respect Terminal49's limits (handle 429 with exponential backoff). - Idempotent: safe to re-run. Skip rows already backfilled. ## PR 5 — Webhook receiver (only if we picked the webhook path) - Add `POST /webhooks/terminal49` endpoint. - Verify HMAC signature using the shared secret from `T49_WEBHOOK_SECRET`. Reject on mismatch with 401. - Handle these events at minimum: - `tracking_request.succeeded`, `tracking_request.failed`, `tracking_request.awaiting_manifest` - `container.transport.vessel_discharged`, `container.transport.available`, `container.transport.full_out` - `container.updated` (use the `changeset`, do not diff) - `container.pickup_lfd.changed` - Persist events idempotently keyed by event ID. - Register the webhook via `POST /v2/webhooks` from a bootstrap script, subscribing only to events we handle. - If our firewall restricts inbound traffic, whitelist Terminal49 IPs from `GET /v2/webhooks/ips`. ## PR 6 — Cutover behind a flag - Add feature flag `TRACKING_PROVIDER` with values `fourkites` (default) and `terminal49`. - Route all reads through the flag. FourKites stays available as a fallback for one release cycle. - Flip staging to `terminal49`, verify parity report is clean, then flip production. ## PR 7 — Cleanup - Delete the FourKites client, its tests, its env vars, its dedupe cache, and any equipment-code parsing helpers Terminal49 makes redundant. - Remove the shadow provider and the feature flag. - Update README and any runbooks. Note the new webhook endpoint if applicable. # Definition of done - All FourKites call sites now go through Terminal49. - Webhook (or polling) is live in production. - Parity report shows no unexplained divergences for our top 10 SCACs. - Holds, fees, and LFD are exposed to whichever downstream system needs them (dashboard, alerts, customer emails). Do not migrate without wiring these up. They are the reason to do this properly. - FourKites dependency, env vars, and dead code are gone from the repo. # Ask me before you - Choose between the webhook path and the polling path. Default to webhooks unless our infra makes an inbound HTTPS endpoint hard. - Change the shape of any function FourKites callers use today. Prefer a normalization layer inside the Terminal49 client. - Add a new dependency. Prefer stdlib and what is already in the repo. - Touch anything outside the tracking integration. ``` ## Getting help Send us your list of active container and bill of lading numbers and we will load them rather than making you script the backfill. If something in this mapping is wrong or incomplete, tell us. We would rather fix the page than have you work around it. Every endpoint, with request and response schemas Typed client with retries and pagination built in Carriers, terminals, rail, and field availability Simulate success and failure outcomes # Migrating from Gnosis Freight Source: https://terminal49.com/docs/migrate/gnosisfreight Map Gnosis Freight CLM Platform fields, OAuth2 auth, and container polling to Terminal49 equivalents. Includes request mapping and checklist. If you have Gnosis Freight's CLM platform in production, this page maps it onto Terminal49 field by field, so you can cut over without reverse-engineering our schema. There is no compatibility shim. You will change your request code and your response parsing. Gnosis and Terminal49 both track ocean containers through terminals and rail, so most of the work is a rename and reshape, not a redesign. ## Start in sixty seconds Signing up and getting a key is self-serve. Sign up at [app.terminal49.com](https://app.terminal49.com). The free plan tracks up to 10 active containers. Create a key at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). ```bash theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "tracking_request", "attributes": { "request_number": "YOUR_BOL_NUMBER", "request_type": "bill_of_lading", "scac": "MAEU" } } }' ``` The full API key is shown once, right after you create it. Copy it before you navigate away. After that it is masked and cannot be revealed. If you miss it, create a new key and delete the old one. The free plan tracks up to 10 active containers. Creating tracking requests through the API works right away; reading tracking data back through the API requires a free 7-day API trial (same 10-container limit) — contact us via in-app chat or [support@terminal49.com](mailto:support@terminal49.com) and we enable it. If you want to test without a live shipment, use the [test tracking numbers](/docs/api-docs/useful-info/test-numbers), which simulate success and failure outcomes. **Migration offer:** sign up now and the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id) — vessel schedules, AIS positions, and projected routes — is free for your first month. ## The architectural shift Gnosis's CLM platform (tracking engine "Marlo") and Terminal49 both integrate with ocean carriers, terminals, and rail. The difference is in how you fetch the result and how much of the operational picture arrives already computed for you. Get an OAuth2 token via POST /api/auth/token. Create a tracking request via POST /api/v1/tracking\_requests/ with an array of MBL numbers. Poll GET /api/v1/containers/ for state, or read a separate webhook schema for push updates. POST /tracking\_requests once, per bill of lading. Terminal49 polls carriers, terminals, and rail, then POSTs to your endpoint as things change. Container objects carry holds, fees, and last free day directly. The biggest mechanical change is the tracking request shape: Gnosis accepts an array of MBL numbers in one call, while Terminal49 takes one bill of lading, booking, or container number per tracking request. If you batch MBLs today, you will loop over that array and issue one request per number. ## Quick comparison | | Gnosis Freight | Terminal49 | | ---------------------- | ---------------------------------------------------------- | --------------------------------------------- | | Tracking model | Create tracking request, then poll or receive push updates | Register once, then push or poll | | Authentication | OAuth2 password grant, `Bearer` token | `Authorization: Token` header | | Base URL | `https://api.freight.fyi` | `https://api.terminal49.com/v2` | | Content type | `application/json` | `application/vnd.api+json` | | Response format | Custom JSON, paginated container list | JSON:API | | Webhooks | Schema published, dictionary of field names and types | 30+ named events, HMAC-signed | | Carrier identification | Inferred from MBL number | `scac`, or `auto_detect_vocc_scac` | | Modes covered | Ocean, rail, air, customs, drayage execution | Ocean and rail | | Fee prediction | Predictive amounts (`gnosis_estimated_*` fields) | Terminal-reported amounts only, no prediction | | Getting an API key | Issued by an account rep or via the CLM portal | Self-serve | ## Authentication Gnosis uses an OAuth2 password grant: exchange a username and password for a bearer token, then send that token on every request. Terminal49 uses a static API key sent as a token, with no separate token-exchange step. ```bash Gnosis Freight theme={null} # Step 1: get a token curl -X POST https://api.freight.fyi/api/auth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=YOUR_USERNAME&password=YOUR_PASSWORD" # Step 2: use it curl -X POST https://api.freight.fyi/api/v1/tracking_requests/ \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{"mbl_numbers": ["MRKU9465770"]}' ``` ```bash Terminal49 theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_T49_KEY" \ -d '{"data":{"type":"tracking_request","attributes":{ "request_number":"MRKU9465770", "request_type":"bill_of_lading"}}}' ``` Note the prefix. Gnosis uses `Bearer`. Terminal49 uses `Token`. Gnosis tokens are issued from a username and password, and (depending on how your account is configured) may expire and need refreshing. Terminal49 API keys are static: generate one in the dashboard and use it until you rotate it yourself. There is no token-refresh flow to build. ## Request parameter mapping | Gnosis Freight parameter | Terminal49 equivalent | Notes | | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mbl_numbers` (array) | `request_number` + `request_type: "bill_of_lading"` | Terminal49 takes one identifier per tracking request, not an array. Loop over your MBL array and issue one `POST /tracking_requests` per number. | | Carrier detected automatically from MBL | `scac`, or `auto_detect_vocc_scac: true` | Terminal49 can also infer the SCAC when you set `auto_detect_vocc_scac: true`, or by calling [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) first. Auto-detection is asynchronous; a failed inference fails the tracking request with `scac_auto_detect_failed`. | | `organization_uuid` query param | Not applicable | Terminal49 scopes tracking requests to your account by API key; there is no separate organization identifier to pass. | | Booking, per-container, and air variants of create tracking request | `request_type: "booking_number"` or `request_type: "container"` for ocean; no air equivalent | Container-number tracking requests are currently in beta. Air cargo has no Terminal49 equivalent. | | Polling `GET /api/v1/containers/` | `POST /v2/webhooks`, or `GET /v2/containers` | Register a webhook to be notified of changes instead of polling, or keep polling if that fits your architecture better. | | Re-poll a specific container | `PATCH /v2/containers/{id}/refresh` | Forces an immediate pull from all sources. Paid feature — requires account enablement, limited to 10 requests per minute. | Gnosis's `POST /api/v1/tracking_requests/` accepts multiple MBL numbers in a single call and returns one tracking request per number internally. Terminal49's `POST /v2/tracking_requests` is one call per bill of lading, booking, or container. Track by BOL and we return every container on that bill of lading as related container resources. ## Response field mapping Terminal49 is JSON:API compliant, so relationships between shipments, containers, ports, and terminals are explicit rather than something you reassemble from ID references. Gnosis returns a flat, paginated container list from `GET /api/v1/containers/` (`{metadata, containers: [...]}`); Terminal49 splits the same information across shipment, container, port, and terminal resources. Use the [`include` parameter](/docs/api-docs/in-depth-guides/including-resources) to sideload related resources in one call instead of chasing IDs. ### Shipment level | Gnosis Freight | Terminal49 | | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mbl` / `mbls` | `shipment.attributes.bill_of_lading_number` | | Carrier inferred from MBL | `shipment.attributes.shipping_line_scac`, `shipment.attributes.shipping_line_name` | | `por_locode`, `por_city` | `shipment.relationships.port_of_lading` (place of receipt, where applicable) | | `pol_locode`, `pol_city` | `shipment.relationships.port_of_lading` | | `pod_locode`, `pod_city` | `shipment.relationships.port_of_discharge` | | `vessel_eta_dt` | `shipment.attributes.pod_eta_at` | | `gnosis_vessel_eta_dt` (predictive ETA) | No equivalent. Terminal49 does not publish a separate predictive ETA field. | | `vessel_ata_dt` | `shipment.attributes.pod_ata_at` | | `mother_vessel` / `mother_vessel_imo` | `shipment.attributes.pod_vessel_name` / `pod_vessel_imo` | | `mother_voyage` | Not returned as a discrete voyage-number field | | `current_vessel`, `first_vessel` | Not applicable — Terminal49 exposes the vessel active for the current leg via [transport events](/docs/api-docs/api-reference/containers/get-a-containers-transport-events) rather than named "current"/"first" vessel fields | | `transshipments` | [Transshipment transport events](/docs/api-docs/webhooks/event-catalog) (arrived, discharged, loaded, departed) | | `barge_journeys` | [Feeder vessel transport events](/docs/api-docs/webhooks/event-catalog) (arrived, discharged, loaded, departed) | ### Container level | Gnosis Freight | Terminal49 | | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `container_number` | `container.attributes.number` | | `uuid` | `container.id` | | `container_journey_start_key` | No direct equivalent | | `container_status` (e.g. "At Origin", "On the Water", "Awaiting Discharge") | `container.attributes.current_status` (Terminal49's own status values — see [Container Statuses](/docs/api-docs/in-depth-guides/container-statuses)) | | `tracking` (bool) | Whether the parent tracking request or shipment is active; see `shipment.attributes.tracked` and [stop/resume tracking](/docs/api-docs/api-reference/shipments/stop-tracking-shipment) | | `available_for_pickup` (bool) | `container.attributes.available_for_pickup` — same name, same boolean | | Equipment size/type (not itemized in the verified field list) | `container.attributes.equipment_type` + `equipment_length` + `equipment_height` | Terminal49 normalizes equipment into three fields: type (dry, reefer, open top, flat rack, bulk, tank), length (10, 20, 40, 45), and height (standard, high cube). If you parse a combined size/type code from Gnosis today, you can delete that parsing step. ### Locations, facilities, and vessels | Gnosis Freight | Terminal49 | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `pol_locode` / `pod_locode` / `por_locode` | `port.attributes.code` on the relevant port relationship | | `pol_city` / `pod_city` / `por_city` | `port.attributes.name` | | `pol_terminal_name` | `terminal.attributes.name` on `port_of_lading_terminal` | | `pod_terminal_name` (with FIRMS code) | `terminal.attributes.name` + `terminal.attributes.firms_code` on `pod_terminal` | | Terminal SMDG code (not in Gnosis's verified field list) | `terminal.attributes.smdg_code` | | Terminal BIC facility code (not in Gnosis's verified field list) | `terminal.attributes.bic_facility_code` | | `mother_vessel` / `mother_vessel_imo` | `shipment.attributes.pod_vessel_name` / `pod_vessel_imo` | | Vessel MMSI, call sign, position | Available via the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo) for MMSI and position; call sign is not returned | ## Milestone and event mapping Gnosis exposes milestones as dated fields on the container object (`loaded_on_vessel_dt`, `discharged_dt`, and so on) plus a separate webhook schema endpoint. Terminal49 exposes the same milestones as normalized transport events and pushes each one to your webhook. | Gnosis Freight field | Milestone | Terminal49 event | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | `loaded_on_vessel_dt` | Loaded on vessel at origin | `container.transport.vessel_loaded` | | Vessel departure | Vessel departed origin | `container.transport.vessel_departed` | | `vessel_ata_dt` | Vessel arrived at destination | `container.transport.vessel_arrived` | | `discharged_dt` | Discharged from vessel | `container.transport.vessel_discharged` | | `available_for_pickup` flipping true | Available for pickup | `container.transport.available` | | `available_for_pickup` flipping false | No longer available for pickup | `container.transport.not_available` | | `is_railing` / `loaded_on_rail_dt` | Loaded onto rail car | `container.transport.rail_loaded` | | Rail departure | Rail car departed | `container.transport.rail_departed` | | `rail_ata_dt` | Rail car arrived | `container.transport.rail_arrived` | | `rail_discharged_dt` | Container unloaded from rail car | `container.transport.rail_unloaded` | | `rail_notify_dt` | Arrived at final inland destination | `container.transport.arrived_at_inland_destination` | | `customs_clearance_dt` | No direct transport-event equivalent. See the `customs` entry on `holds_at_pod_terminal` for a hold-based signal instead. | — | Terminal49 also emits milestones Gnosis's verified field list has no equivalent for: * **Empty picked up / returned:** `container.transport.empty_out` and `.empty_in` * **Full gated in / out:** `container.transport.full_in` and `.full_out` * **Vessel berthed:** `container.transport.vessel_berthed` * **Transshipment:** arrived, discharged, loaded, departed * **Feeder vessel and barge:** arrived, discharged, loaded, departed * **ETA changes:** `container.transport.estimated.vessel_arrived`, `shipment.estimated.arrival` See the full [event catalog](/docs/api-docs/webhooks/event-catalog). ### Registering a webhook Gnosis publishes its webhook shape as a schema dictionary at `GET /api/v1/webhooks/containers/webhook_schema`, which you inspect and subscribe to out of band. Terminal49 webhooks are self-service: register a URL and a list of named events directly. ```bash theme={null} curl -X POST https://api.terminal49.com/v2/webhooks \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "webhook", "attributes": { "url": "https://your-endpoint.example.com/t49", "active": true, "events": [ "container.transport.vessel_discharged", "container.transport.available", "container.pickup_lfd.changed" ] } } }' ``` Payloads are HMAC-signed. See [webhook setup](/docs/api-docs/in-depth-guides/webhooks) for signature verification, and [List webhook IPs](/docs/api-docs/api-reference/webhooks/list-webhook-ips) if your firewall restricts inbound traffic. ## What you gain This is the part worth reading even if the rest is mechanical. Terminal49 integrates with terminals directly, and normalizes holds, fees, and last free day into a fixed shape you don't have to reconcile yourself. ### Holds `holds_at_pod_terminal` is an array of active holds blocking pickup: ```json theme={null} { "holds_at_pod_terminal": [ { "name": "customs", "status": "hold", "description": "CBP HOLD" }, { "name": "freight", "status": "hold", "description": null } ] } ``` Hold names are `freight`, `customs`, `USDA`, `VACIS`, `TMF`, and `other`. Status is `hold` or `pending`. When a hold clears, the object is removed from the array. There is no released state. Hold names are case-sensitive. `USDA`, `VACIS`, and `TMF` are uppercase; `freight`, `customs`, and `other` are lowercase. Match exactly. Gnosis's `holds` object is a dictionary of hold types where a `true` value means the container is held by that party. Terminal49's shape is an array of objects, one per active hold, each carrying a name, a status, and a description. Rewrite any code that checks `holds.customs === true` to instead check whether an entry with `name: "customs"` exists in the array. ### Fees `fees_at_pod_terminal` carries type, amount, and currency: ```json theme={null} { "fees_at_pod_terminal": [ { "type": "demurrage", "amount": 850.00, "currency_code": "USD" }, { "type": "exam", "amount": 450.00, "currency_code": "USD" } ] } ``` Fee types are `demurrage`, `extended_dwell_time`, `exam`, `total`, and `other`. Some terminals report a `total` line item alongside individual fees. Filter it out before summing or you will double-count. Gnosis's `demurrage_amount` maps to a `demurrage` entry in `fees_at_pod_terminal`. Gnosis also publishes predictive fields — `gnosis_estimated_demurrage_amount`, `gnosis_estimated_detention_amount`, and `gnosis_estimated_next_day_demurrage_amount` — that project charges before the terminal reports them. Terminal49 has no equivalent: `fees_at_pod_terminal` only reports amounts the terminal has actually posted. If your workflow depends on predictive fee amounts, budget time to either drop that logic or replace it with your own estimate. ### Last free day `pickup_lfd` is a coalesced value that follows a fixed source priority: shipping line, then terminal, then rail. It does not pick the earliest date. The individual sources are available separately on `import_deadlines`: * `pickup_lfd_line`: the shipping line's LFD (per diem deadline) — the Terminal49 equivalent of Gnosis's `last_free_detention_day_dt` * `pickup_lfd_terminal`: the terminal's LFD (demurrage deadline) — the equivalent of Gnosis's `last_free_demurrage_day_dt` * `pickup_lfd_rail`: the rail carrier's LFD at the inland destination Each has its own webhook event, so you can alert on whichever source your operation cares about. Gnosis's `gnosis_estimated_last_free_demurrage_day_dt` is a predictive estimate; Terminal49 has no equivalent, since `import_deadlines` only carries dates the terminal or line has actually published. ### Release readiness Two fields answer "can I pick this up?" `available_for_pickup` and the holds array: ```javascript theme={null} function isReadyForPickup(container) { const { available_for_pickup, holds_at_pod_terminal } = container.attributes; const hasActiveHolds = holds_at_pod_terminal.some(h => h.status === 'hold'); return available_for_pickup === true && !hasActiveHolds; } ``` Gnosis's `available_for_pickup` field is a straight match — same name, same boolean. The difference is what you check alongside it: swap a dictionary lookup on `holds` for an array scan on `holds_at_pod_terminal`. Full detail in [Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). Holds, fees, LFD, and availability come back on the container object wherever the terminal is a supported source. They are not a paid add-on and they do not require a sales conversation. See [Entitlements](/docs/api-docs/useful-info/entitlements) for the features that do require account enablement. Routing Data (container map and vessel positions), rail LFD, container refresh, and the embeddable map and widget are the gated ones. ## Gotchas that will bite you Gnosis's `POST /api/v1/tracking_requests/` accepts an array of `mbl_numbers` in a single call. Terminal49's `POST /v2/tracking_requests` takes one `request_number` per call. If you currently batch MBLs, loop over the array and fire one request per bill of lading; store the returned `tracking_request.id` per number, not per batch. Gnosis requires exchanging a username and password for a bearer token before every session (and refreshing it as needed). Terminal49 API keys are static — generate one, send it as `Authorization: Token YOUR_API_KEY`, and delete the token-refresh logic entirely. Gnosis returns a flat `{metadata, containers: [...]}` list. Terminal49 relationships are ID references into an `included` array. Use a JSON:API client library, or use `include` to sideload exactly what you need. Parsing raw JSON works but you will write more code than you expect. Gnosis's `holds` object marks each hold type `true` or `false`. Terminal49's `holds_at_pod_terminal` is an array containing only the holds currently active, each with a name, status, and description. Rewrite `holds.customs === true` checks as array-membership checks. Terminal49 stores event timestamps in UTC and returns the matching IANA timezone alongside. Convert for display rather than assuming local time. See [Event Timestamps](/docs/api-docs/in-depth-guides/event-timestamps). `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` mean no active holds or fees. This is the common case. Do not treat it as missing data. Terminal changes (fees, holds, LFD, appointment, availability) arrive on `container.updated` with a `changeset` showing old value first, new value second. Use it instead of diffing state yourself, the way you might diff two `GET /api/v1/containers/` polls today. `POST /tracking_requests` returns immediately with a pending status. The shipment appears once the carrier responds. Subscribe to `tracking_request.succeeded`, `tracking_request.failed`, and `tracking_request.awaiting_manifest` rather than expecting shipment data in the creation response. See [Tracking Request Lifecycle](/docs/api-docs/in-depth-guides/tracking-request-lifecycle). ## Error handling Gnosis returns a `422 HTTPValidationError` with an array of validation details (location, message, type) for malformed requests. Terminal49 uses standard HTTP status codes consistently across every endpoint. | Status | Meaning | | ------ | ------------------------------------------------------------------------------- | | 400 | Malformed request or failed validation | | 401 | Missing or invalid API key | | 403 | Key lacks permission, or the feature is not enabled on your plan | | 404 | Resource does not exist | | 422 | Valid syntax, rejected content. For example, a malformed container number | | 429 | Rate limited. See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) | | 5xx | Terminal49 or an upstream carrier or terminal is unavailable | Rough equivalence for the Gnosis errors you are handling today: | Gnosis Freight | Terminal49 | | --------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | Invalid or expired OAuth2 token | HTTP 401 | | `422 HTTPValidationError` (bad MBL format, missing field) | HTTP 400 or 422 | | Carrier not supported | HTTP 422 | | No data found | `tracking_request.failed`, or `tracking_request.awaiting_manifest` if not yet manifested | | Temporary unavailability | Not surfaced. We retry internally. | The [TypeScript SDK](/docs/sdk/introduction) maps these to typed errors (`AuthenticationError`, `ValidationError`, `RateLimitError`, `UpstreamError`, `FeatureNotEnabledError`, `AuthorizationError`, `NotFoundError`) and retries rate-limit and server errors automatically with exponential backoff. ## Where we are narrower than Gnosis Freight Worth knowing before you commit. **No air cargo tracking.** Gnosis's CLM platform covers air cargo alongside ocean and rail. Terminal49 tracks ocean containers and their rail legs only. If your integration depends on air shipment visibility, this migration handles only the ocean and rail portion. **No drayage execution features.** Gnosis is built as an execution platform with import drayage workflows (`import_drayage`). Terminal49 is a tracking and visibility API — it tells you what happened and what's next, but it does not book, dispatch, or execute drayage moves. **No customs milestone tracking.** Gnosis exposes customs milestones and a `customs_clearance_dt` field directly. Terminal49 has no direct equivalent beyond the `customs` entry on `holds_at_pod_terminal`, which tells you whether a customs hold is currently blocking pickup, not a full customs event timeline. **No predictive fee or ETA estimates.** Gnosis publishes `gnosis_estimated_*` fields for demurrage, detention, next-day demurrage, and LFD, plus a `gnosis_vessel_eta_dt` predictive ETA. Terminal49 reports what carriers and terminals have actually posted — `pod_eta_at`, `fees_at_pod_terminal`, and `import_deadlines` — and does not predict future values. **Carrier count.** Terminal49 integrates directly with 36 ocean carriers, plus 2 more enabled on request, as of 14 August 2026. Check your carrier mix against the [ocean carrier list](/docs/coverage/ocean-carriers) before cutover, and read the known issues section there. We publish the per-carrier field gaps. **Terminal data is North America.** Holds, fees, LFD, and availability come from direct terminal integrations concentrated in the US and Canada, with European ports expanding. Ocean milestones work globally; terminal-level operational data does not yet. **Some fields are source-dependent.** Seal number, container weight, and departure or arrival events vary by carrier. The [field availability reference](/docs/coverage/fields) says which fields are always present and which depend on the carrier, terminal, or journey. ## Migration checklist Everyone does the base path. Then pick a branch. ### Base path Self-serve at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). Copy it immediately, it is shown once. Drop the OAuth2 password-grant token exchange. Move to `Authorization: Token`, a single static key. Compare your Gnosis-tracked carriers against the [carrier list](/docs/coverage/ocean-carriers). Flag anything missing before you cut over. Replace one call with an array of `mbl_numbers` with one `POST /tracking_requests` per bill of lading, booking, or container. Tracking requests start pending. Handle `succeeded`, `failed`, and `awaiting_manifest` rather than expecting data on creation. JSON:API structure, split equipment fields, UTC timestamps with a separate timezone, holds as an array instead of a boolean map. Replace `422 HTTPValidationError` envelope parsing with HTTP status-code checks. Submit tracking requests for everything currently in transit. Send us the list if it is large and we will load it. Gnosis's `gnosis_estimated_*` amounts and ETA have no Terminal49 equivalent. Decide whether to drop that logic or replace it with your own estimate before cutover. ### Then pick one Accept our POST payloads at a public URL. Subscribe only to events you act on. Reject any payload whose signature does not match. Only needed if your firewall restricts inbound traffic. Confirm end-to-end before going live. Remove the loop that called `GET /api/v1/containers/` on a schedule. See [webhook best practices](/docs/api-docs/webhooks/best-practices) for retries and idempotency. The creation response is pending and carries no shipment yet. Keep the tracking request ID, then fetch the tracking request again (or handle `tracking_request.succeeded`) to get the shipment ID once the carrier responds. Point it at `GET /v2/shipments` or `GET /v2/containers` instead of `GET /api/v1/containers/`. No change to how often you poll. Skipped: endpoint setup, signature verification, IP whitelisting, delivery testing. Terminal data changes on a cadence polling tends to miss. If you only adopt webhooks for one thing, make it `container.updated` and `container.pickup_lfd.changed`. ## Migrate with an AI coding agent If you use Cursor, Claude Code, Windsurf, Copilot, or another AI coding assistant, hand it the prompt below. It is written to run a **side-by-side migration**: the agent stands up a Terminal49 client next to your existing Gnosis Freight code, shadows every Gnosis call with a Terminal49 call, diffs the responses, and only cuts over once parity is proven. Point your agent at this page as context (paste the URL or add it as a doc source). The prompt references the mappings above, so the more of this page the agent can see, the better it does. 1. Open your repo in your AI coding tool. 2. Add this page as a documentation source, or paste its URL into the chat. 3. Copy the prompt below into a new chat and send it. 4. Answer the agent's discovery questions (Gnosis client location, env var names, carrier mix). 5. Review each PR the agent opens. It should ship in small, reviewable steps: client, shadow, parity harness, cutover, cleanup. * A `Terminal49Client` alongside your existing `GnosisClient`, sharing the same interface where possible. * A shadow-mode wrapper that calls both providers and logs response diffs without changing behavior. * A parity report per shipment: matched fields, diverged fields, and Terminal49-only fields (holds, fees, LFD). * A feature-flagged cutover: route reads to Terminal49, keep Gnosis as fallback until you flip the flag off. * A webhook receiver with HMAC verification, or a polling scheduler, depending on which path you pick. * A cleanup PR that removes Gnosis code, env vars, the OAuth2 token-refresh logic, dependencies, and dedupe logic. ### The prompt Copy this into your agent. Replace the bracketed placeholders in the **Repo context** block before sending. ```markdown Terminal49 migration agent prompt expandable icon=robot wrap theme={null} You are migrating this codebase from the Gnosis Freight CLM API to the Terminal49 API, side by side. Terminal49's authoritative migration guide is at https://terminal49.com/docs/migrate/gnosisfreight. Use it as the source of truth for field mappings, event names, error codes, and behavior differences. # Repo context (fill this in before running) - Gnosis client lives at: [path/to/gnosis/client.ts] - Gnosis is called from: [list the call sites or "find them"] - Language and framework: [e.g. TypeScript + Node, Python + FastAPI, Ruby on Rails] - Storage for tracking state: [e.g. Postgres table `shipments`] - Current polling schedule: [e.g. cron every 2h via BullMQ] - Carrier mix (SCACs we track most): [e.g. MAEU, MSCU, CMDU, HLCU, ONEY] - Deployment target: [e.g. AWS ECS, Vercel, Fly.io] - Secret store: [e.g. AWS Secrets Manager, `.env`, Doppler] # Rules 1. Do not delete Gnosis code until the cleanup step. Migration is side by side. 2. Ship in small PRs. Each PR must build, pass tests, and be independently revertable. 3. Never invent Terminal49 fields, endpoints, or event names. If the guide does not confirm a mapping, ask me. Do not guess. 4. Terminal49 uses `Authorization: Token ` (not `Bearer`, and no OAuth2 token exchange) and content type `application/vnd.api+json`. Get this right on the first request. 5. Terminal49 is asynchronous. `POST /tracking_requests` returns pending. Data arrives via `tracking_request.succeeded`, `tracking_request.failed`, or `tracking_request.awaiting_manifest` webhooks, or by polling `GET /v2/shipments`. Do not expect shipment data on creation. 6. Terminal49 accepts one bill of lading, booking, or container number per tracking request, not an array like Gnosis's `mbl_numbers`. Loop over any batch and issue one request per number. 7. Timestamps are UTC with a separate IANA `time_zone` field. Do not assume local time. 8. `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` are the normal state, not missing data. A fee `amount` of 0 is valid. Holds are an array of active holds, not a boolean dictionary like Gnosis's `holds` object. 9. On `container.updated`, prefer the `changeset` over diffing state yourself. 10. Terminal49 has no equivalent for Gnosis's `gnosis_estimated_*` predictive fee and ETA fields, air cargo tracking, drayage execution, or customs milestone timelines. Do not attempt to fake these — flag them to me if the app depends on them. # Plan (execute in order, one PR per step) ## PR 1 — Discovery and interface - Grep the repo for every Gnosis call site. List them in the PR description. - Extract the Gnosis client's public surface into an interface (`TrackingProvider` with methods like `track(number, type, scac)`, `getShipment(id)`, `refresh(id)`). - Make the existing Gnosis client implement it. No behavior change. ## PR 2 — Terminal49 client - Add a `Terminal49Client` implementing the same `TrackingProvider` interface. - Auth via `T49_API_KEY` env var, header `Authorization: Token ${key}`. No token exchange, no refresh logic. - Base URL `https://api.terminal49.com/v2`, content type `application/vnd.api+json`. - Implement: - `createTrackingRequest({ request_number, request_type, scac })` returning the tracking request ID. Call this once per MBL number, not once per batch. - `getShipment(id, { include: 'containers,port_of_lading,port_of_discharge,...' })`. - `getContainer(id)`. - `refreshContainer(id)` mapping to `PATCH /v2/containers/{id}/refresh`. - Normalize responses to the same shape Gnosis callers expect today, using the field mapping from the guide. Convert Gnosis's `holds` boolean map into array-membership checks against `holds_at_pod_terminal`. Convert timestamps to UTC + `time_zone`. - Map errors to typed classes: `AuthenticationError` (401), `AuthorizationError` (403), `ValidationError` (400/422), `RateLimitError` (429), `UpstreamError` (5xx), `FeatureNotEnabledError` (403 + feature flag response). - Add unit tests using recorded fixtures. Do not hit the live API in tests. ## PR 3 — Shadow mode - Add a `ShadowProvider` that wraps both clients. On every read: - Call Gnosis as the primary. Return its response. - Fire-and-forget a Terminal49 call for the same identifier. - Log a structured diff: matched fields, diverged fields, T49-only fields. - Gate with env var `TRACKING_SHADOW_MODE=true`. Off by default. - Add a parity report script that aggregates shadow logs by SCAC and field. - Do NOT change what callers see. This step is observation only. ## PR 4 — Backfill script - Write a one-shot script that reads all active shipments from our database and calls `POST /tracking_requests` for each (one per BOL, booking, or container — unbatch any MBL arrays). - Store the returned `tracking_request.id` and eventual `shipment.id` on our records. - Rate-limit to respect Terminal49's limits (handle 429 with exponential backoff). - Idempotent: safe to re-run. Skip rows already backfilled. ## PR 5 — Webhook receiver (only if we picked the webhook path) - Add `POST /webhooks/terminal49` endpoint. - Verify HMAC signature using the shared secret from `T49_WEBHOOK_SECRET`. Reject on mismatch with 401. - Handle these events at minimum: - `tracking_request.succeeded`, `tracking_request.failed`, `tracking_request.awaiting_manifest` - `container.transport.vessel_discharged`, `container.transport.available`, `container.transport.full_out` - `container.updated` (use the `changeset`, do not diff) - `container.pickup_lfd.changed` - Persist events idempotently keyed by event ID. - Register the webhook via `POST /v2/webhooks` from a bootstrap script, subscribing only to events we handle. - If our firewall restricts inbound traffic, whitelist Terminal49 IPs from `GET /v2/webhooks/ips`. ## PR 6 — Cutover behind a flag - Add feature flag `TRACKING_PROVIDER` with values `gnosis` (default) and `terminal49`. - Route all reads through the flag. Gnosis stays available as a fallback for one release cycle. - Flip staging to `terminal49`, verify parity report is clean, then flip production. ## PR 7 — Cleanup - Delete `GnosisClient`, its tests, its env vars, its OAuth2 token-refresh logic, its dedupe cache, and any equipment-code parsing helpers Terminal49 makes redundant. - Remove the shadow provider and the feature flag. - Update README and any runbooks. Note the new webhook endpoint if applicable. # Definition of done - All Gnosis call sites now go through Terminal49. - Webhook (or polling) is live in production. - Parity report shows no unexplained divergences for our top 10 SCACs. - Holds, fees, and LFD are exposed to whichever downstream system needs them (dashboard, alerts, customer emails). Do not migrate without wiring these up. They are the reason to do this properly. - Any code depending on Gnosis's predictive fields, air cargo tracking, drayage execution, or customs milestones has been explicitly reviewed with me — none of these carry over automatically. - Gnosis dependency, env vars, and dead code are gone from the repo. # Ask me before you - Choose between the webhook path and the polling path. Default to webhooks unless our infra makes an inbound HTTPS endpoint hard. - Change the shape of any function Gnosis callers use today. Prefer a normalization layer inside the Terminal49 client. - Add a new dependency. Prefer stdlib and what is already in the repo. - Touch anything outside the tracking integration. Start with PR 1. Post the list of Gnosis call sites and the proposed `TrackingProvider` interface, and wait for my review before writing PR 2. ``` The prompt is deliberately opinionated on side-by-side migration and small PRs. If your team prefers a big-bang cutover or a different branching model, edit the **Plan** section before sending it to your agent. ## Getting help Send us your list of active container and bill of lading numbers and we will load them rather than making you script the backfill. If something in this mapping is wrong or incomplete, tell us. We would rather fix the page than have you work around it. Every endpoint, with request and response schemas Typed client with retries and pagination built in Carriers, terminals, rail, and field availability Simulate success and failure outcomes # Migrating from GoComet Source: https://terminal49.com/docs/migrate/gocomet Map GoComet ocean tracking fields, parameters, and errors to their Terminal49 equivalents. Includes webhook setup, carrier coverage, and a migration checklist. GoComet is a broad logistics platform with freight rate management and procurement alongside its ocean tracking capabilities. Terminal49 focuses only on ocean container visibility, with direct terminal integrations for holds, fees, and last free day. Migrating your tracking to Terminal49 does not touch your GoComet rate management or procurement workflows. This page maps GoComet tracking concepts onto Terminal49 field by field, so you can cut over without reverse-engineering our schema. There is no compatibility shim. You will change your request code and your response parsing. For most integrations that is an afternoon. ## Start in sixty seconds Signing up and getting a key is self-serve. Sign up at [app.terminal49.com](https://app.terminal49.com). The free plan tracks up to 10 active containers. Create a key at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). ```bash theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "tracking_request", "attributes": { "request_number": "YOUR_BOL_NUMBER", "request_type": "bill_of_lading", "scac": "MAEU" } } }' ``` The full API key is shown once, right after you create it. Copy it before you navigate away. After that it is masked and cannot be revealed. If you miss it, create a new key and delete the old one. The free plan tracks up to 10 active containers. Creating tracking requests through the API works right away; reading tracking data back through the API requires a free 7-day API trial (same 10-container limit) — contact us via in-app chat or [support@terminal49.com](mailto:support@terminal49.com) and we enable it. If you want to test without a live shipment, use the [test tracking numbers](/docs/api-docs/useful-info/test-numbers), which simulate success and failure outcomes. **Migration offer:** sign up now and the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id) — vessel schedules, AIS positions, and projected routes — is free for your first month. ## The architectural shift GoComet gives you a shipment-centric model where you call their API with a reference number and get back current state with attached events. You own the schedule, the cache, and the deduplication. Terminal49 splits this in two. You register a tracking request once. We keep it updated and push changes to your webhook. Call `GET` to your provider's shipment resource with a reference number, diff against your cache, dedupe events, then write to your database. Freshness is capped by your polling interval. `POST /tracking_requests` once. Terminal49 polls carriers, terminals, and rail, then POSTs to your endpoint as things change. No cache layer, no dedupe logic. You can keep polling if you prefer. Point your existing scheduler at `GET /v2/shipments` or `GET /v2/containers`. But webhooks are the reason the API is shaped this way, and terminal data (holds, fees, last free day) changes on a cadence that polling tends to miss. ## Quick comparison | | GoComet | Terminal49 | | ----------------------- | ------------------------- | ------------------------------------- | | Tracking model | Poll on demand | Register once, then push or poll | | Authentication | API key via header | `Authorization: Token` header | | Base URL | Your GoComet API endpoint | `https://api.terminal49.com/v2` | | Content type | `application/json` | `application/vnd.api+json` | | Response format | Custom JSON | JSON:API | | Webhooks | Limited or none | 30+ events, HMAC-signed | | Carrier identification | SCAC or carrier name | `scac`, or `auto_detect_vocc_scac` | | Terminal holds and fees | No direct equivalent | Included on the container object | | Last free day | No direct equivalent | Included, with per-source breakdown | | Rail milestones | Limited | North American Class I and short-line | | Getting an API key | Self-serve | Self-serve | ## Authentication Move from your provider's API key header format to ours. ```bash GoComet theme={null} # However your GoComet integration authenticates today, for example: curl "https:///shipment-tracking" \ -H "X-API-Key: YOUR_GOCOMET_KEY" \ -H "Content-Type: application/json" ``` ```bash Terminal49 theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_T49_KEY" \ -d '{"data":{"type":"tracking_request","attributes":{ "request_number":"MRKU9465770", "request_type":"container", "scac":"MAEU"}}}' ``` Note the `Token` prefix. It is not `Bearer`. ## Request parameter mapping | GoComet parameter | Terminal49 equivalent | Notes | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Reference number field | `request_number` | Container number, BOL, or booking | | BOL type parameter | `request_type: "bill_of_lading"` | Master or house BOL | | Container type parameter | `request_type: "container"` | | | Booking type parameter | `request_type: "booking_number"` | | | Carrier identifier field | `scac` | Same SCAC values for most carriers | | Auto-detect parameter | Set `auto_detect_vocc_scac: true`, or call [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) first | Auto-detection runs asynchronously; a failed inference fails the tracking request with `scac_auto_detect_failed` | | Force refresh parameter | `PATCH /v2/containers/{id}/refresh` | Forces an immediate pull from all sources. Paid feature — requires account enablement, limited to 10 requests per minute | | Route parameter | `GET /v2/containers/{id}/map_geojson` — requires the Routing Data entitlement | See [Routing](/docs/api-docs/in-depth-guides/routing) | | API key header | `Authorization` header | | Terminal49 takes one identifier per tracking request. Track by BOL and we return every container on that bill of lading as related container resources. Container-number tracking requests are currently in beta. ## Response field mapping Terminal49 is JSON:API compliant, so relationships between shipments, containers, ports, and terminals are explicit rather than something you reassemble from ID references. Use the [`include` parameter](/docs/api-docs/in-depth-guides/including-resources) to sideload related resources in one call instead of chasing IDs. ### Shipment level | GoComet | Terminal49 | | --------------------- | --------------------------------------------------------- | | BOL field | `shipment.attributes.bill_of_lading_number` | | Carrier SCAC field | `shipment.attributes.shipping_line_scac` | | Carrier name field | `shipment.attributes.shipping_line_name` | | Shipment status field | Derived from container status and milestones | | Origin place field | `shipment.attributes.port_of_lading_*` (place of receipt) | | POL field | `shipment.relationships.port_of_lading` | | POD field | `shipment.relationships.port_of_discharge` | | Destination field | `shipment.relationships.destination` (inland) | | ETA field | `shipment.attributes.pod_eta_at` | ### Container level | GoComet | Terminal49 | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Container number field | `container.attributes.number` | | ISO code field | `container.attributes.equipment_type` + `equipment_length` + `equipment_height` | | Container size field | Same three fields above | | Container status field | `container.attributes.current_status` | | Container events field | Container timestamps, [transport events](/docs/api-docs/api-reference/containers/get-a-containers-transport-events), and webhook events | GoComet likely returns a single ISO code string like `45G1`. Terminal49 splits this into three normalized fields: type (dry, reefer, open top, flat rack, bulk, tank), length (10, 20, 40, 45), and height (standard, high cube). If you were parsing ISO codes yourself, you can delete that code. ### Locations, facilities, and vessels | GoComet | Terminal49 | | ------------------- | ------------------------------------------------------------------------------------------------ | | Port name field | `port.attributes.name` | | Port code field | `port.attributes.code` | | Port lat/lng fields | `port.attributes.latitude` / `.longitude` | | Port timezone field | `port.attributes.time_zone` | | Port country field | `port.attributes.country_code` | | Terminal name field | `terminal.attributes.name` | | Terminal SMDG field | `terminal.attributes.smdg_code` | | Terminal BIC field | `terminal.attributes.bic_facility_code` | | Vessel name field | `shipment.attributes.pod_vessel_name` | | Vessel IMO field | `shipment.attributes.pod_vessel_imo` | | Vessel MMSI field | Available via the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo) | ## Milestone and event mapping GoComet returns a flat events array with event names or codes attached to each shipment. Terminal49 exposes the same milestones as normalized transport events and pushes each one to your webhook. | GoComet event | Milestone | Terminal49 event | | ---------------- | ----------------------------- | --------------------------------------- | | Loaded event | Loaded on vessel at origin | `container.transport.vessel_loaded` | | Departed event | Vessel departed origin | `container.transport.vessel_departed` | | Arrived event | Vessel arrived at destination | `container.transport.vessel_arrived` | | Discharged event | Discharged from vessel | `container.transport.vessel_discharged` | | Gated out event | Gated out at destination | `container.transport.full_out` | | Gated in event | Gated in at origin | `container.transport.full_in` | | — | Empty picked up at origin | `container.transport.empty_out` | | — | Empty returned at destination | `container.transport.empty_in` | Terminal49 also emits milestones GoComet has no equivalent for: * **Vessel berthed:** `container.transport.vessel_berthed` * **Available for pickup:** `container.transport.available` and `.not_available` * **Transshipment:** arrived, discharged, loaded, departed * **Feeder vessel and barge:** arrived, discharged, loaded, departed * **Rail:** loaded, departed, arrived, unloaded, plus `arrived_at_inland_destination` See the full [event catalog](/docs/api-docs/webhooks/event-catalog). ### Registering a webhook ```bash theme={null} curl -X POST https://api.terminal49.com/v2/webhooks \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "webhook", "attributes": { "url": "https://your-endpoint.example.com/t49", "active": true, "events": [ "container.transport.vessel_discharged", "container.transport.available", "container.pickup_lfd.changed" ] } } }' ``` Payloads are HMAC-signed. See [webhook setup](/docs/api-docs/in-depth-guides/webhooks) for signature verification, and [List webhook IPs](/docs/api-docs/api-reference/webhooks/list-webhook-ips) if your firewall restricts inbound traffic. ## What you gain This is the part worth reading even if the rest is mechanical. Terminal49 integrates with terminals directly, not only carriers, so the container object carries operational data that has no GoComet equivalent. ### Holds `holds_at_pod_terminal` is an array of active holds blocking pickup: ```json theme={null} { "holds_at_pod_terminal": [ { "name": "customs", "status": "hold", "description": "CBP HOLD" }, { "name": "freight", "status": "hold", "description": null } ] } ``` Hold names are `freight`, `customs`, `USDA`, `VACIS`, `TMF`, and `other`. Status is `hold` or `pending`. When a hold clears, the object is removed from the array. There is no released state. Hold names are case-sensitive. `USDA`, `VACIS`, and `TMF` are uppercase; `freight`, `customs`, and `other` are lowercase. Match exactly. ### Fees `fees_at_pod_terminal` carries type, amount, and currency: ```json theme={null} { "fees_at_pod_terminal": [ { "type": "demurrage", "amount": 850.00, "currency_code": "USD" }, { "type": "exam", "amount": 450.00, "currency_code": "USD" } ] } ``` Fee types are `demurrage`, `extended_dwell_time`, `exam`, `total`, and `other`. Some terminals report a `total` line item alongside individual fees. Filter it out before summing or you will double-count. ### Last free day `pickup_lfd` is a coalesced value that follows a fixed source priority: shipping line, then terminal, then rail. It does not pick the earliest date. The individual sources are available separately on `import_deadlines`: * `pickup_lfd_line`: the shipping line's LFD (per diem deadline) * `pickup_lfd_terminal`: the terminal's LFD (demurrage deadline) * `pickup_lfd_rail`: the rail carrier's LFD at the inland destination Each has its own webhook event, so you can alert on whichever source your operation cares about. ### Release readiness Two fields answer "can I pick this up?" `available_for_pickup` and the holds array: ```javascript theme={null} function isReadyForPickup(container) { const { available_for_pickup, holds_at_pod_terminal } = container.attributes; const hasActiveHolds = holds_at_pod_terminal.some(h => h.status === 'hold'); return available_for_pickup === true && !hasActiveHolds; } ``` Full detail in [Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). Holds, fees, LFD, and availability come back on the container object wherever the terminal is a supported source. They are not a paid add-on and they do not require a sales conversation. See [Entitlements](/docs/api-docs/useful-info/entitlements) for the features that do require account enablement. Routing Data (container map and vessel positions), rail LFD, container refresh, and the embeddable map and widget are the gated ones. ## Gotchas that will bite you GoComet covers sea, air, and land freight. Terminal49 covers ocean containers only. This migration does not touch your GoComet rate management, procurement workflows, or non-ocean tracking. If you need air or road tracking, plan to keep GoComet for those modes. `POST /tracking_requests` returns immediately with a pending status. The shipment appears once the carrier responds. Subscribe to `tracking_request.succeeded` and `tracking_request.failed` rather than expecting shipment data in the creation response. A request may also land in `awaiting_manifest` if the carrier has not manifested the shipment yet, and we retry automatically. See [Tracking Request Lifecycle](/docs/api-docs/in-depth-guides/tracking-request-lifecycle). Relationships are ID references into an `included` array. Use a JSON:API client library, or use `include` to sideload exactly what you need. Parsing raw JSON works but you will write more code than you expect. GoComet may return local time in event timestamps. Terminal49 stores event timestamps in UTC and returns the matching IANA timezone alongside. Convert for display rather than assuming local time. See [Event Timestamps](/docs/api-docs/in-depth-guides/event-timestamps). `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` mean no active holds or fees. This is the common case. Do not treat it as missing data. Terminal changes (fees, holds, LFD, appointment, availability) arrive on `container.updated` with a `changeset` showing old value first, new value second. Use it instead of diffing state yourself. Terminal49 uses `Authorization: Token YOUR_KEY`. It is not `Bearer`, not `Basic`, and not `X-API-Key`. A missing or wrong prefix returns 401 even with a valid key. ## Error handling GoComet likely returns errors embedded in the response body. Terminal49 uses standard HTTP status codes. Replace envelope checks and message-string matching with status-code checks. | Status | Meaning | | ------ | ------------------------------------------------------------------------------- | | 400 | Malformed request or failed validation | | 401 | Missing or invalid API key | | 403 | Key lacks permission, or the feature is not enabled on your plan | | 404 | Resource does not exist | | 422 | Valid syntax, rejected content. For example, a malformed container number | | 429 | Rate limited. See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) | | 5xx | Terminal49 or an upstream carrier or terminal is unavailable | Rough equivalence for the GoComet errors you are handling today: | GoComet error | Terminal49 | | ------------------------------ | ---------------------------------------------------------------------------------------- | | Invalid API key error | HTTP 401 | | Insufficient permissions error | HTTP 403 | | Rate limit error | HTTP 429 | | Invalid parameter error | HTTP 400 or 422 | | Unsupported carrier error | HTTP 422 | | No data found error | `tracking_request.failed`, or `tracking_request.awaiting_manifest` if not yet manifested | | Service unavailable error | Not surfaced. We retry internally. | The [TypeScript SDK](/docs/sdk/introduction) maps these to typed errors (`AuthenticationError`, `ValidationError`, `RateLimitError`, `UpstreamError`, `FeatureNotEnabledError`, `AuthorizationError`, `NotFoundError`) and retries rate-limit and server errors automatically with exponential backoff. ## Where we are narrower than GoComet Worth knowing before you commit. **Carrier count.** Terminal49 integrates directly with 36 ocean carriers, plus 2 more enabled on request, as of 14 August 2026. GoComet lists substantially more through aggregators. Ours are direct integrations covering the lines that move volume into North America, and each is normalized into one schema. Check your carrier mix against the [ocean carrier list](/docs/coverage/ocean-carriers) before cutover, and read the known issues section there. We publish the per-carrier field gaps. **Terminal data is North America.** Holds, fees, LFD, and availability come from direct terminal integrations concentrated in the US and Canada, with European ports expanding. Ocean milestones work globally; terminal-level operational data does not yet. **No air, parcel, or road tracking.** GoComet covers multi-modal freight. This migration handles only the ocean portion. **No freight rates or sailing schedules.** We do not offer a rate calculator, rate index, or schedule search. Keep GoComet for rate management and procurement. **Some fields are source-dependent.** Seal number, container weight, and departure or arrival events vary by carrier. The [field availability reference](/docs/coverage/fields) says which fields are always present and which depend on the carrier, terminal, or journey. ## Migration checklist Everyone does the base path. Then pick a branch. ### Base path Self-serve at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). Copy it immediately, it is shown once. Move from your provider's API key header format to `Authorization: Token`. Note the `Token` prefix. Compare your GoComet carrier values against the [carrier list](/docs/coverage/ocean-carriers). Flag anything missing before you cut over. One `POST /tracking_requests` per BOL, booking, or container, replacing the per-request lookup. Tracking requests start pending. Handle `succeeded`, `failed`, and `awaiting_manifest` rather than expecting data on creation. JSON:API structure, split equipment fields, UTC timestamps with a separate timezone. Replace envelope checks with HTTP status codes. Submit tracking requests for everything currently in transit. Send us the list if it is large and we will load it. Holds, fees, and LFD are the reason to do this properly rather than porting like for like. ### Then pick one Accept our POST payloads at a public URL. Subscribe only to events you act on. Reject any payload whose signature does not match. Only needed if your firewall restricts inbound traffic. Confirm end-to-end before going live. Remove your dedupe layer along with it. See [webhook best practices](/docs/api-docs/webhooks/best-practices) for retries and idempotency. The creation response is pending and carries no shipment yet. Keep the tracking request ID, then fetch the tracking request again (or handle `tracking_request.succeeded`) to get the shipment ID once the carrier responds. Point it at `GET /v2/shipments` or `GET /v2/containers`. No change to how often you poll. Skipped: endpoint setup, signature verification, IP whitelisting, delivery testing. Terminal data changes on a cadence polling tends to miss. If you only adopt webhooks for one thing, make it `container.updated` and `container.pickup_lfd.changed`. ## Migrate with an AI coding agent If you use Cursor, Claude Code, Windsurf, Copilot, or another AI coding assistant, hand it the prompt below. It is written to run a **side-by-side migration**: the agent stands up a Terminal49 client next to your existing GoComet code, shadows every GoComet call with a Terminal49 call, diffs the responses, and only cuts over once parity is proven. Point your agent at this page as context (paste the URL or add it as a doc source). The prompt references the mappings above, so the more of this page the agent can see, the better it does. 1. Open your repo in your AI coding tool. 2. Add this page as a documentation source, or paste its URL into the chat. 3. Copy the prompt below into a new chat and send it. 4. Answer the agent's discovery questions (GoComet client location, env var names, carrier mix). 5. Review each PR the agent opens. It should ship in small, reviewable steps: client, shadow, parity harness, cutover, cleanup. * A `Terminal49Client` alongside your existing `GoCometClient`, sharing the same interface where possible. * A shadow-mode wrapper that calls both providers and logs response diffs without changing behavior. * A parity report per shipment: matched fields, diverged fields, and Terminal49-only fields (holds, fees, LFD). * A feature-flagged cutover: route reads to Terminal49, keep GoComet as fallback until you flip the flag off. * A webhook receiver with HMAC verification, or a polling scheduler, depending on which path you pick. * A cleanup PR that removes GoComet code, env vars, dependencies, and dedupe logic. ### The prompt Copy this into your agent. Replace the bracketed placeholders in the **Repo context** block before sending. ```markdown Terminal49 migration agent prompt expandable icon=robot wrap theme={null} You are migrating this codebase from the GoComet tracking API to the Terminal49 API, side by side. Terminal49's authoritative migration guide is at https://terminal49.com/docs/migrate/gocomet. Use it as the source of truth for field mappings, event names, error codes, and behavior differences. # Repo context (fill this in before running) - GoComet client lives at: [path/to/gocomet/client.ts] - GoComet is called from: [list the call sites or "find them"] - Language and framework: [e.g. TypeScript + Node, Python + FastAPI, Ruby on Rails] - Storage for tracking state: [e.g. Postgres table `shipments`] - Current polling schedule: [e.g. cron every 2h via BullMQ] - Carrier mix (SCACs we track most): [e.g. MAEU, MSCU, CMDU, HLCU, ONEY] - Deployment target: [e.g. AWS ECS, Vercel, Fly.io] - Secret store: [e.g. AWS Secrets Manager, `.env`, Doppler] # Rules 1. Do not delete GoComet code until the cleanup step. Migration is side by side. 2. Ship in small PRs. Each PR must build, pass tests, and be independently revertable. 3. Never invent Terminal49 fields, endpoints, or event names. If the guide does not confirm a mapping, ask me. Do not guess. 4. Terminal49 uses `Authorization: Token ` (not `Bearer`) and content type `application/vnd.api+json`. Get this right on the first request. 5. Terminal49 is asynchronous. `POST /tracking_requests` returns pending. Data arrives via `tracking_request.succeeded`, `tracking_request.failed`, or `tracking_request.awaiting_manifest` webhooks, or by polling `GET /v2/shipments`. Do not expect shipment data on creation. 6. Timestamps are UTC with a separate IANA `time_zone` field. Do not assume local time. 7. `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` are the normal state, not missing data. A fee `amount` of 0 is valid. 8. On `container.updated`, prefer the `changeset` over diffing state yourself. # Plan (execute in order, one PR per step) ## PR 1 — Discovery and interface - Grep the repo for every GoComet call site. List them in the PR description. - Extract the GoComet client's public surface into an interface (`TrackingProvider` with methods like `track(number, type, sealine)`, `getShipment(id)`, `refresh(id)`). - Make the existing GoComet client implement it. No behavior change. ## PR 2 — Terminal49 client - Add a `Terminal49Client` implementing the same `TrackingProvider` interface. - Auth via `T49_API_KEY` env var, header `Authorization: Token ${key}`. - Base URL `https://api.terminal49.com/v2`, content type `application/vnd.api+json`. - Implement: - `createTrackingRequest({ request_number, request_type, scac })` returning the tracking request ID. - `getShipment(id, { include: 'containers,port_of_lading,port_of_discharge,...' })`. - `getContainer(id)`. - `refreshContainer(id)` mapping to `PATCH /v2/containers/{id}/refresh`. - Normalize responses to the same shape GoComet callers expect today, using the field mapping from the guide. Split `iso_code` into `equipment_type`, `equipment_length`, `equipment_height`. Convert timestamps to UTC + `time_zone`. - Map errors to typed classes: `AuthenticationError` (401), `AuthorizationError` (403), `ValidationError` (400/422), `RateLimitError` (429), `UpstreamError` (5xx), `FeatureNotEnabledError` (403 + feature flag response). - Add unit tests using recorded fixtures. Do not hit the live API in tests. ## PR 3 — Shadow mode - Add a `ShadowProvider` that wraps both clients. On every read: - Call GoComet as the primary. Return its response. - Fire-and-forget a Terminal49 call for the same identifier. - Log a structured diff: matched fields, diverged fields, T49-only fields. - Gate with env var `TRACKING_SHADOW_MODE=true`. Off by default. - Add a parity report script that aggregates shadow logs by SCAC and field. - Do NOT change what callers see. This step is observation only. ## PR 4 — Backfill script - Write a one-shot script that reads all active shipments from our database and calls `POST /tracking_requests` for each (one per BOL, booking, or container). - Store the returned `tracking_request.id` and eventual `shipment.id` on our records. - Rate-limit to respect Terminal49's limits (handle 429 with exponential backoff). - Idempotent: safe to re-run. Skip rows already backfilled. ## PR 5 — Webhook receiver (only if we picked the webhook path) - Add `POST /webhooks/terminal49` endpoint. - Verify HMAC signature using the shared secret from `T49_WEBHOOK_SECRET`. Reject on mismatch with 401. - Handle these events at minimum: - `tracking_request.succeeded`, `tracking_request.failed`, `tracking_request.awaiting_manifest` - `container.transport.vessel_discharged`, `container.transport.available`, `container.transport.full_out` - `container.updated` (use the `changeset`, do not diff) - `container.pickup_lfd.changed` - Persist events idempotently keyed by event ID. - Register the webhook via `POST /v2/webhooks` from a bootstrap script, subscribing only to events we handle. - If our firewall restricts inbound traffic, whitelist Terminal49 IPs from `GET /v2/webhooks/ips`. ## PR 6 — Cutover behind a flag - Add feature flag `TRACKING_PROVIDER` with values `gocomet` (default) and `terminal49`. - Route all reads through the flag. GoComet stays available as a fallback for one release cycle. - Flip staging to `terminal49`, verify parity report is clean, then flip production. ## PR 7 — Cleanup - Delete `GoCometClient`, its tests, its env vars, its dedupe cache, and any ISO-code parsing helpers Terminal49 makes redundant. - Remove the shadow provider and the feature flag. - Update README and any runbooks. Note the new webhook endpoint if applicable. # Definition of done - All GoComet call sites now go through Terminal49. - Webhook (or polling) is live in production. - Parity report shows no unexplained divergences for our top 10 SCACs. - Holds, fees, and LFD are exposed to whichever downstream system needs them (dashboard, alerts, customer emails). Do not migrate without wiring these up. They are the reason to do this properly. - GoComet dependency, env vars, and dead code are gone from the repo. # Ask me before you - Choose between the webhook path and the polling path. Default to webhooks unless our infra makes an inbound HTTPS endpoint hard. - Change the shape of any function GoComet callers use today. Prefer a normalization layer inside the Terminal49 client. - Add a new dependency. Prefer stdlib and what is already in the repo. - Touch anything outside the tracking integration. Start with PR 1. Post the list of GoComet call sites and the proposed `TrackingProvider` interface, and wait for my review before writing PR 2. ``` The prompt is deliberately opinionated on side-by-side migration and small PRs. If your team prefers a big-bang cutover or a different branching model, edit the **Plan** section before sending it to your agent. ## Getting help Send us your list of active container and bill of lading numbers and we will load them rather than making you script the backfill. If something in this mapping is wrong or incomplete, tell us. We would rather fix the page than have you work around it. Every endpoint, with request and response schemas Typed client with retries and pagination built in Carriers, terminals, rail, and field availability Simulate success and failure outcomes # Migrate to Terminal49 Source: https://terminal49.com/docs/migrate/home Migrate container tracking to Terminal49 from project44, FourKites, Vizion, ShipsGo, GoComet, Gnosis Freight, SeaRates, OpenTrack, or Beacon. Terminal49 integrates directly with 36+ ocean carriers and North American terminals. If you are running another provider today, these guides map your current integration onto ours, field by field, so you can cut over without reverse-engineering our schema. Every guide covers the same ground: authentication, request and response mapping, event and webhook equivalents, error handling, the gotchas that will bite you, and a migration checklist. Each one also carries a prompt you can hand to Claude Code, Codex, Cursor, or Copilot to run the migration side by side. ## Why we care so much about terminal data We moved containers before we wrote any code. The company started because we were doing drayage for customers out of Oakland and Virginia. That meant refreshing terminal websites at six in the morning to find out if a box had cleared. It meant sending a driver for a container that turned out to be on a customs hold, and eating the trip. It meant finding out about a last free day the day after it passed. Nobody sells you demurrage. You just get a bill. So when we built the API, terminal data was not a feature we added later to look competitive. It was the entire reason. Holds, fees, last free day, whether the box can actually be picked up right now: that is the data that decides whether your week is expensive, and most tracking APIs still do not return it. If you have ever had to explain a demurrage invoice to a customer, you already understand why this page exists. ## Pick your current provider Poll-based ocean tracking. Move from `api_key` query params and custom JSON to JSON:API and webhooks. Enterprise multi-modal visibility. Focus on the ocean shipments and swap in HMAC-signed webhooks. Multi-modal visibility. Replace the ocean feed with direct carrier and terminal integrations. Reference IDs and webhook events. Map subscriptions onto tracking requests. Ocean tracking API with a similar model. Mostly a rename of concepts you already know. Ocean tracking and analytics. Swap the polling loop for webhooks and normalize event codes. Shipment-centric multi-modal tracking. Move to JSON:API and per-container events. Supply chain visibility. Replace shipment-level polling with per-container webhooks. Container Lifecycle Management platform. Move MBL-based tracking and polling onto tracking requests and webhooks. ## What every migration has in common Create an account at [app.terminal49.com](https://app.terminal49.com) and generate a key at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). The free plan tracks up to 10 active containers, enough for a parity test. Creating tracking requests through the API is immediate; to read tracking data back through the API, ask us to enable the free 7-day API trial (same 10-container limit) via in-app chat or [support@terminal49.com](mailto:support@terminal49.com). `POST /v2/tracking_requests` per BOL, booking, or container. We poll the carrier, terminal, and rail sources on your behalf. Subscribe to the events you act on. 30+ events, HMAC-signed, delivered as changes happen. Polling stays available if you prefer it. Holds, fees, last free day, release readiness. These are the reason to migrate rather than port like for like. **Migration offer:** sign up now and the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id) — vessel schedules, AIS positions, and projected routes — is free for your first month. ## The fields that change how your week goes `holds_at_pod_terminal` lists what is actually blocking the box (`customs`, `freight`, `USDA`, `VACIS`, `TMF`) with status and the terminal's own description. Know before you dispatch a truck. `fees_at_pod_terminal` returns type, amount, and currency for demurrage, exam, and dwell fees, straight from the terminal. The number on the invoice, before the invoice. `pickup_lfd`, broken out by shipping line, terminal, and rail, because they disagree and the difference is money. Each source has its own webhook event. `available_for_pickup` plus the holds array answers "can I actually pick this up right now?" in a single call. ## We are building a network, not a data feed Visibility on its own does not move a container. The people who move it have to be able to see the same thing you do. That is why the dashboard is **free for truckers**. Your drayage partner should not need a license to see the last free day on a box they are picking up for you. And it is **free for small importers and exporters**: a company moving a handful of containers a month should not have to buy enterprise software to avoid a demurrage bill. We do not make money on those accounts. We make money when the whole chain runs better, because that is when the people with volume stay. Most tracking APIs sell one company a window into their own freight. We are trying to get everyone touching the container looking at the same record. ## We are not stopping at North America Our terminal-level data (holds, fees, LFD, availability, FIRMS codes) is deepest across the US and Canada. That is honest, and if you are tracking Asia-to-Europe today it is a real limitation you should weigh. The [coverage pages](/docs/coverage/home) tell you exactly which sources return which fields. But North America is where we started, not where we are stopping. Ocean carrier milestones already work globally. Terminal integrations outside North America are in progress right now, and the [coverage changelog](/docs/coverage/changelog) is where they show up as they go live, not in a press release. The goal has never been a regional product. It is one common record for a container wherever it happens to be. ## When something breaks, you get someone who has done the job Support is not a ticket queue that reads your message back to you. The people you reach have chased chassis, argued with terminals, and paid demurrage they did not see coming. When you say the terminal is showing something different from the carrier, you will not have to explain what that means. If a mapping in these guides is wrong or thin, tell us. We would rather fix the page than have you build around it. ## The core API shape, once Wherever you are coming from, the request pattern is the same. ```bash Create a tracking request theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "tracking_request", "attributes": { "request_number": "YOUR_BOL_NUMBER", "request_type": "bill_of_lading", "scac": "MAEU" } } }' ``` We use `Authorization: Token `, not `Bearer`. Content type is `application/vnd.api+json` (JSON:API). Responses include `data`, `relationships`, and an `included` array. ## Migrate with an AI coding agent Every provider page carries a copyable prompt that walks an AI coding assistant — Claude Code, Codex, Cursor, Copilot, or any agent that can read your repo — through a **side-by-side migration**: stand up a Terminal49 client next to your current provider, shadow every call, diff the responses, and cut over only once parity is proven. The prompts are opinionated about small PRs and reference the mappings on each page, so give your agent the page URL as context. Coming from a provider we have not written up? Use the generic version below and fill in the provider name. ```markdown Terminal49 side-by-side migration prompt (generic) expandable icon=robot wrap theme={null} You are migrating this codebase from our current container tracking provider, [PROVIDER], to the Terminal49 API, side by side. Terminal49's documentation is at https://terminal49.com/docs — use the API reference and the migration guides under https://terminal49.com/docs/migrate/home as the source of truth for field mappings, event names, error codes, and behavior. Never invent Terminal49 fields, endpoints, or event names; if the docs do not confirm a mapping, ask me. # Repo context (fill this in before running) - [PROVIDER] client lives at: [path/to/client] - [PROVIDER] is called from: [list the call sites or "find them"] - Language and framework: [e.g. TypeScript + Node, Python + FastAPI, Ruby on Rails] - Storage for tracking state: [e.g. Postgres table `shipments`] - Current polling schedule: [e.g. cron every 2h] - Carrier mix (SCACs we track most): [e.g. MAEU, MSCU, CMDU, HLCU, ONEY] - Secret store: [e.g. AWS Secrets Manager, `.env`, Doppler] # Terminal49 facts (do not deviate) - Auth header is `Authorization: Token ` — not `Bearer`. - Content type is `application/vnd.api+json`; responses are JSON:API with `data`, `relationships`, and an `included` array. - Base URL is `https://api.terminal49.com/v2`. - Tracking is asynchronous: `POST /tracking_requests` returns pending; data arrives via `tracking_request.succeeded` / `tracking_request.failed` / `tracking_request.awaiting_manifest` webhooks, or by polling `GET /v2/shipments`. Do not expect shipment data on creation. - Timestamps are UTC with a separate IANA `time_zone` field. - `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` are the normal state, not missing data. - On `container.updated`, use the `changeset` instead of diffing state yourself. # Plan (one small, revertable PR per step) 1. Discovery: find every [PROVIDER] call site; extract the client's public surface into a `TrackingProvider` interface; make the existing client implement it. 2. Terminal49 client: implement the same interface against Terminal49 (`createTrackingRequest`, `getShipment` with `include=containers`, `getContainer`). Normalize responses to the shape existing callers expect. Map errors to typed classes by HTTP status (401, 403, 400/422, 404, 429, 5xx). Unit tests on recorded fixtures only. 3. Shadow mode: wrap both clients; [PROVIDER] stays primary; fire-and-forget the same lookup against Terminal49 and log structured diffs. Add a parity report aggregated by SCAC and field. No caller-visible changes. 4. Backfill: one-shot idempotent script that creates a tracking request per active BOL, booking, or container and stores the returned IDs. 5. Webhooks (preferred) or polling: HMAC-verified receiver, idempotent event storage keyed by event ID, registered via `POST /v2/webhooks` for only the events we handle. 6. Cutover behind a `TRACKING_PROVIDER` feature flag; [PROVIDER] stays as fallback for one release cycle; flip staging first, verify the parity report, then production. 7. Cleanup: delete the [PROVIDER] client, env vars, dependencies, and dedupe logic; remove the shadow layer and the flag. # Definition of done - All call sites go through Terminal49; webhooks or polling live in production. - Parity report is clean for our top SCACs. - Holds (`holds_at_pod_terminal`), fees (`fees_at_pod_terminal`), last free day (`pickup_lfd`), and `available_for_pickup` are wired into whichever downstream system acts on them. They are the reason to migrate properly. - [PROVIDER] is gone from the repo. # Ask me before you - Choose webhooks vs polling. - Change any function signature existing callers use. - Add a dependency. - Touch anything outside the tracking integration. ``` ## Start somewhere Carriers, terminals, rail, and per-field availability, including the gaps Every endpoint, with request and response schemas Simulate success and failure before you cut over Give us your active containers and BOLs and we will load them, so you skip the backfill # Migrating from OpenTrack Source: https://terminal49.com/docs/migrate/opentrack Map OpenTrack tracking API fields, parameters, and errors to their Terminal49 equivalents. Includes webhook setup, carrier coverage, and a migration checklist. If you have the OpenTrack API in production, this page maps it onto Terminal49 field by field, so you can cut over without reverse-engineering our schema. OpenTrack and Terminal49 both do ocean container tracking. The differences are Terminal49's direct terminal integrations (holds, fees, last free day, release readiness), 30+ webhook events, and JSON:API. Cutover is largely mechanical. ## Start in sixty seconds Signing up and getting a key is self-serve. Sign up at [app.terminal49.com](https://app.terminal49.com). The free plan tracks up to 10 active containers. Create a key at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). ```bash theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "tracking_request", "attributes": { "request_number": "YOUR_BOL_NUMBER", "request_type": "bill_of_lading", "scac": "MAEU" } } }' ``` The full API key is shown once, right after you create it. Copy it before you navigate away. After that it is masked and cannot be revealed. If you miss it, create a new key and delete the old one. The free plan tracks up to 10 active containers. Creating tracking requests through the API works right away; reading tracking data back through the API requires a free 7-day API trial (same 10-container limit) — contact us via in-app chat or [support@terminal49.com](mailto:support@terminal49.com) and we enable it. If you want to test without a live shipment, use the [test tracking numbers](/docs/api-docs/useful-info/test-numbers), which simulate success and failure outcomes. **Migration offer:** sign up now and the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id) — vessel schedules, AIS positions, and projected routes — is free for your first month. ## The architectural shift OpenTrack gives you REST endpoints. You call it with a number, get current state back, and own the cache, scheduling, and deduplication. Terminal49 splits this in two. You register a tracking request once. We keep it updated and push changes to your webhook. Poll your OpenTrack endpoint on a schedule, diff against your cache, dedupe events, then write to your database. Freshness is capped by your polling interval. `POST /tracking_requests` once. Terminal49 polls carriers, terminals, and rail, then POSTs to your endpoint as things change. No cache layer, no dedupe logic. You can keep polling if you prefer. Point your existing scheduler at `GET /v2/shipments` or `GET /v2/containers`. But webhooks are the reason the API is shaped this way, and terminal data (holds, fees, last free day) changes on a cadence that polling tends to miss. ## Quick comparison | | OpenTrack | Terminal49 | | ----------------------- | -------------------------- | ------------------------------------- | | Tracking model | Poll on demand | Register once, then push or poll | | Authentication | API key in header or query | `Authorization: Token` header | | Base URL | API domain | `https://api.terminal49.com/v2` | | Content type | `application/json` | `application/vnd.api+json` | | Response format | Custom JSON | JSON:API | | Webhooks | May be available | 30+ events, HMAC-signed | | Carrier identification | SCAC or similar | `scac`, or `auto_detect_vocc_scac` | | Terminal holds and fees | No direct equivalent | Included on the container object | | Last free day | No direct equivalent | Included, with per-source breakdown | | Rail milestones | Limited | North American Class I and short-line | | Getting an API key | Signup URL | Self-serve | ## Authentication Move your key into the `Authorization: Token` header. ```bash OpenTrack theme={null} # However your OpenTrack integration authenticates today, for example: curl -H "Authorization: YOUR_OPENTRACK_KEY" \ "https:///shipments" ``` ```bash Terminal49 theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_T49_KEY" \ -d '{"data":{"type":"tracking_request","attributes":{ "request_number":"MRKU9465770", "request_type":"container", "scac":"MAEU"}}}' ``` Note the `Token` prefix. It is not `Bearer`. ## Request parameter mapping | OpenTrack parameter | Terminal49 equivalent | Notes | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Container number field | `request_number` | Pass in the tracking request attributes | | BOL field | `request_type: "bill_of_lading"` | Master or house BOL | | Booking field | `request_type: "booking_number"` | | | SCAC field | `scac` | Same SCAC values for most carriers | | Auto-detect carrier | Set `auto_detect_vocc_scac: true`, or call [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) first | Auto-detection runs asynchronously; a failed inference fails the tracking request with `scac_auto_detect_failed` | | Force refresh | `PATCH /v2/containers/{id}/refresh` | Forces an immediate pull from all sources. Paid feature — requires account enablement, limited to 10 requests per minute | OpenTrack may accept a combined identifier value in a single field. Terminal49 takes one identifier per tracking request. Track by BOL and we return every container on that bill of lading as related container resources. Container-number tracking requests are currently in beta. ## Response field mapping Terminal49 is JSON:API compliant, so relationships between shipments, containers, ports, and terminals are explicit rather than something you reassemble from ID references. Use the [`include` parameter](/docs/api-docs/in-depth-guides/including-resources) to sideload related resources in one call instead of chasing IDs. ### Shipment level | OpenTrack | Terminal49 | | ------------------------ | -------------------------------------------- | | BOL field | `shipment.attributes.bill_of_lading_number` | | SCAC field | `shipment.attributes.shipping_line_scac` | | Carrier name field | `shipment.attributes.shipping_line_name` | | Status field | Derived from container status and milestones | | Origin port field | `shipment.relationships.port_of_lading` | | Destination port field | `shipment.relationships.port_of_discharge` | | Inland destination field | `shipment.relationships.destination` | | ETA field | `shipment.attributes.pod_eta_at` | ### Container level | OpenTrack | Terminal49 | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Container number field | `container.attributes.number` | | ISO code field | `container.attributes.equipment_type` + `equipment_length` + `equipment_height` | | Size/type field | Same three fields above | | Container status field | `container.attributes.current_status` | | Events array | Container timestamps, [transport events](/docs/api-docs/api-reference/containers/get-a-containers-transport-events), and webhook events | OpenTrack may return a single ISO code string like `45G1`. Terminal49 splits this into three normalized fields: type (dry, reefer, open top, flat rack, bulk, tank), length (10, 20, 40, 45), and height (standard, high cube). If you were parsing ISO codes yourself, you can delete that code. ### Locations, facilities, and vessels | OpenTrack | Terminal49 | | ------------------- | ------------------------------------------------------------------------------------------------ | | Port name field | `port.attributes.name` | | Port code field | `port.attributes.code` | | Port lat/lng fields | `port.attributes.latitude` / `.longitude` | | Port timezone field | `port.attributes.time_zone` | | Port country field | `port.attributes.country_code` | | Terminal name field | `terminal.attributes.name` | | Terminal SMDG field | `terminal.attributes.smdg_code` | | Terminal BIC field | `terminal.attributes.bic_facility_code` | | Vessel name field | `shipment.attributes.pod_vessel_name` | | Vessel IMO field | `shipment.attributes.pod_vessel_imo` | | Vessel MMSI field | Available via the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo) | ## Milestone and event mapping OpenTrack returns a flat event array with Event vocabulary. Terminal49 exposes the same milestones as normalized transport events and pushes each one to your webhook. | OpenTrack event | Milestone | Terminal49 event | | ---------------- | ----------------------------- | --------------------------------------- | | Loaded event | Loaded on vessel at origin | `container.transport.vessel_loaded` | | Departed event | Vessel departed origin | `container.transport.vessel_departed` | | Arrived event | Vessel arrived at destination | `container.transport.vessel_arrived` | | Discharged event | Discharged from vessel | `container.transport.vessel_discharged` | | Gated out event | Gated out at destination | `container.transport.full_out` | | Gated in event | Gated in at origin | `container.transport.full_in` | | — | Empty picked up at origin | `container.transport.empty_out` | | — | Empty returned at destination | `container.transport.empty_in` | Terminal49 also emits milestones OpenTrack has no equivalent for: * **Vessel berthed:** `container.transport.vessel_berthed` * **Available for pickup:** `container.transport.available` and `.not_available` * **Transshipment:** arrived, discharged, loaded, departed * **Feeder vessel and barge:** arrived, discharged, loaded, departed * **Rail:** loaded, departed, arrived, unloaded, plus `arrived_at_inland_destination` See the full [event catalog](/docs/api-docs/webhooks/event-catalog). ### Registering a webhook ```bash theme={null} curl -X POST https://api.terminal49.com/v2/webhooks \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "webhook", "attributes": { "url": "https://your-endpoint.example.com/t49", "active": true, "events": [ "container.transport.vessel_discharged", "container.transport.available", "container.pickup_lfd.changed" ] } } }' ``` Payloads are HMAC-signed. See [webhook setup](/docs/api-docs/in-depth-guides/webhooks) for signature verification, and [List webhook IPs](/docs/api-docs/api-reference/webhooks/list-webhook-ips) if your firewall restricts inbound traffic. ## What you gain This is the part worth reading even if the rest is mechanical. Terminal49 integrates with terminals directly, not only carriers, so the container object carries operational data that has no OpenTrack equivalent. ### Holds `holds_at_pod_terminal` is an array of active holds blocking pickup: ```json theme={null} { "holds_at_pod_terminal": [ { "name": "customs", "status": "hold", "description": "CBP HOLD" }, { "name": "freight", "status": "hold", "description": null } ] } ``` Hold names are `freight`, `customs`, `USDA`, `VACIS`, `TMF`, and `other`. Status is `hold` or `pending`. When a hold clears, the object is removed from the array. There is no released state. Hold names are case-sensitive. `USDA`, `VACIS`, and `TMF` are uppercase; `freight`, `customs`, and `other` are lowercase. Match exactly. ### Fees `fees_at_pod_terminal` carries type, amount, and currency: ```json theme={null} { "fees_at_pod_terminal": [ { "type": "demurrage", "amount": 850.00, "currency_code": "USD" }, { "type": "exam", "amount": 450.00, "currency_code": "USD" } ] } ``` Fee types are `demurrage`, `extended_dwell_time`, `exam`, `total`, and `other`. Some terminals report a `total` line item alongside individual fees. Filter it out before summing or you will double-count. ### Last free day `pickup_lfd` is a coalesced value that follows a fixed source priority: shipping line, then terminal, then rail. It does not pick the earliest date. The individual sources are available separately on `import_deadlines`: * `pickup_lfd_line`: the shipping line's LFD (per diem deadline) * `pickup_lfd_terminal`: the terminal's LFD (demurrage deadline) * `pickup_lfd_rail`: the rail carrier's LFD at the inland destination Each has its own webhook event, so you can alert on whichever source your operation cares about. ### Release readiness Two fields answer "can I pick this up?" `available_for_pickup` and the holds array: ```javascript theme={null} function isReadyForPickup(container) { const { available_for_pickup, holds_at_pod_terminal } = container.attributes; const hasActiveHolds = holds_at_pod_terminal.some(h => h.status === 'hold'); return available_for_pickup === true && !hasActiveHolds; } ``` Full detail in [Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). Holds, fees, LFD, and availability come back on the container object wherever the terminal is a supported source. They are not a paid add-on and they do not require a sales conversation. See [Entitlements](/docs/api-docs/useful-info/entitlements) for the features that do require account enablement. Routing Data (container map and vessel positions), rail LFD, container refresh, and the embeddable map and widget are the gated ones. ## Gotchas that will bite you `POST /tracking_requests` returns immediately with a pending status. The shipment appears once the carrier responds. Subscribe to `tracking_request.succeeded` and `tracking_request.failed` rather than expecting shipment data in the creation response. A request may also land in `awaiting_manifest` if the carrier has not manifested the shipment yet, and we retry automatically. See [Tracking Request Lifecycle](/docs/api-docs/in-depth-guides/tracking-request-lifecycle). Relationships are ID references into an `included` array. Use a JSON:API client library, or use `include` to sideload exactly what you need. Parsing raw JSON works but you will write more code than you expect. OpenTrack may return local time or ISO strings. Terminal49 stores event timestamps in UTC and returns the matching IANA timezone alongside. Convert for display rather than assuming local time. See [Event Timestamps](/docs/api-docs/in-depth-guides/event-timestamps). `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` mean no active holds or fees. This is the common case. Do not treat it as missing data. Terminal changes (fees, holds, LFD, appointment, availability) arrive on `container.updated` with a `changeset` showing old value first, new value second. Use it instead of diffing state yourself. Terminal49 uses `Authorization: Token YOUR_KEY`. Many HTTP clients default to `Bearer`. Check your library's default prefix or pass the full header string yourself. ## Error handling OpenTrack returns HTTP 200 for both success and some failure cases, with the error in the response body. Terminal49 uses standard HTTP status codes. Replace body checks and message-string matching with status-code checks. | Status | Meaning | | ------ | ------------------------------------------------------------------------------- | | 400 | Malformed request or failed validation | | 401 | Missing or invalid API key | | 403 | Key lacks permission, or the feature is not enabled on your plan | | 404 | Resource does not exist | | 422 | Valid syntax, rejected content. For example, a malformed container number | | 429 | Rate limited. See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) | | 5xx | Terminal49 or an upstream carrier or terminal is unavailable | Rough equivalence for the OpenTrack errors you are handling today: | OpenTrack error | Terminal49 | | ------------------------- | ---------------------------------------------------------------------------------------- | | Authentication error | HTTP 401 | | Authorization error | HTTP 403 | | Rate limit error | HTTP 429 | | Validation error | HTTP 400 or 422 | | Unsupported carrier error | HTTP 422 | | No data found error | `tracking_request.failed`, or `tracking_request.awaiting_manifest` if not yet manifested | | Server error | Not surfaced. We retry internally or return HTTP 5xx. | The [TypeScript SDK](/docs/sdk/introduction) maps these to typed errors (`AuthenticationError`, `ValidationError`, `RateLimitError`, `UpstreamError`, `FeatureNotEnabledError`, `AuthorizationError`, `NotFoundError`) and retries rate-limit and server errors automatically with exponential backoff. ## Where we are narrower than OpenTrack Worth knowing before you commit. **Carrier count.** Terminal49 integrates directly with 36 ocean carriers, plus 2 more enabled on request, as of 14 August 2026. Check your carrier mix against the [ocean carrier list](/docs/coverage/ocean-carriers) before cutover, and read the known issues section there. We publish the per-carrier field gaps. **Terminal data is North America.** Holds, fees, LFD, and availability come from direct terminal integrations concentrated in the US and Canada, with European ports expanding. Ocean milestones work globally; terminal-level operational data does not yet. **No air, parcel, or road tracking.** If your integration covers those modes, this migration handles only the ocean portion. **Some fields are source-dependent.** Seal number, container weight, and departure or arrival events vary by carrier. The [field availability reference](/docs/coverage/fields) says which fields are always present and which depend on the carrier, terminal, or journey. ## Migration checklist Everyone does the base path. Then pick a branch. ### Base path Self-serve at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). Copy it immediately, it is shown once. Move the key to `Authorization: Token`. Note the `Token` prefix. Compare your OpenTrack carrier values against the [carrier list](/docs/coverage/ocean-carriers). Flag anything missing before you cut over. One `POST /tracking_requests` per BOL, booking, or container, replacing the per-request lookup. Tracking requests start pending. Handle `succeeded`, `failed`, and `awaiting_manifest` rather than expecting data on creation. JSON:API structure, split equipment fields, UTC timestamps with a separate timezone. Replace body checks with HTTP status codes. Submit tracking requests for everything currently in transit. Send us the list if it is large and we will load it. Holds, fees, and LFD are the reason to do this properly rather than porting like for like. ### Then pick one Accept our POST payloads at a public URL. Subscribe only to events you act on. Reject any payload whose signature does not match. Only needed if your firewall restricts inbound traffic. Confirm end-to-end before going live. Remove your dedupe layer along with it. See [webhook best practices](/docs/api-docs/webhooks/best-practices) for retries and idempotency. The creation response is pending and carries no shipment yet. Keep the tracking request ID, then fetch the tracking request again (or handle `tracking_request.succeeded`) to get the shipment ID once the carrier responds. Point it at `GET /v2/shipments` or `GET /v2/containers`. No change to how often you poll. Skipped: endpoint setup, signature verification, IP whitelisting, delivery testing. Terminal data changes on a cadence polling tends to miss. If you only adopt webhooks for one thing, make it `container.updated` and `container.pickup_lfd.changed`. ## Migrate with an AI coding agent If you use Cursor, Claude Code, Windsurf, Copilot, or another AI coding assistant, hand it the prompt below. It is written to run a **side-by-side migration**: the agent stands up a Terminal49 client next to your existing OpenTrack code, shadows every OpenTrack call with a Terminal49 call, diffs the responses, and only cuts over once parity is proven. Point your agent at this page as context (paste the URL or add it as a doc source). The prompt references the mappings above, so the more of this page the agent can see, the better it does. 1. Open your repo in your AI coding tool. 2. Add this page as a documentation source, or paste its URL into the chat. 3. Copy the prompt below into a new chat and send it. 4. Answer the agent's discovery questions (OpenTrack client location, env var names, carrier mix). 5. Review each PR the agent opens. It should ship in small, reviewable steps: client, shadow, parity harness, cutover, cleanup. * A `Terminal49Client` alongside your existing `OpenTrackClient`, sharing the same interface where possible. * A shadow-mode wrapper that calls both providers and logs response diffs without changing behavior. * A parity report per shipment: matched fields, diverged fields, and Terminal49-only fields (holds, fees, LFD). * A feature-flagged cutover: route reads to Terminal49, keep OpenTrack as fallback until you flip the flag off. * A webhook receiver with HMAC verification, or a polling scheduler, depending on which path you pick. * A cleanup PR that removes OpenTrack code, env vars, dependencies, and dedupe logic. ### The prompt Copy this into your agent. Replace the bracketed placeholders in the **Repo context** block before sending. ```markdown Terminal49 migration agent prompt expandable icon=robot wrap theme={null} You are migrating this codebase from the OpenTrack tracking API to the Terminal49 API, side by side. Terminal49's authoritative migration guide is at https://terminal49.com/docs/migrate/opentrack. Use it as the source of truth for field mappings, event names, error codes, and behavior differences. # Repo context (fill this in before running) - OpenTrack client lives at: [path/to/opentrack/client.ts] - OpenTrack is called from: [list the call sites or "find them"] - Language and framework: [e.g. TypeScript + Node, Python + FastAPI, Ruby on Rails] - Storage for tracking state: [e.g. Postgres table `shipments`] - Current polling schedule: [e.g. cron every 2h via BullMQ] - Carrier mix (SCACs we track most): [e.g. MAEU, MSCU, CMDU, HLCU, ONEY] - Deployment target: [e.g. AWS ECS, Vercel, Fly.io] - Secret store: [e.g. AWS Secrets Manager, `.env`, Doppler] # Rules 1. Do not delete OpenTrack code until the cleanup step. Migration is side by side. 2. Ship in small PRs. Each PR must build, pass tests, and be independently revertable. 3. Never invent Terminal49 fields, endpoints, or event names. If the guide does not confirm a mapping, ask me. Do not guess. 4. Terminal49 uses `Authorization: Token ` (not `Bearer`) and content type `application/vnd.api+json`. Get this right on the first request. 5. Terminal49 is asynchronous. `POST /tracking_requests` returns pending. Data arrives via `tracking_request.succeeded`, `tracking_request.failed`, or `tracking_request.awaiting_manifest` webhooks, or by polling `GET /v2/shipments`. Do not expect shipment data on creation. 6. Timestamps are UTC with a separate IANA `time_zone` field. Do not assume local time. 7. `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` are the normal state, not missing data. A fee `amount` of 0 is valid. 8. On `container.updated`, prefer the `changeset` over diffing state yourself. # Plan (execute in order, one PR per step) ## PR 1 — Discovery and interface - Grep the repo for every OpenTrack call site. List them in the PR description. - Extract the OpenTrack client's public surface into an interface (`TrackingProvider` with methods like `track(number, type, scac)`, `getShipment(id)`, `refresh(id)`). - Make the existing OpenTrack client implement it. No behavior change. ## PR 2 — Terminal49 client - Add a `Terminal49Client` implementing the same `TrackingProvider` interface. - Auth via `T49_API_KEY` env var, header `Authorization: Token ${key}`. - Base URL `https://api.terminal49.com/v2`, content type `application/vnd.api+json`. - Implement: - `createTrackingRequest({ request_number, request_type, scac })` returning the tracking request ID. - `getShipment(id, { include: 'containers,port_of_lading,port_of_discharge,...' })`. - `getContainer(id)`. - `refreshContainer(id)` mapping to `PATCH /v2/containers/{id}/refresh`. - Normalize responses to the same shape OpenTrack callers expect today, using the field mapping from the guide. Split `iso_code` into `equipment_type`, `equipment_length`, `equipment_height`. Convert timestamps to UTC + `time_zone`. - Map errors to typed classes: `AuthenticationError` (401), `AuthorizationError` (403), `ValidationError` (400/422), `RateLimitError` (429), `UpstreamError` (5xx), `FeatureNotEnabledError` (403 + feature flag response). - Add unit tests using recorded fixtures. Do not hit the live API in tests. ## PR 3 — Shadow mode - Add a `ShadowProvider` that wraps both clients. On every read: - Call OpenTrack as the primary. Return its response. - Fire-and-forget a Terminal49 call for the same identifier. - Log a structured diff: matched fields, diverged fields, T49-only fields. - Gate with env var `TRACKING_SHADOW_MODE=true`. Off by default. - Add a parity report script that aggregates shadow logs by SCAC and field. - Do NOT change what callers see. This step is observation only. ## PR 4 — Backfill script - Write a one-shot script that reads all active shipments from our database and calls `POST /tracking_requests` for each (one per BOL, booking, or container). - Store the returned `tracking_request.id` and eventual `shipment.id` on our records. - Rate-limit to respect Terminal49's limits (handle 429 with exponential backoff). - Idempotent: safe to re-run. Skip rows already backfilled. ## PR 5 — Webhook receiver (only if we picked the webhook path) - Add `POST /webhooks/terminal49` endpoint. - Verify HMAC signature using the shared secret from `T49_WEBHOOK_SECRET`. Reject on mismatch with 401. - Handle these events at minimum: - `tracking_request.succeeded`, `tracking_request.failed`, `tracking_request.awaiting_manifest` - `container.transport.vessel_discharged`, `container.transport.available`, `container.transport.full_out` - `container.updated` (use the `changeset`, do not diff) - `container.pickup_lfd.changed` - Persist events idempotently keyed by event ID. - Register the webhook via `POST /v2/webhooks` from a bootstrap script, subscribing only to events we handle. - If our firewall restricts inbound traffic, whitelist Terminal49 IPs from `GET /v2/webhooks/ips`. ## PR 6 — Cutover behind a flag - Add feature flag `TRACKING_PROVIDER` with values `opentrack` (default) and `terminal49`. - Route all reads through the flag. OpenTrack stays available as a fallback for one release cycle. - Flip staging to `terminal49`, verify parity report is clean, then flip production. ## PR 7 — Cleanup - Delete `OpenTrackClient`, its tests, its env vars, its dedupe cache, and any ISO-code parsing helpers Terminal49 makes redundant. - Remove the shadow provider and the feature flag. - Update README and any runbooks. Note the new webhook endpoint if applicable. # Definition of done - All OpenTrack call sites now go through Terminal49. - Webhook (or polling) is live in production. - Parity report shows no unexplained divergences for our top 10 SCACs. - Holds, fees, and LFD are exposed to whichever downstream system needs them (dashboard, alerts, customer emails). Do not migrate without wiring these up. They are the reason to do this properly. - OpenTrack dependency, env vars, and dead code are gone from the repo. # Ask me before you - Choose between the webhook path and the polling path. Default to webhooks unless our infra makes an inbound HTTPS endpoint hard. - Change the shape of any function OpenTrack callers use today. Prefer a normalization layer inside the Terminal49 client. - Add a new dependency. Prefer stdlib and what is already in the repo. - Touch anything outside the tracking integration. Start with PR 1. Post the list of OpenTrack call sites and the proposed `TrackingProvider` interface, and wait for my review before writing PR 2. ``` The prompt is deliberately opinionated on side-by-side migration and small PRs. If your team prefers a big-bang cutover or a different branching model, edit the **Plan** section before sending it to your agent. ## Getting help Send us your list of active container and bill of lading numbers and we will load them rather than making you script the backfill. If something in this mapping is wrong or incomplete, tell us. We would rather fix the page than have you work around it. Every endpoint, with request and response schemas Typed client with retries and pagination built in Carriers, terminals, rail, and field availability Simulate success and failure outcomes # Migrating from Project44 Source: https://terminal49.com/docs/migrate/project44 Map project44 ocean tracking fields, webhooks, and errors to Terminal49 equivalents. Covers container holds, per diem fees, and checklist. If you are using Project44 for ocean visibility, this guide maps the ocean portion of Project44 onto Terminal49 field by field, so you can replace the ocean leg without reverse-engineering our schema. Terminal49 is ocean and terminal focused. We do not cover road, rail, LTL, or air. If your Project44 integration spans modes, this migration handles only the ocean shipments. There is no compatibility shim. You will change your authentication, request shape, and response parsing. For most integrations that is an afternoon. Terminal49 self-serves. Create an account, generate a free API key, and start tracking up to 10 active containers at no cost. Terminal data: holds, fees, and last free day are included on the container object out of the box. ## Start in sixty seconds Signing up and getting a key is self-serve. Go to [app.terminal49.com](https://app.terminal49.com). The free plan tracks up to 10 active containers. Visit [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). Copy the key immediately. It is shown once and then masked forever. ```bash theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "tracking_request", "attributes": { "request_number": "YOUR_BOL_NUMBER", "request_type": "bill_of_lading", "scac": "MAEU" } } }' ``` The full API key is shown once, right after you create it. Copy it before you navigate away — after that it is masked and cannot be revealed. If you miss it, create a new key and delete the old one. The free plan tracks up to 10 active containers. Creating tracking requests through the API works right away; reading tracking data back through the API requires a free 7-day API trial (same 10-container limit) — contact us via in-app chat or [support@terminal49.com](mailto:support@terminal49.com) and we enable it. If you want to test without a live shipment, use the [test tracking numbers](/docs/api-docs/useful-info/test-numbers), which simulate success and failure outcomes. **Migration offer:** sign up now and the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id) — vessel schedules, AIS positions, and projected routes — is free for your first month. ## The architectural shift Project44 tracks multi-modal shipments with legs. Terminal49 tracks ocean shipments and containers directly. You register a tracking request once, and we keep it updated and push changes to your webhook. OAuth client credentials to get a bearer token, then call `GET` or polling endpoints for shipment status. Multi-modal legs are bundled in one shipment resource. You own the cache, the schedule, and the deduplication across road, rail, and ocean. `POST /tracking_requests` once → Terminal49 polls carriers, terminals, and rail → we POST to your endpoint as things change → write to your database. No cache layer, no dedupe logic. You can keep polling if you prefer — point your existing scheduler at `GET /v2/shipments` or `GET /v2/containers`. But webhooks are the reason the API is shaped this way, and terminal data (holds, fees, last free day) changes on a cadence that polling tends to miss. ## Quick comparison | | Project44 | Terminal49 | | ----------------------- | -------------------------------------- | ------------------------------------- | | Tracking model | Poll on demand, multi-modal legs | Register once, then push or poll | | Authentication | OAuth2 client credentials bearer token | `Authorization: Token` header | | Base URL | `api.project44.com` | `https://api.terminal49.com/v2` | | Content type | `application/json` | `application/vnd.api+json` | | Response format | Custom JSON | JSON:API | | Webhooks | Webhook subscriptions available | 30+ events, HMAC-signed | | Carrier identification | SCAC or carrier code | `scac`, or `auto_detect_vocc_scac` | | Terminal holds and fees | No direct equivalent | Included on the container object | | Last free day | No direct equivalent | Included, with per-source breakdown | | Rail milestones | Limited | North American Class I and short-line | | Multi-modal | Ocean, road, rail, air, LTL | Ocean and terminal only | | Getting an API key | Account setup with Project44 | Self-serve | ## Authentication Project44 uses OAuth2 client credentials. You request a bearer token from their token endpoint and include it in every call as `Authorization: Bearer`. Terminal49 uses a static API key as `Authorization: Token`. ```bash Project44 theme={null} # 1. Request bearer token curl -X POST https:// \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" # 2. Use bearer token on every call curl https://api.project44.com/ \ -H "Authorization: Bearer YOUR_BEARER_TOKEN" ``` ```bash Terminal49 theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_T49_KEY" \ -d '{"data":{"type":"tracking_request","attributes":{ "request_number":"MRKU9465770", "request_type":"container", "scac":"MAEU"}}}' ``` Note the `Token` prefix. It is not `Bearer`. ## Request parameter mapping | Project44 parameter | Terminal49 equivalent | Notes | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Your Project44 shipment identifier | `request_number` | Use BOL, booking, or container number | | Container number tracking | `request_type: "container"` | | | Bill of lading tracking | `request_type: "bill_of_lading"` | Master or house BOL | | Booking number tracking | `request_type: "booking_number"` | | | SCAC / carrier code | `scac` | Same SCAC values for most carriers | | Omit carrier / auto-detect | Set `auto_detect_vocc_scac: true`, or call [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) first | Auto-detection runs asynchronously; a failed inference fails the tracking request with `scac_auto_detect_failed` | | Force refresh / requery | `PATCH /v2/containers/{id}/refresh` | Forces an immediate pull from all sources. Paid feature — requires account enablement, limited to 10 requests per minute | | Shipment legs / routes | `GET /v2/containers/{id}/map_geojson` — requires the Routing Data entitlement | See [Routing](/docs/api-docs/in-depth-guides/routing) | | OAuth `client_id` / `client_secret` | `Authorization: Token` header | Single static key, no token rotation | Project44 bundles multi-modal legs in one shipment. Terminal49 expects one identifier per tracking request. Track by BOL and we return every container on that bill of lading as related container resources. Container-number tracking requests are currently in beta. If you were tracking a single leg in Project44, send the corresponding container or BOL number to Terminal49. ## Response field mapping Terminal49 is JSON:API compliant, so relationships between shipments, containers, ports, and terminals are explicit rather than something you reassemble from ID references. Use the [`include` parameter](/docs/api-docs/in-depth-guides/including-resources) to sideload related resources in one call instead of chasing IDs. ### Shipment level | Project44 | Terminal49 | | --------------------------- | -------------------------------------------- | | Your Project44 shipment ID | `shipment.id` (UUID) | | Bill of lading number | `shipment.attributes.bill_of_lading_number` | | Carrier / SCAC code | `shipment.attributes.shipping_line_scac` | | Carrier name | `shipment.attributes.shipping_line_name` | | Shipment status | Derived from container status and milestones | | Place of receipt | `shipment.attributes.port_of_lading_*` | | Port of loading | `shipment.relationships.port_of_lading` | | Port of discharge | `shipment.relationships.port_of_discharge` | | Final destination (inland) | `shipment.relationships.destination` | | Predictive ETA | `shipment.attributes.pod_eta_at` | | Your Project44 cached state | No equivalent — refresh is managed for you | ### Container level | Project44 | Terminal49 | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Container number | `container.attributes.number` | | ISO code | `container.attributes.equipment_type` + `equipment_length` + `equipment_height` | | Container size / type | Same three fields above | | Container status | `container.attributes.current_status` | | Transport events / legs | Container timestamps, [transport events](/docs/api-docs/api-reference/containers/get-a-containers-transport-events), and webhook events | Project44 returns a single ISO code string like `45G1`. Terminal49 splits this into three normalized fields: type (dry, reefer, open top, flat rack, bulk, tank), length (10, 20, 40, 45), and height (standard, high cube). If you were parsing ISO codes yourself, you can delete that code. ### Locations, facilities, and vessels | Project44 | Terminal49 | | ------------------------- | ------------------------------------------------------------------------------------------------ | | Location / port name | `port.attributes.name` | | Location code / UN/LOCODE | `port.attributes.code` | | Latitude / longitude | `port.attributes.latitude` / `.longitude` | | Timezone | `port.attributes.time_zone` | | Country code | `port.attributes.country_code` | | Terminal / facility name | `terminal.attributes.name` | | Terminal SMDG code | `terminal.attributes.smdg_code` | | Terminal BIC code | `terminal.attributes.bic_facility_code` | | Vessel name | `shipment.attributes.pod_vessel_name` | | Vessel IMO | `shipment.attributes.pod_vessel_imo` | | Vessel MMSI | Available via the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo) | | Vessel call sign, flag | Not returned | ## Milestone and event mapping Project44 uses event codes or status fields on shipment legs. Terminal49 exposes the same milestones as normalized transport events and pushes each one to your webhook. | Project44 ocean event | Milestone | Terminal49 event | | ------------------------- | ----------------------------- | --------------------------------------- | | Vessel loaded event | Loaded on vessel at origin | `container.transport.vessel_loaded` | | Vessel departed event | Vessel departed origin | `container.transport.vessel_departed` | | Vessel arrived event | Vessel arrived at destination | `container.transport.vessel_arrived` | | Vessel discharged event | Discharged from vessel | `container.transport.vessel_discharged` | | Container gated out event | Gated out at destination | `container.transport.full_out` | | Container gated in event | Gated in at origin | `container.transport.full_in` | | — | Empty picked up at origin | `container.transport.empty_out` | | — | Empty returned at destination | `container.transport.empty_in` | Terminal49 also emits milestones Project44 has no equivalent for: * **Vessel berthed** — `container.transport.vessel_berthed` * **Available for pickup** — `container.transport.available` and `.not_available` * **Transshipment** — arrived, discharged, loaded, departed * **Feeder vessel and barge** — arrived, discharged, loaded, departed * **Rail** — loaded, departed, arrived, unloaded, plus `arrived_at_inland_destination` See the full [event catalog](/docs/api-docs/webhooks/event-catalog). ### Registering a webhook ```bash theme={null} curl -X POST https://api.terminal49.com/v2/webhooks \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "webhook", "attributes": { "url": "https://your-endpoint.example.com/t49", "active": true, "events": [ "container.transport.vessel_discharged", "container.transport.available", "container.pickup_lfd.changed" ] } } }' ``` Payloads are HMAC-signed. See [webhook setup](/docs/api-docs/in-depth-guides/webhooks) for signature verification, and [List webhook IPs](/docs/api-docs/api-reference/webhooks/list-webhook-ips) if your firewall restricts inbound traffic. ## What you gain This is the part worth reading even if the rest is mechanical. Terminal49 integrates with terminals directly, not only carriers, so the container object carries operational data that has no Project44 equivalent. ### Holds `holds_at_pod_terminal` is an array of active holds blocking pickup: ```json theme={null} { "holds_at_pod_terminal": [ { "name": "customs", "status": "hold", "description": "CBP HOLD" }, { "name": "freight", "status": "hold", "description": null } ] } ``` Hold names are `freight`, `customs`, `USDA`, `VACIS`, `TMF`, and `other`. Status is `hold` or `pending`. When a hold clears, the object is removed from the array — there is no released state. Hold names are case-sensitive. `USDA`, `VACIS`, and `TMF` are uppercase; `freight`, `customs`, and `other` are lowercase. Match exactly. ### Fees `fees_at_pod_terminal` carries type, amount, and currency: ```json theme={null} { "fees_at_pod_terminal": [ { "type": "demurrage", "amount": 850.00, "currency_code": "USD" }, { "type": "exam", "amount": 450.00, "currency_code": "USD" } ] } ``` Fee types are `demurrage`, `extended_dwell_time`, `exam`, `total`, and `other`. Some terminals report a `total` line item alongside individual fees. Filter it out before summing or you will double-count. ### Last free day `pickup_lfd` is a coalesced value that follows a fixed source priority: shipping line, then terminal, then rail. It does not pick the earliest date. The individual sources are available separately on `import_deadlines`: * `pickup_lfd_line` — the shipping line's LFD (per diem deadline) * `pickup_lfd_terminal` — the terminal's LFD (demurrage deadline) * `pickup_lfd_rail` — the rail carrier's LFD at the inland destination Each has its own webhook event, so you can alert on whichever source your operation cares about. ### Release readiness Two fields answer "can I pick this up?" — `available_for_pickup` and the holds array: ```javascript theme={null} function isReadyForPickup(container) { const { available_for_pickup, holds_at_pod_terminal } = container.attributes; const hasActiveHolds = holds_at_pod_terminal.some(h => h.status === 'hold'); return available_for_pickup === true && !hasActiveHolds; } ``` Full detail in [Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). Holds, fees, LFD, and availability come back on the container object wherever the terminal is a supported source. They are not a paid add-on and they do not require a sales conversation. See [Entitlements](/docs/api-docs/useful-info/entitlements) for the features that do require account enablement. ## Gotchas that will bite you Project44 bearer tokens expire and must be refreshed. Terminal49 keys do not expire and do not rotate. Delete your token refresh logic. If you rotate for security, create a new key in the dashboard and replace the old one. Project44 bundles ocean, road, rail, and air legs in one shipment resource. Terminal49 is ocean only. You must extract the ocean leg and send the BOL or container number to Terminal49. Mode-crossing logic you built in Project44 will not carry over. Project44 returns flat JSON objects. Terminal49 returns JSON:API with `data`, `relationships`, and `included`. Use a JSON:API client library, or use `include` to sideload exactly what you need. Parsing raw JSON works but you will write more code than you expect. `POST /tracking_requests` returns immediately with a pending status. The shipment appears once the carrier responds. Subscribe to `tracking_request.succeeded` and `tracking_request.failed` rather than expecting shipment data in the creation response. A request may also land in `awaiting_manifest` if the carrier has not manifested the shipment yet — we retry automatically. See [Tracking Request Lifecycle](/docs/api-docs/in-depth-guides/tracking-request-lifecycle). Project44 timestamp handling varies by API version and resource. Terminal49 stores event timestamps in UTC and returns the matching IANA timezone alongside. Convert for display rather than assuming local time. See [Event Timestamps](/docs/api-docs/in-depth-guides/event-timestamps). `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` mean no active holds or fees. This is the common case. Do not treat it as missing data. It means the terminal reported the fee type but has not posted an amount yet. Common for demurrage in the first day or two after discharge. Terminal changes (fees, holds, LFD, appointment, availability) arrive on `container.updated` with a `changeset` showing old value first, new value second. Use it instead of diffing state yourself. ## Error handling Project44 returns errors within the response body, sometimes alongside partial data, sometimes with an error envelope. Terminal49 uses standard HTTP status codes. Replace envelope checks with status-code checks. | Status | Meaning | | ------ | ------------------------------------------------------------------------------- | | 400 | Malformed request or failed validation | | 401 | Missing or invalid API key | | 403 | Key lacks permission, or the feature is not enabled on your plan | | 404 | Resource does not exist | | 422 | Valid syntax, rejected content — for example a malformed container number | | 429 | Rate limited. See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) | | 5xx | Terminal49 or an upstream carrier or terminal is unavailable | Rough equivalence for the Project44 errors you are handling today: | Project44 error pattern | Terminal49 | | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | Invalid or expired bearer token | HTTP 401 | | Insufficient scope / unauthorized client | HTTP 403 | | Rate limit exceeded | HTTP 429 | | Invalid request parameters, malformed identifier | HTTP 400 or 422 | | Shipment not found, unsupported SCAC | HTTP 422 | | Carrier temporary unavailability, no response | `tracking_request.failed`, or `tracking_request.awaiting_manifest` if not yet manifested. Not surfaced directly — we retry internally. | The [TypeScript SDK](/docs/sdk/introduction) maps these to typed errors (`AuthenticationError`, `ValidationError`, `RateLimitError`, `UpstreamError`, `FeatureNotEnabledError`, `AuthorizationError`, `NotFoundError`) and retries rate-limit and server errors automatically with exponential backoff. ## Where we are narrower than Project44 Worth knowing before you commit. **Carrier count.** Terminal49 integrates directly with 36 ocean carriers, plus 2 more enabled on request — as of 14 August 2026. Project44 lists more. Ours are direct integrations covering the lines that move volume into North America, and each is normalized into one schema. Check your carrier mix against the [ocean carrier list](/docs/coverage/ocean-carriers) before cutover, and read the known issues section there — we publish the per-carrier field gaps. **Terminal data is North America.** Holds, fees, LFD, and availability come from direct terminal integrations concentrated in the US and Canada, with European ports expanding. Ocean milestones work globally; terminal-level operational data does not yet. **No road, rail, air, or LTL tracking.** If your Project44 integration covers those modes, this migration handles only the ocean portion. You will need to keep Project44 or another provider for the non-ocean legs. **No freight rates or sailing schedules.** We do not offer a rate calculator, rate index, or schedule search. **Some fields are source-dependent.** Seal number, container weight, and departure or arrival events vary by carrier. The [field availability reference](/docs/coverage/fields) says which fields are always present and which depend on the carrier, terminal, or journey. ## Migration checklist Everyone does the base path. Then pick a branch. ### Base path Self-serve at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). Copy it immediately — it is shown once. Project44 bearer tokens expire. Terminal49 keys do not. Replace your token acquisition and refresh logic with a static `Authorization: Token` header. Compare your Project44 carrier codes against the [carrier list](/docs/coverage/ocean-carriers). Flag anything missing before you cut over. Extract ocean legs from your Project44 shipments. Send the corresponding BOL, booking, or container number to Terminal49. One `POST /tracking_requests` per BOL, booking, or container, replacing the per-request lookup. Tracking requests start pending. Handle `succeeded`, `failed`, and `awaiting_manifest` rather than expecting data on creation. JSON:API structure, split equipment fields, UTC timestamps with a separate timezone. Replace envelope checks with HTTP status codes. Submit tracking requests for everything currently in transit. Send us the list if it is large and we will load it. Holds, fees, and LFD are the reason to do this properly rather than porting like for like. ### Then pick one 1. Expose an HTTPS endpoint that accepts our POST payloads. 2. Register a webhook and subscribe only to events you act on. 3. Verify HMAC signatures. 4. Whitelist our IPs if your firewall restricts inbound traffic. 5. Trigger a test delivery before going live. 6. Retire your polling job and your dedupe layer. See [webhook best practices](/docs/api-docs/webhooks/best-practices) for retries and idempotency. 1. Store the tracking request ID from the creation response. The shipment ID arrives later — the creation response is pending with no shipment attached; fetch the tracking request again (or handle `tracking_request.succeeded`) to get it once the carrier responds. 2. Repoint your existing scheduler at `GET /v2/shipments` or `GET /v2/containers`. 3. Keep your existing cadence. Skipped: endpoint setup, signature verification, IP whitelisting, delivery testing. Terminal data changes on a cadence polling tends to miss. If you only adopt webhooks for one thing, make it `container.updated` and `container.pickup_lfd.changed`. ## Migrate with an AI coding agent If you use Claude Code, Cursor, Codex, Windsurf, Copilot, or another AI coding assistant, hand it the prompt below. It is written to run a **side-by-side migration**: the agent stands up a Terminal49 client next to your existing project44 code, shadows every project44 call with a Terminal49 call, diffs the responses, and only cuts over once parity is proven. Point your agent at this page as context (paste the URL or add it as a doc source). The prompt references the mappings above, so the more of this page the agent can see, the better it does. 1. Open your repo in your AI coding tool. 2. Add this page as a documentation source, or paste its URL into the chat. 3. Copy the prompt below into a new chat and send it. 4. Answer the agent's discovery questions (project44 client location, env var names, carrier mix). 5. Review each PR the agent opens. It should ship in small, reviewable steps: client, shadow, parity harness, cutover, cleanup. * A `Terminal49Client` alongside your existing project44 client, sharing the same interface where possible. * A shadow-mode wrapper that calls both providers and logs response diffs without changing behavior. * A parity report per shipment: matched fields, diverged fields, and Terminal49-only fields (holds, fees, LFD). * A feature-flagged cutover: route reads to Terminal49, keep project44 as fallback until you flip the flag off. * A webhook receiver with HMAC verification, or a polling scheduler, depending on which path you pick. * A cleanup PR that removes project44 code, env vars, dependencies, and dedupe logic. ### The prompt Copy this into your agent. Replace the bracketed placeholders in the **Repo context** block before sending. ```markdown Terminal49 migration agent prompt expandable icon=robot wrap theme={null} You are migrating this codebase from the project44 API to the Terminal49 API, side by side. Terminal49's authoritative migration guide is at https://terminal49.com/docs/migrate/project44. Use it as the source of truth for field mappings, event names, error codes, and behavior differences. # Repo context (fill this in before running) - project44 client lives at: [path/to/client] - project44 is called from: [list the call sites or "find them"] - Language and framework: [e.g. TypeScript + Node, Python + FastAPI, Ruby on Rails] - Storage for tracking state: [e.g. Postgres table `shipments`] - Current polling schedule: [e.g. cron every 2h] - Carrier mix (SCACs we track most): [e.g. MAEU, MSCU, CMDU, HLCU, ONEY] - Deployment target: [e.g. AWS ECS, Vercel, Fly.io] - Secret store: [e.g. AWS Secrets Manager, `.env`, Doppler] # Rules 1. Do not delete project44 code until the cleanup step. Migration is side by side. 2. Ship in small PRs. Each PR must build, pass tests, and be independently revertable. 3. Never invent Terminal49 fields, endpoints, or event names. If the guide does not confirm a mapping, ask me. Do not guess. 4. Terminal49 uses `Authorization: Token ` (not `Bearer`) and content type `application/vnd.api+json`. Get this right on the first request. 5. Terminal49 is asynchronous. `POST /tracking_requests` returns pending. Data arrives via `tracking_request.succeeded`, `tracking_request.failed`, or `tracking_request.awaiting_manifest` webhooks, or by polling `GET /v2/shipments`. Do not expect shipment data on creation. 6. Timestamps are UTC with a separate IANA `time_zone` field. Do not assume local time. 7. `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` are the normal state, not missing data. A fee `amount` of 0 is valid. 8. On `container.updated`, prefer the `changeset` over diffing state yourself. 9. Terminal49 uses a static API key. Delete the OAuth2 client-credentials token flow, token refresh logic, and any token-refresh cron jobs in the cleanup step — do not port them. 10. Terminal49 covers the ocean leg only. Keep project44 air, road, rail, and LTL tracking in place. Extract the ocean leg from each multi-modal shipment before mapping it. # Plan (execute in order, one PR per step) ## PR 1 — Discovery and interface - Grep the repo for every project44 call site. List them in the PR description. - Extract the project44 client's public surface into an interface (`TrackingProvider` with methods like `track(number, type, carrier)`, `getShipment(id)`, `refresh(id)`). - Make the existing project44 client implement it. No behavior change. ## PR 2 — Terminal49 client - Add a `Terminal49Client` implementing the same `TrackingProvider` interface. - Auth via `T49_API_KEY` env var, header `Authorization: Token ${key}`. - Base URL `https://api.terminal49.com/v2`, content type `application/vnd.api+json`. - Implement: - `createTrackingRequest({ request_number, request_type, scac })` returning the tracking request ID. - `getShipment(id, { include: 'containers,port_of_lading,port_of_discharge' })`. - `getContainer(id)`. - `refreshContainer(id)` mapping to `PATCH /v2/containers/{id}/refresh` (a paid feature — skip it unless our account has it enabled). - Normalize responses to the same shape project44 callers expect today, using the field mapping from the guide. Split equipment codes into `equipment_type`, `equipment_length`, `equipment_height`. Convert timestamps to UTC + `time_zone`. - Map errors to typed classes: `AuthenticationError` (401), `AuthorizationError` (403), `ValidationError` (400/422), `RateLimitError` (429), `UpstreamError` (5xx), `FeatureNotEnabledError` (403 + feature flag response). - Add unit tests using recorded fixtures. Do not hit the live API in tests. ## PR 3 — Shadow mode - Add a `ShadowProvider` that wraps both clients. On every read: - Call project44 as the primary. Return its response. - Fire-and-forget a Terminal49 call for the same identifier. - Log a structured diff: matched fields, diverged fields, T49-only fields. - Gate with env var `TRACKING_SHADOW_MODE=true`. Off by default. - Add a parity report script that aggregates shadow logs by SCAC and field. - Do NOT change what callers see. This step is observation only. ## PR 4 — Backfill script - Write a one-shot script that reads all active shipments from our database and calls `POST /tracking_requests` for each (one per BOL, booking, or container). - Store the returned `tracking_request.id` and eventual `shipment.id` on our records. - Rate-limit to respect Terminal49's limits (handle 429 with exponential backoff). - Idempotent: safe to re-run. Skip rows already backfilled. ## PR 5 — Webhook receiver (only if we picked the webhook path) - Add `POST /webhooks/terminal49` endpoint. - Verify HMAC signature using the shared secret from `T49_WEBHOOK_SECRET`. Reject on mismatch with 401. - Handle these events at minimum: - `tracking_request.succeeded`, `tracking_request.failed`, `tracking_request.awaiting_manifest` - `container.transport.vessel_discharged`, `container.transport.available`, `container.transport.full_out` - `container.updated` (use the `changeset`, do not diff) - `container.pickup_lfd.changed` - Persist events idempotently keyed by event ID. - Register the webhook via `POST /v2/webhooks` from a bootstrap script, subscribing only to events we handle. - If our firewall restricts inbound traffic, whitelist Terminal49 IPs from `GET /v2/webhooks/ips`. ## PR 6 — Cutover behind a flag - Add feature flag `TRACKING_PROVIDER` with values `project44` (default) and `terminal49`. - Route all reads through the flag. project44 stays available as a fallback for one release cycle. - Flip staging to `terminal49`, verify parity report is clean, then flip production. ## PR 7 — Cleanup - Delete the project44 client, its tests, its env vars, its dedupe cache, and any equipment-code parsing helpers Terminal49 makes redundant. - Remove the shadow provider and the feature flag. - Update README and any runbooks. Note the new webhook endpoint if applicable. # Definition of done - All project44 call sites now go through Terminal49. - Webhook (or polling) is live in production. - Parity report shows no unexplained divergences for our top 10 SCACs. - Holds, fees, and LFD are exposed to whichever downstream system needs them (dashboard, alerts, customer emails). Do not migrate without wiring these up. They are the reason to do this properly. - project44 dependency, env vars, and dead code are gone from the repo. # Ask me before you - Choose between the webhook path and the polling path. Default to webhooks unless our infra makes an inbound HTTPS endpoint hard. - Change the shape of any function project44 callers use today. Prefer a normalization layer inside the Terminal49 client. - Add a new dependency. Prefer stdlib and what is already in the repo. - Touch anything outside the tracking integration. ``` ## Getting help Send us your list of active container and bill of lading numbers and we will load them rather than making you script the backfill. If something in this mapping is wrong or incomplete, tell us. We would rather fix the page than have you work around it. Every endpoint, with request and response schemas Typed client with retries and pagination built in Carriers, terminals, rail, and field availability Simulate success and failure outcomes # Migrate from SeaRates to Terminal49 API Source: https://terminal49.com/docs/migrate/searates Map SeaRates tracking API fields, parameters, and errors to their Terminal49 equivalents. Includes webhook setup, carrier coverage, and a migration checklist. If you have the SeaRates tracking API in production, this page maps it onto Terminal49 field by field, so you can cut over without reverse-engineering our schema. There is no compatibility shim. You will change your request code and your response parsing. For most integrations that is an afternoon. ## Start in sixty seconds Signing up and getting a key is self-serve. Sign up at [app.terminal49.com](https://app.terminal49.com). The free plan tracks up to 10 active containers. Create a key at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). ```bash theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "tracking_request", "attributes": { "request_number": "YOUR_BOL_NUMBER", "request_type": "bill_of_lading", "scac": "MAEU" } } }' ``` The full API key is shown once, right after you create it. Copy it before you navigate away. After that it is masked and cannot be revealed. If you miss it, create a new key and delete the old one. The free plan tracks up to 10 active containers. Creating tracking requests through the API works right away; reading tracking data back through the API requires a free 7-day API trial (same 10-container limit) — contact us via in-app chat or [support@terminal49.com](mailto:support@terminal49.com) and we enable it. If you want to test without a live shipment, use the [test tracking numbers](/docs/api-docs/useful-info/test-numbers), which simulate success and failure outcomes. **Migration offer:** sign up now and the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id) — vessel schedules, AIS positions, and projected routes — is free for your first month. ## The architectural shift SeaRates gives you one endpoint. You call it with a number, get current state back, and own the schedule, the cache, and the deduplication. Terminal49 splits this in two. You register a tracking request once. We keep it updated and push changes to your webhook. Cron every few hours, `GET /tracking`, diff against your cache, dedupe events, then write to your database. Every call spends quota. Freshness is capped by your polling interval. `POST /tracking_requests` once. Terminal49 polls carriers, terminals, and rail, then POSTs to your endpoint as things change. No cache layer, no dedupe logic. You can keep polling if you prefer. Point your existing scheduler at `GET /v2/shipments` or `GET /v2/containers`. But webhooks are the reason the API is shaped this way, and terminal data (holds, fees, last free day) changes on a cadence that polling tends to miss. ## Quick comparison | | SeaRates | Terminal49 | | ----------------------- | ------------------------------- | ------------------------------------- | | Tracking model | Poll on demand | Register once, then push or poll | | Authentication | `api_key` query parameter | `Authorization: Token` header | | Base URL | `https://tracking.searates.com` | `https://api.terminal49.com/v2` | | Content type | `application/json` | `application/vnd.api+json` | | Response format | Custom JSON | JSON:API | | Webhooks | None | 30+ events, HMAC-signed | | Carrier identification | `sealine` (SCAC) | `scac`, or `auto_detect_vocc_scac` | | Terminal holds and fees | No direct equivalent | Included on the container object | | Last free day | No direct equivalent | Included, with per-source breakdown | | Rail milestones | Limited | North American Class I and short-line | | Embeddable widget | Open lookup, any container | Your tracked shipments only (add-on) | | Getting an API key | Self-serve | Self-serve | ## Authentication Move the key out of the query string and into a header. ```bash SeaRates theme={null} curl "https://tracking.searates.com/tracking\ ?api_key=YOUR_SEARATES_KEY\ &number=MRKU9465770\ &sealine=MAEU\ &type=CT" ``` ```bash Terminal49 theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_T49_KEY" \ -d '{"data":{"type":"tracking_request","attributes":{ "request_number":"MRKU9465770", "request_type":"container", "scac":"MAEU"}}}' ``` Note the `Token` prefix. It is not `Bearer`. ## Request parameter mapping | SeaRates parameter | Terminal49 equivalent | Notes | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `number` | `request_number` | | | `type=CT` | `request_type: "container"` | | | `type=BL` | `request_type: "bill_of_lading"` | Master or house BOL | | `type=BK` | `request_type: "booking_number"` | | | `sealine` | `scac` | Same SCAC values for most carriers | | `sealine=auto` | Set `auto_detect_vocc_scac: true`, or call [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) first | Auto-detection runs asynchronously; a failed inference fails the tracking request with `scac_auto_detect_failed` | | `force_update` | `PATCH /v2/containers/{id}/refresh` | Forces an immediate pull from all sources. Paid feature — requires account enablement, limited to 10 requests per minute | | `route` | `GET /v2/containers/{id}/map_geojson` — requires the Routing Data entitlement | See [Routing](/docs/api-docs/in-depth-guides/routing) | | `ais` | Vessel endpoints and container GeoJSON | Not inline on the tracking response | | `api_key` | `Authorization` header | | SeaRates accepts a combined `BL_NUMBER/CONTAINER_NUMBER` value in a single `number` field. Terminal49 takes one identifier per tracking request. Track by BOL and we return every container on that bill of lading as related container resources. Container-number tracking requests are currently in beta. ## Response field mapping Terminal49 is JSON:API compliant, so relationships between shipments, containers, ports, and terminals are explicit rather than something you reassemble from ID references. Use the [`include` parameter](/docs/api-docs/in-depth-guides/including-resources) to sideload related resources in one call instead of chasing IDs. ### Shipment level | SeaRates | Terminal49 | | --------------------------------------------- | --------------------------------------------------------- | | `data.metadata.number` | `shipment.attributes.bill_of_lading_number` | | `data.metadata.sealine` | `shipment.attributes.shipping_line_scac` | | `data.metadata.sealine_name` | `shipment.attributes.shipping_line_name` | | `data.metadata.status` | Derived from container status and milestones | | `data.route.prepol` | `shipment.attributes.port_of_lading_*` (place of receipt) | | `data.route.pol` | `shipment.relationships.port_of_lading` | | `data.route.pod` | `shipment.relationships.port_of_discharge` | | `data.route.postpod` | `shipment.relationships.destination` (inland) | | `data.route.pod.predictive_eta` | `shipment.attributes.pod_eta_at` | | `data.metadata.from_cache`, `cache_expires` | No equivalent. Refresh is managed for you. | | `data.metadata.api_calls`, `unique_shipments` | No equivalent. Not returned per response. | ### Container level | SeaRates | Terminal49 | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `data.containers[].number` | `container.attributes.number` | | `data.containers[].iso_code` | `container.attributes.equipment_type` + `equipment_length` + `equipment_height` | | `data.containers[].size_type` | Same three fields above | | `data.containers[].status` | `container.attributes.current_status` | | `data.containers[].events[]` | Container timestamps, [transport events](/docs/api-docs/api-reference/containers/get-a-containers-transport-events), and webhook events | SeaRates returns a single `iso_code` string like `45G1`. Terminal49 splits this into three normalized fields: type (dry, reefer, open top, flat rack, bulk, tank), length (10, 20, 40, 45), and height (standard, high cube). If you were parsing ISO codes yourself, you can delete that code. ### Locations, facilities, and vessels | SeaRates | Terminal49 | | ----------------------------------- | ------------------------------------------------------------------------------------------------ | | `data.locations[].name` | `port.attributes.name` | | `data.locations[].locode` | `port.attributes.code` | | `data.locations[].lat` / `.lng` | `port.attributes.latitude` / `.longitude` | | `data.locations[].timezone` | `port.attributes.time_zone` | | `data.locations[].country_code` | `port.attributes.country_code` | | `data.facilities[].name` | `terminal.attributes.name` | | `data.facilities[].smdg_code` | `terminal.attributes.smdg_code` | | `data.facilities[].bic_code` | `terminal.attributes.bic_facility_code` | | `data.vessels[].name` | `shipment.attributes.pod_vessel_name` | | `data.vessels[].imo` | `shipment.attributes.pod_vessel_imo` | | `data.vessels[].mmsi` | Available via the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo) | | `data.vessels[].call_sign`, `.flag` | Not returned | ## Milestone and event mapping SeaRates returns a flat `events[]` array with an `event_code`. Terminal49 exposes the same milestones as normalized transport events and pushes each one to your webhook. | SeaRates `event_code` | Milestone | Terminal49 event | | --------------------- | ----------------------------- | --------------------------------------- | | `LOAD` | Loaded on vessel at origin | `container.transport.vessel_loaded` | | `DEPA` | Vessel departed origin | `container.transport.vessel_departed` | | `ARRI` | Vessel arrived at destination | `container.transport.vessel_arrived` | | `DISC` | Discharged from vessel | `container.transport.vessel_discharged` | | `PICK` | Gated out at destination | `container.transport.full_out` | | `GTIN` | Gated in at origin | `container.transport.full_in` | | — | Empty picked up at origin | `container.transport.empty_out` | | — | Empty returned at destination | `container.transport.empty_in` | Terminal49 also emits milestones SeaRates has no equivalent for: * **Vessel berthed:** `container.transport.vessel_berthed` * **Available for pickup:** `container.transport.available` and `.not_available` * **Transshipment:** arrived, discharged, loaded, departed * **Feeder vessel and barge:** arrived, discharged, loaded, departed * **Rail:** loaded, departed, arrived, unloaded, plus `arrived_at_inland_destination` See the full [event catalog](/docs/api-docs/webhooks/event-catalog). ### Registering a webhook ```bash theme={null} curl -X POST https://api.terminal49.com/v2/webhooks \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "webhook", "attributes": { "url": "https://your-endpoint.example.com/t49", "active": true, "events": [ "container.transport.vessel_discharged", "container.transport.available", "container.pickup_lfd.changed" ] } } }' ``` Payloads are HMAC-signed. See [webhook setup](/docs/api-docs/in-depth-guides/webhooks) for signature verification, and [List webhook IPs](/docs/api-docs/api-reference/webhooks/list-webhook-ips) if your firewall restricts inbound traffic. ## What you gain This is the part worth reading even if the rest is mechanical. Terminal49 integrates with terminals directly, not only carriers, so the container object carries operational data that has no SeaRates equivalent. ### Holds `holds_at_pod_terminal` is an array of active holds blocking pickup: ```json theme={null} { "holds_at_pod_terminal": [ { "name": "customs", "status": "hold", "description": "CBP HOLD" }, { "name": "freight", "status": "hold", "description": null } ] } ``` Hold names are `freight`, `customs`, `USDA`, `VACIS`, `TMF`, and `other`. Status is `hold` or `pending`. When a hold clears, the object is removed from the array. There is no released state. Hold names are case-sensitive. `USDA`, `VACIS`, and `TMF` are uppercase; `freight`, `customs`, and `other` are lowercase. Match exactly. ### Fees `fees_at_pod_terminal` carries type, amount, and currency: ```json theme={null} { "fees_at_pod_terminal": [ { "type": "demurrage", "amount": 850.00, "currency_code": "USD" }, { "type": "exam", "amount": 450.00, "currency_code": "USD" } ] } ``` Fee types are `demurrage`, `extended_dwell_time`, `exam`, `total`, and `other`. Some terminals report a `total` line item alongside individual fees. Filter it out before summing or you will double-count. ### Last free day `pickup_lfd` is a coalesced value that follows a fixed source priority: shipping line, then terminal, then rail. It does not pick the earliest date. The individual sources are available separately on `import_deadlines`: * `pickup_lfd_line`: the shipping line's LFD (per diem deadline) * `pickup_lfd_terminal`: the terminal's LFD (demurrage deadline) * `pickup_lfd_rail`: the rail carrier's LFD at the inland destination Each has its own webhook event, so you can alert on whichever source your operation cares about. ### Release readiness Two fields answer "can I pick this up?" `available_for_pickup` and the holds array: ```javascript theme={null} function isReadyForPickup(container) { const { available_for_pickup, holds_at_pod_terminal } = container.attributes; const hasActiveHolds = holds_at_pod_terminal.some(h => h.status === 'hold'); return available_for_pickup === true && !hasActiveHolds; } ``` Full detail in [Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). Holds, fees, LFD, and availability come back on the container object wherever the terminal is a supported source. They are not a paid add-on and they do not require a sales conversation. See [Entitlements](/docs/api-docs/useful-info/entitlements) for the features that do require account enablement. Routing Data (container map and vessel positions), rail LFD, container refresh, and the embeddable map and widget are the gated ones. ## Replacing the SeaRates widget If you embedded the SeaRates tracking widget on your own website, read this before you swap in ours. They behave differently and the difference matters. **The SeaRates widget is an open lookup.** Any visitor types any container number and gets a result, whether or not that shipment has anything to do with you. **The Terminal49 widget is a customer portal.** It resolves only shipments and containers already tracked in your Terminal49 account. A visitor entering a container you are not tracking gets nothing back. For most freight forwarders this is the behaviour you actually want. Your customers see their shipments, and you are not running a free public lookup service on your own domain. But if you were relying on open lookup, this is a real change and you should plan for it. The embed is two lines: ```html theme={null}
``` Your customers can search by master bill of lading, container number, or any reference number you have tagged a shipment with. We suggest a dedicated page at `yourcompany.com/track`. The widget is an add-on, priced against container volume, and the publishable key comes from [support@terminal49.com](mailto:support@terminal49.com) rather than the developer portal. Email us and we will get you set up. If you want a map rather than a lookup form, see the [Map Embed Guide](/docs/api-docs/in-depth-guides/terminal49-map). Same publishable key, renders live vessel positions and routes. ## Gotchas that will bite you SeaRates returns local time as `YYYY-MM-DD HH:MM:SS`. Terminal49 stores event timestamps in UTC and returns the matching IANA timezone alongside. Convert for display rather than assuming local time. See [Event Timestamps](/docs/api-docs/in-depth-guides/event-timestamps). `POST /tracking_requests` returns immediately with a pending status. The shipment appears once the carrier responds. Subscribe to `tracking_request.succeeded` and `tracking_request.failed` rather than expecting shipment data in the creation response. A request may also land in `awaiting_manifest` if the carrier has not manifested the shipment yet, and we retry automatically. See [Tracking Request Lifecycle](/docs/api-docs/in-depth-guides/tracking-request-lifecycle). `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` mean no active holds or fees. This is the common case. Do not treat it as missing data. It means the terminal reported the fee type but has not posted an amount yet. Common for demurrage in the first day or two after discharge. Relationships are ID references into an `included` array. Use a JSON:API client library, or use `include` to sideload exactly what you need. Parsing raw JSON works but you will write more code than you expect. Terminal changes (fees, holds, LFD, appointment, availability) arrive on `container.updated` with a `changeset` showing old value first, new value second. Use it instead of diffing state yourself. ## Error handling SeaRates returns HTTP 200 for both success and failure, with the error in the response envelope. Terminal49 uses standard HTTP status codes. Replace envelope checks and message-string matching with status-code checks. | Status | Meaning | | ------ | ------------------------------------------------------------------------------- | | 400 | Malformed request or failed validation | | 401 | Missing or invalid API key | | 403 | Key lacks permission, or the feature is not enabled on your plan | | 404 | Resource does not exist | | 422 | Valid syntax, rejected content. For example, a malformed container number | | 429 | Rate limited. See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) | | 5xx | Terminal49 or an upstream carrier or terminal is unavailable | Rough equivalence for the SeaRates errors you are handling today: | SeaRates error | Terminal49 | | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | `API_KEY_WRONG`, `API_KEY_EXPIRED` | HTTP 401 | | `API_KEY_ACCESS_DENIED` | HTTP 403 | | `API_KEY_LIMIT_REACHED`, `API_KEY_RATE_LIMIT` | HTTP 429 | | `WRONG_PARAMETERS`, `WRONG_NUMBER`, `WRONG_TYPE` | HTTP 400 or 422 | | `WRONG_SEALINE`, `SEALINE_NOT_SUPPORTED` | HTTP 422 | | `SEALINE_HASNT_PROVIDE_INFO`, `NO_CONTAINERS`, `NO_EVENTS` | `tracking_request.failed`, or `tracking_request.awaiting_manifest` if not yet manifested | | `AUTO_CANT_DETECT_SEALINE` | Infer endpoint returns no prediction | | `SEALINE_TEMPORARY_DISABLED`, `SEALINE_NO_RESPONSE` | Not surfaced. We retry internally. | The [TypeScript SDK](/docs/sdk/introduction) maps these to typed errors (`AuthenticationError`, `ValidationError`, `RateLimitError`, `UpstreamError`, `FeatureNotEnabledError`, `AuthorizationError`, `NotFoundError`) and retries rate-limit and server errors automatically with exponential backoff. ## Where we are narrower than SeaRates Worth knowing before you commit. **Carrier count.** Terminal49 integrates directly with 36 ocean carriers, plus 2 more enabled on request, as of 14 August 2026. SeaRates lists substantially more. Ours are direct integrations covering the lines that move volume into North America, and each is normalized into one schema. Check your carrier mix against the [ocean carrier list](/docs/coverage/ocean-carriers) before cutover, and read the known issues section there. We publish the per-carrier field gaps. **Terminal data is North America.** Holds, fees, LFD, and availability come from direct terminal integrations concentrated in the US and Canada, with European ports expanding. Ocean milestones work globally; terminal-level operational data does not yet. **No air, parcel, or road tracking.** If your integration covers those modes, this migration handles only the ocean portion. **No freight rates or sailing schedules.** We do not offer a rate calculator, rate index, or schedule search. **Some fields are source-dependent.** Seal number, container weight, and departure or arrival events vary by carrier. The [field availability reference](/docs/coverage/fields) says which fields are always present and which depend on the carrier, terminal, or journey. ## Migration checklist Everyone does the base path. Then pick a branch. ### Base path Self-serve at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). Copy it immediately, it is shown once. Move the key from a query parameter to `Authorization: Token`. Note the `Token` prefix. Compare your SeaRates `sealine` values against the [carrier list](/docs/coverage/ocean-carriers). Flag anything missing before you cut over. One `POST /tracking_requests` per BOL, booking, or container, replacing the per-request lookup. Tracking requests start pending. Handle `succeeded`, `failed`, and `awaiting_manifest` rather than expecting data on creation. JSON:API structure, split equipment fields, UTC timestamps with a separate timezone. Replace envelope checks with HTTP status codes. Submit tracking requests for everything currently in transit. Send us the list if it is large and we will load it. Holds, fees, and LFD are the reason to do this properly rather than porting like for like. ### Then pick one Accept our POST payloads at a public URL. Subscribe only to events you act on. Reject any payload whose signature does not match. Only needed if your firewall restricts inbound traffic. Confirm end-to-end before going live. Remove your dedupe layer along with it. See [webhook best practices](/docs/api-docs/webhooks/best-practices) for retries and idempotency. The creation response is pending and carries no shipment yet. Keep the tracking request ID, then fetch the tracking request again (or handle `tracking_request.succeeded`) to get the shipment ID once the carrier responds. Point it at `GET /v2/shipments` or `GET /v2/containers`. No change to how often you poll. Skipped: endpoint setup, signature verification, IP whitelisting, delivery testing. Terminal data changes on a cadence polling tends to miss. If you only adopt webhooks for one thing, make it `container.updated` and `container.pickup_lfd.changed`. ## Migrate with an AI coding agent If you use Cursor, Claude Code, Windsurf, Copilot, or another AI coding assistant, hand it the prompt below. It is written to run a **side-by-side migration**: the agent stands up a Terminal49 client next to your existing SeaRates code, shadows every SeaRates call with a Terminal49 call, diffs the responses, and only cuts over once parity is proven. Point your agent at this page as context (paste the URL or add it as a doc source). The prompt references the mappings above, so the more of this page the agent can see, the better it does. 1. Open your repo in your AI coding tool. 2. Add this page as a documentation source, or paste its URL into the chat. 3. Copy the prompt below into a new chat and send it. 4. Answer the agent's discovery questions (SeaRates client location, env var names, carrier mix). 5. Review each PR the agent opens. It should ship in small, reviewable steps: client, shadow, parity harness, cutover, cleanup. * A `Terminal49Client` alongside your existing `SeaRatesClient`, sharing the same interface where possible. * A shadow-mode wrapper that calls both providers and logs response diffs without changing behavior. * A parity report per shipment: matched fields, diverged fields, and Terminal49-only fields (holds, fees, LFD). * A feature-flagged cutover: route reads to Terminal49, keep SeaRates as fallback until you flip the flag off. * A webhook receiver with HMAC verification, or a polling scheduler, depending on which path you pick. * A cleanup PR that removes SeaRates code, env vars, dependencies, and dedupe logic. ### The prompt Copy this into your agent. Replace the bracketed placeholders in the **Repo context** block before sending. ```markdown Terminal49 migration agent prompt expandable icon=robot wrap theme={null} You are migrating this codebase from the SeaRates tracking API to the Terminal49 API, side by side. Terminal49's authoritative migration guide is at https://terminal49.com/docs/migrate/searates. Use it as the source of truth for field mappings, event names, error codes, and behavior differences. # Repo context (fill this in before running) - SeaRates client lives at: [path/to/searates/client.ts] - SeaRates is called from: [list the call sites or "find them"] - Language and framework: [e.g. TypeScript + Node, Python + FastAPI, Ruby on Rails] - Storage for tracking state: [e.g. Postgres table `shipments`] - Current polling schedule: [e.g. cron every 2h via BullMQ] - Carrier mix (SCACs we track most): [e.g. MAEU, MSCU, CMDU, HLCU, ONEY] - Deployment target: [e.g. AWS ECS, Vercel, Fly.io] - Secret store: [e.g. AWS Secrets Manager, `.env`, Doppler] # Rules 1. Do not delete SeaRates code until the cleanup step. Migration is side by side. 2. Ship in small PRs. Each PR must build, pass tests, and be independently revertable. 3. Never invent Terminal49 fields, endpoints, or event names. If the guide does not confirm a mapping, ask me. Do not guess. 4. Terminal49 uses `Authorization: Token ` (not `Bearer`) and content type `application/vnd.api+json`. Get this right on the first request. 5. Terminal49 is asynchronous. `POST /tracking_requests` returns pending. Data arrives via `tracking_request.succeeded`, `tracking_request.failed`, or `tracking_request.awaiting_manifest` webhooks, or by polling `GET /v2/shipments`. Do not expect shipment data on creation. 6. Timestamps are UTC with a separate IANA `time_zone` field. Do not assume local time. 7. `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` are the normal state, not missing data. A fee `amount` of 0 is valid. 8. On `container.updated`, prefer the `changeset` over diffing state yourself. # Plan (execute in order, one PR per step) ## PR 1 — Discovery and interface - Grep the repo for every SeaRates call site. List them in the PR description. - Extract the SeaRates client's public surface into an interface (`TrackingProvider` with methods like `track(number, type, sealine)`, `getShipment(id)`, `refresh(id)`). - Make the existing SeaRates client implement it. No behavior change. ## PR 2 — Terminal49 client - Add a `Terminal49Client` implementing the same `TrackingProvider` interface. - Auth via `T49_API_KEY` env var, header `Authorization: Token ${key}`. - Base URL `https://api.terminal49.com/v2`, content type `application/vnd.api+json`. - Implement: - `createTrackingRequest({ request_number, request_type, scac })` returning the tracking request ID. - `getShipment(id, { include: 'containers,port_of_lading,port_of_discharge,...' })`. - `getContainer(id)`. - `refreshContainer(id)` mapping to `PATCH /v2/containers/{id}/refresh`. - Normalize responses to the same shape SeaRates callers expect today, using the field mapping from the guide. Split `iso_code` into `equipment_type`, `equipment_length`, `equipment_height`. Convert timestamps to UTC + `time_zone`. - Map errors to typed classes: `AuthenticationError` (401), `AuthorizationError` (403), `ValidationError` (400/422), `RateLimitError` (429), `UpstreamError` (5xx), `FeatureNotEnabledError` (403 + feature flag response). - Add unit tests using recorded fixtures. Do not hit the live API in tests. ## PR 3 — Shadow mode - Add a `ShadowProvider` that wraps both clients. On every read: - Call SeaRates as the primary. Return its response. - Fire-and-forget a Terminal49 call for the same identifier. - Log a structured diff: matched fields, diverged fields, T49-only fields. - Gate with env var `TRACKING_SHADOW_MODE=true`. Off by default. - Add a parity report script that aggregates shadow logs by SCAC and field. - Do NOT change what callers see. This step is observation only. ## PR 4 — Backfill script - Write a one-shot script that reads all active shipments from our database and calls `POST /tracking_requests` for each (one per BOL, booking, or container). - Store the returned `tracking_request.id` and eventual `shipment.id` on our records. - Rate-limit to respect Terminal49's limits (handle 429 with exponential backoff). - Idempotent: safe to re-run. Skip rows already backfilled. ## PR 5 — Webhook receiver (only if we picked the webhook path) - Add `POST /webhooks/terminal49` endpoint. - Verify HMAC signature using the shared secret from `T49_WEBHOOK_SECRET`. Reject on mismatch with 401. - Handle these events at minimum: - `tracking_request.succeeded`, `tracking_request.failed`, `tracking_request.awaiting_manifest` - `container.transport.vessel_discharged`, `container.transport.available`, `container.transport.full_out` - `container.updated` (use the `changeset`, do not diff) - `container.pickup_lfd.changed` - Persist events idempotently keyed by event ID. - Register the webhook via `POST /v2/webhooks` from a bootstrap script, subscribing only to events we handle. - If our firewall restricts inbound traffic, whitelist Terminal49 IPs from `GET /v2/webhooks/ips`. ## PR 6 — Cutover behind a flag - Add feature flag `TRACKING_PROVIDER` with values `searates` (default) and `terminal49`. - Route all reads through the flag. SeaRates stays available as a fallback for one release cycle. - Flip staging to `terminal49`, verify parity report is clean, then flip production. ## PR 7 — Cleanup - Delete `SeaRatesClient`, its tests, its env vars, its dedupe cache, and any ISO-code parsing helpers Terminal49 makes redundant. - Remove the shadow provider and the feature flag. - Update README and any runbooks. Note the new webhook endpoint if applicable. # Definition of done - All SeaRates call sites now go through Terminal49. - Webhook (or polling) is live in production. - Parity report shows no unexplained divergences for our top 10 SCACs. - Holds, fees, and LFD are exposed to whichever downstream system needs them (dashboard, alerts, customer emails). Do not migrate without wiring these up. They are the reason to do this properly. - SeaRates dependency, env vars, and dead code are gone from the repo. # Ask me before you - Choose between the webhook path and the polling path. Default to webhooks unless our infra makes an inbound HTTPS endpoint hard. - Change the shape of any function SeaRates callers use today. Prefer a normalization layer inside the Terminal49 client. - Add a new dependency. Prefer stdlib and what is already in the repo. - Touch anything outside the tracking integration. Start with PR 1. Post the list of SeaRates call sites and the proposed `TrackingProvider` interface, and wait for my review before writing PR 2. ``` The prompt is deliberately opinionated on side-by-side migration and small PRs. If your team prefers a big-bang cutover or a different branching model, edit the **Plan** section before sending it to your agent. ## Getting help Send us your list of active container and bill of lading numbers and we will load them rather than making you script the backfill. If something in this mapping is wrong or incomplete, tell us. We would rather fix the page than have you work around it. Every endpoint, with request and response schemas Typed client with retries and pagination built in Carriers, terminals, rail, and field availability Simulate success and failure outcomes # Migrating from ShipsGo Source: https://terminal49.com/docs/migrate/shipsgo Map ShipsGo container tracking API fields, webhooks, and errors to Terminal49 equivalents. Covers carrier coverage, widget replacement, checklist. If you have the ShipsGo container tracking API in production, this page maps it onto Terminal49 field by field, so you can cut over without reverse-engineering our schema. There is no compatibility shim. You will change your request code and your response parsing. For most integrations that is an afternoon. ## Start in sixty seconds Signing up and getting a key is self-serve. 1. [Create an account](https://app.terminal49.com) — the free plan tracks up to 10 active containers. 2. Generate a key at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). 3. Run this: ```bash theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "tracking_request", "attributes": { "request_number": "YOUR_BOL_NUMBER", "request_type": "bill_of_lading", "scac": "MAEU" } } }' ``` The full API key is shown once, right after you create it. Copy it before you navigate away — after that it is masked and cannot be revealed. If you miss it, create a new key and delete the old one. The free plan tracks up to 10 active containers. Creating tracking requests through the API works right away; reading tracking data back through the API requires a free 7-day API trial (same 10-container limit) — contact us via in-app chat or [support@terminal49.com](mailto:support@terminal49.com) and we enable it. If you want to test without a live shipment, use the [test tracking numbers](/docs/api-docs/useful-info/test-numbers), which simulate success and failure outcomes. **Migration offer:** sign up now and the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id) — vessel schedules, AIS positions, and projected routes — is free for your first month. ## The architectural shift ShipsGo gives you endpoints to create, list, and retrieve shipments. You call them on demand, get current state back, and own the schedule, the cache, and the deduplication. Terminal49 splits this in two. You register a tracking request once. We keep it updated and push changes to your webhook. Cron every few hours -> GET or POST to your shipment endpoints -> diff against your cache -> dedupe events -> write to your database. Every call spends credits. Freshness is capped by your polling interval. `POST /tracking_requests` once -> Terminal49 polls carriers, terminals, and rail -> we POST to your endpoint as things change -> write to your database. No cache layer, no dedupe logic. You can keep polling if you prefer — point your existing scheduler at `GET /v2/shipments` or `GET /v2/containers`. But webhooks are the reason the API is shaped this way, and terminal data (holds, fees, last free day) changes on a cadence that polling tends to miss. ## Quick comparison | | ShipsGo | Terminal49 | | --------------------------- | ------------------------------------ | ----------------------------------------------------------------- | | Tracking model | Poll on demand | Register once, then push or poll | | Authentication | `X-Shipsgo-User-Token` header | `Authorization: Token` header | | Base URL | `https://api.shipsgo.com/v2` | `https://api.terminal49.com/v2` | | Content type | `application/json` | `application/vnd.api+json` | | Response format | Custom JSON | JSON:API | | Webhooks | Supported for shipment updates | 30+ events, HMAC-signed | | Carrier identification | `carrier` (SCAC) | `scac`, or `auto_detect_vocc_scac` | | Credit per tracking request | Yes, one credit per creation | No per-track credits | | Rate limit | 100 requests per minute | See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) | | Terminal holds and fees | No direct equivalent | Included on the container object | | Last free day | No direct equivalent | Included, with per-source breakdown | | Rail milestones | Limited | North American Class I and short-line | | Embeddable widget | Open lookup, any container; live map | Your tracked shipments only (add-on) | | Getting an API key | Self-serve in dashboard | Self-serve in developer portal | ## Authentication Switch from ShipsGo's token header to Terminal49's token header. ```bash ShipsGo theme={null} curl -X GET https://api.shipsgo.com/v2/ocean/shipments \ -H "X-Shipsgo-User-Token: YOUR_SHIPSGO_TOKEN" \ -H "Accept: application/json" ``` ```bash Terminal49 theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_T49_KEY" \ -d '{"data":{"type":"tracking_request","attributes":{ "request_number":"MRKU9465770", "request_type":"container", "scac":"MAEU"}}}' ``` Note the `Token` prefix. It is not `Bearer`. ## Request parameter mapping | ShipsGo parameter | Terminal49 equivalent | Notes | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `containerNumber` | `request_number` with `request_type: "container"` | | | `bookingNumber` | `request_number` with `request_type: "booking_number"` | | | `blNumber` | `request_number` with `request_type: "bill_of_lading"` | Master or house BOL | | `carrier` | `scac` | Same SCAC values for most carriers | | `carrier: "OTHERS"` | Set `auto_detect_vocc_scac: true`, or call [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) first | Auto-detection runs asynchronously; a failed inference fails the tracking request with `scac_auto_detect_failed` | | `reference` | Shipment reference tag | Set on the shipment or container after creation | | `followers`, `tags` | Shipment reference or your own metadata | Store in your system against the Terminal49 shipment ID | | `filters[status]` | `GET /v2/shipments` or `GET /v2/containers` query filters | | | `skip` / `take` | Pagination via JSON:API `page` params | Use the SDK for automatic pagination | | `mapPoint=true` | Vessel endpoints and container GeoJSON | Not inline on the tracking response | | `extended=true` | Always included in Terminal49 responses | No toggle needed | ShipsGo allows creating a shipment and then retrieving it by its generated ID. Terminal49 also returns an ID, but the canonical identifier for ocean tracking is the combination of `request_number` and `request_type`. You will track by BOL, booking, or container number directly rather than maintaining a separate shipment ID mapping. ## Response field mapping Terminal49 is JSON:API compliant, so relationships between shipments, containers, ports, and terminals are explicit rather than something you reassemble from ID references. Use the [`include` parameter](/docs/api-docs/in-depth-guides/including-resources) to sideload related resources in one call instead of chasing IDs. ### Shipment level | ShipsGo (v2) | Terminal49 | | ------------------------------ | ------------------------------------------------------------------------------------------------ | | `shipment.reference` | Your own metadata tag; store against the shipment | | `shipment.carrier.code` | `shipment.attributes.shipping_line_scac` | | `shipment.carrier.name` | `shipment.attributes.shipping_line_name` | | `shipment.status` | Derived from container status and milestones | | `shipment.pol` | `shipment.relationships.port_of_lading` | | `shipment.pod` | `shipment.relationships.port_of_discharge` | | `shipment.eta` | `shipment.attributes.pod_eta_at` | | `shipment.etd` | Derived from vessel departure events | | `shipment.voyage.vesselName` | `shipment.attributes.pod_vessel_name` | | `shipment.voyage.voyageNumber` | Available via the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo) | | `shipment.transitTime` | Calculate from departure and arrival timestamps | | `shipment.co2Emissions` | Not returned | | `shipment.createdAt` | `tracking_request.created_at` | | `shipment.updatedAt` | `container.updated` or `tracking_request.succeeded` | ### Container level | ShipsGo (v2) | Terminal49 | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `container.number` | `container.attributes.number` | | `container.type` | `container.attributes.equipment_type` | | `container.size` | `container.attributes.equipment_length` | | `container.height` | `container.attributes.equipment_height` | | `container.status` | `container.attributes.current_status` | | `container.events[]` | Container timestamps, [transport events](/docs/api-docs/api-reference/containers/get-a-containers-transport-events), and webhook events | ShipsGo returns a combined container type or ISO code. Terminal49 splits this into three normalized fields: type (dry, reefer, open top, flat rack, bulk, tank), length (10, 20, 40, 45), and height (standard, high cube). If you were parsing ISO codes yourself, you can delete that code. ### Locations, facilities, and vessels | ShipsGo (v2) | Terminal49 | | -------------------------------- | ------------------------------------------------------------------------------------------------ | | `port.name` | `port.attributes.name` | | `port.code` | `port.attributes.code` | | `port.country` | `port.attributes.country_code` | | `port.latitude` / `.longitude` | `port.attributes.latitude` / `.longitude` | | `port.timezone` | `port.attributes.time_zone` | | `terminal.name` | `terminal.attributes.name` | | `terminal.smdgCode` | `terminal.attributes.smdg_code` | | `vessel.name` | `shipment.attributes.pod_vessel_name` | | `vessel.imo` | `shipment.attributes.pod_vessel_imo` | | `vessel.mmsi` | Available via the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo) | | `vessel.latitude` / `.longitude` | Available via the Vessels API | ## Milestone and event mapping ShipsGo returns milestones in a flat `events[]` array with names like "Loaded", "Sailing", and "Discharged". Terminal49 exposes the same milestones as normalized transport events and pushes each one to your webhook. | ShipsGo event name | Milestone | Terminal49 event | | ----------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------- | | `Booked` | Booking confirmed | No direct equivalent — the nearest signal is `tracking_request.succeeded` when tracking begins | | `Loaded` | Loaded on vessel at origin | `container.transport.vessel_loaded` | | `Sailing` | Vessel departed origin | `container.transport.vessel_departed` | | `Arrived` | Vessel arrived at destination | `container.transport.vessel_arrived` | | `Discharged` | Discharged from vessel | `container.transport.vessel_discharged` | | `Gate out confirmation` | Gated out at destination | `container.transport.full_out` | | — | Gated in at origin | `container.transport.full_in` | | — | Empty picked up at origin | `container.transport.empty_out` | | — | Empty returned at destination | `container.transport.empty_in` | ShipsGo also emits a "Not Released" status, which maps to active holds present in `container.attributes.holds_at_pod_terminal`. Terminal49 also emits milestones ShipsGo has no equivalent for: * **Vessel berthed** — `container.transport.vessel_berthed` * **Available for pickup** — `container.transport.available` and `.not_available` * **Transshipment** — arrived, discharged, loaded, departed * **Feeder vessel and barge** — arrived, discharged, loaded, departed * **Rail** — loaded, departed, arrived, unloaded, plus `arrived_at_inland_destination` See the full [event catalog](/docs/api-docs/webhooks/event-catalog). ### Registering a webhook ```bash theme={null} curl -X POST https://api.terminal49.com/v2/webhooks \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "webhook", "attributes": { "url": "https://your-endpoint.example.com/t49", "active": true, "events": [ "container.transport.vessel_discharged", "container.transport.available", "container.pickup_lfd.changed" ] } } }' ``` Payloads are HMAC-signed. See [webhook setup](/docs/api-docs/in-depth-guides/webhooks) for signature verification, and [List webhook IPs](/docs/api-docs/api-reference/webhooks/list-webhook-ips) if your firewall restricts inbound traffic. ## What you gain This is the part worth reading even if the rest is mechanical. Terminal49 integrates with terminals directly, not only carriers, so the container object carries operational data that has no ShipsGo equivalent. ### Holds `holds_at_pod_terminal` is an array of active holds blocking pickup: ```json theme={null} { "holds_at_pod_terminal": [ { "name": "customs", "status": "hold", "description": "CBP HOLD" }, { "name": "freight", "status": "hold", "description": null } ] } ``` Hold names are `freight`, `customs`, `USDA`, `VACIS`, `TMF`, and `other`. Status is `hold` or `pending`. When a hold clears, the object is removed from the array — there is no released state. Hold names are case-sensitive. `USDA`, `VACIS`, and `TMF` are uppercase; `freight`, `customs`, and `other` are lowercase. Match exactly. ### Fees `fees_at_pod_terminal` carries type, amount, and currency: ```json theme={null} { "fees_at_pod_terminal": [ { "type": "demurrage", "amount": 850.00, "currency_code": "USD" }, { "type": "exam", "amount": 450.00, "currency_code": "USD" } ] } ``` Fee types are `demurrage`, `extended_dwell_time`, `exam`, `total`, and `other`. Some terminals report a `total` line item alongside individual fees. Filter it out before summing or you will double-count. ### Last free day `pickup_lfd` is a coalesced value that follows a fixed source priority: shipping line, then terminal, then rail. It does not pick the earliest date. The individual sources are available separately on `import_deadlines`: * `pickup_lfd_line` — the shipping line's LFD (per diem deadline) * `pickup_lfd_terminal` — the terminal's LFD (demurrage deadline) * `pickup_lfd_rail` — the rail carrier's LFD at the inland destination Each has its own webhook event, so you can alert on whichever source your operation cares about. ### Release readiness Two fields answer "can I pick this up?" — `available_for_pickup` and the holds array: ```javascript theme={null} function isReadyForPickup(container) { const { available_for_pickup, holds_at_pod_terminal } = container.attributes; const hasActiveHolds = holds_at_pod_terminal.some(h => h.status === 'hold'); return available_for_pickup === true && !hasActiveHolds; } ``` Full detail in [Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). Holds, fees, LFD, and availability come back on the container object wherever the terminal is a supported source. They are not a paid add-on and they do not require a sales conversation. See [Entitlements](/docs/api-docs/useful-info/entitlements) for the features that do require account enablement — Routing Data (container map and vessel positions), rail LFD, container refresh, and the embeddable map and widget are the gated ones. ## Replacing the ShipsGo widget If you embedded the ShipsGo tracking widget or live map on your own website, read this before you swap in ours. They behave differently and the difference matters. **The ShipsGo widget and map are open lookups.** Any visitor types any container number and gets a result, whether or not that shipment has anything to do with you. **The Terminal49 widget is a customer portal.** It resolves only shipments and containers already tracked in your Terminal49 account. A visitor entering a container you are not tracking gets nothing back. For most freight forwarders this is the behaviour you actually want — your customers see their shipments, and you are not running a free public lookup service on your own domain. But if you were relying on open lookup, this is a real change and you should plan for it. The embed is two lines: ```html theme={null}
``` Your customers can search by master bill of lading, container number, or any reference number you have tagged a shipment with. We suggest a dedicated page at `yourcompany.com/track`. The widget is an add-on, priced against container volume, and the publishable key comes from [support@terminal49.com](mailto:support@terminal49.com) rather than the developer portal. Email us and we will get you set up. If you want a map rather than a lookup form, see the [Map Embed Guide](/docs/api-docs/in-depth-guides/terminal49-map) — same publishable key, renders live vessel positions and routes. ## Gotchas that will bite you ShipsGo returns local time in `YYYY-MM-DD HH:MM:SS` format. Terminal49 stores event timestamps in UTC and returns the matching IANA timezone alongside. Convert for display rather than assuming local time. See [Event Timestamps](/docs/api-docs/in-depth-guides/event-timestamps). `POST /tracking_requests` returns immediately with a pending status. The shipment appears once the carrier responds. Subscribe to `tracking_request.succeeded` and `tracking_request.failed` rather than expecting shipment data in the creation response. A request may also land in `awaiting_manifest` if the carrier has not manifested the shipment yet — we retry automatically. See [Tracking Request Lifecycle](/docs/api-docs/in-depth-guides/tracking-request-lifecycle). `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` mean no active holds or fees. This is the common case. Do not treat it as missing data. It means the terminal reported the fee type but has not posted an amount yet. Common for demurrage in the first day or two after discharge. Relationships are ID references into an `included` array. Use a JSON:API client library, or use `include` to sideload exactly what you need. Parsing raw JSON works but you will write more code than you expect. Terminal changes (fees, holds, LFD, appointment, availability) arrive on `container.updated` with a `changeset` showing old value first, new value second. Use it instead of diffing state yourself. ## Error handling ShipsGo returns standard HTTP status codes alongside a JSON error body. Terminal49 also uses standard HTTP status codes. Replace any body-level error parsing with status-code checks. | Status | Meaning | | ------ | ------------------------------------------------------------------------------- | | 400 | Malformed request or failed validation | | 401 | Missing or invalid API key | | 403 | Key lacks permission, or the feature is not enabled on your plan | | 404 | Resource does not exist | | 422 | Valid syntax, rejected content — for example a malformed container number | | 429 | Rate limited. See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) | | 5xx | Terminal49 or an upstream carrier or terminal is unavailable | Rough equivalence for the ShipsGo errors you are handling today: | ShipsGo error | Terminal49 | | ------------------------------------ | ------------------------------------------------------------------------------------- | | HTTP 401 (Unauthorized) | HTTP 401 | | HTTP 402 (Payment Required) | Not applicable; no per-track credits | | HTTP 403 (Forbidden) | HTTP 403 | | HTTP 404 (Not Found) | HTTP 404 | | HTTP 409 (Conflict) | Duplicates handled by idempotency; see note below | | HTTP 422 (Unprocessable Content) | HTTP 422 | | HTTP 429 (Too Many Requests) | HTTP 429 | | Rate limit headers (`X-RateLimit-*`) | See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) for current headers | | Duplicate shipment (409) | Submitting the same tracking request returns the existing tracking request; no error | | Credit exhausted (402) | Not applicable; does not apply to Terminal49 | Terminal49 does not charge per-track credits. If you were managing credit budgets and duplicate checks in ShipsGo to avoid burning credits, you can remove that logic. The [TypeScript SDK](/docs/sdk/introduction) maps these to typed errors (`AuthenticationError`, `ValidationError`, `RateLimitError`, `UpstreamError`, `FeatureNotEnabledError`, `AuthorizationError`, `NotFoundError`) and retries rate-limit and server errors automatically with exponential backoff. ## Where we are narrower than ShipsGo Worth knowing before you commit. **Carrier count.** Terminal49 integrates directly with 36 ocean carriers, plus 2 more enabled on request — as of 14 August 2026. ShipsGo lists substantially more. Ours are direct integrations covering the lines that move volume into North America, and each is normalized into one schema. Check your carrier mix against the [ocean carrier list](/docs/coverage/ocean-carriers) before cutover, and read the known issues section there — we publish the per-carrier field gaps. **Terminal data is North America.** Holds, fees, LFD, and availability come from direct terminal integrations concentrated in the US and Canada, with European ports expanding. Ocean milestones work globally; terminal-level operational data does not yet. **No air, parcel, or road tracking.** If your ShipsGo integration covers air cargo, this migration handles only the ocean portion. **No freight rates or sailing schedules.** We do not offer a rate calculator, rate index, or schedule search. **Some fields are source-dependent.** Seal number, container weight, and departure or arrival events vary by carrier. The [field availability reference](/docs/coverage/fields) says which fields are always present and which depend on the carrier, terminal, or journey. ## Migration checklist Everyone does the base path. Then pick a branch. ### Base path Self-serve at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). Copy it immediately — it is shown once. Replace `X-Shipsgo-User-Token` with `Authorization: Token`. Note the `Token` prefix. Compare your ShipsGo carrier codes against the [carrier list](/docs/coverage/ocean-carriers). Flag anything missing before you cut over. One `POST /tracking_requests` per BOL, booking, or container, replacing the per-request lookup. Tracking requests start pending. Handle `succeeded`, `failed`, and `awaiting_manifest` rather than expecting data on creation. JSON:API structure, split equipment fields, UTC timestamps with a separate timezone. Replace credit-exhaustion and duplicate checks with standard HTTP status codes. Remove credit budget logic. Submit tracking requests for everything currently in transit. Send us the list if it is large and we will load it. Holds, fees, and LFD are the reason to do this properly rather than porting like for like. ### Then pick one 1. Expose an HTTPS endpoint that accepts our POST payloads. 2. Register a webhook and subscribe only to events you act on. 3. Verify HMAC signatures. 4. Whitelist our IPs if your firewall restricts inbound traffic. 5. Trigger a test delivery before going live. 6. Retire your polling job and your dedupe layer. See [webhook best practices](/docs/api-docs/webhooks/best-practices) for retries and idempotency. 1. Store the tracking request ID from the creation response. The shipment ID arrives later — the creation response is pending with no shipment attached; fetch the tracking request again (or handle `tracking_request.succeeded`) to get it once the carrier responds. 2. Repoint your existing scheduler at `GET /v2/shipments` or `GET /v2/containers`. 3. Keep your existing cadence. Skipped: endpoint setup, signature verification, IP whitelisting, delivery testing. Terminal data changes on a cadence polling tends to miss. If you only adopt webhooks for one thing, make it `container.updated` and `container.pickup_lfd.changed`. ## Migrate with an AI coding agent If you use Claude Code, Cursor, Codex, Windsurf, Copilot, or another AI coding assistant, hand it the prompt below. It is written to run a **side-by-side migration**: the agent stands up a Terminal49 client next to your existing ShipsGo code, shadows every ShipsGo call with a Terminal49 call, diffs the responses, and only cuts over once parity is proven. Point your agent at this page as context (paste the URL or add it as a doc source). The prompt references the mappings above, so the more of this page the agent can see, the better it does. 1. Open your repo in your AI coding tool. 2. Add this page as a documentation source, or paste its URL into the chat. 3. Copy the prompt below into a new chat and send it. 4. Answer the agent's discovery questions (ShipsGo client location, env var names, carrier mix). 5. Review each PR the agent opens. It should ship in small, reviewable steps: client, shadow, parity harness, cutover, cleanup. * A `Terminal49Client` alongside your existing ShipsGo client, sharing the same interface where possible. * A shadow-mode wrapper that calls both providers and logs response diffs without changing behavior. * A parity report per shipment: matched fields, diverged fields, and Terminal49-only fields (holds, fees, LFD). * A feature-flagged cutover: route reads to Terminal49, keep ShipsGo as fallback until you flip the flag off. * A webhook receiver with HMAC verification, or a polling scheduler, depending on which path you pick. * A cleanup PR that removes ShipsGo code, env vars, dependencies, and dedupe logic. ### The prompt Copy this into your agent. Replace the bracketed placeholders in the **Repo context** block before sending. ```markdown Terminal49 migration agent prompt expandable icon=robot wrap theme={null} You are migrating this codebase from the ShipsGo API to the Terminal49 API, side by side. Terminal49's authoritative migration guide is at https://terminal49.com/docs/migrate/shipsgo. Use it as the source of truth for field mappings, event names, error codes, and behavior differences. # Repo context (fill this in before running) - ShipsGo client lives at: [path/to/client] - ShipsGo is called from: [list the call sites or "find them"] - Language and framework: [e.g. TypeScript + Node, Python + FastAPI, Ruby on Rails] - Storage for tracking state: [e.g. Postgres table `shipments`] - Current polling schedule: [e.g. cron every 2h] - Carrier mix (SCACs we track most): [e.g. MAEU, MSCU, CMDU, HLCU, ONEY] - Deployment target: [e.g. AWS ECS, Vercel, Fly.io] - Secret store: [e.g. AWS Secrets Manager, `.env`, Doppler] # Rules 1. Do not delete ShipsGo code until the cleanup step. Migration is side by side. 2. Ship in small PRs. Each PR must build, pass tests, and be independently revertable. 3. Never invent Terminal49 fields, endpoints, or event names. If the guide does not confirm a mapping, ask me. Do not guess. 4. Terminal49 uses `Authorization: Token ` (not `Bearer`) and content type `application/vnd.api+json`. Get this right on the first request. 5. Terminal49 is asynchronous. `POST /tracking_requests` returns pending. Data arrives via `tracking_request.succeeded`, `tracking_request.failed`, or `tracking_request.awaiting_manifest` webhooks, or by polling `GET /v2/shipments`. Do not expect shipment data on creation. 6. Timestamps are UTC with a separate IANA `time_zone` field. Do not assume local time. 7. `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` are the normal state, not missing data. A fee `amount` of 0 is valid. 8. On `container.updated`, prefer the `changeset` over diffing state yourself. 9. Terminal49 has no credit system. Remove ShipsGo credit-balance checks and credit-exhaustion error handling in the cleanup step — do not port them. # Plan (execute in order, one PR per step) ## PR 1 — Discovery and interface - Grep the repo for every ShipsGo call site. List them in the PR description. - Extract the ShipsGo client's public surface into an interface (`TrackingProvider` with methods like `track(number, type, carrier)`, `getShipment(id)`, `refresh(id)`). - Make the existing ShipsGo client implement it. No behavior change. ## PR 2 — Terminal49 client - Add a `Terminal49Client` implementing the same `TrackingProvider` interface. - Auth via `T49_API_KEY` env var, header `Authorization: Token ${key}`. - Base URL `https://api.terminal49.com/v2`, content type `application/vnd.api+json`. - Implement: - `createTrackingRequest({ request_number, request_type, scac })` returning the tracking request ID. - `getShipment(id, { include: 'containers,port_of_lading,port_of_discharge' })`. - `getContainer(id)`. - `refreshContainer(id)` mapping to `PATCH /v2/containers/{id}/refresh` (a paid feature — skip it unless our account has it enabled). - Normalize responses to the same shape ShipsGo callers expect today, using the field mapping from the guide. Split equipment codes into `equipment_type`, `equipment_length`, `equipment_height`. Convert timestamps to UTC + `time_zone`. - Map errors to typed classes: `AuthenticationError` (401), `AuthorizationError` (403), `ValidationError` (400/422), `RateLimitError` (429), `UpstreamError` (5xx), `FeatureNotEnabledError` (403 + feature flag response). - Add unit tests using recorded fixtures. Do not hit the live API in tests. ## PR 3 — Shadow mode - Add a `ShadowProvider` that wraps both clients. On every read: - Call ShipsGo as the primary. Return its response. - Fire-and-forget a Terminal49 call for the same identifier. - Log a structured diff: matched fields, diverged fields, T49-only fields. - Gate with env var `TRACKING_SHADOW_MODE=true`. Off by default. - Add a parity report script that aggregates shadow logs by SCAC and field. - Do NOT change what callers see. This step is observation only. ## PR 4 — Backfill script - Write a one-shot script that reads all active shipments from our database and calls `POST /tracking_requests` for each (one per BOL, booking, or container). - Store the returned `tracking_request.id` and eventual `shipment.id` on our records. - Rate-limit to respect Terminal49's limits (handle 429 with exponential backoff). - Idempotent: safe to re-run. Skip rows already backfilled. ## PR 5 — Webhook receiver (only if we picked the webhook path) - Add `POST /webhooks/terminal49` endpoint. - Verify HMAC signature using the shared secret from `T49_WEBHOOK_SECRET`. Reject on mismatch with 401. - Handle these events at minimum: - `tracking_request.succeeded`, `tracking_request.failed`, `tracking_request.awaiting_manifest` - `container.transport.vessel_discharged`, `container.transport.available`, `container.transport.full_out` - `container.updated` (use the `changeset`, do not diff) - `container.pickup_lfd.changed` - Persist events idempotently keyed by event ID. - Register the webhook via `POST /v2/webhooks` from a bootstrap script, subscribing only to events we handle. - If our firewall restricts inbound traffic, whitelist Terminal49 IPs from `GET /v2/webhooks/ips`. ## PR 6 — Cutover behind a flag - Add feature flag `TRACKING_PROVIDER` with values `shipsgo` (default) and `terminal49`. - Route all reads through the flag. ShipsGo stays available as a fallback for one release cycle. - Flip staging to `terminal49`, verify parity report is clean, then flip production. ## PR 7 — Cleanup - Delete the ShipsGo client, its tests, its env vars, its dedupe cache, and any equipment-code parsing helpers Terminal49 makes redundant. - Remove the shadow provider and the feature flag. - Update README and any runbooks. Note the new webhook endpoint if applicable. # Definition of done - All ShipsGo call sites now go through Terminal49. - Webhook (or polling) is live in production. - Parity report shows no unexplained divergences for our top 10 SCACs. - Holds, fees, and LFD are exposed to whichever downstream system needs them (dashboard, alerts, customer emails). Do not migrate without wiring these up. They are the reason to do this properly. - ShipsGo dependency, env vars, and dead code are gone from the repo. # Ask me before you - Choose between the webhook path and the polling path. Default to webhooks unless our infra makes an inbound HTTPS endpoint hard. - Change the shape of any function ShipsGo callers use today. Prefer a normalization layer inside the Terminal49 client. - Add a new dependency. Prefer stdlib and what is already in the repo. - Touch anything outside the tracking integration. ``` ## Getting help Send us your list of active container and bill of lading numbers and we will load them rather than making you script the backfill. If something in this mapping is wrong or incomplete, tell us. We would rather fix the page than have you work around it. Every endpoint, with request and response schemas Typed client with retries and pagination built in Carriers, terminals, rail, and field availability Simulate success and failure outcomes # Migrating from Vizion Source: https://terminal49.com/docs/migrate/vizion Map Vizion container tracking reference IDs, webhooks, and API fields to Terminal49 equivalents. Includes authentication, mapping, and checklist. If you have the Vizion tracking API in production, this page maps it onto Terminal49 field by field, so you can cut over without reverse-engineering our schema. There is no compatibility shim. You will change your request code and your response parsing. Because Vizion and Terminal49 are architecturally similar, this is mostly a rename and reshape migration. ## Start in sixty seconds Signing up and getting a key is self-serve. Sign up at [app.terminal49.com](https://app.terminal49.com). The free plan tracks up to 10 active containers. Create a key at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). ```bash theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "tracking_request", "attributes": { "request_number": "YOUR_BOL_NUMBER", "request_type": "bill_of_lading", "scac": "MAEU" } } }' ``` The full API key is shown once, right after you create it. Copy it before you navigate away. After that it is masked and cannot be revealed. If you miss it, create a new key and delete the old one. The free plan tracks up to 10 active containers. Creating tracking requests through the API works right away; reading tracking data back through the API requires a free 7-day API trial (same 10-container limit) — contact us via in-app chat or [support@terminal49.com](mailto:support@terminal49.com) and we enable it. If you want to test without a live shipment, use the [test tracking numbers](/docs/api-docs/useful-info/test-numbers), which simulate success and failure outcomes. **Migration offer:** sign up now and the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id) — vessel schedules, AIS positions, and projected routes — is free for your first month. ## The architectural shift Vizion and Terminal49 already think the same way: create a reference once, receive updates via webhooks or poll the reference. The shift is what we attach to that reference. Create a reference via POST /references. Poll GET /references//updates or receive webhooks. Parse flat JSON responses and map Vizion event names to your internal model. POST /tracking\_requests once. Terminal49 polls carriers, terminals, and rail, then POSTs to your endpoint as things change. Container objects carry holds, fees, and last free day directly. The biggest difference is not the workflow. It is the data surface. Terminal49 integrates directly with terminals, not just carriers, so the container object carries operational data that has no Vizion equivalent. ## Quick comparison | | Vizion | Terminal49 | | ----------------------- | ------------------------------------------------- | ------------------------------------- | | Tracking model | Reference subscription, then push or poll | Register once, then push or poll | | Authentication | `X-API-Key` header | `Authorization: Token` header | | Base URL | `https://prod.vizionapi.com` | `https://api.terminal49.com/v2` | | Content type | `application/json` | `application/vnd.api+json` | | Response format | Custom JSON | JSON:API | | Webhooks | Subscription-based | 30+ events, HMAC-signed | | Carrier identification | Carrier reference or SCAC | `scac`, or `auto_detect_vocc_scac` | | Terminal holds and fees | No direct equivalent | Included on the container object | | Last free day | No direct equivalent | Included, with per-source breakdown | | Rail milestones | Limited | North American Class I and short-line | | Getting an API key | Typically requires sales contact for higher tiers | Self-serve | ## Authentication Move from Vizion's `X-API-Key` header to Terminal49's `Authorization: Token` header. Note the content type change. ```bash Vizion theme={null} curl -X POST https://prod.vizionapi.com/references \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_VIZION_KEY" \ -d '{"container_id": "MRKU9465770", "carrier_code": "MAEU"}' ``` ```bash Terminal49 theme={null} curl -X POST https://api.terminal49.com/v2/tracking_requests \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_T49_KEY" \ -d '{"data":{"type":"tracking_request","attributes":{ "request_number":"MRKU9465770", "request_type":"container", "scac":"MAEU"}}}' ``` Note the `Token` prefix. It is not `Bearer`. Vizion reads the key from the `X-API-Key` header. Terminal49 reads it from `Authorization: Token YOUR_T49_KEY` — a different header name and a prefixed value, so change both. ## Request parameter mapping | Vizion parameter | Terminal49 equivalent | Notes | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `container_id` or `bill_of_lading` | `request_number` | One identifier per tracking request | | `carrier_code` | `scac` | Same SCAC values for most carriers | | `carrier_code` omitted | Set `auto_detect_vocc_scac: true`, or call [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) first | Auto-detection runs asynchronously; a failed inference fails the tracking request with `scac_auto_detect_failed` | | `callback_url` | `POST /v2/webhooks` | Register once, subscribe to specific events | | Re-poll / refresh | `PATCH /v2/containers/{id}/refresh` | Forces an immediate pull from all sources. Paid feature — requires account enablement, limited to 10 requests per minute | | Your internal reference tags | `shipment.attributes.reference_numbers` | Tag shipments with your internal IDs | Vizion accepts a carrier reference or name alongside the number. Terminal49 takes a SCAC (`request_type` + `scac`) or omits the SCAC to use Infer. Track by BOL and we return every container on that bill of lading as related container resources. Container-number tracking requests are currently in beta. ## Response field mapping Terminal49 is JSON:API compliant, so relationships between shipments, containers, ports, and terminals are explicit rather than something you reassemble from ID references. Use the [`include` parameter](/docs/api-docs/in-depth-guides/including-resources) to sideload related resources in one call instead of chasing IDs. ### Shipment level | Vizion | Terminal49 | | --------------------- | -------------------------------------------- | | Bill of lading number | `shipment.attributes.bill_of_lading_number` | | Carrier SCAC | `shipment.attributes.shipping_line_scac` | | Carrier name | `shipment.attributes.shipping_line_name` | | Shipment status | Derived from container status and milestones | | Port of loading | `shipment.relationships.port_of_lading` | | Port of discharge | `shipment.relationships.port_of_discharge` | | Inland destination | `shipment.relationships.destination` | | ETA at discharge port | `shipment.attributes.pod_eta_at` | | Vessel name | `shipment.attributes.pod_vessel_name` | | Vessel IMO | `shipment.attributes.pod_vessel_imo` | ### Container level | Vizion | Terminal49 | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Container number | `container.attributes.number` | | Equipment / ISO code | `container.attributes.equipment_type` + `equipment_length` + `equipment_height` | | Container size / type | Same three fields above | | Container status | `container.attributes.current_status` | | Events array | Container timestamps, [transport events](/docs/api-docs/api-reference/containers/get-a-containers-transport-events), and webhook events | Vizion returns a single ISO code string like `45G1`. Terminal49 splits this into three normalized fields: type (dry, reefer, open top, flat rack, bulk, tank), length (10, 20, 40, 45), and height (standard, high cube). If you were parsing ISO codes yourself, you can delete that code. ### Locations, facilities, and vessels | Vizion | Terminal49 | | -------------------------- | ------------------------------------------------------------------------------------------------ | | Port name | `port.attributes.name` | | Port UN/LOCODE | `port.attributes.code` | | Port coordinates | `port.attributes.latitude` / `.longitude` | | Port timezone | `port.attributes.time_zone` | | Port country | `port.attributes.country_code` | | Terminal / facility name | `terminal.attributes.name` | | Terminal SMDG code | `terminal.attributes.smdg_code` | | Terminal BIC facility code | `terminal.attributes.bic_facility_code` | | Vessel name | `shipment.attributes.pod_vessel_name` | | Vessel IMO | `shipment.attributes.pod_vessel_imo` | | Vessel MMSI | Available via the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo) | | Vessel call sign / flag | Not returned | ## Milestone and event mapping Vizion returns event arrays via updates or webhooks. Terminal49 exposes the same milestones as normalized transport events and pushes each one to your webhook. | Vizion event | Milestone | Terminal49 event | | ---------------------- | ----------------------------- | --------------------------------------- | | Loaded on vessel | Loaded on vessel at origin | `container.transport.vessel_loaded` | | Vessel departed | Vessel departed origin | `container.transport.vessel_departed` | | Vessel arrived | Vessel arrived at destination | `container.transport.vessel_arrived` | | Discharged from vessel | Discharged from vessel | `container.transport.vessel_discharged` | | Gated out (full out) | Gated out at destination | `container.transport.full_out` | | Gated in (full in) | Gated in at origin | `container.transport.full_in` | | Empty picked up | Empty picked up at origin | `container.transport.empty_out` | | Empty returned | Empty returned at destination | `container.transport.empty_in` | Terminal49 also emits milestones Vizion has no equivalent for: * **Vessel berthed:** `container.transport.vessel_berthed` * **Available for pickup:** `container.transport.available` and `.not_available` * **Transshipment:** arrived, discharged, loaded, departed * **Feeder vessel and barge:** arrived, discharged, loaded, departed * **Rail:** loaded, departed, arrived, unloaded, plus `arrived_at_inland_destination` See the full [event catalog](/docs/api-docs/webhooks/event-catalog). ### Registering a webhook ```bash theme={null} curl -X POST https://api.terminal49.com/v2/webhooks \ -H "Content-Type: application/vnd.api+json" \ -H "Authorization: Token YOUR_API_KEY" \ -d '{ "data": { "type": "webhook", "attributes": { "url": "https://your-endpoint.example.com/t49", "active": true, "events": [ "container.transport.vessel_discharged", "container.transport.available", "container.pickup_lfd.changed" ] } } }' ``` Payloads are HMAC-signed. See [webhook setup](/docs/api-docs/in-depth-guides/webhooks) for signature verification, and [List webhook IPs](/docs/api-docs/api-reference/webhooks/list-webhook-ips) if your firewall restricts inbound traffic. ## What you gain This is the part worth reading even if the rest is mechanical. Terminal49 integrates with terminals directly, not only carriers, so the container object carries operational data that has no Vizion equivalent. ### Holds `holds_at_pod_terminal` is an array of active holds blocking pickup: ```json theme={null} { "holds_at_pod_terminal": [ { "name": "customs", "status": "hold", "description": "CBP HOLD" }, { "name": "freight", "status": "hold", "description": null } ] } ``` Hold names are `freight`, `customs`, `USDA`, `VACIS`, `TMF`, and `other`. Status is `hold` or `pending`. When a hold clears, the object is removed from the array. There is no released state. Hold names are case-sensitive. `USDA`, `VACIS`, and `TMF` are uppercase; `freight`, `customs`, and `other` are lowercase. Match exactly. ### Fees `fees_at_pod_terminal` carries type, amount, and currency: ```json theme={null} { "fees_at_pod_terminal": [ { "type": "demurrage", "amount": 850.00, "currency_code": "USD" }, { "type": "exam", "amount": 450.00, "currency_code": "USD" } ] } ``` Fee types are `demurrage`, `extended_dwell_time`, `exam`, `total`, and `other`. Some terminals report a `total` line item alongside individual fees. Filter it out before summing or you will double-count. ### Last free day `pickup_lfd` is a coalesced value that follows a fixed source priority: shipping line, then terminal, then rail. It does not pick the earliest date. The individual sources are available separately on `import_deadlines`: * `pickup_lfd_line`: the shipping line's LFD (per diem deadline) * `pickup_lfd_terminal`: the terminal's LFD (demurrage deadline) * `pickup_lfd_rail`: the rail carrier's LFD at the inland destination Each has its own webhook event, so you can alert on whichever source your operation cares about. ### Release readiness Two fields answer "can I pick this up?" `available_for_pickup` and the holds array: ```javascript theme={null} function isReadyForPickup(container) { const { available_for_pickup, holds_at_pod_terminal } = container.attributes; const hasActiveHolds = holds_at_pod_terminal.some(h => h.status === 'hold'); return available_for_pickup === true && !hasActiveHolds; } ``` Full detail in [Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees). Holds, fees, LFD, and availability come back on the container object wherever the terminal is a supported source. They are not a paid add-on and they do not require a sales conversation. See [Entitlements](/docs/api-docs/useful-info/entitlements) for the features that do require account enablement. Routing Data (container map and vessel positions), rail LFD, container refresh, and the embeddable map and widget are the gated ones. ## Gotchas that will bite you Vizion calls them references. Terminal49 calls them tracking requests. The lifecycle is the same: create, poll or webhook, delete. Just rename your internal variable and store the `tracking_request.id` where you stored `reference.id`. Relationships are ID references into an `included` array. Use a JSON:API client library, or use `include` to sideload exactly what you need. Parsing raw JSON works but you will write more code than you expect. Vizion may return local or UTC depending on the field. Terminal49 stores event timestamps in UTC and returns the matching IANA timezone alongside. Convert for display rather than assuming local time. See [Event Timestamps](/docs/api-docs/in-depth-guides/event-timestamps). `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` mean no active holds or fees. This is the common case. Do not treat it as missing data. Terminal changes (fees, holds, LFD, appointment, availability) arrive on `container.updated` with a `changeset` showing old value first, new value second. Use it instead of diffing state yourself. `POST /tracking_requests` returns immediately with a pending status. The shipment appears once the carrier responds. Subscribe to `tracking_request.succeeded`, `tracking_request.failed`, and `tracking_request.awaiting_manifest` rather than expecting shipment data in the creation response. See [Tracking Request Lifecycle](/docs/api-docs/in-depth-guides/tracking-request-lifecycle). ## Error handling Vizion returns errors in a response envelope or via HTTP status codes depending on the endpoint. Terminal49 uses standard HTTP status codes consistently. Replace envelope checks and message-string matching with status-code checks. | Status | Meaning | | ------ | ------------------------------------------------------------------------------- | | 400 | Malformed request or failed validation | | 401 | Missing or invalid API key | | 403 | Key lacks permission, or the feature is not enabled on your plan | | 404 | Resource does not exist | | 422 | Valid syntax, rejected content. For example, a malformed container number | | 429 | Rate limited. See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) | | 5xx | Terminal49 or an upstream carrier or terminal is unavailable | Rough equivalence for the Vizion errors you are handling today: | Vizion error | Terminal49 | | ------------------------ | ---------------------------------------------------------------------------------------- | | Auth error | HTTP 401 | | Permission error | HTTP 403 | | Rate limit error | HTTP 429 | | Validation error | HTTP 400 or 422 | | Carrier not supported | HTTP 422 | | No data found | `tracking_request.failed`, or `tracking_request.awaiting_manifest` if not yet manifested | | Temporary unavailability | Not surfaced. We retry internally. | The [TypeScript SDK](/docs/sdk/introduction) maps these to typed errors (`AuthenticationError`, `ValidationError`, `RateLimitError`, `UpstreamError`, `FeatureNotEnabledError`, `AuthorizationError`, `NotFoundError`) and retries rate-limit and server errors automatically with exponential backoff. ## Where we are narrower than Vizion Worth knowing before you commit. **Carrier count.** Terminal49 integrates directly with 36 ocean carriers, plus 2 more enabled on request, as of 14 August 2026. Vizion lists substantially more. Ours are direct integrations covering the lines that move volume into North America, and each is normalized into one schema. Check your carrier mix against the [ocean carrier list](/docs/coverage/ocean-carriers) before cutover, and read the known issues section there. We publish the per-carrier field gaps. **Terminal data is North America.** Holds, fees, LFD, and availability come from direct terminal integrations concentrated in the US and Canada, with European ports expanding. Ocean milestones work globally; terminal-level operational data does not yet. **No air, parcel, or road tracking.** If your integration covers those modes, this migration handles only the ocean portion. **No freight rates or sailing schedules.** We do not offer a rate calculator, rate index, or schedule search. **Some fields are source-dependent.** Seal number, container weight, and departure or arrival events vary by carrier. The [field availability reference](/docs/coverage/fields) says which fields are always present and which depend on the carrier, terminal, or journey. ## Migration checklist Everyone does the base path. Then pick a branch. ### Base path Self-serve at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). Copy it immediately, it is shown once. Move from your Vizion API key header to `Authorization: Token`. Note the `Token` prefix. Compare your Vizion carrier values against the [carrier list](/docs/coverage/ocean-carriers). Flag anything missing before you cut over. One `POST /tracking_requests` per BOL, booking, or container, replacing the per-reference creation. Tracking requests start pending. Handle `succeeded`, `failed`, and `awaiting_manifest` rather than expecting data on creation. JSON:API structure, split equipment fields, UTC timestamps with a separate timezone. Replace envelope checks with HTTP status codes. Submit tracking requests for everything currently in transit. Send us the list if it is large and we will load it. Holds, fees, and LFD are the reason to do this properly rather than porting like for like. ### Then pick one Accept our POST payloads at a public URL. Subscribe only to events you act on. Reject any payload whose signature does not match. Only needed if your firewall restricts inbound traffic. Confirm end-to-end before going live. Remove your dedupe layer along with it. See [webhook best practices](/docs/api-docs/webhooks/best-practices) for retries and idempotency. The creation response is pending and carries no shipment yet. Keep the tracking request ID, then fetch the tracking request again (or handle `tracking_request.succeeded`) to get the shipment ID once the carrier responds. Point it at `GET /v2/shipments` or `GET /v2/containers`. No change to how often you poll. Skipped: endpoint setup, signature verification, IP whitelisting, delivery testing. Terminal data changes on a cadence polling tends to miss. If you only adopt webhooks for one thing, make it `container.updated` and `container.pickup_lfd.changed`. ## Migrate with an AI coding agent If you use Cursor, Claude Code, Windsurf, Copilot, or another AI coding assistant, hand it the prompt below. It is written to run a **side-by-side migration**: the agent stands up a Terminal49 client next to your existing Vizion code, shadows every Vizion call with a Terminal49 call, diffs the responses, and only cuts over once parity is proven. Point your agent at this page as context (paste the URL or add it as a doc source). The prompt references the mappings above, so the more of this page the agent can see, the better it does. 1. Open your repo in your AI coding tool. 2. Add this page as a documentation source, or paste its URL into the chat. 3. Copy the prompt below into a new chat and send it. 4. Answer the agent's discovery questions (Vizion client location, env var names, carrier mix). 5. Review each PR the agent opens. It should ship in small, reviewable steps: client, shadow, parity harness, cutover, cleanup. * A `Terminal49Client` alongside your existing `VizionClient`, sharing the same interface where possible. * A shadow-mode wrapper that calls both providers and logs response diffs without changing behavior. * A parity report per shipment: matched fields, diverged fields, and Terminal49-only fields (holds, fees, LFD). * A feature-flagged cutover: route reads to Terminal49, keep Vizion as fallback until you flip the flag off. * A webhook receiver with HMAC verification, or a polling scheduler, depending on which path you pick. * A cleanup PR that removes Vizion code, env vars, dependencies, and dedupe logic. ### The prompt Copy this into your agent. Replace the bracketed placeholders in the **Repo context** block before sending. ```markdown Terminal49 migration agent prompt expandable icon=robot wrap theme={null} You are migrating this codebase from the Vizion tracking API to the Terminal49 API, side by side. Terminal49's authoritative migration guide is at https://terminal49.com/docs/migrate/vizion. Use it as the source of truth for field mappings, event names, error codes, and behavior differences. # Repo context (fill this in before running) - Vizion client lives at: [path/to/vizion/client.ts] - Vizion is called from: [list the call sites or "find them"] - Language and framework: [e.g. TypeScript + Node, Python + FastAPI, Ruby on Rails] - Storage for tracking state: [e.g. Postgres table `shipments`] - Current polling schedule: [e.g. cron every 2h via BullMQ] - Carrier mix (SCACs we track most): [e.g. MAEU, MSCU, CMDU, HLCU, ONEY] - Deployment target: [e.g. AWS ECS, Vercel, Fly.io] - Secret store: [e.g. AWS Secrets Manager, `.env`, Doppler] # Rules 1. Do not delete Vizion code until the cleanup step. Migration is side by side. 2. Ship in small PRs. Each PR must build, pass tests, and be independently revertable. 3. Never invent Terminal49 fields, endpoints, or event names. If the guide does not confirm a mapping, ask me. Do not guess. 4. Terminal49 uses `Authorization: Token ` (not `Bearer`) and content type `application/vnd.api+json`. Get this right on the first request. 5. Terminal49 is asynchronous. `POST /tracking_requests` returns pending. Data arrives via `tracking_request.succeeded`, `tracking_request.failed`, or `tracking_request.awaiting_manifest` webhooks, or by polling `GET /v2/shipments`. Do not expect shipment data on creation. 6. Timestamps are UTC with a separate IANA `time_zone` field. Do not assume local time. 7. `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` are the normal state, not missing data. A fee `amount` of 0 is valid. 8. On `container.updated`, prefer the `changeset` over diffing state yourself. # Plan (execute in order, one PR per step) ## PR 1 — Discovery and interface - Grep the repo for every Vizion call site. List them in the PR description. - Extract the Vizion client's public surface into an interface (`TrackingProvider` with methods like `track(number, type, scac)`, `getShipment(id)`, `refresh(id)`). - Make the existing Vizion client implement it. No behavior change. ## PR 2 — Terminal49 client - Add a `Terminal49Client` implementing the same `TrackingProvider` interface. - Auth via `T49_API_KEY` env var, header `Authorization: Token ${key}`. - Base URL `https://api.terminal49.com/v2`, content type `application/vnd.api+json`. - Implement: - `createTrackingRequest({ request_number, request_type, scac })` returning the tracking request ID. - `getShipment(id, { include: 'containers,port_of_lading,port_of_discharge,...' })`. - `getContainer(id)`. - `refreshContainer(id)` mapping to `PATCH /v2/containers/{id}/refresh`. - Normalize responses to the same shape Vizion callers expect today, using the field mapping from the guide. Split `iso_code` into `equipment_type`, `equipment_length`, `equipment_height`. Convert timestamps to UTC + `time_zone`. - Map errors to typed classes: `AuthenticationError` (401), `AuthorizationError` (403), `ValidationError` (400/422), `RateLimitError` (429), `UpstreamError` (5xx), `FeatureNotEnabledError` (403 + feature flag response). - Add unit tests using recorded fixtures. Do not hit the live API in tests. ## PR 3 — Shadow mode - Add a `ShadowProvider` that wraps both clients. On every read: - Call Vizion as the primary. Return its response. - Fire-and-forget a Terminal49 call for the same identifier. - Log a structured diff: matched fields, diverged fields, T49-only fields. - Gate with env var `TRACKING_SHADOW_MODE=true`. Off by default. - Add a parity report script that aggregates shadow logs by SCAC and field. - Do NOT change what callers see. This step is observation only. ## PR 4 — Backfill script - Write a one-shot script that reads all active shipments from our database and calls `POST /tracking_requests` for each (one per BOL, booking, or container). - Store the returned `tracking_request.id` and eventual `shipment.id` on our records. - Rate-limit to respect Terminal49's limits (handle 429 with exponential backoff). - Idempotent: safe to re-run. Skip rows already backfilled. ## PR 5 — Webhook receiver (only if we picked the webhook path) - Add `POST /webhooks/terminal49` endpoint. - Verify HMAC signature using the shared secret from `T49_WEBHOOK_SECRET`. Reject on mismatch with 401. - Handle these events at minimum: - `tracking_request.succeeded`, `tracking_request.failed`, `tracking_request.awaiting_manifest` - `container.transport.vessel_discharged`, `container.transport.available`, `container.transport.full_out` - `container.updated` (use the `changeset`, do not diff) - `container.pickup_lfd.changed` - Persist events idempotently keyed by event ID. - Register the webhook via `POST /v2/webhooks` from a bootstrap script, subscribing only to events we handle. - If our firewall restricts inbound traffic, whitelist Terminal49 IPs from `GET /v2/webhooks/ips`. ## PR 6 — Cutover behind a flag - Add feature flag `TRACKING_PROVIDER` with values `vizion` (default) and `terminal49`. - Route all reads through the flag. Vizion stays available as a fallback for one release cycle. - Flip staging to `terminal49`, verify parity report is clean, then flip production. ## PR 7 — Cleanup - Delete `VizionClient`, its tests, its env vars, its dedupe cache, and any ISO-code parsing helpers Terminal49 makes redundant. - Remove the shadow provider and the feature flag. - Update README and any runbooks. Note the new webhook endpoint if applicable. # Definition of done - All Vizion call sites now go through Terminal49. - Webhook (or polling) is live in production. - Parity report shows no unexplained divergences for our top 10 SCACs. - Holds, fees, and LFD are exposed to whichever downstream system needs them (dashboard, alerts, customer emails). Do not migrate without wiring these up. They are the reason to do this properly. - Vizion dependency, env vars, and dead code are gone from the repo. # Ask me before you - Choose between the webhook path and the polling path. Default to webhooks unless our infra makes an inbound HTTPS endpoint hard. - Change the shape of any function Vizion callers use today. Prefer a normalization layer inside the Terminal49 client. - Add a new dependency. Prefer stdlib and what is already in the repo. - Touch anything outside the tracking integration. Start with PR 1. Post the list of Vizion call sites and the proposed `TrackingProvider` interface, and wait for my review before writing PR 2. ``` The prompt is deliberately opinionated on side-by-side migration and small PRs. If your team prefers a big-bang cutover or a different branching model, edit the **Plan** section before sending it to your agent. ## Getting help Send us your list of active container and bill of lading numbers and we will load them rather than making you script the backfill. If something in this mapping is wrong or incomplete, tell us. We would rather fix the page than have you work around it. Every endpoint, with request and response schemas Typed client with retries and pagination built in Carriers, terminals, rail, and field availability Simulate success and failure outcomes # Terminal49 SDK Error Handling Source: https://terminal49.com/docs/sdk/error-handling Catch and handle Terminal49 TypeScript SDK errors, including API validation failures and network timeouts, so your integration recovers gracefully. The SDK throws typed errors you can catch and handle based on the error type. ## Error types | Error | Cause | | ------------------------ | ------------------------------------------ | | `AuthenticationError` | Invalid or missing API key | | `AuthorizationError` | Valid API key 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 theme={null} 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 key'); } 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 theme={null} 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 | # Terminal49 SDK Filtering and Pagination Source: https://terminal49.com/docs/sdk/filtering-pagination Query shipments and containers with status, date, and carrier filters, and paginate large result sets using the Terminal49 TypeScript SDK. ## Filtering shipments Pass filter parameters to narrow results: ```typescript theme={null} 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 theme={null} 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 theme={null} 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 theme={null} 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 theme={null} const containers = await client.containers.list({ status: 'in_transit', port: 'USLAX', }); ``` # Terminal49 TypeScript SDK Source: https://terminal49.com/docs/sdk/introduction Use the Terminal49 TypeScript SDK to track containers, retrieve shipment data, and receive real-time updates from your Node.js or TypeScript application. 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/api-keys)) ## Install ```bash theme={null} npm install @terminal49/sdk ``` Package links: * [npm package](https://www.npmjs.com/package/@terminal49/sdk) * [GitHub source](https://github.com/Terminal49/API/tree/main/sdks/typescript-sdk) ## Support policy * Node.js is the only supported runtime; browser, Deno, and Bun are not supported. * Versioning follows npm package versions. Pin a specific version in production and review the [changelog](/docs/updates/home) before upgrading. * Use of the SDK is governed by [Terminal49's terms of service](https://www.terminal49.com/terms). The npm package does not declare an open-source license. ## Setup Store your API key as an environment variable: ```bash theme={null} export T49_API_TOKEN=your_api_key ``` Then initialize the client: ```typescript theme={null} 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](/docs/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 Browse SDK classes, interfaces, and types # Terminal49 SDK Methods Reference Source: https://terminal49.com/docs/sdk/methods Review every Terminal49 TypeScript SDK method organized by resource — shipments, containers, tracking requests, webhooks, and more — with usage examples. The SDK exposes a `Terminal49Client` with methods grouped by resource type. Each method corresponds to an [API endpoint](/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. See [holds, fees, and release readiness](/docs/api-docs/in-depth-guides/holds-and-fees) for context. | | `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 theme={null} 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 theme={null} const client = new Terminal49Client({ apiToken: process.env.T49_API_TOKEN!, defaultFormat: 'mapped', }); ``` List methods also accept pagination options: ```typescript theme={null} const shipments = await client.shipments.list({}, { page: 1, pageSize: 25, format: 'mapped', }); ``` See [Filtering & Pagination](/docs/sdk/filtering-pagination) for details. # Terminal49 SDK Quickstart Source: https://terminal49.com/docs/sdk/quickstart Install the Terminal49 TypeScript SDK, track a container, list shipments, and retrieve live tracking data in a few minutes with working examples. 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](/docs/sdk/introduction) and set your `T49_API_TOKEN` environment variable. ## Complete example ```typescript theme={null} 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](/docs/sdk/methods) — See all available operations * [Filtering & Pagination](/docs/sdk/filtering-pagination) — Query large datasets efficiently * [Webhooks](/docs/api-docs/in-depth-guides/webhooks) — Get notified when shipments update # Class: AuthenticationError Source: https://terminal49.com/docs/sdk/reference/client/classes/AuthenticationError AuthenticationError class in the Terminal49 TypeScript SDK, thrown when the API token is missing or invalid and the request returns HTTP 401. # Class: AuthenticationError Thrown when the API token is invalid or missing (HTTP 401). ## Extends * [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error) ## Constructors ### Constructor > **new AuthenticationError**(`message`, `status?`, `details?`): `AuthenticationError` #### Parameters | Parameter | Type | Default value | | ---------- | --------- | ------------- | | `message` | `string` | `undefined` | | `status` | `number` | `401` | | `details?` | `unknown` | `undefined` | #### Returns `AuthenticationError` #### Overrides [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`constructor`](/docs/sdk/reference/client/classes/Terminal49Error#constructor) ## Properties | Property | Modifier | Type | Description | Inherited from | | ------------------ | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `cause?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`cause`](/docs/sdk/reference/client/classes/Terminal49Error#property-cause) | | `details?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`details`](/docs/sdk/reference/client/classes/Terminal49Error#property-details) | | `message` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`message`](/docs/sdk/reference/client/classes/Terminal49Error#property-message) | | `name` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`name`](/docs/sdk/reference/client/classes/Terminal49Error#property-name) | | `stack?` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stack`](/docs/sdk/reference/client/classes/Terminal49Error#property-stack) | | `status?` | `public` | `number` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`status`](/docs/sdk/reference/client/classes/Terminal49Error#property-status) | | `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured *after* the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stackTraceLimit`](/docs/sdk/reference/client/classes/Terminal49Error#property-stacktracelimit) | ## Methods ### captureStackTrace() > `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` Creates a `.stack` property on `targetObject`, which when accessed returns a string representing the location in the code at which `Error.captureStackTrace()` was called. ```js theme={null} const myObject = {}; Error.captureStackTrace(myObject); myObject.stack; // Similar to `new Error().stack` ``` The first line of the trace will be prefixed with `${myObject.name}: ${myObject.message}`. The optional `constructorOpt` argument accepts a function. If given, all frames above `constructorOpt`, including `constructorOpt`, will be omitted from the generated stack trace. The `constructorOpt` argument is useful for hiding implementation details of error generation from the user. For instance: ```js theme={null} function a() { b(); } function b() { c(); } function c() { // Create an error without stack trace to avoid calculating the stack trace twice. const { stackTraceLimit } = Error; Error.stackTraceLimit = 0; const error = new Error(); Error.stackTraceLimit = stackTraceLimit; // Capture the stack trace above function b Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace throw error; } a(); ``` #### Parameters | Parameter | Type | | ----------------- | ---------- | | `targetObject` | `object` | | `constructorOpt?` | `Function` | #### Returns `void` #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`captureStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#capturestacktrace) *** ### prepareStackTrace() > `static` **prepareStackTrace**(`err`, `stackTraces`): `any` #### Parameters | Parameter | Type | | ------------- | ------------- | | `err` | `Error` | | `stackTraces` | `CallSite`\[] | #### Returns `any` #### See [https://v8.dev/docs/stack-trace-api#customizing-stack-traces](https://v8.dev/docs/stack-trace-api#customizing-stack-traces) #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`prepareStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#preparestacktrace) # Class: AuthorizationError Source: https://terminal49.com/docs/sdk/reference/client/classes/AuthorizationError AuthorizationError class in the Terminal49 TypeScript SDK, thrown when an API token lacks permission for a request and the API responds with HTTP 403. # Class: AuthorizationError Thrown when the API token is valid but lacks permission for the request (HTTP 403). ## Extends * [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error) ## Extended by * [`FeatureNotEnabledError`](/docs/sdk/reference/client/classes/FeatureNotEnabledError) ## Constructors ### Constructor > **new AuthorizationError**(`message`, `status?`, `details?`): `AuthorizationError` #### Parameters | Parameter | Type | Default value | | ---------- | --------- | ------------- | | `message` | `string` | `undefined` | | `status` | `number` | `403` | | `details?` | `unknown` | `undefined` | #### Returns `AuthorizationError` #### Overrides [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`constructor`](/docs/sdk/reference/client/classes/Terminal49Error#constructor) ## Properties | Property | Modifier | Type | Description | Inherited from | | ------------------ | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `cause?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`cause`](/docs/sdk/reference/client/classes/Terminal49Error#property-cause) | | `details?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`details`](/docs/sdk/reference/client/classes/Terminal49Error#property-details) | | `message` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`message`](/docs/sdk/reference/client/classes/Terminal49Error#property-message) | | `name` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`name`](/docs/sdk/reference/client/classes/Terminal49Error#property-name) | | `stack?` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stack`](/docs/sdk/reference/client/classes/Terminal49Error#property-stack) | | `status?` | `public` | `number` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`status`](/docs/sdk/reference/client/classes/Terminal49Error#property-status) | | `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured *after* the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stackTraceLimit`](/docs/sdk/reference/client/classes/Terminal49Error#property-stacktracelimit) | ## Methods ### captureStackTrace() > `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` Creates a `.stack` property on `targetObject`, which when accessed returns a string representing the location in the code at which `Error.captureStackTrace()` was called. ```js theme={null} const myObject = {}; Error.captureStackTrace(myObject); myObject.stack; // Similar to `new Error().stack` ``` The first line of the trace will be prefixed with `${myObject.name}: ${myObject.message}`. The optional `constructorOpt` argument accepts a function. If given, all frames above `constructorOpt`, including `constructorOpt`, will be omitted from the generated stack trace. The `constructorOpt` argument is useful for hiding implementation details of error generation from the user. For instance: ```js theme={null} function a() { b(); } function b() { c(); } function c() { // Create an error without stack trace to avoid calculating the stack trace twice. const { stackTraceLimit } = Error; Error.stackTraceLimit = 0; const error = new Error(); Error.stackTraceLimit = stackTraceLimit; // Capture the stack trace above function b Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace throw error; } a(); ``` #### Parameters | Parameter | Type | | ----------------- | ---------- | | `targetObject` | `object` | | `constructorOpt?` | `Function` | #### Returns `void` #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`captureStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#capturestacktrace) *** ### prepareStackTrace() > `static` **prepareStackTrace**(`err`, `stackTraces`): `any` #### Parameters | Parameter | Type | | ------------- | ------------- | | `err` | `Error` | | `stackTraces` | `CallSite`\[] | #### Returns `any` #### See [https://v8.dev/docs/stack-trace-api#customizing-stack-traces](https://v8.dev/docs/stack-trace-api#customizing-stack-traces) #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`prepareStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#preparestacktrace) # Class: FeatureNotEnabledError Source: https://terminal49.com/docs/sdk/reference/client/classes/FeatureNotEnabledError FeatureNotEnabledError in the Terminal49 TypeScript SDK, thrown when a feature requires a plan upgrade and the API responds with an HTTP 403 status. # Class: FeatureNotEnabledError Thrown when the requested feature requires a plan upgrade (HTTP 403). ## Extends * [`AuthorizationError`](/docs/sdk/reference/client/classes/AuthorizationError) ## Constructors ### Constructor > **new FeatureNotEnabledError**(`message`, `status?`, `details?`): `FeatureNotEnabledError` #### Parameters | Parameter | Type | Default value | | ---------- | --------- | ------------- | | `message` | `string` | `undefined` | | `status` | `number` | `403` | | `details?` | `unknown` | `undefined` | #### Returns `FeatureNotEnabledError` #### Overrides [`AuthorizationError`](/docs/sdk/reference/client/classes/AuthorizationError).[`constructor`](/docs/sdk/reference/client/classes/AuthorizationError#constructor) ## Properties | Property | Modifier | Type | Description | Inherited from | | ------------------ | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cause?` | `public` | `unknown` | - | [`AuthorizationError`](/docs/sdk/reference/client/classes/AuthorizationError).[`cause`](/docs/sdk/reference/client/classes/AuthorizationError#property-cause) | | `details?` | `public` | `unknown` | - | [`AuthorizationError`](/docs/sdk/reference/client/classes/AuthorizationError).[`details`](/docs/sdk/reference/client/classes/AuthorizationError#property-details) | | `message` | `public` | `string` | - | [`AuthorizationError`](/docs/sdk/reference/client/classes/AuthorizationError).[`message`](/docs/sdk/reference/client/classes/AuthorizationError#property-message) | | `name` | `public` | `string` | - | [`AuthorizationError`](/docs/sdk/reference/client/classes/AuthorizationError).[`name`](/docs/sdk/reference/client/classes/AuthorizationError#property-name) | | `stack?` | `public` | `string` | - | [`AuthorizationError`](/docs/sdk/reference/client/classes/AuthorizationError).[`stack`](/docs/sdk/reference/client/classes/AuthorizationError#property-stack) | | `status?` | `public` | `number` | - | [`AuthorizationError`](/docs/sdk/reference/client/classes/AuthorizationError).[`status`](/docs/sdk/reference/client/classes/AuthorizationError#property-status) | | `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured *after* the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | [`AuthorizationError`](/docs/sdk/reference/client/classes/AuthorizationError).[`stackTraceLimit`](/docs/sdk/reference/client/classes/AuthorizationError#property-stacktracelimit) | ## Methods ### captureStackTrace() > `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` Creates a `.stack` property on `targetObject`, which when accessed returns a string representing the location in the code at which `Error.captureStackTrace()` was called. ```js theme={null} const myObject = {}; Error.captureStackTrace(myObject); myObject.stack; // Similar to `new Error().stack` ``` The first line of the trace will be prefixed with `${myObject.name}: ${myObject.message}`. The optional `constructorOpt` argument accepts a function. If given, all frames above `constructorOpt`, including `constructorOpt`, will be omitted from the generated stack trace. The `constructorOpt` argument is useful for hiding implementation details of error generation from the user. For instance: ```js theme={null} function a() { b(); } function b() { c(); } function c() { // Create an error without stack trace to avoid calculating the stack trace twice. const { stackTraceLimit } = Error; Error.stackTraceLimit = 0; const error = new Error(); Error.stackTraceLimit = stackTraceLimit; // Capture the stack trace above function b Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace throw error; } a(); ``` #### Parameters | Parameter | Type | | ----------------- | ---------- | | `targetObject` | `object` | | `constructorOpt?` | `Function` | #### Returns `void` #### Inherited from [`AuthorizationError`](/docs/sdk/reference/client/classes/AuthorizationError).[`captureStackTrace`](/docs/sdk/reference/client/classes/AuthorizationError#capturestacktrace) *** ### prepareStackTrace() > `static` **prepareStackTrace**(`err`, `stackTraces`): `any` #### Parameters | Parameter | Type | | ------------- | ------------- | | `err` | `Error` | | `stackTraces` | `CallSite`\[] | #### Returns `any` #### See [https://v8.dev/docs/stack-trace-api#customizing-stack-traces](https://v8.dev/docs/stack-trace-api#customizing-stack-traces) #### Inherited from [`AuthorizationError`](/docs/sdk/reference/client/classes/AuthorizationError).[`prepareStackTrace`](/docs/sdk/reference/client/classes/AuthorizationError#preparestacktrace) # Class: NetworkError Source: https://terminal49.com/docs/sdk/reference/client/classes/NetworkError NetworkError class in the Terminal49 TypeScript SDK, thrown for transport-level failures such as DNS failures, connection resets, and failed fetch calls. # Class: NetworkError Thrown when a transport-level failure occurs before a response is received — a DNS failure, a refused/reset connection, or an otherwise failed `fetch`. Has no HTTP status because no response was produced. ## Extends * [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error) ## Constructors ### Constructor > **new NetworkError**(`message`, `details?`): `NetworkError` #### Parameters | Parameter | Type | | ---------- | --------- | | `message` | `string` | | `details?` | `unknown` | #### Returns `NetworkError` #### Overrides [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`constructor`](/docs/sdk/reference/client/classes/Terminal49Error#constructor) ## Properties | Property | Modifier | Type | Description | Inherited from | | ------------------ | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `cause?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`cause`](/docs/sdk/reference/client/classes/Terminal49Error#property-cause) | | `details?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`details`](/docs/sdk/reference/client/classes/Terminal49Error#property-details) | | `message` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`message`](/docs/sdk/reference/client/classes/Terminal49Error#property-message) | | `name` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`name`](/docs/sdk/reference/client/classes/Terminal49Error#property-name) | | `stack?` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stack`](/docs/sdk/reference/client/classes/Terminal49Error#property-stack) | | `status?` | `public` | `number` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`status`](/docs/sdk/reference/client/classes/Terminal49Error#property-status) | | `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured *after* the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stackTraceLimit`](/docs/sdk/reference/client/classes/Terminal49Error#property-stacktracelimit) | ## Methods ### captureStackTrace() > `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` Creates a `.stack` property on `targetObject`, which when accessed returns a string representing the location in the code at which `Error.captureStackTrace()` was called. ```js theme={null} const myObject = {}; Error.captureStackTrace(myObject); myObject.stack; // Similar to `new Error().stack` ``` The first line of the trace will be prefixed with `${myObject.name}: ${myObject.message}`. The optional `constructorOpt` argument accepts a function. If given, all frames above `constructorOpt`, including `constructorOpt`, will be omitted from the generated stack trace. The `constructorOpt` argument is useful for hiding implementation details of error generation from the user. For instance: ```js theme={null} function a() { b(); } function b() { c(); } function c() { // Create an error without stack trace to avoid calculating the stack trace twice. const { stackTraceLimit } = Error; Error.stackTraceLimit = 0; const error = new Error(); Error.stackTraceLimit = stackTraceLimit; // Capture the stack trace above function b Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace throw error; } a(); ``` #### Parameters | Parameter | Type | | ----------------- | ---------- | | `targetObject` | `object` | | `constructorOpt?` | `Function` | #### Returns `void` #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`captureStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#capturestacktrace) *** ### prepareStackTrace() > `static` **prepareStackTrace**(`err`, `stackTraces`): `any` #### Parameters | Parameter | Type | | ------------- | ------------- | | `err` | `Error` | | `stackTraces` | `CallSite`\[] | #### Returns `any` #### See [https://v8.dev/docs/stack-trace-api#customizing-stack-traces](https://v8.dev/docs/stack-trace-api#customizing-stack-traces) #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`prepareStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#preparestacktrace) # Class: NotFoundError Source: https://terminal49.com/docs/sdk/reference/client/classes/NotFoundError NotFoundError class in the Terminal49 TypeScript SDK, thrown when a requested shipment, container, or other resource does not exist and returns HTTP 404. # Class: NotFoundError Thrown when the requested resource does not exist (HTTP 404). ## Extends * [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error) ## Constructors ### Constructor > **new NotFoundError**(`message`, `status?`, `details?`): `NotFoundError` #### Parameters | Parameter | Type | Default value | | ---------- | --------- | ------------- | | `message` | `string` | `undefined` | | `status` | `number` | `404` | | `details?` | `unknown` | `undefined` | #### Returns `NotFoundError` #### Overrides [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`constructor`](/docs/sdk/reference/client/classes/Terminal49Error#constructor) ## Properties | Property | Modifier | Type | Description | Inherited from | | ------------------ | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `cause?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`cause`](/docs/sdk/reference/client/classes/Terminal49Error#property-cause) | | `details?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`details`](/docs/sdk/reference/client/classes/Terminal49Error#property-details) | | `message` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`message`](/docs/sdk/reference/client/classes/Terminal49Error#property-message) | | `name` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`name`](/docs/sdk/reference/client/classes/Terminal49Error#property-name) | | `stack?` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stack`](/docs/sdk/reference/client/classes/Terminal49Error#property-stack) | | `status?` | `public` | `number` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`status`](/docs/sdk/reference/client/classes/Terminal49Error#property-status) | | `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured *after* the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stackTraceLimit`](/docs/sdk/reference/client/classes/Terminal49Error#property-stacktracelimit) | ## Methods ### captureStackTrace() > `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` Creates a `.stack` property on `targetObject`, which when accessed returns a string representing the location in the code at which `Error.captureStackTrace()` was called. ```js theme={null} const myObject = {}; Error.captureStackTrace(myObject); myObject.stack; // Similar to `new Error().stack` ``` The first line of the trace will be prefixed with `${myObject.name}: ${myObject.message}`. The optional `constructorOpt` argument accepts a function. If given, all frames above `constructorOpt`, including `constructorOpt`, will be omitted from the generated stack trace. The `constructorOpt` argument is useful for hiding implementation details of error generation from the user. For instance: ```js theme={null} function a() { b(); } function b() { c(); } function c() { // Create an error without stack trace to avoid calculating the stack trace twice. const { stackTraceLimit } = Error; Error.stackTraceLimit = 0; const error = new Error(); Error.stackTraceLimit = stackTraceLimit; // Capture the stack trace above function b Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace throw error; } a(); ``` #### Parameters | Parameter | Type | | ----------------- | ---------- | | `targetObject` | `object` | | `constructorOpt?` | `Function` | #### Returns `void` #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`captureStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#capturestacktrace) *** ### prepareStackTrace() > `static` **prepareStackTrace**(`err`, `stackTraces`): `any` #### Parameters | Parameter | Type | | ------------- | ------------- | | `err` | `Error` | | `stackTraces` | `CallSite`\[] | #### Returns `any` #### See [https://v8.dev/docs/stack-trace-api#customizing-stack-traces](https://v8.dev/docs/stack-trace-api#customizing-stack-traces) #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`prepareStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#preparestacktrace) # Class: RateLimitError Source: https://terminal49.com/docs/sdk/reference/client/classes/RateLimitError RateLimitError class in the Terminal49 TypeScript SDK, thrown on HTTP 429 when the API rate limit is exceeded; the SDK retries these requests automatically. # Class: RateLimitError Thrown when the API rate limit has been exceeded (HTTP 429). The SDK retries automatically. ## Extends * [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error) ## Constructors ### Constructor > **new RateLimitError**(`message`, `status?`, `details?`): `RateLimitError` #### Parameters | Parameter | Type | Default value | | ---------- | --------- | ------------- | | `message` | `string` | `undefined` | | `status` | `number` | `429` | | `details?` | `unknown` | `undefined` | #### Returns `RateLimitError` #### Overrides [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`constructor`](/docs/sdk/reference/client/classes/Terminal49Error#constructor) ## Properties | Property | Modifier | Type | Description | Inherited from | | ------------------ | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `cause?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`cause`](/docs/sdk/reference/client/classes/Terminal49Error#property-cause) | | `details?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`details`](/docs/sdk/reference/client/classes/Terminal49Error#property-details) | | `message` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`message`](/docs/sdk/reference/client/classes/Terminal49Error#property-message) | | `name` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`name`](/docs/sdk/reference/client/classes/Terminal49Error#property-name) | | `stack?` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stack`](/docs/sdk/reference/client/classes/Terminal49Error#property-stack) | | `status?` | `public` | `number` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`status`](/docs/sdk/reference/client/classes/Terminal49Error#property-status) | | `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured *after* the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stackTraceLimit`](/docs/sdk/reference/client/classes/Terminal49Error#property-stacktracelimit) | ## Methods ### captureStackTrace() > `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` Creates a `.stack` property on `targetObject`, which when accessed returns a string representing the location in the code at which `Error.captureStackTrace()` was called. ```js theme={null} const myObject = {}; Error.captureStackTrace(myObject); myObject.stack; // Similar to `new Error().stack` ``` The first line of the trace will be prefixed with `${myObject.name}: ${myObject.message}`. The optional `constructorOpt` argument accepts a function. If given, all frames above `constructorOpt`, including `constructorOpt`, will be omitted from the generated stack trace. The `constructorOpt` argument is useful for hiding implementation details of error generation from the user. For instance: ```js theme={null} function a() { b(); } function b() { c(); } function c() { // Create an error without stack trace to avoid calculating the stack trace twice. const { stackTraceLimit } = Error; Error.stackTraceLimit = 0; const error = new Error(); Error.stackTraceLimit = stackTraceLimit; // Capture the stack trace above function b Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace throw error; } a(); ``` #### Parameters | Parameter | Type | | ----------------- | ---------- | | `targetObject` | `object` | | `constructorOpt?` | `Function` | #### Returns `void` #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`captureStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#capturestacktrace) *** ### prepareStackTrace() > `static` **prepareStackTrace**(`err`, `stackTraces`): `any` #### Parameters | Parameter | Type | | ------------- | ------------- | | `err` | `Error` | | `stackTraces` | `CallSite`\[] | #### Returns `any` #### See [https://v8.dev/docs/stack-trace-api#customizing-stack-traces](https://v8.dev/docs/stack-trace-api#customizing-stack-traces) #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`prepareStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#preparestacktrace) # Class: Terminal49Client Source: https://terminal49.com/docs/sdk/reference/client/classes/Terminal49Client Terminal49Client class reference for the TypeScript SDK, used to create tracking requests and fetch shipments, containers, and events from Node.js apps. # Class: Terminal49Client Server-side TypeScript client for the Terminal49 JSON:API. Use this client to create tracking requests, list and fetch shipments and containers, retrieve transport events, and work with core Terminal49 tracking data from Node.js applications. ## Constructors ### Constructor > **new Terminal49Client**(`config`): `Terminal49Client` #### Parameters | Parameter | Type | | --------- | ---------------------------------------------------------------------------------------- | | `config` | [`Terminal49ClientConfig`](/docs/sdk/reference/client/interfaces/Terminal49ClientConfig) | #### Returns `Terminal49Client` ## Properties | Property | Modifier | Type | | ----------------------------------- | -------- | ---------------------------------------------------------------------------------------------- | | `containers` | `public` | [`ContainerManager`](/docs/sdk/reference/client/managers/classes/ContainerManager) | | `customFieldDefinitions` | `public` | `object` | | `customFieldDefinitions.create` | `public` | (`payload`, `options?`) => `Promise`\<`any`> | | `customFieldDefinitions.delete` | `public` | (`id`, `options?`) => `Promise`\<`any`> | | `customFieldDefinitions.get` | `public` | (`id`, `options?`) => `Promise`\<`any`> | | `customFieldDefinitions.list` | `public` | (`options?`) => `Promise`\<`any`> | | `customFieldDefinitions.update` | `public` | (`id`, `payload`, `options?`) => `Promise`\<`any`> | | `customFieldOptions` | `public` | `object` | | `customFieldOptions.create` | `public` | (`definitionId`, `payload`, `options?`) => `Promise`\<`any`> | | `customFieldOptions.delete` | `public` | (`definitionId`, `optionId`, `options?`) => `Promise`\<`any`> | | `customFieldOptions.get` | `public` | (`definitionId`, `optionId`, `options?`) => `Promise`\<`any`> | | `customFieldOptions.list` | `public` | (`definitionId`, `options?`) => `Promise`\<`any`> | | `customFieldOptions.update` | `public` | (`definitionId`, `optionId`, `payload`, `options?`) => `Promise`\<`any`> | | `customFields` | `public` | `object` | | `customFields.create` | `public` | (`payload`, `options?`) => `Promise`\<`any`> | | `customFields.delete` | `public` | (`id`, `options?`) => `Promise`\<`any`> | | `customFields.get` | `public` | (`id`, `options?`) => `Promise`\<`any`> | | `customFields.list` | `public` | (`options?`) => `Promise`\<`any`> | | `customFields.update` | `public` | (`id`, `payload`, `options?`) => `Promise`\<`any`> | | `metroAreas` | `public` | `object` | | `metroAreas.get` | `public` | (`id`, `options?`) => `Promise`\<`any`> | | `parties` | `public` | `object` | | `parties.get` | `public` | (`id`, `options?`) => `Promise`\<`any`> | | `parties.list` | `public` | (`options?`) => `Promise`\<`any`> | | `ports` | `public` | `object` | | `ports.get` | `public` | (`id`, `options?`) => `Promise`\<`any`> | | `shipments` | `public` | [`ShipmentManager`](/docs/sdk/reference/client/managers/classes/ShipmentManager) | | `shippingLines` | `public` | [`ShippingLineManager`](/docs/sdk/reference/client/managers/classes/ShippingLineManager) | | `terminals` | `public` | `object` | | `terminals.get` | `public` | (`id`, `options?`) => `Promise`\<`any`> | | `trackingRequests` | `public` | [`TrackingRequestManager`](/docs/sdk/reference/client/managers/classes/TrackingRequestManager) | | `vessels` | `public` | `object` | | `vessels.futurePositions` | `public` | (`id`, `options?`) => `Promise`\<`any`> | | `vessels.futurePositionsWithCoords` | `public` | (`id`, `options?`) => `Promise`\<`any`> | | `vessels.get` | `public` | (`id`, `options?`) => `Promise`\<`any`> | | `vessels.getByImo` | `public` | (`imo`, `options?`) => `Promise`\<`any`> | | `webhookNotifications` | `public` | `object` | | `webhookNotifications.examples` | `public` | (`event?`, `options?`) => `Promise`\<`any`> | | `webhookNotifications.get` | `public` | (`id`, `options?`) => `Promise`\<`any`> | | `webhookNotifications.list` | `public` | (`options?`) => `Promise`\<`any`> | | `webhooks` | `public` | `object` | | `webhooks.create` | `public` | (`payload`, `options?`) => `Promise`\<`any`> | | `webhooks.delete` | `public` | (`id`, `options?`) => `Promise`\<`any`> | | `webhooks.get` | `public` | (`id`, `options?`) => `Promise`\<`any`> | | `webhooks.ips` | `public` | (`options?`) => `Promise`\<`any`> | | `webhooks.list` | `public` | (`options?`) => `Promise`\<`any`> | | `webhooks.update` | `public` | (`id`, `payload`, `options?`) => `Promise`\<`any`> | ## Methods ### createCustomField() > **createCustomField**(`payload`, `options?`): `Promise`\<`any`> Create a custom field assignment. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `payload` | `Record`\<`string`, `unknown`> | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### createCustomFieldDefinition() > **createCustomFieldDefinition**(`payload`, `options?`): `Promise`\<`any`> Create a custom field definition. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `payload` | `Record`\<`string`, `unknown`> | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### createCustomFieldOption() > **createCustomFieldOption**(`definitionId`, `payload`, `options?`): `Promise`\<`any`> Create a custom field option. #### Parameters | Parameter | Type | | -------------- | ------------------------------------------------------------------------- | | `definitionId` | `string` | | `payload` | `Record`\<`string`, `unknown`> | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### createTrackingRequest() > **createTrackingRequest**(`params`): `Promise`\<`any`> Create a tracking request with an explicit number type and carrier SCAC, or ask the API to auto-detect the carrier SCAC. #### Parameters | Parameter | Type | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `params` | \{ `autoDetectVoccScac?`: `boolean`; `refNumbers?`: `string`\[]; `requestNumber`: `string`; `requestType`: [`TrackingRequestType`](/docs/sdk/reference/client/managers/type-aliases/TrackingRequestType); `scac?`: `string`; `shipmentTags?`: `string`\[]; } | | `params.autoDetectVoccScac?` | `boolean` | | `params.refNumbers?` | `string`\[] | | `params.requestNumber` | `string` | | `params.requestType` | [`TrackingRequestType`](/docs/sdk/reference/client/managers/type-aliases/TrackingRequestType) | | `params.scac?` | `string` | | `params.shipmentTags?` | `string`\[] | #### Returns `Promise`\<`any`> *** ### createTrackingRequestFromInfer() > **createTrackingRequestFromInfer**(`number`, `options?`): `Promise`\<\{ `infer`: `any`; `trackingRequest`: `any`; }> Infer carrier/number type, then create a tracking request from the result. #### Parameters | Parameter | Type | | --------- | ------------------------------------------------------------------------------------------------------------------------------- | | `number` | `string` | | `options` | [`CreateTrackingRequestFromInferOptions`](/docs/sdk/reference/client/managers/interfaces/CreateTrackingRequestFromInferOptions) | #### Returns `Promise`\<\{ `infer`: `any`; `trackingRequest`: `any`; }> *** ### createWebhook() > **createWebhook**(`payload`, `options?`): `Promise`\<`any`> Create a webhook. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `payload` | `Record`\<`string`, `unknown`> | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### deleteCustomField() > **deleteCustomField**(`id`, `options?`): `Promise`\<`any`> Delete a custom field assignment. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### deleteCustomFieldDefinition() > **deleteCustomFieldDefinition**(`id`, `options?`): `Promise`\<`any`> Delete a custom field definition. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### deleteCustomFieldOption() > **deleteCustomFieldOption**(`definitionId`, `optionId`, `options?`): `Promise`\<`any`> Delete a custom field option. #### Parameters | Parameter | Type | | -------------- | ------------------------------------------------------------------------- | | `definitionId` | `string` | | `optionId` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### deleteWebhook() > **deleteWebhook**(`id`, `options?`): `Promise`\<`any`> Delete a webhook. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### deserialize() > **deserialize**\<`T`>(`document`): `T` Deserialize a JSON:API document into plain objects. Useful when you want a simplified shape instead of JSON:API. #### Type Parameters | Type Parameter | | -------------- | | `T` | #### Parameters | Parameter | Type | | ---------- | --------- | | `document` | `unknown` | #### Returns `T` #### Remarks The cast is unchecked at runtime — the caller is responsible for verifying the returned shape matches `T`. *** ### getContainer() > **getContainer**(`id`, `include?`, `options?`): `Promise`\<`any`> Fetch a container by ID with optional included relationships. #### Parameters | Parameter | Type | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | `string` | | `include` | [`IncludeParam`](/docs/sdk/reference/types/options/type-aliases/IncludeParam)\<[`ContainerInclude`](/docs/sdk/reference/types/options/type-aliases/ContainerInclude)> | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getContainerRawEvents() > **getContainerRawEvents**(`id`, `options?`): `Promise`\<`any`> Fetch raw carrier/terminal events for a container. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getContainerRoute() > **getContainerRoute**(`id`, `options?`): `Promise`\<`any`> Fetch routing details for a container. This may require a paid Terminal49 feature. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getContainerTransportEvents() > **getContainerTransportEvents**(`id`, `options?`): `Promise`\<`any`> Fetch normalized transport events for a container. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getCustomField() > **getCustomField**(`id`, `options?`): `Promise`\<`any`> Fetch a custom field assignment by ID. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getCustomFieldDefinition() > **getCustomFieldDefinition**(`id`, `options?`): `Promise`\<`any`> Fetch a custom field definition by ID. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getCustomFieldOption() > **getCustomFieldOption**(`definitionId`, `optionId`, `options?`): `Promise`\<`any`> Fetch a custom field option. #### Parameters | Parameter | Type | | -------------- | ------------------------------------------------------------------------- | | `definitionId` | `string` | | `optionId` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getDemurrage() > **getDemurrage**(`containerId`): `Promise`\<`any`> Return a demurrage-focused subset of container fields. #### Parameters | Parameter | Type | | ------------- | -------- | | `containerId` | `string` | #### Returns `Promise`\<`any`> *** ### getMetroArea() > **getMetroArea**(`id`, `options?`): `Promise`\<`any`> Fetch a metro area by ID. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getParty() > **getParty**(`id`, `options?`): `Promise`\<`any`> Fetch a party by ID. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getPort() > **getPort**(`id`, `options?`): `Promise`\<`any`> Fetch a port by ID. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getRailMilestones() > **getRailMilestones**(`containerId`): `Promise`\<`any`> Return rail milestone fields and rail transport events for a container. #### Parameters | Parameter | Type | | ------------- | -------- | | `containerId` | `string` | #### Returns `Promise`\<`any`> *** ### getShipment() > **getShipment**(`id`, `includeContainers?`, `options?`): `Promise`\<`any`> Fetch a shipment by ID, optionally including related containers. #### Parameters | Parameter | Type | Default value | | ------------------- | ------------------------------------------------------------------------------------ | ------------- | | `id` | `string` | `undefined` | | `includeContainers` | `boolean` | `true` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) & `object` | `undefined` | #### Returns `Promise`\<`any`> *** ### getTerminal() > **getTerminal**(`id`, `options?`): `Promise`\<`any`> Fetch a terminal by ID. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getTrackingRequest() > **getTrackingRequest**(`id`, `options?`): `Promise`\<`any`> Fetch a tracking request by ID. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------------------ | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) & `object` | #### Returns `Promise`\<`any`> *** ### getVessel() > **getVessel**(`id`, `options?`): `Promise`\<`any`> Fetch a vessel by ID. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getVesselByImo() > **getVesselByImo**(`imo`, `options?`): `Promise`\<`any`> Fetch a vessel by IMO number. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `imo` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getVesselFuturePositions() > **getVesselFuturePositions**(`id`, `options?`): `Promise`\<`any`> Fetch future vessel positions. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getVesselFuturePositionsWithCoords() > **getVesselFuturePositionsWithCoords**(`id`, `options?`): `Promise`\<`any`> Fetch future vessel positions with coordinates. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getWebhook() > **getWebhook**(`id`, `options?`): `Promise`\<`any`> Fetch a webhook by ID. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getWebhookIps() > **getWebhookIps**(`options?`): `Promise`\<`any`> List webhook source IP ranges. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getWebhookNotification() > **getWebhookNotification**(`id`, `options?`): `Promise`\<`any`> Fetch a webhook notification by ID. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### getWebhookNotificationExamples() > **getWebhookNotificationExamples**(`event?`, `options?`): `Promise`\<`any`> Fetch example webhook notification payloads. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `event?` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### inferTrackingNumber() > **inferTrackingNumber**(`number`): `Promise`\<`any`> Infer a tracking number's type and likely carrier candidates. #### Parameters | Parameter | Type | | --------- | -------- | | `number` | `string` | #### Returns `Promise`\<`any`> *** ### listContainers() > **listContainers**(`filters?`, `options?`): `Promise`\<`any`> List containers with optional filters and pagination. #### Parameters | Parameter | Type | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `filters` | \{ `carrier?`: `string`; `include?`: [`IncludeParam`](/docs/sdk/reference/types/options/type-aliases/IncludeParam)\<[`ContainerInclude`](/docs/sdk/reference/types/options/type-aliases/ContainerInclude)>; `port?`: `string`; `status?`: `string`; `updatedAfter?`: `string`; } | | `filters.carrier?` | `string` | | `filters.include?` | [`IncludeParam`](/docs/sdk/reference/types/options/type-aliases/IncludeParam)\<[`ContainerInclude`](/docs/sdk/reference/types/options/type-aliases/ContainerInclude)> | | `filters.port?` | `string` | | `filters.status?` | `string` | | `filters.updatedAfter?` | `string` | | `options?` | [`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions) | #### Returns `Promise`\<`any`> *** ### listCustomFieldDefinitions() > **listCustomFieldDefinitions**(`options?`): `Promise`\<`any`> List custom field definitions. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `options?` | [`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions) | #### Returns `Promise`\<`any`> *** ### listCustomFieldOptions() > **listCustomFieldOptions**(`definitionId`, `options?`): `Promise`\<`any`> List options for a custom field definition. #### Parameters | Parameter | Type | | -------------- | ------------------------------------------------------------------------- | | `definitionId` | `string` | | `options?` | [`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions) | #### Returns `Promise`\<`any`> *** ### listCustomFields() > **listCustomFields**(`options?`): `Promise`\<`any`> List custom field assignments. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `options?` | [`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions) | #### Returns `Promise`\<`any`> *** ### listParties() > **listParties**(`options?`): `Promise`\<`any`> List parties. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `options?` | [`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions) | #### Returns `Promise`\<`any`> *** ### listShipments() > **listShipments**(`filters?`, `options?`): `Promise`\<`any`> List shipments with optional filters and pagination. #### Parameters | Parameter | Type | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `filters` | \{ `carrier?`: `string`; `include?`: [`IncludeParam`](/docs/sdk/reference/types/options/type-aliases/IncludeParam)\<[`ShipmentInclude`](/docs/sdk/reference/types/options/type-aliases/ShipmentInclude)>; `includeContainers?`: `boolean`; `port?`: `string`; `status?`: `string`; `updatedAfter?`: `string`; } | | `filters.carrier?` | `string` | | `filters.include?` | [`IncludeParam`](/docs/sdk/reference/types/options/type-aliases/IncludeParam)\<[`ShipmentInclude`](/docs/sdk/reference/types/options/type-aliases/ShipmentInclude)> | | `filters.includeContainers?` | `boolean` | | `filters.port?` | `string` | | `filters.status?` | `string` | | `filters.updatedAfter?` | `string` | | `options?` | [`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions) | #### Returns `Promise`\<`any`> *** ### listShippingLines() > **listShippingLines**(`search?`, `options?`): `Promise`\<`any`> List supported shipping lines, optionally filtered by search text. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `search?` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### listTrackingRequests() > **listTrackingRequests**(`filters?`, `options?`): `Promise`\<`any`> List tracking requests with optional filters and pagination. #### Parameters | Parameter | Type | | ---------- | --------------------------------------------------------------------------------------------------------- | | `filters` | [`TrackingRequestListFilters`](/docs/sdk/reference/client/managers/interfaces/TrackingRequestListFilters) | | `options?` | [`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions) | #### Returns `Promise`\<`any`> *** ### listTrackRequests() > **listTrackRequests**(`filters?`, `options?`): `Promise`\<`any`> Alias for [listTrackingRequests](#listtrackingrequests). #### Parameters | Parameter | Type | | ---------- | --------------------------------------------------------------------------------------------------------- | | `filters` | [`TrackingRequestListFilters`](/docs/sdk/reference/client/managers/interfaces/TrackingRequestListFilters) | | `options?` | [`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions) | #### Returns `Promise`\<`any`> *** ### listWebhookNotifications() > **listWebhookNotifications**(`options?`): `Promise`\<`any`> List webhook notifications. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `options?` | [`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions) | #### Returns `Promise`\<`any`> *** ### listWebhooks() > **listWebhooks**(`options?`): `Promise`\<`any`> List webhooks. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `options?` | [`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions) | #### Returns `Promise`\<`any`> *** ### refreshContainer() > **refreshContainer**(`id`, `options?`): `Promise`\<`any`> Request an immediate refresh for a container. This may require a paid Terminal49 feature. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### resumeTrackingShipment() > **resumeTrackingShipment**(`id`, `options?`): `Promise`\<`any`> Resume tracking a previously stopped shipment. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### search() > **search**(`query`): `Promise`\<`any`> Search across shipments and containers by number, reference, or keyword. #### Parameters | Parameter | Type | | --------- | -------- | | `query` | `string` | #### Returns `Promise`\<`any`> *** ### stopTrackingShipment() > **stopTrackingShipment**(`id`, `options?`): `Promise`\<`any`> Stop tracking a shipment and its containers. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### trackContainer() > **trackContainer**(`params`): `Promise`\<`any`> Convenience helper for creating a tracking request from a container or booking number. #### Parameters | Parameter | Type | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `params` | \{ `autoDetectVoccScac?`: `boolean`; `bookingNumber?`: `string`; `containerNumber?`: `string`; `refNumbers?`: `string`\[]; `scac?`: `string`; } | | `params.autoDetectVoccScac?` | `boolean` | | `params.bookingNumber?` | `string` | | `params.containerNumber?` | `string` | | `params.refNumbers?` | `string`\[] | | `params.scac?` | `string` | #### Returns `Promise`\<`any`> *** ### updateCustomField() > **updateCustomField**(`id`, `payload`, `options?`): `Promise`\<`any`> Update a custom field assignment. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `payload` | `Record`\<`string`, `unknown`> | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### updateCustomFieldDefinition() > **updateCustomFieldDefinition**(`id`, `payload`, `options?`): `Promise`\<`any`> Update a custom field definition. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `payload` | `Record`\<`string`, `unknown`> | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### updateCustomFieldOption() > **updateCustomFieldOption**(`definitionId`, `optionId`, `payload`, `options?`): `Promise`\<`any`> Update a custom field option. #### Parameters | Parameter | Type | | -------------- | ------------------------------------------------------------------------- | | `definitionId` | `string` | | `optionId` | `string` | | `payload` | `Record`\<`string`, `unknown`> | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### updateShipment() > **updateShipment**(`id`, `attrs`, `options?`): `Promise`\<`any`> Update shipment attributes such as reference numbers or tags. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `attrs` | `Record`\<`string`, `any`> | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### updateTrackingRequest() > **updateTrackingRequest**(`id`, `attrs`, `options?`): `Promise`\<`any`> Update tracking request attributes. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `attrs` | `Record`\<`string`, `any`> | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### updateWebhook() > **updateWebhook**(`id`, `payload`, `options?`): `Promise`\<`any`> Update a webhook. #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `payload` | `Record`\<`string`, `unknown`> | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> # Class: Terminal49Error Source: https://terminal49.com/docs/sdk/reference/client/classes/Terminal49Error Terminal49Error base class for all Terminal49 TypeScript SDK errors, subclassed by status-specific errors like RateLimitError and ValidationError. # Class: Terminal49Error Base error for all Terminal49 API errors. Subclassed by status-specific errors. ## Extends * `Error` ## Extended by * [`AuthenticationError`](/docs/sdk/reference/client/classes/AuthenticationError) * [`AuthorizationError`](/docs/sdk/reference/client/classes/AuthorizationError) * [`NetworkError`](/docs/sdk/reference/client/classes/NetworkError) * [`NotFoundError`](/docs/sdk/reference/client/classes/NotFoundError) * [`RateLimitError`](/docs/sdk/reference/client/classes/RateLimitError) * [`TimeoutError`](/docs/sdk/reference/client/classes/TimeoutError) * [`UpstreamError`](/docs/sdk/reference/client/classes/UpstreamError) * [`ValidationError`](/docs/sdk/reference/client/classes/ValidationError) ## Constructors ### Constructor > **new Terminal49Error**(`message`, `status?`, `details?`): `Terminal49Error` #### Parameters | Parameter | Type | | ---------- | --------- | | `message` | `string` | | `status?` | `number` | | `details?` | `unknown` | #### Returns `Terminal49Error` #### Overrides `Error.constructor` ## Properties | Property | Modifier | Type | Description | Inherited from | | ------------------ | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | `cause?` | `public` | `unknown` | - | `Error.cause` | | `details?` | `public` | `unknown` | - | - | | `message` | `public` | `string` | - | `Error.message` | | `name` | `public` | `string` | - | `Error.name` | | `stack?` | `public` | `string` | - | `Error.stack` | | `status?` | `public` | `number` | - | - | | `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured *after* the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | `Error.stackTraceLimit` | ## Methods ### captureStackTrace() > `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` Creates a `.stack` property on `targetObject`, which when accessed returns a string representing the location in the code at which `Error.captureStackTrace()` was called. ```js theme={null} const myObject = {}; Error.captureStackTrace(myObject); myObject.stack; // Similar to `new Error().stack` ``` The first line of the trace will be prefixed with `${myObject.name}: ${myObject.message}`. The optional `constructorOpt` argument accepts a function. If given, all frames above `constructorOpt`, including `constructorOpt`, will be omitted from the generated stack trace. The `constructorOpt` argument is useful for hiding implementation details of error generation from the user. For instance: ```js theme={null} function a() { b(); } function b() { c(); } function c() { // Create an error without stack trace to avoid calculating the stack trace twice. const { stackTraceLimit } = Error; Error.stackTraceLimit = 0; const error = new Error(); Error.stackTraceLimit = stackTraceLimit; // Capture the stack trace above function b Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace throw error; } a(); ``` #### Parameters | Parameter | Type | | ----------------- | ---------- | | `targetObject` | `object` | | `constructorOpt?` | `Function` | #### Returns `void` #### Inherited from `Error.captureStackTrace` *** ### prepareStackTrace() > `static` **prepareStackTrace**(`err`, `stackTraces`): `any` #### Parameters | Parameter | Type | | ------------- | ------------- | | `err` | `Error` | | `stackTraces` | `CallSite`\[] | #### Returns `any` #### See [https://v8.dev/docs/stack-trace-api#customizing-stack-traces](https://v8.dev/docs/stack-trace-api#customizing-stack-traces) #### Inherited from `Error.prepareStackTrace` # Class: TimeoutError Source: https://terminal49.com/docs/sdk/reference/client/classes/TimeoutError TimeoutError class in the Terminal49 TypeScript SDK, thrown when a request exceeds the configured request timeout and is aborted before a response arrives. # Class: TimeoutError Thrown when a request exceeds the configured request timeout and is aborted by the SDK. Has no HTTP status because no response was produced. ## Extends * [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error) ## Constructors ### Constructor > **new TimeoutError**(`message?`, `details?`): `TimeoutError` #### Parameters | Parameter | Type | Default value | | ---------- | --------- | --------------------- | | `message` | `string` | `'Request timed out'` | | `details?` | `unknown` | `undefined` | #### Returns `TimeoutError` #### Overrides [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`constructor`](/docs/sdk/reference/client/classes/Terminal49Error#constructor) ## Properties | Property | Modifier | Type | Description | Inherited from | | ------------------ | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `cause?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`cause`](/docs/sdk/reference/client/classes/Terminal49Error#property-cause) | | `details?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`details`](/docs/sdk/reference/client/classes/Terminal49Error#property-details) | | `message` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`message`](/docs/sdk/reference/client/classes/Terminal49Error#property-message) | | `name` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`name`](/docs/sdk/reference/client/classes/Terminal49Error#property-name) | | `stack?` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stack`](/docs/sdk/reference/client/classes/Terminal49Error#property-stack) | | `status?` | `public` | `number` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`status`](/docs/sdk/reference/client/classes/Terminal49Error#property-status) | | `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured *after* the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stackTraceLimit`](/docs/sdk/reference/client/classes/Terminal49Error#property-stacktracelimit) | ## Methods ### captureStackTrace() > `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` Creates a `.stack` property on `targetObject`, which when accessed returns a string representing the location in the code at which `Error.captureStackTrace()` was called. ```js theme={null} const myObject = {}; Error.captureStackTrace(myObject); myObject.stack; // Similar to `new Error().stack` ``` The first line of the trace will be prefixed with `${myObject.name}: ${myObject.message}`. The optional `constructorOpt` argument accepts a function. If given, all frames above `constructorOpt`, including `constructorOpt`, will be omitted from the generated stack trace. The `constructorOpt` argument is useful for hiding implementation details of error generation from the user. For instance: ```js theme={null} function a() { b(); } function b() { c(); } function c() { // Create an error without stack trace to avoid calculating the stack trace twice. const { stackTraceLimit } = Error; Error.stackTraceLimit = 0; const error = new Error(); Error.stackTraceLimit = stackTraceLimit; // Capture the stack trace above function b Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace throw error; } a(); ``` #### Parameters | Parameter | Type | | ----------------- | ---------- | | `targetObject` | `object` | | `constructorOpt?` | `Function` | #### Returns `void` #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`captureStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#capturestacktrace) *** ### prepareStackTrace() > `static` **prepareStackTrace**(`err`, `stackTraces`): `any` #### Parameters | Parameter | Type | | ------------- | ------------- | | `err` | `Error` | | `stackTraces` | `CallSite`\[] | #### Returns `any` #### See [https://v8.dev/docs/stack-trace-api#customizing-stack-traces](https://v8.dev/docs/stack-trace-api#customizing-stack-traces) #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`prepareStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#preparestacktrace) # Class: UpstreamError Source: https://terminal49.com/docs/sdk/reference/client/classes/UpstreamError UpstreamError class in the Terminal49 TypeScript SDK, thrown when a carrier or terminal upstream API is unavailable and the request returns an HTTP 5xx status. # Class: UpstreamError Thrown when the carrier or terminal upstream API is unavailable (HTTP 5xx). ## Extends * [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error) ## Constructors ### Constructor > **new UpstreamError**(`message`, `status?`, `details?`): `UpstreamError` #### Parameters | Parameter | Type | Default value | | ---------- | --------- | ------------- | | `message` | `string` | `undefined` | | `status` | `number` | `500` | | `details?` | `unknown` | `undefined` | #### Returns `UpstreamError` #### Overrides [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`constructor`](/docs/sdk/reference/client/classes/Terminal49Error#constructor) ## Properties | Property | Modifier | Type | Description | Inherited from | | ------------------ | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `cause?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`cause`](/docs/sdk/reference/client/classes/Terminal49Error#property-cause) | | `details?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`details`](/docs/sdk/reference/client/classes/Terminal49Error#property-details) | | `message` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`message`](/docs/sdk/reference/client/classes/Terminal49Error#property-message) | | `name` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`name`](/docs/sdk/reference/client/classes/Terminal49Error#property-name) | | `stack?` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stack`](/docs/sdk/reference/client/classes/Terminal49Error#property-stack) | | `status?` | `public` | `number` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`status`](/docs/sdk/reference/client/classes/Terminal49Error#property-status) | | `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured *after* the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stackTraceLimit`](/docs/sdk/reference/client/classes/Terminal49Error#property-stacktracelimit) | ## Methods ### captureStackTrace() > `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` Creates a `.stack` property on `targetObject`, which when accessed returns a string representing the location in the code at which `Error.captureStackTrace()` was called. ```js theme={null} const myObject = {}; Error.captureStackTrace(myObject); myObject.stack; // Similar to `new Error().stack` ``` The first line of the trace will be prefixed with `${myObject.name}: ${myObject.message}`. The optional `constructorOpt` argument accepts a function. If given, all frames above `constructorOpt`, including `constructorOpt`, will be omitted from the generated stack trace. The `constructorOpt` argument is useful for hiding implementation details of error generation from the user. For instance: ```js theme={null} function a() { b(); } function b() { c(); } function c() { // Create an error without stack trace to avoid calculating the stack trace twice. const { stackTraceLimit } = Error; Error.stackTraceLimit = 0; const error = new Error(); Error.stackTraceLimit = stackTraceLimit; // Capture the stack trace above function b Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace throw error; } a(); ``` #### Parameters | Parameter | Type | | ----------------- | ---------- | | `targetObject` | `object` | | `constructorOpt?` | `Function` | #### Returns `void` #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`captureStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#capturestacktrace) *** ### prepareStackTrace() > `static` **prepareStackTrace**(`err`, `stackTraces`): `any` #### Parameters | Parameter | Type | | ------------- | ------------- | | `err` | `Error` | | `stackTraces` | `CallSite`\[] | #### Returns `any` #### See [https://v8.dev/docs/stack-trace-api#customizing-stack-traces](https://v8.dev/docs/stack-trace-api#customizing-stack-traces) #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`prepareStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#preparestacktrace) # Class: ValidationError Source: https://terminal49.com/docs/sdk/reference/client/classes/ValidationError ValidationError class in the Terminal49 TypeScript SDK, thrown when a request payload fails server-side validation and the API returns HTTP 400 or 422. # Class: ValidationError Thrown when the request payload fails server-side validation (HTTP 400/422). ## Extends * [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error) ## Constructors ### Constructor > **new ValidationError**(`message`, `status?`, `details?`): `ValidationError` #### Parameters | Parameter | Type | Default value | | ---------- | --------- | ------------- | | `message` | `string` | `undefined` | | `status` | `number` | `400` | | `details?` | `unknown` | `undefined` | #### Returns `ValidationError` #### Overrides [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`constructor`](/docs/sdk/reference/client/classes/Terminal49Error#constructor) ## Properties | Property | Modifier | Type | Description | Inherited from | | ------------------ | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `cause?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`cause`](/docs/sdk/reference/client/classes/Terminal49Error#property-cause) | | `details?` | `public` | `unknown` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`details`](/docs/sdk/reference/client/classes/Terminal49Error#property-details) | | `message` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`message`](/docs/sdk/reference/client/classes/Terminal49Error#property-message) | | `name` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`name`](/docs/sdk/reference/client/classes/Terminal49Error#property-name) | | `stack?` | `public` | `string` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stack`](/docs/sdk/reference/client/classes/Terminal49Error#property-stack) | | `status?` | `public` | `number` | - | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`status`](/docs/sdk/reference/client/classes/Terminal49Error#property-status) | | `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured *after* the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`stackTraceLimit`](/docs/sdk/reference/client/classes/Terminal49Error#property-stacktracelimit) | ## Methods ### captureStackTrace() > `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` Creates a `.stack` property on `targetObject`, which when accessed returns a string representing the location in the code at which `Error.captureStackTrace()` was called. ```js theme={null} const myObject = {}; Error.captureStackTrace(myObject); myObject.stack; // Similar to `new Error().stack` ``` The first line of the trace will be prefixed with `${myObject.name}: ${myObject.message}`. The optional `constructorOpt` argument accepts a function. If given, all frames above `constructorOpt`, including `constructorOpt`, will be omitted from the generated stack trace. The `constructorOpt` argument is useful for hiding implementation details of error generation from the user. For instance: ```js theme={null} function a() { b(); } function b() { c(); } function c() { // Create an error without stack trace to avoid calculating the stack trace twice. const { stackTraceLimit } = Error; Error.stackTraceLimit = 0; const error = new Error(); Error.stackTraceLimit = stackTraceLimit; // Capture the stack trace above function b Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace throw error; } a(); ``` #### Parameters | Parameter | Type | | ----------------- | ---------- | | `targetObject` | `object` | | `constructorOpt?` | `Function` | #### Returns `void` #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`captureStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#capturestacktrace) *** ### prepareStackTrace() > `static` **prepareStackTrace**(`err`, `stackTraces`): `any` #### Parameters | Parameter | Type | | ------------- | ------------- | | `err` | `Error` | | `stackTraces` | `CallSite`\[] | #### Returns `any` #### See [https://v8.dev/docs/stack-trace-api#customizing-stack-traces](https://v8.dev/docs/stack-trace-api#customizing-stack-traces) #### Inherited from [`Terminal49Error`](/docs/sdk/reference/client/classes/Terminal49Error).[`prepareStackTrace`](/docs/sdk/reference/client/classes/Terminal49Error#preparestacktrace) # client module: Terminal49 TypeScript SDK reference Source: https://terminal49.com/docs/sdk/reference/client/index Reference index for the Terminal49 TypeScript SDK client module, listing the Terminal49Client class, configuration interface, and the API error class hierarchy. # client ## Classes | Class | Description | | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [AuthenticationError](/docs/sdk/reference/client/classes/AuthenticationError) | Thrown when the API token is invalid or missing (HTTP 401). | | [AuthorizationError](/docs/sdk/reference/client/classes/AuthorizationError) | Thrown when the API token is valid but lacks permission for the request (HTTP 403). | | [FeatureNotEnabledError](/docs/sdk/reference/client/classes/FeatureNotEnabledError) | Thrown when the requested feature requires a plan upgrade (HTTP 403). | | [NetworkError](/docs/sdk/reference/client/classes/NetworkError) | Thrown when a transport-level failure occurs before a response is received — a DNS failure, a refused/reset connection, or an otherwise failed `fetch`. Has no HTTP status because no response was produced. | | [NotFoundError](/docs/sdk/reference/client/classes/NotFoundError) | Thrown when the requested resource does not exist (HTTP 404). | | [RateLimitError](/docs/sdk/reference/client/classes/RateLimitError) | Thrown when the API rate limit has been exceeded (HTTP 429). The SDK retries automatically. | | [Terminal49Client](/docs/sdk/reference/client/classes/Terminal49Client) | Server-side TypeScript client for the Terminal49 JSON:API. | | [Terminal49Error](/docs/sdk/reference/client/classes/Terminal49Error) | Base error for all Terminal49 API errors. Subclassed by status-specific errors. | | [TimeoutError](/docs/sdk/reference/client/classes/TimeoutError) | Thrown when a request exceeds the configured request timeout and is aborted by the SDK. Has no HTTP status because no response was produced. | | [UpstreamError](/docs/sdk/reference/client/classes/UpstreamError) | Thrown when the carrier or terminal upstream API is unavailable (HTTP 5xx). | | [ValidationError](/docs/sdk/reference/client/classes/ValidationError) | Thrown when the request payload fails server-side validation (HTTP 400/422). | ## Interfaces | Interface | Description | | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | [Terminal49ClientConfig](/docs/sdk/reference/client/interfaces/Terminal49ClientConfig) | Configuration for [Terminal49Client](/docs/sdk/reference/client/classes/Terminal49Client). | # Class: AuthInterceptor Source: https://terminal49.com/docs/sdk/reference/client/interceptors/classes/AuthInterceptor AuthInterceptor reference for the Terminal49 TypeScript SDK, attaching the API token as a bearer Authorization header on every outgoing request from the client. # Class: AuthInterceptor ## Constructors ### Constructor > **new AuthInterceptor**(`apiToken`, `accountId?`): `AuthInterceptor` #### Parameters | Parameter | Type | | ------------ | -------- | | `apiToken` | `string` | | `accountId?` | `string` | #### Returns `AuthInterceptor` ## Methods ### onRequest() > **onRequest**(`__namedParameters`): `Request` #### Parameters | Parameter | Type | | ------------------- | ---------------------------------------------------------- | | `__namedParameters` | `Pick`\<`MiddlewareCallbackParams`, `"id"` \| `"request"`> | #### Returns `Request` # Class: ErrorMappingInterceptor Source: https://terminal49.com/docs/sdk/reference/client/interceptors/classes/ErrorMappingInterceptor ErrorMappingInterceptor in the Terminal49 TypeScript SDK maps non-2xx HTTP responses to typed Terminal49Error subclasses like RateLimit and ValidationError. # Class: ErrorMappingInterceptor ## Constructors ### Constructor > **new ErrorMappingInterceptor**(): `ErrorMappingInterceptor` #### Returns `ErrorMappingInterceptor` ## Methods ### onResponse() > **onResponse**(`__namedParameters`): `Promise`\<`Response`> #### Parameters | Parameter | Type | | ------------------- | --------------------------------------------------------------------- | | `__namedParameters` | `Pick`\<`MiddlewareCallbackParams`, `"id"` \| `"request"`> & `object` | #### Returns `Promise`\<`Response`> # Class: RetryInterceptor Source: https://terminal49.com/docs/sdk/reference/client/interceptors/classes/RetryInterceptor RetryInterceptor class in the Terminal49 TypeScript SDK, automatically retrying failed requests on rate-limit and server errors with exponential backoff. # Class: RetryInterceptor Retries transient failures with backoff. Two kinds of failure are handled: * A response with a retryable status (429 / 5xx), handled in `onResponse`. * A thrown transport error (DNS/connection/"fetch failed"), handled in `onError` — these never reach `onResponse` because `fetch` rejected. Retries are gated by shouldRetryRequest: idempotent methods are always eligible, but non-idempotent writes are only retried when the caller supplied an `Idempotency-Key` header. 429 backoff honors the server's `Retry-After`. ## Constructors ### Constructor > **new RetryInterceptor**(`maxRetries`, `fetchImpl?`): `RetryInterceptor` #### Parameters | Parameter | Type | Default value | | ------------ | -------------------------------------------- | ------------- | | `maxRetries` | `number` | `undefined` | | `fetchImpl` | (`input`, `init?`) => `Promise`\<`Response`> | `fetch` | #### Returns `RetryInterceptor` ## Methods ### onError() > **onError**(`__namedParameters`): `Promise`\<`Error` | `Response`> Recover from a thrown transport error by retrying eligible requests. If a retry produces a response we return it (openapi-fetch then runs the normal `onResponse` chain on it); otherwise we surface a normalized NetworkError so error mapping is consistent with the response path. #### Parameters | Parameter | Type | | ------------------- | --------------------------------------------------------------------- | | `__namedParameters` | `Pick`\<`MiddlewareCallbackParams`, `"id"` \| `"request"`> & `object` | #### Returns `Promise`\<`Error` | `Response`> *** ### onRequest() > **onRequest**(`__namedParameters`): `Request` #### Parameters | Parameter | Type | | ------------------- | ---------------------------------------------------------- | | `__namedParameters` | `Pick`\<`MiddlewareCallbackParams`, `"id"` \| `"request"`> | #### Returns `Request` *** ### onResponse() > **onResponse**(`__namedParameters`): `Promise`\<`Response`> #### Parameters | Parameter | Type | | ------------------- | --------------------------------------------------------------------- | | `__namedParameters` | `Pick`\<`MiddlewareCallbackParams`, `"id"` \| `"request"`> & `object` | #### Returns `Promise`\<`Response`> # client/interceptors: Terminal49 SDK request interceptors Source: https://terminal49.com/docs/sdk/reference/client/interceptors/index Reference for the Terminal49 TypeScript SDK interceptors module, covering authentication, retry, and error-mapping middleware applied to every API request. # client/interceptors ## Classes | Class | Description | | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | [AuthInterceptor](/docs/sdk/reference/client/interceptors/classes/AuthInterceptor) | - | | [ErrorMappingInterceptor](/docs/sdk/reference/client/interceptors/classes/ErrorMappingInterceptor) | - | | [RetryInterceptor](/docs/sdk/reference/client/interceptors/classes/RetryInterceptor) | Retries transient failures with backoff. Two kinds of failure are handled: | ## Type Aliases | Type Alias | Description | | ------------------------------------------------------------------------------- | ----------- | | [Interceptor](/docs/sdk/reference/client/interceptors/type-aliases/Interceptor) | - | # Type Alias: Interceptor Source: https://terminal49.com/docs/sdk/reference/client/interceptors/type-aliases/Interceptor Interceptor type alias in the Terminal49 TypeScript SDK, defining the openapi-fetch middleware shape used by auth, retry, and error-mapping interceptors. # Type Alias: Interceptor > **Interceptor** = `Middleware` # Interface: Terminal49ClientConfig Source: https://terminal49.com/docs/sdk/reference/client/interfaces/Terminal49ClientConfig Terminal49ClientConfig interface for the TypeScript SDK, configuring API token, base URL, default response format, retry count, and custom fetch implementation. # Interface: Terminal49ClientConfig Configuration for [Terminal49Client](/docs/sdk/reference/client/classes/Terminal49Client). ## Properties | Property | Type | Description | | ----------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `accountId?` | `string` | Account id to send as `x-account-id` for user-scoped bearer tokens. | | `apiBaseUrl?` | `string` | API base URL. Defaults to `https://api.terminal49.com/v2`. | | `apiToken` | `string` | Terminal49 API token. Pass either the raw token or a value prefixed with `Token ` or `Bearer `. | | `defaultFormat?` | [`ResponseFormat`](/docs/sdk/reference/types/options/type-aliases/ResponseFormat) | Default response format for methods that support mapped responses. Defaults to `raw`. | | `fetchImpl?` | (`input`, `init?`) => `Promise`\<`Response`> | Optional fetch implementation, useful for tests or custom runtimes. | | `maxRetries?` | `number` | Number of retry attempts for rate-limit and server errors. Defaults to `2`. | | `timeoutMs?` | `number` | Per-request timeout in milliseconds. A hung request is aborted once it elapses and rejected with a `TimeoutError`. Defaults to `30000`. Set to `0` to disable the timeout. | # Class: BaseManager Source: https://terminal49.com/docs/sdk/reference/client/managers/classes/BaseManager BaseManager class in the Terminal49 TypeScript SDK, the shared base for ContainerManager, ShipmentManager, ShippingLineManager, and TrackingRequestManager. # Class: BaseManager ## Extended by * [`ContainerManager`](/docs/sdk/reference/client/managers/classes/ContainerManager) * [`ShipmentManager`](/docs/sdk/reference/client/managers/classes/ShipmentManager) * [`ShippingLineManager`](/docs/sdk/reference/client/managers/classes/ShippingLineManager) * [`TrackingRequestManager`](/docs/sdk/reference/client/managers/classes/TrackingRequestManager) ## Constructors ### Constructor > **new BaseManager**(`transport`, `defaultFormat?`): `BaseManager` #### Parameters | Parameter | Type | Default value | | --------------- | --------------------------------------------------------------------------------- | ------------- | | `transport` | [`Transport`](/docs/sdk/reference/client/transport/classes/Transport) | `undefined` | | `defaultFormat` | [`ResponseFormat`](/docs/sdk/reference/types/options/type-aliases/ResponseFormat) | `'raw'` | #### Returns `BaseManager` # Class: ContainerManager Source: https://terminal49.com/docs/sdk/reference/client/managers/classes/ContainerManager ContainerManager reference for the Terminal49 TypeScript SDK, used to list containers, fetch container details, and retrieve raw and mapped transport events. # Class: ContainerManager ## Extends * [`BaseManager`](/docs/sdk/reference/client/managers/classes/BaseManager) ## Constructors ### Constructor > **new ContainerManager**(`transport`, `defaultFormat?`): `ContainerManager` #### Parameters | Parameter | Type | Default value | | --------------- | --------------------------------------------------------------------------------- | ------------- | | `transport` | [`Transport`](/docs/sdk/reference/client/transport/classes/Transport) | `undefined` | | `defaultFormat` | [`ResponseFormat`](/docs/sdk/reference/types/options/type-aliases/ResponseFormat) | `'raw'` | #### Returns `ContainerManager` #### Inherited from [`BaseManager`](/docs/sdk/reference/client/managers/classes/BaseManager).[`constructor`](/docs/sdk/reference/client/managers/classes/BaseManager#constructor) ## Methods ### customFields() > **customFields**(`id`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### demurrage() > **demurrage**(`id`): `Promise`\<`any`> #### Parameters | Parameter | Type | | --------- | -------- | | `id` | `string` | #### Returns `Promise`\<`any`> *** ### events() > **events**(`id`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### get() > **get**(`id`, `include?`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | `string` | | `include` | [`IncludeParam`](/docs/sdk/reference/types/options/type-aliases/IncludeParam)\<[`ContainerInclude`](/docs/sdk/reference/types/options/type-aliases/ContainerInclude)> | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### iterate() > **iterate**(`filters?`, `options?`): `AsyncGenerator`\<[`Container`](/docs/sdk/reference/types/models/interfaces/Container), `void`, `unknown`> #### Parameters | Parameter | Type | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `filters` | \{ `carrier?`: `string`; `include?`: [`IncludeParam`](/docs/sdk/reference/types/options/type-aliases/IncludeParam)\<[`ContainerInclude`](/docs/sdk/reference/types/options/type-aliases/ContainerInclude)>; `port?`: `string`; `status?`: `string`; `updatedAfter?`: `string`; } \| `undefined` | | `options?` | `Omit`\<[`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions), `"page"`> | #### Returns `AsyncGenerator`\<[`Container`](/docs/sdk/reference/types/models/interfaces/Container), `void`, `unknown`> *** ### list() > **list**(`filters?`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `filters` | \{ `carrier?`: `string`; `include?`: [`IncludeParam`](/docs/sdk/reference/types/options/type-aliases/IncludeParam)\<[`ContainerInclude`](/docs/sdk/reference/types/options/type-aliases/ContainerInclude)>; `port?`: `string`; `status?`: `string`; `updatedAfter?`: `string`; } | | `filters.carrier?` | `string` | | `filters.include?` | [`IncludeParam`](/docs/sdk/reference/types/options/type-aliases/IncludeParam)\<[`ContainerInclude`](/docs/sdk/reference/types/options/type-aliases/ContainerInclude)> | | `filters.port?` | `string` | | `filters.status?` | `string` | | `filters.updatedAfter?` | `string` | | `options?` | [`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions) | #### Returns `Promise`\<`any`> *** ### map() > **map**(`id`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### rawEvents() > **rawEvents**(`id`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### refresh() > **refresh**(`id`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### route() > **route**(`id`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### setCustomField() > **setCustomField**(`id`, `fieldId`, `value`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `fieldId` | `string` | | `value` | `unknown` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> # Class: ShipmentManager Source: https://terminal49.com/docs/sdk/reference/client/managers/classes/ShipmentManager ShipmentManager class in the Terminal49 TypeScript SDK, used to list shipments, fetch a shipment by ID, and include related containers, ports, and terminals. # Class: ShipmentManager ## Extends * [`BaseManager`](/docs/sdk/reference/client/managers/classes/BaseManager) ## Constructors ### Constructor > **new ShipmentManager**(`transport`, `defaultFormat?`): `ShipmentManager` #### Parameters | Parameter | Type | Default value | | --------------- | --------------------------------------------------------------------------------- | ------------- | | `transport` | [`Transport`](/docs/sdk/reference/client/transport/classes/Transport) | `undefined` | | `defaultFormat` | [`ResponseFormat`](/docs/sdk/reference/types/options/type-aliases/ResponseFormat) | `'raw'` | #### Returns `ShipmentManager` #### Inherited from [`BaseManager`](/docs/sdk/reference/client/managers/classes/BaseManager).[`constructor`](/docs/sdk/reference/client/managers/classes/BaseManager#constructor) ## Methods ### customFields() > **customFields**(`id`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### get() > **get**(`id`, `includeContainers?`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | Default value | | ------------------- | ------------------------------------------------------------------------------------ | ------------- | | `id` | `string` | `undefined` | | `includeContainers` | `boolean` | `true` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) & `object` | `undefined` | #### Returns `Promise`\<`any`> *** ### iterate() > **iterate**(`filters?`, `options?`): `AsyncGenerator`\<[`Shipment`](/docs/sdk/reference/types/models/interfaces/Shipment), `void`, `unknown`> #### Parameters | Parameter | Type | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `filters` | \{ `carrier?`: `string`; `include?`: [`IncludeParam`](/docs/sdk/reference/types/options/type-aliases/IncludeParam)\<[`ShipmentInclude`](/docs/sdk/reference/types/options/type-aliases/ShipmentInclude)>; `includeContainers?`: `boolean`; `number?`: `string`; `port?`: `string`; `status?`: `string`; `trackingStopped?`: `boolean`; `updatedAfter?`: `string`; } \| `undefined` | | `options?` | `Omit`\<[`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions), `"page"`> | #### Returns `AsyncGenerator`\<[`Shipment`](/docs/sdk/reference/types/models/interfaces/Shipment), `void`, `unknown`> *** ### list() > **list**(`filters?`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | Description | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `filters` | \{ `carrier?`: `string`; `include?`: [`IncludeParam`](/docs/sdk/reference/types/options/type-aliases/IncludeParam)\<[`ShipmentInclude`](/docs/sdk/reference/types/options/type-aliases/ShipmentInclude)>; `includeContainers?`: `boolean`; `number?`: `string`; `port?`: `string`; `status?`: `string`; `trackingStopped?`: `boolean`; `updatedAfter?`: `string`; } | - | | `filters.carrier?` | `string` | - | | `filters.include?` | [`IncludeParam`](/docs/sdk/reference/types/options/type-aliases/IncludeParam)\<[`ShipmentInclude`](/docs/sdk/reference/types/options/type-aliases/ShipmentInclude)> | - | | `filters.includeContainers?` | `boolean` | - | | `filters.number?` | `string` | Search shipments by the original tracking `request_number`. | | `filters.port?` | `string` | - | | `filters.status?` | `string` | - | | `filters.trackingStopped?` | `boolean` | Filter shipments by whether they are still tracking. Maps to the supported `filter[tracking_stopped]`. | | `filters.updatedAfter?` | `string` | - | | `options?` | [`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions) | - | #### Returns `Promise`\<`any`> *** ### resumeTracking() > **resumeTracking**(`id`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### setCustomField() > **setCustomField**(`id`, `fieldId`, `value`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `fieldId` | `string` | | `value` | `unknown` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### stopTracking() > **stopTracking**(`id`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> *** ### update() > **update**(`id`, `attrs`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `attrs` | `Record`\<`string`, `any`> | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> # Class: ShippingLineManager Source: https://terminal49.com/docs/sdk/reference/client/managers/classes/ShippingLineManager ShippingLineManager reference for the Terminal49 TypeScript SDK, providing methods to list supported ocean carriers and look up a single shipping line by SCAC. # Class: ShippingLineManager ## Extends * [`BaseManager`](/docs/sdk/reference/client/managers/classes/BaseManager) ## Constructors ### Constructor > **new ShippingLineManager**(`transport`, `defaultFormat?`): `ShippingLineManager` #### Parameters | Parameter | Type | Default value | | --------------- | --------------------------------------------------------------------------------- | ------------- | | `transport` | [`Transport`](/docs/sdk/reference/client/transport/classes/Transport) | `undefined` | | `defaultFormat` | [`ResponseFormat`](/docs/sdk/reference/types/options/type-aliases/ResponseFormat) | `'raw'` | #### Returns `ShippingLineManager` #### Inherited from [`BaseManager`](/docs/sdk/reference/client/managers/classes/BaseManager).[`constructor`](/docs/sdk/reference/client/managers/classes/BaseManager#constructor) ## Methods ### list() > **list**(`search?`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `search?` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> # Class: TrackingRequestManager Source: https://terminal49.com/docs/sdk/reference/client/managers/classes/TrackingRequestManager TrackingRequestManager in the Terminal49 TypeScript SDK, used to create tracking requests by bill of lading, container, or booking number and list their status. # Class: TrackingRequestManager ## Extends * [`BaseManager`](/docs/sdk/reference/client/managers/classes/BaseManager) ## Constructors ### Constructor > **new TrackingRequestManager**(`transport`, `defaultFormat?`): `TrackingRequestManager` #### Parameters | Parameter | Type | Default value | | --------------- | --------------------------------------------------------------------------------- | ------------- | | `transport` | [`Transport`](/docs/sdk/reference/client/transport/classes/Transport) | `undefined` | | `defaultFormat` | [`ResponseFormat`](/docs/sdk/reference/types/options/type-aliases/ResponseFormat) | `'raw'` | #### Returns `TrackingRequestManager` #### Inherited from [`BaseManager`](/docs/sdk/reference/client/managers/classes/BaseManager).[`constructor`](/docs/sdk/reference/client/managers/classes/BaseManager#constructor) ## Methods ### create() > **create**(`params`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `params` | \{ `autoDetectVoccScac?`: `boolean`; `refNumbers?`: `string`\[]; `requestNumber`: `string`; `requestType`: [`TrackingRequestType`](/docs/sdk/reference/client/managers/type-aliases/TrackingRequestType); `scac?`: `string`; `shipmentTags?`: `string`\[]; } | | `params.autoDetectVoccScac?` | `boolean` | | `params.refNumbers?` | `string`\[] | | `params.requestNumber` | `string` | | `params.requestType` | [`TrackingRequestType`](/docs/sdk/reference/client/managers/type-aliases/TrackingRequestType) | | `params.scac?` | `string` | | `params.shipmentTags?` | `string`\[] | #### Returns `Promise`\<`any`> *** ### createFromInfer() > **createFromInfer**(`number`, `options?`): `Promise`\<\{ `infer`: `any`; `trackingRequest`: `any`; }> #### Parameters | Parameter | Type | | --------- | ------------------------------------------------------------------------------------------------------------------------------- | | `number` | `string` | | `options` | [`CreateTrackingRequestFromInferOptions`](/docs/sdk/reference/client/managers/interfaces/CreateTrackingRequestFromInferOptions) | #### Returns `Promise`\<\{ `infer`: `any`; `trackingRequest`: `any`; }> *** ### get() > **get**(`id`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------------------ | | `id` | `string` | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) & `object` | #### Returns `Promise`\<`any`> *** ### inferNumber() > **inferNumber**(`number`): `Promise`\<`any`> #### Parameters | Parameter | Type | | --------- | -------- | | `number` | `string` | #### Returns `Promise`\<`any`> *** ### iterate() > **iterate**(`filters?`, `options?`): `AsyncGenerator`\<[`TrackingRequest`](/docs/sdk/reference/types/models/interfaces/TrackingRequest), `void`, `unknown`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------------------------------------------------------ | | `filters` | [`TrackingRequestListFilters`](/docs/sdk/reference/client/managers/interfaces/TrackingRequestListFilters) \| `undefined` | | `options?` | `Omit`\<[`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions), `"page"`> | #### Returns `AsyncGenerator`\<[`TrackingRequest`](/docs/sdk/reference/types/models/interfaces/TrackingRequest), `void`, `unknown`> *** ### list() > **list**(`filters?`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | --------------------------------------------------------------------------------------------------------- | | `filters` | [`TrackingRequestListFilters`](/docs/sdk/reference/client/managers/interfaces/TrackingRequestListFilters) | | `options?` | [`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions) | #### Returns `Promise`\<`any`> *** ### update() > **update**(`id`, `attrs`, `options?`): `Promise`\<`any`> #### Parameters | Parameter | Type | | ---------- | ------------------------------------------------------------------------- | | `id` | `string` | | `attrs` | `Record`\<`string`, `any`> | | `options?` | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) | #### Returns `Promise`\<`any`> # client/managers: Terminal49 SDK resource managers Source: https://terminal49.com/docs/sdk/reference/client/managers/index Reference for the Terminal49 TypeScript SDK managers module: container, shipment, shipping line, and tracking request managers exposed by the Terminal49Client. # client/managers ## Classes | Class | Description | | -------------------------------------------------------------------------------------------- | ----------- | | [BaseManager](/docs/sdk/reference/client/managers/classes/BaseManager) | - | | [ContainerManager](/docs/sdk/reference/client/managers/classes/ContainerManager) | - | | [ShipmentManager](/docs/sdk/reference/client/managers/classes/ShipmentManager) | - | | [ShippingLineManager](/docs/sdk/reference/client/managers/classes/ShippingLineManager) | - | | [TrackingRequestManager](/docs/sdk/reference/client/managers/classes/TrackingRequestManager) | - | ## Interfaces | Interface | Description | | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | [CreateTrackingRequestFromInferOptions](/docs/sdk/reference/client/managers/interfaces/CreateTrackingRequestFromInferOptions) | - | | [IterateOptions](/docs/sdk/reference/client/managers/interfaces/IterateOptions) | Options accepted by BaseManager.createIterator. | | [TrackingRequestListFilters](/docs/sdk/reference/client/managers/interfaces/TrackingRequestListFilters) | - | ## Type Aliases | Type Alias | Description | | ------------------------------------------------------------------------------------------- | ----------- | | [TrackingRequestType](/docs/sdk/reference/client/managers/type-aliases/TrackingRequestType) | - | ## Variables | Variable | Description | | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [DEFAULT\_ITERATE\_MAX\_PAGES](/docs/sdk/reference/client/managers/variables/DEFAULT_ITERATE_MAX_PAGES) | Hard safety caps for BaseManager.createIterator. They exist so a no-op or mistakenly broad filter cannot silently walk the entire dataset (and make thousands of requests). They are deliberately large enough not to interfere with realistic pagination, and can be raised per call via `maxPages` / `maxRows` when a caller genuinely needs more. | | [DEFAULT\_ITERATE\_MAX\_ROWS](/docs/sdk/reference/client/managers/variables/DEFAULT_ITERATE_MAX_ROWS) | - | # Interface: CreateTrackingRequestFromInferOptions Source: https://terminal49.com/docs/sdk/reference/client/managers/interfaces/CreateTrackingRequestFromInferOptions CreateTrackingRequestFromInferOptions interface in the Terminal49 TypeScript SDK, used to auto-detect carrier and tracking number type when creating a request. # Interface: CreateTrackingRequestFromInferOptions ## Properties | Property | Type | | ---------------- | ----------- | | `numberType?` | `string` | | `refNumbers?` | `string`\[] | | `scac?` | `string` | | `shipmentTags?` | `string`\[] | # Interface: IterateOptions Source: https://terminal49.com/docs/sdk/reference/client/managers/interfaces/IterateOptions IterateOptions interface in the Terminal49 TypeScript SDK, configuring maxPages and maxRows safety caps for the BaseManager createIterator pagination helper. # Interface: IterateOptions Options accepted by BaseManager.createIterator. ## Properties | Property | Type | Description | | ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `maxPages?` | `number` | Maximum number of pages to fetch. Defaults to [DEFAULT\_ITERATE\_MAX\_PAGES](/docs/sdk/reference/client/managers/variables/DEFAULT_ITERATE_MAX_PAGES). | | `maxRows?` | `number` | Maximum number of rows to yield. Defaults to [DEFAULT\_ITERATE\_MAX\_ROWS](/docs/sdk/reference/client/managers/variables/DEFAULT_ITERATE_MAX_ROWS). | | `pageSize?` | `number` | Records per page passed to the underlying list call. | # Interface: TrackingRequestListFilters Source: https://terminal49.com/docs/sdk/reference/client/managers/interfaces/TrackingRequestListFilters TrackingRequestListFilters interface in the Terminal49 TypeScript SDK, filters tracking request list responses and includes related shipment and container data. # Interface: TrackingRequestListFilters ## Indexable > \[`key`: `string`]: [`IncludeParam`](/docs/sdk/reference/types/options/type-aliases/IncludeParam)\<[`TrackingRequestInclude`](/docs/sdk/reference/types/options/type-aliases/TrackingRequestInclude)> | `undefined` ## Properties | Property | Type | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `include?` | [`IncludeParam`](/docs/sdk/reference/types/options/type-aliases/IncludeParam)\<[`TrackingRequestInclude`](/docs/sdk/reference/types/options/type-aliases/TrackingRequestInclude)> | # Type Alias: TrackingRequestType Source: https://terminal49.com/docs/sdk/reference/client/managers/type-aliases/TrackingRequestType TrackingRequestType alias in the Terminal49 TypeScript SDK enumerating the supported request types: container, bill_of_lading, and booking_number. # Type Alias: TrackingRequestType > **TrackingRequestType** = `"container"` | `"bill_of_lading"` | `"booking_number"` # Variable: DEFAULT\_ITERATE\_MAX\_PAGES Source: https://terminal49.com/docs/sdk/reference/client/managers/variables/DEFAULT_ITERATE_MAX_PAGES DEFAULT_ITERATE_MAX_PAGES constant in the Terminal49 TypeScript SDK, the default safety cap of 1000 pages applied to BaseManager createIterator pagination. # Variable: DEFAULT\_ITERATE\_MAX\_PAGES > `const` **DEFAULT\_ITERATE\_MAX\_PAGES**: `1000` = `1000` Hard safety caps for BaseManager.createIterator. They exist so a no-op or mistakenly broad filter cannot silently walk the entire dataset (and make thousands of requests). They are deliberately large enough not to interfere with realistic pagination, and can be raised per call via `maxPages` / `maxRows` when a caller genuinely needs more. # Variable: DEFAULT\_ITERATE\_MAX\_ROWS Source: https://terminal49.com/docs/sdk/reference/client/managers/variables/DEFAULT_ITERATE_MAX_ROWS DEFAULT_ITERATE_MAX_ROWS constant in the Terminal49 TypeScript SDK, the default safety cap of 100,000 rows applied to BaseManager createIterator pagination. # Variable: DEFAULT\_ITERATE\_MAX\_ROWS > `const` **DEFAULT\_ITERATE\_MAX\_ROWS**: `100000` = `100_000` # Class: Transport Source: https://terminal49.com/docs/sdk/reference/client/transport/classes/Transport Transport class in the Terminal49 TypeScript SDK, the low-level HTTP layer that wraps openapi-fetch with authentication, retry, and error-mapping interceptors. # Class: Transport ## Constructors ### Constructor > **new Transport**(`config`): `Transport` #### Parameters | Parameter | Type | | --------- | ------------------------------------------------------------------------------------ | | `config` | [`TransportConfig`](/docs/sdk/reference/client/transport/interfaces/TransportConfig) | #### Returns `Transport` ## Properties | Property | Modifier | Type | | ---------- | -------- | -------- | | `baseUrl` | `public` | `string` | | `client` | `public` | `Client` | ## Methods ### execute() > **execute**\<`T`>(`fn`): `Promise`\<`T`> #### Type Parameters | Type Parameter | Default type | | -------------- | ------------ | | `T` | `any` | #### Parameters | Parameter | Type | | --------- | ------------------------------------------------------- | | `fn` | () => `Promise`\<`FetchResponse`\<`any`, `any`, `any`>> | #### Returns `Promise`\<`T`> *** ### executeManual() > **executeManual**\<`T`>(`input`, `init?`): `Promise`\<`T`> Run a request that has no entry in the generated OpenAPI types (currently only `search()`) through the same Auth -> Retry -> ErrorMapping pipeline the typed client uses, including the timeout-wrapped fetch. Successful bodies are read with readSuccessBody so a non-JSON success body is surfaced rather than silently collapsed to `undefined`. #### Type Parameters | Type Parameter | Default type | | -------------- | ------------ | | `T` | `any` | #### Parameters | Parameter | Type | | --------- | ------------------------------ | | `input` | `string` \| `Request` \| `URL` | | `init?` | `RequestInit` | #### Returns `Promise`\<`T`> *** ### use() > **use**(`interceptor`): `void` #### Parameters | Parameter | Type | | ------------- | ------------ | | `interceptor` | `Middleware` | #### Returns `void` # client/transport: Terminal49 SDK HTTP transport layer Source: https://terminal49.com/docs/sdk/reference/client/transport/index Reference for the Terminal49 TypeScript SDK transport module: the Transport class, TransportConfig interface, and ApiClient type used by all resource managers. # client/transport ## Classes | Class | Description | | ------------------------------------------------------------------- | ----------- | | [Transport](/docs/sdk/reference/client/transport/classes/Transport) | - | ## Interfaces | Interface | Description | | ---------------------------------------------------------------------------------- | ----------- | | [TransportConfig](/docs/sdk/reference/client/transport/interfaces/TransportConfig) | - | ## Type Aliases | Type Alias | Description | | ------------------------------------------------------------------------ | ----------- | | [ApiClient](/docs/sdk/reference/client/transport/type-aliases/ApiClient) | - | ## Variables | Variable | Description | | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | [DEFAULT\_REQUEST\_TIMEOUT\_MS](/docs/sdk/reference/client/transport/variables/DEFAULT_REQUEST_TIMEOUT_MS) | Default per-request timeout (ms) applied when the caller does not override it. | # Interface: TransportConfig Source: https://terminal49.com/docs/sdk/reference/client/transport/interfaces/TransportConfig TransportConfig interface in the Terminal49 TypeScript SDK, configures API token, base URL, max retries, and optional fetch implementation for transport. # Interface: TransportConfig ## Properties | Property | Type | Description | | -------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `accountId?` | `string` | - | | `apiToken` | `string` | - | | `baseUrl` | `string` | - | | `fetchImpl?` | (`input`, `init?`) => `Promise`\<`Response`> | - | | `maxRetries?` | `number` | - | | `timeoutMs?` | `number` | Per-request timeout in milliseconds. Defaults to [DEFAULT\_REQUEST\_TIMEOUT\_MS](/docs/sdk/reference/client/transport/variables/DEFAULT_REQUEST_TIMEOUT_MS). Set to `0` to disable the timeout. | # Type Alias: ApiClient Source: https://terminal49.com/docs/sdk/reference/client/transport/type-aliases/ApiClient ApiClient type alias in the Terminal49 TypeScript SDK, representing the typed openapi-fetch client returned by createClient and used internally by Transport. # Type Alias: ApiClient > **ApiClient** = `ReturnType`\<*typeof* `createClient`> # Variable: DEFAULT\_REQUEST\_TIMEOUT\_MS Source: https://terminal49.com/docs/sdk/reference/client/transport/variables/DEFAULT_REQUEST_TIMEOUT_MS DEFAULT_REQUEST_TIMEOUT_MS constant in the Terminal49 TypeScript SDK: the 30-second per-request timeout Transport uses when callers do not override it. # Variable: DEFAULT\_REQUEST\_TIMEOUT\_MS > `const` **DEFAULT\_REQUEST\_TIMEOUT\_MS**: `30000` = `30_000` Default per-request timeout (ms) applied when the caller does not override it. # Terminal49 TypeScript SDK API reference Source: https://terminal49.com/docs/sdk/reference/index API reference for the Terminal49 TypeScript SDK, with modules for the client, interceptors, managers, transport, response models, and request option types. # TypeScript SDK API Reference ## Modules | Module | Description | | -------------------------------------------------------------- | ----------- | | [client](/docs/sdk/reference/client) | - | | [client/interceptors](/docs/sdk/reference/client/interceptors) | - | | [client/managers](/docs/sdk/reference/client/managers) | - | | [client/transport](/docs/sdk/reference/client/transport) | - | | [types/models](/docs/sdk/reference/types/models) | - | | [types/options](/docs/sdk/reference/types/options) | - | # types/models: Terminal49 SDK mapped response models Source: https://terminal49.com/docs/sdk/reference/types/models/index Mapped response models in the Terminal49 TypeScript SDK: Container, Shipment, ShippingLine, TrackingRequest, Route, PaginatedResult, and PaginationLinks. # types/models ## Interfaces | Interface | Description | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------- | | [Container](/docs/sdk/reference/types/models/interfaces/Container) | Simplified container model returned by mapped SDK responses. | | [PaginatedResult](/docs/sdk/reference/types/models/interfaces/PaginatedResult) | Mapped list response containing records plus pagination metadata. | | [PaginationLinks](/docs/sdk/reference/types/models/interfaces/PaginationLinks) | Pagination links returned by Terminal49 list endpoints. | | [Route](/docs/sdk/reference/types/models/interfaces/Route) | Simplified container route model returned by mapped SDK responses. | | [Shipment](/docs/sdk/reference/types/models/interfaces/Shipment) | Simplified shipment model returned by mapped SDK responses. | | [ShippingLine](/docs/sdk/reference/types/models/interfaces/ShippingLine) | Simplified shipping line returned by mapped SDK responses. | | [TrackingRequest](/docs/sdk/reference/types/models/interfaces/TrackingRequest) | Simplified tracking request model returned by mapped SDK responses. | # Interface: Container Source: https://terminal49.com/docs/sdk/reference/types/models/interfaces/Container Container interface in the Terminal49 TypeScript SDK, the mapped response model with container number, status, ETA, route, demurrage, holds, and fees. # Interface: Container Simplified container model returned by mapped SDK responses. ## Indexable > \[`key`: `string`]: `any` ## Properties | 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`](/docs/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` | - | # Interface: PaginatedResult Source: https://terminal49.com/docs/sdk/reference/types/models/interfaces/PaginatedResult PaginatedResult interface in the Terminal49 TypeScript SDK, a generic mapped list response with an items array, pagination links, and total record metadata. # Interface: PaginatedResult\ Mapped list response containing records plus pagination metadata. ## Type Parameters | Type Parameter | | -------------- | | `T` | ## Properties | Property | Type | | --------- | -------------------------------------------------------------------------------- | | `items` | `T`\[] | | `links?` | [`PaginationLinks`](/docs/sdk/reference/types/models/interfaces/PaginationLinks) | | `meta?` | `Record`\<`string`, `any`> | # Interface: PaginationLinks Source: https://terminal49.com/docs/sdk/reference/types/models/interfaces/PaginationLinks PaginationLinks interface in the Terminal49 TypeScript SDK, exposing self, first, prev, next, last, and current page URLs returned by JSON:API list endpoints. # Interface: PaginationLinks Pagination links returned by Terminal49 list endpoints. ## Properties | Property | Type | | ----------- | -------- | | `current?` | `string` | | `first?` | `string` | | `last?` | `string` | | `next?` | `string` | | `prev?` | `string` | | `self?` | `string` | # Interface: Route Source: https://terminal49.com/docs/sdk/reference/types/models/interfaces/Route Route interface in the Terminal49 TypeScript SDK, the mapped container route model with ordered transport legs, locations, timestamps, and total leg count. # Interface: Route Simplified container route model returned by mapped SDK responses. ## Properties | Property | Type | | ------------- | ------------------ | | `createdAt?` | `string` \| `null` | | `id?` | `string` | | `locations` | `object`\[] | | `totalLegs` | `number` | | `updatedAt?` | `string` \| `null` | # Interface: Shipment Source: https://terminal49.com/docs/sdk/reference/types/models/interfaces/Shipment Shipment interface in the Terminal49 TypeScript SDK, the mapped response model with bill of lading, containers, ports, terminals, ETAs, and customer fields. # Interface: Shipment Simplified shipment model returned by mapped SDK responses. ## Indexable > \[`key`: `string`]: `any` ## Properties | Property | Type | | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `billOfLading?` | `string` | | `containers?` | `object`\[] | | `customerName?` | `string` | | `id` | `string` | | `ports?` | `object` | | `ports.destination?` | \{ `ata?`: `string` \| `null`; `eta?`: `string` \| `null`; `locode?`: `string` \| `null`; `name?`: `string` \| `null`; `terminal?`: \{ `firmsCode?`: `string`; `id?`: `string`; `name?`: `string`; `nickname?`: `string`; } \| `null`; `timezone?`: `string` \| `null`; } \| `null` | | `ports.portOfDischarge?` | \{ `ata?`: `string` \| `null`; `code?`: `string` \| `null`; `countryCode?`: `string` \| `null`; `eta?`: `string` \| `null`; `locode?`: `string` \| `null`; `name?`: `string` \| `null`; `originalEta?`: `string` \| `null`; `terminal?`: \{ `firmsCode?`: `string`; `id?`: `string`; `name?`: `string`; `nickname?`: `string`; } \| `null`; `timezone?`: `string` \| `null`; } \| `null` | | `ports.portOfLading?` | \{ `atd?`: `string` \| `null`; `code?`: `string` \| `null`; `countryCode?`: `string` \| `null`; `etd?`: `string` \| `null`; `locode?`: `string` \| `null`; `name?`: `string` \| `null`; `timezone?`: `string` \| `null`; } \| `null` | | `shippingLineScac?` | `string` | | `tracking?` | `object` | | `tracking.lineTrackingLastAttemptedAt?` | `string` \| `null` | | `tracking.lineTrackingLastSucceededAt?` | `string` \| `null` | | `tracking.lineTrackingStoppedAt?` | `string` \| `null` | | `tracking.lineTrackingStoppedReason?` | `string` \| `null` | # Interface: ShippingLine Source: https://terminal49.com/docs/sdk/reference/types/models/interfaces/ShippingLine ShippingLine interface in the Terminal49 TypeScript SDK, the mapped response model for ocean carriers with SCAC code, name, BOL prefix, notes, and short name. # Interface: ShippingLine Simplified shipping line returned by mapped SDK responses. ## Properties | 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` | - | # Interface: TrackingRequest Source: https://terminal49.com/docs/sdk/reference/types/models/interfaces/TrackingRequest TrackingRequest interface in the Terminal49 TypeScript SDK, the mapped response model with request number, status, tracked container, and reference numbers. # Interface: TrackingRequest Simplified tracking request model returned by mapped SDK responses. ## Indexable > \[`key`: `string`]: `any` ## Properties | Property | Type | | ----------------- | ------------------------------------------------------------------------------ | | `container?` | [`Container`](/docs/sdk/reference/types/models/interfaces/Container) \| `null` | | `id` | `string` | | `refNumbers?` | `string`\[] | | `requestNumber?` | `string` | | `requestType?` | `string` | | `scac?` | `string` | | `shipment?` | [`Shipment`](/docs/sdk/reference/types/models/interfaces/Shipment) \| `null` | | `status?` | `string` | # types/options: Terminal49 SDK call and include options Source: https://terminal49.com/docs/sdk/reference/types/options/index Terminal49 TypeScript SDK option types: CallOptions, ListOptions, ResponseFormat, IncludeParam, and per-resource include aliases for shipments and containers. # types/options ## Interfaces | Interface | Description | | ----------------------------------------------------------------------- | ----------------------------------------------------- | | [CallOptions](/docs/sdk/reference/types/options/interfaces/CallOptions) | Per-call options accepted by single-resource methods. | | [ListOptions](/docs/sdk/reference/types/options/interfaces/ListOptions) | Per-call options accepted by list methods. | ## Type Aliases | Type Alias | Description | | ----------------------------------------------------------------------------------------------- | ---------------------------------------------- | | [ContainerInclude](/docs/sdk/reference/types/options/type-aliases/ContainerInclude) | - | | [IncludeParam](/docs/sdk/reference/types/options/type-aliases/IncludeParam) | - | | [ResponseFormat](/docs/sdk/reference/types/options/type-aliases/ResponseFormat) | Controls how SDK methods return API responses. | | [ShipmentInclude](/docs/sdk/reference/types/options/type-aliases/ShipmentInclude) | - | | [TrackingRequestInclude](/docs/sdk/reference/types/options/type-aliases/TrackingRequestInclude) | - | # Interface: CallOptions Source: https://terminal49.com/docs/sdk/reference/types/options/interfaces/CallOptions CallOptions interface in the Terminal49 TypeScript SDK, per-call options accepted by single-resource methods to override the default response format. # Interface: CallOptions Per-call options accepted by single-resource methods. ## Extended by * [`ListOptions`](/docs/sdk/reference/types/options/interfaces/ListOptions) ## Properties | Property | Type | Description | | ---------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------ | | `format?` | [`ResponseFormat`](/docs/sdk/reference/types/options/type-aliases/ResponseFormat) | Override the client's default response format for this call. | # Interface: ListOptions Source: https://terminal49.com/docs/sdk/reference/types/options/interfaces/ListOptions ListOptions interface in the Terminal49 TypeScript SDK, extends CallOptions with page, pageSize, and filter inputs for paginated list endpoint responses. # Interface: ListOptions Per-call options accepted by list methods. ## Extends * [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions) ## Properties | Property | Type | Description | Inherited from | | ------------ | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `format?` | [`ResponseFormat`](/docs/sdk/reference/types/options/type-aliases/ResponseFormat) | Override the client's default response format for this call. | [`CallOptions`](/docs/sdk/reference/types/options/interfaces/CallOptions).[`format`](/docs/sdk/reference/types/options/interfaces/CallOptions#property-format) | | `maxPages?` | `number` | Maximum number of pages `iterate()` will fetch before stopping. Defaults to the manager's safety cap; raise it to walk past the default bound. | - | | `maxRows?` | `number` | Maximum number of rows `iterate()` will yield before stopping. Defaults to the manager's safety cap; raise it to walk past the default bound. | - | | `page?` | `number` | 1-based page number. | - | | `pageSize?` | `number` | Number of records per page. | - | # Type Alias: ContainerInclude Source: https://terminal49.com/docs/sdk/reference/types/options/type-aliases/ContainerInclude ContainerInclude type alias in the Terminal49 TypeScript SDK listing valid sideload values for container endpoints: shipment, terminals, and transport events. # Type Alias: ContainerInclude > **ContainerInclude** = `"shipment"` | `"pod_terminal"` | `"pickup_facility"` | `"transport_events"` # Type Alias: IncludeParam Source: https://terminal49.com/docs/sdk/reference/types/options/type-aliases/IncludeParam IncludeParam generic type alias in the Terminal49 TypeScript SDK, accepts a readonly array of typed include values or a comma-separated JSON:API include string. # Type Alias: IncludeParam\ > **IncludeParam**\<`TInclude`> = readonly `TInclude`\[] | `string` ## Type Parameters | Type Parameter | | ----------------------------- | | `TInclude` *extends* `string` | # Type Alias: ResponseFormat Source: https://terminal49.com/docs/sdk/reference/types/options/type-aliases/ResponseFormat ResponseFormat type alias in the Terminal49 TypeScript SDK, controlling whether methods return raw JSON:API responses, mapped models, or both for each request. # Type Alias: ResponseFormat > **ResponseFormat** = `"raw"` | `"mapped"` | `"both"` Controls how SDK methods return API responses. # Type Alias: ShipmentInclude Source: https://terminal49.com/docs/sdk/reference/types/options/type-aliases/ShipmentInclude ShipmentInclude type alias in the Terminal49 TypeScript SDK listing valid sideload values for shipment endpoints: containers, ports, terminals, and destination. # Type Alias: ShipmentInclude > **ShipmentInclude** = `"containers"` | `"pod_terminal"` | `"port_of_lading"` | `"port_of_discharge"` | `"destination"` | `"destination_terminal"` # Type Alias: TrackingRequestInclude Source: https://terminal49.com/docs/sdk/reference/types/options/type-aliases/TrackingRequestInclude TrackingRequestInclude type alias in the Terminal49 TypeScript SDK listing valid sideload values for tracking request endpoints: shipment and container. # Type Alias: TrackingRequestInclude > **TrackingRequestInclude** = `"shipment"` | `"container"` # TypeScript SDK Authentication Source: https://terminal49.com/docs/sdk/typescript/authentication Redirects to the Terminal49 TypeScript SDK authentication guide. Learn how to configure your API key and authenticate all SDK requests securely. This page has moved. * [SDK Introduction](/docs/sdk/introduction) # TypeScript SDK Methods Reference Source: https://terminal49.com/docs/sdk/typescript/available-methods Redirects to the Terminal49 TypeScript SDK methods reference. Browse all available client methods for shipments, containers, and tracking requests. This page has moved. * [Methods Reference](/docs/sdk/methods) # TypeScript SDK Error Handling Source: https://terminal49.com/docs/sdk/typescript/error-handling Redirects to the Terminal49 TypeScript SDK error handling guide. Learn how to catch API errors, network timeouts, and validation failures. This page has moved. * [Error Handling](/docs/sdk/error-handling) # TypeScript SDK Filtering Source: https://terminal49.com/docs/sdk/typescript/filtering Redirects to the Terminal49 TypeScript SDK filtering guide. Learn how to filter shipments and containers by status, carrier, date, and other fields. This page has moved. * [Filtering & Pagination](/docs/sdk/filtering-pagination) # TypeScript SDK Installation Source: https://terminal49.com/docs/sdk/typescript/installation Redirects to the Terminal49 TypeScript SDK installation guide. Install the SDK package from npm and configure it for your Node.js or TypeScript project. This page has moved. * [SDK Introduction](/docs/sdk/introduction) # TypeScript SDK Pagination Source: https://terminal49.com/docs/sdk/typescript/pagination Redirects to the Terminal49 TypeScript SDK pagination guide. Learn how to paginate large result sets when listing shipments, containers, or events. This page has moved. * [Filtering & Pagination](/docs/sdk/filtering-pagination) # TypeScript SDK Quickstart Source: https://terminal49.com/docs/sdk/typescript/quickstart Redirects to the Terminal49 TypeScript SDK quickstart. Get started tracking containers and retrieving shipment data in minutes with working examples. This page has moved. * [SDK Quickstart](/docs/sdk/quickstart) # Terminal49 API and DataSync Updates Source: https://terminal49.com/docs/updates/home Release notes and changelog for Terminal49 API and DataSync updates, covering new features, endpoint changes, schema updates, and resolved issues. This page contains updates to **Terminal49 API** and **DataSync** only (not general product updates). ### Party roles on tracking request creation `POST /v2/tracking_requests` accepts `shipper`, `consignee`, `notify_party`, `customs_broker`, `freight_forwarder`, and `pickup_dray_carrier` relationships next to `customer`. One party per role. The roles are copied to the shipment when it is created, and `PATCH /v2/tracking_requests/{id}/resubmit` carries them over to the new request. *** ### Party role endpoints The endpoints the dashboard uses to assign parties are now documented for API use. * `GET`, `POST /v2/shipments/{id}/party_roles` and `DELETE /v2/shipments/{id}/party_roles/{id}` * `GET`, `POST /v2/containers/{id}/party_roles` and `DELETE /v2/containers/{id}/party_roles/{id}` (`pickup_dray_carrier` only) * `flag[parties]=true` on `GET /v2/shipments` and `GET /v2/shipments/{id}` adds the `party_roles` relationship No action required. Full flow from party creation to reading roles back on the shipment ### New Delivery Order workflow A new **Delivery Order** experience lets you draft a DO, choose destinations from your party network, send it to the dray carrier, and follow the carrier's response through a public review link — no more sending PDFs by email and chasing acknowledgements. * **Draft, edit, and send** — build a DO from the pickup triage view, pick a destination and contacts from the party network, and send it to the assigned dray carrier * **Public review link** — carriers open a shareable link (no login), see the container list, and accept or push back with notes; state moves through a machine (draft → sent → accepted / declined) and expires on its own if left unanswered * **View from the container** — the container modal and pickup triage row now surface a **View DO** action once a delivery order exists, opening the same draft view for review or resend View the container resource *** ### New OOCL and Georgia Ports Authority integrations * **OOCL / OOLU via CargoSmart SCCT** — the OOLU tracker now runs against the CargoSmart Shipment Connect (SCCT) JSON API. Container, booking, and B/L lookups all flow through the shared adapter, with better handling for multiline booking numbers, transshipments, and rail-to-inland moves. Customs holds and container events keep populating as before * **Georgia Ports Authority (GPA) Savannah API** — a native API tracker for the Port of Savannah (USSAV) covering canonical container states, available-for-pickup, and vessel discharge *** ### Redesigned POD Last Free Day cell The **Smart LFD** column has been replaced with a **POD LFD** cell that reads at a glance: * **Date-first layout with source chips** — the date sits on the left, followed by monochrome one-letter chips for each source (**S** for the shipping line, **T** for the terminal) and an inline qualifier (**Est.** for calculated, **Manual** for user-entered). When the sources agree, one date and both chips render; when they diverge, both dates show with the binding (earlier) date first * **Restrained coloring** — red only after the deadline has passed with no pickup seen, grey once picked up, plain otherwise. The old "approaching" highlight is retired * **Clearer empty states** — the cell reads **Not discharged** before free time starts, and **+ Add LFD** once discharged with nothing reported * **Detail panel** — clicking the cell opens a coverage breakdown per source, and the shipment page shows POD and IND deadlines together * **Sortable Smart LFD** — the column now sorts (and exports) instead of silently ignoring the header click; non-opted accounts sort by reported values only Calculated LFDs are now computed for every account, but only **displayed** to accounts opted into Smart LFD. Inland (IND) LFDs remain gated behind `smart_lfd_ind` because reliable inland calendars are not yet available for every location. Customer-entered LFDs are now reported as a **manual** selection so cells can label them as such. Container resource with LFD fields *** ### Wider carrier release status coverage Release-status parsing has been added to more steamship trackers so the `release_statuses` on containers reflects what the carrier actually reports: * **COSU / Cosco** — customs and freight release now surfaced * **HDMU / Hyundai (mobile)** — customs, freight, terminal, and line release parsed * **MATS / Matson (mobile and shared)** — customs and freight release parsed * **ONEY / ONE** — customs, freight, and terminal release parsed * **YMLU / Yang Ming (API and web)** — customs, freight, terminal, and line release parsed How to work with holds and release data *** ### Terminal holds v2 rollout continues Terminal-line holds are now routed through the shared holds v2 pipeline across APM, APM Miami, eModal, GCT Canada, Navis, Ports America, Savannah, SSA Marine, Tideworks, TMS, TMS API, and Voyager Track. Alongside the routing: * **Backfill of active facility holds** — a one-shot task reconciles the current active-holds state from the last terminal update so no in-flight holds are lost during migration * **Admin toggle for holds v2** — the account edit view carries a `holds_v2` toggle for controlled enablement * **Terminal-published hold gates** parsed and attributed * **Attributable terminal hold releases** emitted with the terminal named as the release source * **Source wording preserved as `detail`** — each hold source's own phrasing (carrier note, terminal note) is exposed as `detail`, alongside the normalized type *** ### Dashboard and admin updates * **Dray Carrier on the expanded container row** — the expanded container row on the shipment page now shows the container's dray carrier with inline **+ Assign**, **Change carrier**, and **Remove assignment**. Reads and writes share the same store as the shipment sidebar * **"Pickup Dray Carrier" renamed to "Dray Carrier"** — the label is now consistent across the container dashboard column, the bulk-upload field, filters, and CSV exports. Role keys, API attributes, and query params are unchanged * **Complete Plan available in admin contracts** — the subscription plan dropdown in the admin contract form now offers **Complete**, alongside Essential and Lite * **Party contact and location editor in the side panel** — the new parties side panel exposes account-scoped party contacts and locations with a full edit view * **Admin container delete from a B/L restored** — Terminal49 admins can again remove an individual container from a shipment's B/L, with a confirmation dialog naming the container and the B/L. The action is gated behind a new admin-only policy on `DELETE /v2/containers/:id` * **Terminal49 accounts can read container terminal history** — `/v2/containers/:id/container_updates` no longer 404s for Terminal49 staff viewing a customer's shipment, so the Terminal history tab renders for support and engineering *** ### Customer moves to the party model `PartyRole(role: "customer")` is now the source of truth for a shipment's or tracking request's customer. The legacy `customer_id` column is frozen and every write path now writes a party role instead. For API and dashboard users: * **New customers can be parties without a linked account** — creating a customer no longer implicitly creates a shipper Account; customers without a linked account are exposed under their party id (customers with a linked account keep exposing their account id) * **Customer id inputs accept both id kinds** — tracking-request create, shipment update, bulk add, and merge accept either a party id or an account id and resolve to the right customer role * **Customer dropdown is stable across reloads** — dashboards read customers through the parties endpoint so customers without a linked account stay visible after refresh Attach customers to shipments *** ### Bug fixes * **CMA CGM barge events restored** — the actual barge arrival and departure event codes (`ABA` / `ABD`) are now mapped to feeder arrival and departure. Previously the actuals were unmapped and the planned twins (`PBA` / `PBD`) were dropped when the actuals landed, so river-port origins like Phnom Penh reported no departure at all. `pol_atd_at` and `pol_etd_at` now populate for these shipments * **CMA CGM POL resolved from the transport call** — the port of lading now reads from the `DEPA`/`POL` transport call rather than a vessel-departed event that might be dropped for a missing timestamp. Downstream fields (`empty_out_at`, `pol_full_in_at`, `pol_vessel_loaded_at`, `pol_vessel_departed_at`) populate again * **YMLU last free days placed by carrier label** — Yang Ming's terminal and ramp LFDs now attach to the POD or the inland destination based on the block's label ("Terminal Last Free Date" / "Ramp Last Free Date"), instead of being geocoded from a facility string that regularly parsed to the wrong port. Both deadlines are captured when a shipment discharges at one location and rails to another * **Maersk payloads without a `containers` key** — Maersk (MAEU/SEAU/SAFM/SEJJ) tracking responses that omit the equipment list no longer crash the transformer. The shipment update proceeds even when the carrier has not yet assigned containers * **Maersk locations resolve by city + state** — Maersk event locations now carry their state so city matching resolves correctly instead of colliding with same-named cities elsewhere * **Full In / Empty Out stamped away from the POL** — `pol_full_in_at` and `empty_out_at` are now stamped wherever the container gates in, including inland depots and ICDs. The POL-only gate cost roughly 63,000 previously eligible events per month * **Exact locode beats a fuzzy city guess** — location matching now resolves an explicit locode before falling back to a city string. Carriers naming metro-area locodes (like `THLKR` "Lat Krabang") no longer resolve 130 km away * **Locode aliases apply to inland locations** — curated aliases such as `CAYEG` and `USMCI` now resolve on inland rail and airport-adjacent locations, not just seaports * **Cplus CSX terminal code mapped to DP World Hong Kong (CT3)** — the legacy CSX prefix from the Hutchison Cplus feed no longer warns and now maps to the current DP World Hong Kong CT3 terminal * **TraPac LA future-dated updates dropped** — TraPac LA terminal updates timestamped in the future are now discarded instead of overwriting current state * **HMM container lookups no longer crash on invalid numbers** — unrecognized HMM container numbers are now classified as awaiting-manifest (matching the B/L path) rather than raising in the tracker * **GCT Canada `!NOBLIMPORT` hold** — the Deltaport `!NOBLIMPORT` token is now recognized as an "other" hold with a friendlier description instead of surfacing the raw token and Sentry-ing every scrape * **Goport batches survive per-container failures** — a single container's failed moves lookup no longer aborts the whole scrape; the rest of the batch reports normally and the failing container is retried * **Goport tolerates non-JSON portal responses** — an expired-session HTML login page (returned under HTTP 200) is now caught and treated as a transient portal error instead of a critical unknown exception * **HTTPI SSL and timeout errors classified as transient** — Hapag-Lloyd SOAP lookups that hit an SSL or timeout error are now marked `FAILED_REASON_UNREACHABLE` and retried, rather than surfacing as `FAILED_REASON_BAD_RESPONSE` * **GO! Port password expiry named** — SCSPA terminals (GO! Port) now surface a `password_expired` credential error when the portal returns `pwdChangeRequired`, instead of a generic HTTP 403 * **OOLU CFFI resilience** — CargoSmart scrape failures are classified accurately, and Cloudflare ticket challenges retry rather than failing the run ### New Livorno terminal integration Added a native terminal tracker for the **Port of Livorno (ITLIV)** via the public Tuscan Port Community System. One endpoint covers all Livorno terminals, and when a container lookup returns not-found the tracker automatically retries by bill of lading (with a SCAC-prefixed fallback) so manifested boxes still resolve. Container, vessel, arrival, and B/L milestones flow through the standard pipeline. View the container resource *** ### Unified parties experience The Parties surface across the dashboard has been consolidated into a single component and data flow, with several new affordances landing at the same time: * **Customer and other parties on the shipment sidebar** — the container details drawer now shows a Parties section (including Customer) with inline `+ Assign` and `Change` affordances, rendered between the map and Shipment Fields * **Add parties from the shipments dashboard** — the PARTIES column on the shipment list now offers a `+ add parties` empty state and a hover-pencil edit on rows that already have chips, opening the same editor used on the shipment details page * **Editable party cells on the container dashboard** — inline party adds and per-role bulk updates are available directly from container rows * **Confirmation before removals** — removing a party from the Parties card, the shipment Parties tab, or a dray-carrier assignment now prompts to confirm, naming the party, role, and scope it is leaving. Replacing a party still skips the dialog * **Standalone Customer column retired from the shipment list** — the shipments list no longer renders a standalone Customer column; the customer surfaces as a "Cus" chip in the Parties column, and the extra width goes to Parties * **Duplicate Customer field removed from Bulk Update Shipments** — the Flatfile bulk-update modal no longer shows Customer twice; parties are the single source for Customer *** ### Extended retry window for paid tracking requests Paid tracking requests now retry for a **longer window** before giving up, and the effective expiry time is surfaced on the track page so you can see when a pending request will stop being retried. Auto-detected requests continue to use their own retry cap. Tracking request API reference *** ### Explicit release status across terminal integrations Terminal-container parsers across the integration set have been rolled forward to report **explicit hold release status** through a shared normalization layer. The change consolidates release semantics behind one interface and unifies how APM, APM Miami, ATP Altamira, BEST Barcelona, BGT Click, CMSAMX, DP World Vancouver, e-Brama, eModal, ETS Link, Felixstowe, FMS API, GCT Canada, Goport, HHLA, and other trackers report cleared holds — with cleared entries no longer written inline alongside active holds. Downstream, `holds` remains the active-holds surface you already read from. How to work with holds and fees data *** ### Custom fields polish * **Rename Preset to Template** — the "Add a Field" modal tab, empty state, admin "Create Template?" toggle, banner, and tooltips now use "Template" instead of "Preset" (internal identifiers unchanged) * **Archived custom fields visible with unarchive/delete** — archived custom fields now appear grayed out for admins with clear unarchive and delete actions, instead of being hidden entirely * **Longer field labels** — the custom field label column now expands to 50% for readability * **Hover reveals all custom field values** — hovering a shipment or container's custom field row now expands the display so every configured value is visible without opening a detail view Custom Fields API reference *** ### Updates * **Shipment number over normalized number** — dashboard views that previously displayed the internal normalized reference now show the shipment number, matching what your team enters and searches for * **Delivery-order and reference-list refresh polish** — creating a new template refreshes the list on success and drops the just-created template from the pick list, so it doesn't appear as a selectable option in its own creation flow *** ### Bug fixes * **HMM B/Ls no longer fail before manifest** — Hyundai (HMM/HDMU) bills of lading submitted before the carrier assigns a container used to fail permanently as `invalid_number` on the first attempt because HMM's response for a not-yet-manifested B/L is byte-identical to its response for an unknown one. HMM B/L requests now park as awaiting-manifest and retry until the account's retry cap, matching Hapag-Lloyd's pre-booking behavior. Container lookups are unchanged * **Bulk-upload customer assignments preserved** — CSV bulk uploads that created new customers could save affected rows with a null customer, because the follow-up read to match customers by name was served by a replica that hadn't caught up. Assignments now match against the records already in hand, and the customer list endpoint is served from the primary * **HIT Cplus terminal oscillation debounced** — pre-discharge, the Hutchison Cplus feed's `terminal` field is a berth prediction that can oscillate between the ACT8/CHT8 sibling yards at Kwai Tsing while a box is "Inbound On-board." Each flip was re-attributing the container's POD terminal and emitting a spurious `container_pod_terminal_changed_event`. Pre-discharge readings now keep the already-assigned Cplus sibling terminal; once the box is ashore the terminal becomes authoritative again * **Chittagong Port Authority (BDCGP) reliability restored** — the `cpatos.gov.bd` portal was rejecting Terminal49's datacenter egress with 4xx errors on every request. The Cpatos scraper now runs through residential egress, restoring container tracking at Chittagong * **Trapac 403 classification** — Trapac forbidden responses are now classified correctly instead of surfacing as unknown tracker errors * **HDMU invalid-number handling** — additional HDMU invalid-number edge cases are now handled cleanly instead of failing the tracker run * **Metabase dashboards endpoint hardened** — the customer-facing `/v2/account_metabase_dashboards` endpoint no longer honors arbitrary `?include` values that could expose sensitive dashboard configuration. Reports continue to render normally from the inline label and signed iframe URL ### New Oman terminal integrations Two new terminal trackers extend Terminal49's Middle East coverage: * **APM Terminals Salalah (OMSLL)** — one of the largest transshipment hubs in the region, now delivering container-level milestones (vessel discharge, gate-out, empty return) for imports moving through the port. Transshipment-only histories are handled cleanly so reused container numbers can't overwrite tracked imports * **Oman International Container Terminal — Sohar (OMSOH)** — infrastructure is in place for OICT Sohar and will light up once haulier account provisioning completes View the container resource *** ### Customer columns and filters on the party model The dashboard **Customer** column, filter, bulk import, and bulk update have all been migrated to the unified **party model**. Customer selections now share the same underlying record used by every other party role (consignee, shipper, notify, dray carrier), so: * Customers set via the track-page bulk import propagate to the created shipment automatically * Customer filters accept either a customer account id or a party id — saved views keep working either way * Newly created and renamed customers appear immediately in customer pickers and lists * The Customer column renders reliably on the container dashboard for accounts using the party model The internal parties feature flag has been retired — parties are now the single source of truth for customer data across the dashboard. *** ### Terminal particulars fill in container details When a shipping line hasn't yet published container equipment details, Terminal49 now **backfills equipment type, size, height, seal number, and weight from ocean terminal scrapers**. Carrier-provided values and manual edits are preserved — terminal data only fills gaps, and the equipment trio (type/size/height) is only written all-or-nothing so cargo records never end up with partial defaults. Rail terminals are excluded so line-haul equipment codes aren't overwritten. View the container resource *** ### Clickable URLs in custom fields URLs entered into **short-text custom fields** on shipments and containers now render as **clickable links** in the dashboard, so linked TMS records, shared docs, and support tickets are one click away without copy-paste. CSV exports remain plain text. Custom Fields API reference *** ### Updates * **Parties panel always visible on shipment details** — the parties panel is now anchored to the right side of the shipment details page at all viewport widths, instead of collapsing to the bottom on narrower screens * **In-use custom field templates protected from deletion** — custom field templates that are already attached to shipments or containers can no longer be deleted, so existing values can't be orphaned * **Clearer stop/resume tracking failures** — when the dashboard cannot stop or resume tracking on a shipment, it now surfaces a specific reason instead of silently succeeding or failing; stop-tracking no longer leaves shipments in a half-stopped state that kept getting refreshed * **IMO GISIS vessel-particulars enrichment restored** — IMO GISIS (vessel name, flag, dimensions, and other particulars) is back online as an enrichment source after solving its Cloudflare Turnstile challenge via CapSolver *** ### Bug fixes * **YMLU POD frozen at transshipment hub** — Yang Ming (YMLU) shipments could show their port of discharge stuck at a transshipment hub instead of advancing to the true final discharge port; POD now reflects the correct destination * **Stop tracking on cancelled tracking requests** — clicking Stop Tracking on a shipment whose tracking request had been cancelled mid-scrape returned a 500 error; the action now succeeds cleanly, and mid-scrape cancellations no longer generate spurious background job errors * **Point-of-Virginia availability parser** — fixed a crash in the POV availability parser that could fail a container lookup * **WorkOS post-login destination** — signing in after a WorkOS logout-recovery flow now returns you to the page you were originally trying to reach, instead of dropping you on the default landing page ### New terminal integrations Five more terminals join Terminal49's live tracker coverage: * **BEST Barcelona (ESBCN)** — Hutchison's BEST terminal in the Port of Barcelona via the shared Hutchison `ubi` mobile JSON API, with a BEST-specific status map (Loaded On Vessel, In Yard, Gate Out) verified across the full lifecycle * **Basra Gateway Terminal (IQUQR)** — ICTSI's Umm Qasr terminal via the BGT CLICK Container Lifetime lookup; covers both BGT West and BGT East * **Chittagong Port Authority (BDCGP)** — one integration covers all four Chittagong container terminals (NCT, CCT, PCT, GCB) via the public CTMS portal, with per-record firms-code attribution and an empty-return event so gated-out boxes advance to `empty_returned` * **Gdynia BCT (PLGDY)** — Baltic Container Terminal Gdynia via ICTSI's e-Brama portal, with seal numbers and gross/cargo weights surfaced and hardened against the portal's periodic slow-response episodes * **Contecon Guayaquil (ECGYE)** — ICTSI's Guayaquil container terminal View the container resource *** ### New BNSF RailPASS mobile rail tracker Added a new **BNSF RailPASS** rail tracker that hits the BNSF mobile app path for container status. It runs alongside the existing BNSF integration to give rail shipments more redundancy against upstream outages. *** ### Drag-to-reorder columns on container dashboards Container column order is now **drag-and-drop**. A grip appears on hover over any non-sticky column header in the **Containers** and **Containers at Risk** dashboards — drag it onto another header to move the column, with a blue edge indicator showing where the drop will land. Sticky columns (like container number) stay pinned on the leading edge. Column order is persisted per view, so your layout is preserved across sessions. *** ### Per-container custom fields Custom fields of type **Cargo** can now target a **specific container** on a shipment instead of always broadcasting to every container. You can also attach **initial custom field values** at tracking-request creation time — values are staged on the tracking request and applied to the shipment and containers as soon as they exist. *** ### Account-level custom field limits Custom field access is no longer gated by feature flags. Every account now has a `custom_field_limit` **entitlement** enforced at value-creation time, exposed on the account API, and overridable per account by admins. The custom field UI is always visible; the limit governs how many active field values you can attach. *** ### Updates * **SSL release status enrichment** — steamship line release statuses are now enriched with additional detail, so line-release progress is more actionable on the container view * **Terminal-released holds visible without polluting legacy holds** — holds cleared at the terminal are now surfaced through the new hold model without appearing in the legacy holds UI, so older integrations keep their current view while the new hold surface gains coverage * **Containers at Risk dashboard on the shared table** — the at-risk dashboard now renders through the same `Container::View` component as the main Containers dashboards, picking up shared filters, column resizing, drag-to-reorder, and consistent defaults * **Container row lifecycle actions refined** — inline container actions on the shipment page have been reorganized by lifecycle state, with appointment editors gated by the current lifecycle stage and container weight now editable from the shipment index * **HIT Cplus transhipment mapping** — Hong Kong HIT Cplus records for inbound transhipment (`Transhipment Laden` + `Inbound On-board` → **On Ship**, + `On Dock`/`At pier` → **Grounded**) now map correctly instead of storing a blank status * **HHLA Hamburg retries** — a transient connection drop or 5xx on HHLA's COAST API now retries once per container lookup instead of failing the whole batch's scrape log * **Port Houston enrichments** — the Port Houston EVP tracker now surfaces **seal numbers** and picks up multi-mode holds and reefer LFD / paid-thru dates * **Removed V1 shipment refresh endpoints** — the V1 shipment-level manual refresh actions (`refresh_containers`, `refresh_shipment`, `refresh_rail`) have been retired now that container-level refresh via the V2 API (`POST /v2/containers/refresh`) supersedes them * **Retired V1 UI models** — roughly 20 unused V1 Ember Data models and their adapters have been removed; public API consumers should already be on V2 *** ### Bug fixes * **HIT Cplus terminal re-attribution at gate-out** — at pickup the Cplus feed rewrites the terminal to `HIT4` regardless of which Kwai Tsing yard actually held the box, moving non-HIT pickups to `HKHKGHITHK` and emitting a spurious `pod_terminal_changed` event. The parser now defers the firms code on HIT-named gate-out records so the pickup stays on the terminal that held the container * **MAEU Access Denied classification** — Maersk (`MAEU`) Access Denied responses are now classified as a distinct error instead of being reported as unknown failures * **Trailer Bridge tracking** — Trailer Bridge (`TRBR`) reference numbers are now classified as booking numbers, so TRBR bookings track correctly * **Re-tracking a bill of lading after stopping** — you can now re-submit a tracking request for a BL after previously stopping tracking, instead of being blocked by the prior request * **ONEY container-less bookings** — Ocean Network Express bookings with no containers no longer emit spurious COP events * **ContainerHold reconciliation** — the hold reconciler no longer fabricates cleared-hold records when the terminal returns a blank holds field; existing holds are preserved when the field is missing rather than empty * **Termont scraper** — a WordPress scraper contract change was corrected, and blank `status_info.section` payloads now fall back quietly to the terminal's firms code instead of raising Sentry noise * **GCT Canada holds** — bare `INTACT EXAM` and `HOLD` tokens are now mapped correctly, and customs/freight holds are preserved when the Holds field is blank (only a literal `null` clears them) * **APM otherHolds Integer** — APM Terminals occasionally returns `otherHolds` as an Integer; the parser now coerces to string before splitting so scrapes no longer fail, and `0`/`"0"` is treated as no other holds * **eModal unit statuses** — the `UV`, `UR`, `I`, `O`, and bare in-yard `Y` unit status codes are now mapped to the correct availability instead of falling through to unknown-status Sentry noise * **POV HREW hold** — Port of Virginia `HREW NON-EXAM HOLD` and `shipmentAction I` (gate-in) records are now mapped instead of reporting as unknown status * **MRTML LFD parsing** — Manzanillo International Terminal LFD values with trailing punctuation and month-name formats now parse correctly (unparseable values return `nil` instead of raising) * **wheresmycontainer timestamps** — variable-precision fractional-second timestamps with timezone offsets now parse, and `Inbound by Rail` maps to a status * **Trapacweb Capsolver responses** — non-JSON Capsolver bodies are normalized before dig, so scrapes surface a proper `CaptchaError` instead of a `NoMethodError` * **Goport blank rows** — empty portal data payloads are now treated as scrape failures instead of raising, so cargos are not incorrectly marked inactive * **ScrapeTerminalJob sparse results** — sparse scraper results no longer raise on `nil.merge`, and cargos omitted from a scrape run are no longer deactivated ### New international terminal integrations Six new terminal trackers expand Terminal49's coverage well beyond North America: * **HHLA Hamburg (DEHAM)** — one integration covers all three HHLA container terminals (Burchardkai, Tollerort, Altenwerder) via the shared public COAST API, with Ready-To-Load status and outbound impediments surfaced as holds * **Hong Kong (HKHKG)** — a single Hutchison Cplus integration covers HIT, ACT, COSCO-HIT (CHT), and MTL at Kwai Tsing; the per-record terminal field auto-corrects `pod_terminal` across operators * **DP World Jebel Ali (AEJEA)** — Dubai Trade Container Enquiry, reachable directly (no browser required) * **RSGT Jeddah (SAJED)** — Red Sea Gateway Terminal E-Track portal * **DP World Jeddah** — Navis N4 Community Access Portal via the public DP World container status JSON API * **Saudi Global Ports (SADMM)** — Dammam terminal tracking via the public Saudi Global Ports portal View the container resource *** ### POD terminal discovery beyond North America Destination-terminal discovery sweeps used to be hard-gated to US/CA/MX ports of discharge. With the new international terminal integrations live, discovery and near-arrival rescans now run for any POD that has a scrapable terminal — so Hamburg, Hong Kong, and future international terminals start collecting terminal data automatically instead of being one-shot at tracking creation. *** ### New CMA CGM mobile tracker Added a third CMA CGM (CMDU) tracking path — **CmduMobile** — that hits the CMA CGM mobile app gateway with Kong HMAC authentication. It runs alongside the existing DCSA KeyId API and website scraper, giving CMDU shipments more redundancy against upstream outages and rate limits. *** ### LFD operating-data pipeline Smart LFD now understands per-terminal **truck-gate operating schedules and holiday closures**. A new datasets pipeline sources terminal working days and calendar exceptions (holidays, weather closures, operational closures), and the demurrage calculator counts holiday closures as free days under `include_holidays` rules. Ocean Terminal Savannah is the first newly-FIRMS-scoped facility to join the pipeline. View the container resource *** ### Bill of lading release gates Added a new **BL-grain release gates** model — line, freight, and the carrier's customs decision — each with an explicit state (unknown → pending → held → cleared) and gate-specific disposition (e.g. customs exam, in-bond, documentation; line BL not surrendered). This is the foundation for reconciling release readiness at the bill of lading level, in addition to the container-level holds you already see. How to work with holds and fees data *** ### Hide containers in shipments A new user preference lets you **hide the containers section** in shipment views by default. Handy if you work primarily at the shipment grain and want a tighter list without expanding each row. *** ### Updates * **Container edit actions restored** — inline container edits are back on the shipment details page, with stricter equipment-type validation on update and admin reference edits working again * **Shipment index and details on the V2 API** — the shipment listing, details, and edit views have been migrated fully to the Shipments V2 API for a leaner, more consistent data model * **Customer filters now backed by parties** — `filter[customer_id]` on shipments and containers, and `filter[customer_name]` on containers, now match a customer party role's party (falling back to the shipment creator when no customer role is set). Filter keys and response shapes are unchanged * **API-key access paused when billing is locked** — a billing lock now also pauses API-key (programmatic) access, matching how it already pauses dashboard access. Paying automatically restores API access with no separate unlock code * **HLCU routed through curl\_cffi** — Hapag-Lloyd API tracking now runs through the trawler curl\_cffi path for improved reliability against upstream fingerprinting * **Shared FIRMS code disambiguation** — LFD tariff importers now use the row's port to pick the right terminal when a FIRMS code is shared between facilities (e.g. BNSF Hobart at USCRC vs USLAX), instead of binding to an arbitrary one * **Ocean Terminal Savannah FIRMS code** — set FIRMS code `L704` on Ocean Terminal Savannah so it can be terminal-scoped for LFD operating data alongside Garden City * **V1 API surface cleanup** — retired 20 unused V1 controllers (cargos, ports, terminals, voyages, leads, messages, shares, documents, api\_keys, and others) now that the dashboard runs on V2. Public API consumers should already be on V2 *** ### Bug fixes * **HLCU gate event misclassification** — off-dock drayage gate-ins on Hapag-Lloyd API responses (e.g. LA terminal → Long Beach rail ramp) were being rewritten to `empty_in`, incorrectly flipping containers to empty-returned mid-journey and auto-stopping tracking. Only the final movement of the journey is treated as the empty return now; unmapped rail gate events also produce proper rail arrivals so destination and inland ETAs populate on IPI shipments * **False HLCU empty returns remediated** — cargos incorrectly flipped to empty-returned by the previous HLCU behavior have been rolled back, their tracking requests resumed, and shipments reprocessed * **OOLU multi-booking BL numbers** — on multi-booking bills of lading, the OOCL mobile API returns every booking newline-joined with status labels; Terminal49 now parses this as a list and picks the entry matching the tracked reference (or the first non-rejected booking), and re-cleans previously-persisted multi-line values * **TILH removed from vessel schedule ingest** — TILH-sourced schedules were producing unreliable vessel events and have been dropped from the vessel schedule pipeline * **Reduced Sentry noise on party creation** — expected "party already exists" cases no longer report to Sentry ### Zapier 2.0 integration The **Terminal49 Zapier integration** has been fully rewritten as version 2.0. The new version is powered by the official Terminal49 TypeScript SDK, ships **31 webhook-based triggers** (tracking requests, transport events for vessel/full/empty milestones, transshipment and feeder events, delivery and pickup events, and more), and returns a cleaner flat payload with a `_raw` object for advanced use. Version 1.0 remains available on Zapier indefinitely — 2.0 ships as a separate version so you can migrate at your own pace. Connect Terminal49 to 6,000+ apps *** ### Tracking request SCAC auto-detect Tracking requests can now be created **without a SCAC**. When `scac` is omitted, Terminal49 uses [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) during tracking to pick the correct carrier and number type. The auto-detect flag is preserved across resubmits, and the resolved SCAC flows through to webhooks so downstream systems always see the actual carrier. Tracking request API reference *** ### New Hapag-Lloyd public API tracker Added a direct **Hapag-Lloyd (HLCU) public API** tracker for container, bill of lading, and booking lookups. HLCU tracking requests are also now included in the periodic **booking sweep**, so bookings without containers are refreshed alongside the rest of your Hapag-Lloyd shipments. *** ### New MAEU and OOLU Browser Use trackers * **MAEU (Maersk) — `maeubu`** — a new Browser Use Cloud tracker for Maersk, running against a residential-fingerprinted browser for improved reliability * **OOLU (OOCL) — `oolubu`** — a new Browser Use Cloud tracker for OOCL that reuses the existing extraction path while auto-clearing the Cloudflare Turnstile challenge * **OOLU direct path retired** — the previous `oolu_direct` integration has been removed now that `oolubu` supersedes it *** ### MSC US import demurrage tariff rules Published a new **MSC (MSCU)** US-import demurrage tariff rule dataset for Smart LFD, sourced from Descartes WsHelp and refreshed on a daily filename sweep. Calculated Last Free Day values for MSC containers now reflect Mediterranean Shipping's published tariff, including CY port, rail base, and named-ramp overrides. View the container resource *** ### Party filter and chips on the shipment dashboard The shipment dashboard now supports **filtering by party**, and party assignments render as inline **chips** on the shipment table so you can see consignee, shipper, notify, dray carrier, and other roles at a glance without opening the shipment. *** ### Updates * **Manage account tags without an admin** — users with the right permission can now create, rename, and remove account tags directly; Terminal49 admins can also manage tags on behalf of accounts * **Shipment Details on the Shipments V2 API** — the shipment details page has been migrated fully to the Shipments V2 API for a leaner, more consistent data model behind the UI * **Faraday connection failure causes surfaced** — outbound HTTP failures from carrier and terminal integrations now expose their underlying cause (timeout, DNS, TLS, connection reset), so retry classification and dashboards reflect the real reason * **Server-side entitlement checks on webhooks** — webhook management and trigger endpoints now enforce entitlement checks server-side, making sure only accounts with the data-out API entitlement can operate on webhooks *** ### Bug fixes * **HLCU groups without container numbers** — Hapag-Lloyd shipment groups that returned no container numbers no longer cause the tracker run to fail * **HLCU 500 mapped to awaiting manifest** — Hapag-Lloyd `error.internalServer` responses (HTTP 500) are now mapped to an awaiting-manifest state instead of surfacing as a hard tracker error * **Destination Port no longer steals focus** — fixed a regression where the Destination Port filter auto-focused on shipments page load; the BOL/reference/container search field is now focused instead, matching standard dashboard UX * **Discarded cargos removed after update** — updating a shipment now removes any user cargos that were discarded in the update, instead of leaving stale cargo records behind * **Reduced WorkOS auth Sentry noise** — expected WorkOS sign-in denials and stale-code callbacks no longer report to Sentry, so real auth errors stand out * **MAEU Browser Use resource loading** — tightened resource blocking on the new MAEU Browser Use integration for faster page loads while preserving the Cloudflare clearance path * **Lazy-load fixes on the dashboard** — resolved two lazy-loading issues in dashboard views that could show empty state before data arrived ### Zapier 2.0 integration The **Terminal49 Zapier integration** has been fully rewritten as version 2.0. The new version is powered by the official Terminal49 TypeScript SDK, ships **31 webhook-based triggers** (tracking requests, transport events for vessel/full/empty milestones, transshipment and feeder events, delivery and pickup events, and more), and returns a cleaner flat payload with a `_raw` object for advanced use. Version 1.0 remains available on Zapier indefinitely — 2.0 ships as a separate version so you can migrate at your own pace. Connect Terminal49 to 6,000+ apps *** ### Tracking request SCAC auto-detect Tracking requests can now be created **without a SCAC**. When `scac` is omitted, Terminal49 uses [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) during tracking to pick the correct carrier and number type. The auto-detect flag is preserved across resubmits, and the resolved SCAC flows through to webhooks so downstream systems always see the actual carrier. Tracking request API reference *** ### New Hapag-Lloyd public API tracker Added a direct **Hapag-Lloyd (HLCU) public API** tracker for container, bill of lading, and booking lookups. HLCU tracking requests are also now included in the periodic **booking sweep**, so bookings without containers are refreshed alongside the rest of your Hapag-Lloyd shipments. *** ### New MAEU and OOLU Browser Use trackers * **MAEU (Maersk) — `maeubu`** — a new Browser Use Cloud tracker for Maersk, running against a residential-fingerprinted browser for improved reliability * **OOLU (OOCL) — `oolubu`** — a new Browser Use Cloud tracker for OOCL that reuses the existing extraction path while auto-clearing the Cloudflare Turnstile challenge * **OOLU direct path retired** — the previous `oolu_direct` integration has been removed now that `oolubu` supersedes it *** ### MSC US import demurrage tariff rules Published a new **MSC (MSCU)** US-import demurrage tariff rule dataset for Smart LFD, sourced from Descartes WsHelp and refreshed on a daily filename sweep. Calculated Last Free Day values for MSC containers now reflect Mediterranean Shipping's published tariff, including CY port, rail base, and named-ramp overrides. View the container resource *** ### Party filter and chips on the shipment dashboard The shipment dashboard now supports **filtering by party**, and party assignments render as inline **chips** on the shipment table so you can see consignee, shipper, notify, dray carrier, and other roles at a glance without opening the shipment. *** ### Updates * **Manage account tags without an admin** — users with the right permission can now create, rename, and remove account tags directly; Terminal49 admins can also manage tags on behalf of accounts * **Shipment Details on the Shipments V2 API** — the shipment details page has been migrated fully to the Shipments V2 API for a leaner, more consistent data model behind the UI * **Faraday connection failure causes surfaced** — outbound HTTP failures from carrier and terminal integrations now expose their underlying cause (timeout, DNS, TLS, connection reset), so retry classification and dashboards reflect the real reason * **Server-side entitlement checks on webhooks** — webhook management and trigger endpoints now enforce entitlement checks server-side, making sure only accounts with the data-out API entitlement can operate on webhooks *** ### Bug fixes * **HLCU groups without container numbers** — Hapag-Lloyd shipment groups that returned no container numbers no longer cause the tracker run to fail * **HLCU 500 mapped to awaiting manifest** — Hapag-Lloyd `error.internalServer` responses (HTTP 500) are now mapped to an awaiting-manifest state instead of surfacing as a hard tracker error * **Destination Port no longer steals focus** — fixed a regression where the Destination Port filter auto-focused on shipments page load; the BOL/reference/container search field is now focused instead, matching standard dashboard UX * **Discarded cargos removed after update** — updating a shipment now removes any user cargos that were discarded in the update, instead of leaving stale cargo records behind * **Reduced WorkOS auth Sentry noise** — expected WorkOS sign-in denials and stale-code callbacks no longer report to Sentry, so real auth errors stand out * **MAEU Browser Use resource loading** — tightened resource blocking on the new MAEU Browser Use integration for faster page loads while preserving the Cloudflare clearance path * **Lazy-load fixes on the dashboard** — resolved two lazy-loading issues in dashboard views that could show empty state before data arrived ### Customize dashboard view cards You can now **reorder and choose which view cards** appear on your dashboard home. Drag to rearrange, hide cards you don't use, and the layout persists per user — so each teammate can tune the dashboard to the views they work from. *** ### Party filters and columns on the container dashboard The container dashboard now supports **filtering and grouping by parties and party roles** (consignee, shipper, notify party, dray carrier, and more). Party roles are also available as columns and in CSV exports, so you can slice the dashboard by who owns each leg and export the same view downstream. *** ### Datetime column format preference Date and datetime columns now respect a **per-user display format preference**. Pick the format that matches how your team reads dates and Terminal49 will use it consistently across dashboard tables. *** ### Calculated LFD feedback in the Smart LFD panel The Smart LFD detail panel now includes a **feedback widget** for calculated (tariff-based) Last Free Days. Tell us whether the calculated LFD looks right, supply a corrected date when it doesn't, and add a reason (custom contract, free days extended, other). Feedback flows into our tariff validation pipeline so calculated LFDs keep improving carrier by carrier. View the container resource *** ### New carrier integrations * **OOCL (OOLU) API tracker** — added a new direct API path for OOCL container, bill of lading, and booking lookups, improving reliability over the public site scrape * **CMA CGM (CMDU) `cmduweb` scraper** — added a new tnt-api-based scraper integration for CMA CGM as an alternative data source *** ### Updates * **Account managers can change user roles** — accounts with the user-manager role can now change other users' roles directly, without escalating to an admin * **CMA PBD events mapped to feeder departed** — CMA CGM "Port Boundary Departure" events now map to a feeder-departed transport event, so feeder legs show up in the timeline with the right phase * **MCP connected client account resolution** — MCP requests now resolve to your primary account, and the audience matcher accepts a trailing slash on the resource URL, so OAuth-aware MCP clients connect more reliably * **WorkOS MCP tokens without `client_id`** — MCP access tokens issued by WorkOS no longer require a `client_id` claim, broadening compatibility with OAuth-aware MCP clients *** ### Bug fixes * **UPRR rail Last Free Day off-by-one** — Union Pacific rail LFDs no longer display a day early. Bare-date LFDs from UPRR are now anchored mid-day so the calendar date is stable across US timezones, and existing midnight-UTC values have been backfilled * **CMSAMX (Contecon Manzanillo) restored** — switched CMSAMX container scraping to the Contecon ISCS backend after the public site went down, restoring container status, holds, and availability at Manzanillo * **SEAU (Sealand) async refreshes** — fixed an issue where scheduled refreshes for Sealand shipments returned an invalid-integration error. SEAU shipments now refresh on schedule the same way new SEAU tracking requests do * **Dray carrier no longer shown after removal** — clearing a dray carrier from a shipment or cargo now updates the party display immediately instead of leaving the removed carrier visible * **A295 (Boston/MCT) and L239 (POMTOC) reliability** — isolated the Tideworks `.io` terminals to a dedicated proxy pool and refreshed A295 credentials, restoring terminal data collection after a sustained block * **OOLU Cloudflare handling reverted** — rolled back a change to OOLU's Cloudflare challenge handling that caused regressions; OOLU continues to track via the established path while the new API tracker rolls out * **WorkOS auth flow hardening** — tightened the WorkOS-backed sign-in flow against edge cases in token validation and session recovery ### Inland (IND) Pickup & Delivery dashboard The Pickup & Delivery workspace now has a dedicated **Inland (IND) destination** view alongside the existing Port of Discharge view, with a segmented control to switch between them. Each phase — pickup-needed, delivery-needed, empty-return-needed, and completed — shows inland-specific columns (IND ATA, Rail LFD, IND Full Out) and filters so the milestones, gating, and sorts all line up with the inland leg. *** ### WorkOS OAuth gateway for the MCP server The Terminal49 **MCP server** now supports OAuth via WorkOS, in addition to API keys. OAuth-aware clients like ChatGPT and Claude connectors can connect to `https://mcp.terminal49.com` and complete the standard MCP OAuth discovery and authorization flow. API-key clients continue to work — use the `Token` scheme for API keys; `Bearer` is now reserved for WorkOS OAuth access tokens. Connect your AI client to Terminal49 *** ### Dray carrier assignment You can now assign a **dray carrier** at the shipment level. Once set, the dray carrier propagates to the shipment's cargos, so dispatchers and downstream views see the same trucker without re-entering it per container. *** ### Parties first-time experience and bulk import The Parties experience now includes a first-time guided introduction, a Parties card with stats, hover tooltips on the card header, and the ability to **create parties directly from the sidebar**. The track page now also supports **bulk party import** during tracking-request creation, so consignee, shipper, notify, and other roles flow through to the resulting shipment and containers automatically. Both account creators and admins can now manage parties without an additional feature flag. *** ### New steamship line trackers: MAEU and SEAU * **MAEU (Maersk)** — added a new Trawler-based tracker for Maersk shipments, replacing the previous integration path * **SEAU (Sealand)** — added a new Trawler2-based tracker for Sealand container, bill of lading, and booking lookups *** ### HLCU (Hapag-Lloyd) demurrage tariff rules Published a new **HLCU (Hapag-Lloyd)** US-import demurrage tariff rule dataset, modeling Merchant-Haulage port demurrage and inland (rail) demurrage. Calculated Last Free Day values for HLCU containers now reflect Hapag-Lloyd's published tariffs. View the container resource *** ### Updates * **Manually add IND Full Out** — the inland-destination view of the Pickup & Delivery dashboard now supports manually adding an IND Full Out event inline, matching the editing capability already available for POD milestones * **Clear manually-set dates on the Pickup Dashboard** — blank-submitting an editable date now clears it when the value was set by hand, and rejects with a clear error when the value came from a carrier or terminal. Changing a date invalidates the contradicted source event so the manual value takes precedence * **Stale bill of lading reset on container retries** — when a shipment's bill of lading number changes, container tracking now resets the stale BL on the next retry instead of holding onto the old reference * **`/v2/shipping_lines` scoped to your integrations** — the `/v2/shipping_lines` listing now only returns carriers your account has an enabled integration for, so the list reflects what you can actually track * **Comma-separated tag input** — the dashboard tag component now accepts multiple tags pasted or typed as a comma-separated list in addition to one-at-a-time entry * **Stricter rail transport event handling** — rail transport events now require a location to be accepted, and terminal-sourced rail events flow through the same pipeline as other transport events * **Bookings preview removed from the left nav** — the experimental Bookings entry no longer appears in the dashboard sidebar while the workspace continues to evolve *** ### Bug fixes * **Refresh Tracking only when signed in** — the Refresh Tracking action no longer appears on public track pages for signed-out users, matching what's actually available to them * **CMSAMX scraper restored after Contecon migration** — fixed terminal data collection at CMSA Manzanillo after Contecon's site migration, restoring container availability and hold data * **APMweb CFFI `no_event_history`** — fixed a NoMethodError that could occur in the APMweb CFFI agent when a shipment had no event history, so the tracker run completes cleanly * **Public SMLU integration disabled** — disabled the public SMLU web integration that was no longer returning reliable data * **Transport event source serialization** — invalidated transport events now serialize their source correctly on the API and dashboard timeline * **`exists` / `not_exists` filter operators** — fixed `exists` and `not_exists` filtering on relationship and reference fields, and stopped emitting raw values in CSV exports for those queries * **Duplicate PAYG contracts on activation** — fixed a race that could create a duplicate Pay-As-You-Go contract during account activation * **Cargo events reprocessing** — force-reprocessing shipment cargo events now refreshes derived state consistently after upstream fixes ### WorkOS hosted signup onboarding New accounts can now sign up through the **WorkOS hosted authentication flow**. The end-to-end onboarding handles email verification, recovery from unknown sign-ins, and concurrent same-email signup races automatically. Existing sign-in continues to work unchanged. *** ### OOCL direct-to-track-page scraper Added a new **`oolu_direct`** OOCL integration that navigates straight to the tracking host, skipping the OOCL home page and its Cloudflare Turnstile challenge. The result is fewer scrape failures and faster, more reliable container, bill of lading, and booking lookups for OOCL (OOLU) shipments. *** ### Calculated LFD in the Pickup & Delivery dashboard The **Pickup & Delivery** workspace now surfaces the Smart LFD **calculated Last Free Day** alongside reported LFDs, so containers without a carrier- or terminal-reported LFD still show a tariff-based date to plan against. View the container resource *** ### Bulk add parties from the Network page The Network page now supports **bulk party imports**, so you can add many consignees, shippers, notify parties, and other roles to your network in a single upload instead of one at a time. *** ### Updates * **Wayfair arrival notice extraction** — new versioned schema with Maersk Canada CCN carrier-code extraction, PO reference truncation, freight payment terms derived from the marks block, and tighter classification so CMA CGM pre-arrival notices and multi-page Hapag-Lloyd Canada documents are no longer misclassified * **Wayfair house bill of lading consistency** — draft and final Wayfair HBLs for the same shipment now produce consistent `hs_codes` values, and a new `consignee.fax` field is captured * **Party assignment defaults and sorting** — party pickers default to the previously assigned party, parties are sorted for easier selection, and the role color palette has been toned down * **Custom Fields on Shipment Details** — custom shipment and container fields have been reorganized on the shipment details page for a cleaner layout and clearer separation between scopes * **Editable cells on Pickup & Delivery** — inline editing on the Pickup & Delivery phase tables has been polished, with a production-safe edit hook so save and validation behave consistently *** ### Bug fixes * **LFD no longer shown past POD pickup** — Last Free Day values are now suppressed once a container has been picked up from the port of discharge, so the dashboard and API stop surfacing irrelevant deadlines * **YMLU transshipment events** — Fixed remaining issues with YMLU transshipment events so port-of-discharge and transshipment legs are reported correctly * **NSRR blocked HTML responses** — Norfolk Southern responses that return a block page are now handled cleanly instead of failing the tracker run, with retries handled upstream * **Container event ordering** — Improved event ordering in container status updates, so derived statuses follow the true chronology of vessel, terminal, and rail events * **Stripe 35-day period cap** — Fixed a Stripe timestamp issue that could cap subscription period dates at 35 days, restoring accurate end-of-period timing on PAYG accounts ### Customize dashboard view cards You can now **reorder and choose which view cards** appear on your dashboard home. Drag to rearrange, hide cards you don't use, and the layout persists per user — so each teammate can tune the dashboard to the views they work from. *** ### Party filters and columns on the container dashboard The container dashboard now supports **filtering and grouping by parties and party roles** (consignee, shipper, notify party, dray carrier, and more). Party roles are also available as columns and in CSV exports, so you can slice the dashboard by who owns each leg and export the same view downstream. *** ### Datetime column format preference Date and datetime columns now respect a **per-user display format preference**. Pick the format that matches how your team reads dates and Terminal49 will use it consistently across dashboard tables. *** ### Calculated LFD feedback in the Smart LFD panel The Smart LFD detail panel now includes a **feedback widget** for calculated (tariff-based) Last Free Days. Tell us whether the calculated LFD looks right, supply a corrected date when it doesn't, and add a reason (custom contract, free days extended, other). Feedback flows into our tariff validation pipeline so calculated LFDs keep improving carrier by carrier. View the container resource *** ### New carrier integrations * **OOCL (OOLU) API tracker** — added a new direct API path for OOCL container, bill of lading, and booking lookups, improving reliability over the public site scrape * **CMA CGM (CMDU) `cmduweb` scraper** — added a new tnt-api-based scraper integration for CMA CGM as an alternative data source *** ### Updates * **Account managers can change user roles** — accounts with the user-manager role can now change other users' roles directly, without escalating to an admin * **CMA PBD events mapped to feeder departed** — CMA CGM "Port Boundary Departure" events now map to a feeder-departed transport event, so feeder legs show up in the timeline with the right phase * **MCP connected client account resolution** — MCP requests now resolve to your primary account, and the audience matcher accepts a trailing slash on the resource URL, so OAuth-aware MCP clients connect more reliably * **WorkOS MCP tokens without `client_id`** — MCP access tokens issued by WorkOS no longer require a `client_id` claim, broadening compatibility with OAuth-aware MCP clients *** ### Bug fixes * **UPRR rail Last Free Day off-by-one** — Union Pacific rail LFDs no longer display a day early. Bare-date LFDs from UPRR are now anchored mid-day so the calendar date is stable across US timezones, and existing midnight-UTC values have been backfilled * **CMSAMX (Contecon Manzanillo) restored** — switched CMSAMX container scraping to the Contecon ISCS backend after the public site went down, restoring container status, holds, and availability at Manzanillo * **SEAU (Sealand) async refreshes** — fixed an issue where scheduled refreshes for Sealand shipments returned an invalid-integration error. SEAU shipments now refresh on schedule the same way new SEAU tracking requests do * **Dray carrier no longer shown after removal** — clearing a dray carrier from a shipment or cargo now updates the party display immediately instead of leaving the removed carrier visible * **A295 (Boston/MCT) and L239 (POMTOC) reliability** — isolated the Tideworks `.io` terminals to a dedicated proxy pool and refreshed A295 credentials, restoring terminal data collection after a sustained block * **OOLU Cloudflare handling reverted** — rolled back a change to OOLU's Cloudflare challenge handling that caused regressions; OOLU continues to track via the established path while the new API tracker rolls out * **WorkOS auth flow hardening** — tightened the WorkOS-backed sign-in flow against edge cases in token validation and session recovery ### Inland (IND) Pickup & Delivery dashboard The Pickup & Delivery workspace now has a dedicated **Inland (IND) destination** view alongside the existing Port of Discharge view, with a segmented control to switch between them. Each phase — pickup-needed, delivery-needed, empty-return-needed, and completed — shows inland-specific columns (IND ATA, Rail LFD, IND Full Out) and filters so the milestones, gating, and sorts all line up with the inland leg. *** ### WorkOS OAuth gateway for the MCP server The Terminal49 **MCP server** now supports OAuth via WorkOS, in addition to API keys. OAuth-aware clients like ChatGPT and Claude connectors can connect to `https://mcp.terminal49.com` and complete the standard MCP OAuth discovery and authorization flow. API-key clients continue to work — use the `Token` scheme for API keys; `Bearer` is now reserved for WorkOS OAuth access tokens. Connect your AI client to Terminal49 *** ### Dray carrier assignment You can now assign a **dray carrier** at the shipment level. Once set, the dray carrier propagates to the shipment's cargos, so dispatchers and downstream views see the same trucker without re-entering it per container. *** ### Parties first-time experience and bulk import The Parties experience now includes a first-time guided introduction, a Parties card with stats, hover tooltips on the card header, and the ability to **create parties directly from the sidebar**. The track page now also supports **bulk party import** during tracking-request creation, so consignee, shipper, notify, and other roles flow through to the resulting shipment and containers automatically. Both account creators and admins can now manage parties without an additional feature flag. *** ### New steamship line trackers: MAEU and SEAU * **MAEU (Maersk)** — added a new Trawler-based tracker for Maersk shipments, replacing the previous integration path * **SEAU (Sealand)** — added a new Trawler2-based tracker for Sealand container, bill of lading, and booking lookups *** ### HLCU (Hapag-Lloyd) demurrage tariff rules Published a new **HLCU (Hapag-Lloyd)** US-import demurrage tariff rule dataset, modeling Merchant-Haulage port demurrage and inland (rail) demurrage. Calculated Last Free Day values for HLCU containers now reflect Hapag-Lloyd's published tariffs. View the container resource *** ### Updates * **Manually add IND Full Out** — the inland-destination view of the Pickup & Delivery dashboard now supports manually adding an IND Full Out event inline, matching the editing capability already available for POD milestones * **Clear manually-set dates on the Pickup Dashboard** — blank-submitting an editable date now clears it when the value was set by hand, and rejects with a clear error when the value came from a carrier or terminal. Changing a date invalidates the contradicted source event so the manual value takes precedence * **Stale bill of lading reset on container retries** — when a shipment's bill of lading number changes, container tracking now resets the stale BL on the next retry instead of holding onto the old reference * **`/v2/shipping_lines` scoped to your integrations** — the `/v2/shipping_lines` listing now only returns carriers your account has an enabled integration for, so the list reflects what you can actually track * **Comma-separated tag input** — the dashboard tag component now accepts multiple tags pasted or typed as a comma-separated list in addition to one-at-a-time entry * **Stricter rail transport event handling** — rail transport events now require a location to be accepted, and terminal-sourced rail events flow through the same pipeline as other transport events * **Bookings preview removed from the left nav** — the experimental Bookings entry no longer appears in the dashboard sidebar while the workspace continues to evolve *** ### Bug fixes * **Refresh Tracking only when signed in** — the Refresh Tracking action no longer appears on public track pages for signed-out users, matching what's actually available to them * **CMSAMX scraper restored after Contecon migration** — fixed terminal data collection at CMSA Manzanillo after Contecon's site migration, restoring container availability and hold data * **APMweb CFFI `no_event_history`** — fixed a NoMethodError that could occur in the APMweb CFFI agent when a shipment had no event history, so the tracker run completes cleanly * **Public SMLU integration disabled** — disabled the public SMLU web integration that was no longer returning reliable data * **Transport event source serialization** — invalidated transport events now serialize their source correctly on the API and dashboard timeline * **`exists` / `not_exists` filter operators** — fixed `exists` and `not_exists` filtering on relationship and reference fields, and stopped emitting raw values in CSV exports for those queries * **Duplicate PAYG contracts on activation** — fixed a race that could create a duplicate Pay-As-You-Go contract during account activation * **Cargo events reprocessing** — force-reprocessing shipment cargo events now refreshes derived state consistently after upstream fixes ### WorkOS hosted signup onboarding New accounts can now sign up through the **WorkOS hosted authentication flow**. The end-to-end onboarding handles email verification, recovery from unknown sign-ins, and concurrent same-email signup races automatically. Existing sign-in continues to work unchanged. *** ### OOCL direct-to-track-page scraper Added a new **`oolu_direct`** OOCL integration that navigates straight to the tracking host, skipping the OOCL home page and its Cloudflare Turnstile challenge. The result is fewer scrape failures and faster, more reliable container, bill of lading, and booking lookups for OOCL (OOLU) shipments. *** ### Calculated LFD in the Pickup & Delivery dashboard The **Pickup & Delivery** workspace now surfaces the Smart LFD **calculated Last Free Day** alongside reported LFDs, so containers without a carrier- or terminal-reported LFD still show a tariff-based date to plan against. View the container resource *** ### Bulk add parties from the Network page The Network page now supports **bulk party imports**, so you can add many consignees, shippers, notify parties, and other roles to your network in a single upload instead of one at a time. *** ### Updates * **Wayfair arrival notice extraction** — new versioned schema with Maersk Canada CCN carrier-code extraction, PO reference truncation, freight payment terms derived from the marks block, and tighter classification so CMA CGM pre-arrival notices and multi-page Hapag-Lloyd Canada documents are no longer misclassified * **Wayfair house bill of lading consistency** — draft and final Wayfair HBLs for the same shipment now produce consistent `hs_codes` values, and a new `consignee.fax` field is captured * **Party assignment defaults and sorting** — party pickers default to the previously assigned party, parties are sorted for easier selection, and the role color palette has been toned down * **Custom Fields on Shipment Details** — custom shipment and container fields have been reorganized on the shipment details page for a cleaner layout and clearer separation between scopes * **Editable cells on Pickup & Delivery** — inline editing on the Pickup & Delivery phase tables has been polished, with a production-safe edit hook so save and validation behave consistently *** ### Bug fixes * **LFD no longer shown past POD pickup** — Last Free Day values are now suppressed once a container has been picked up from the port of discharge, so the dashboard and API stop surfacing irrelevant deadlines * **YMLU transshipment events** — Fixed remaining issues with YMLU transshipment events so port-of-discharge and transshipment legs are reported correctly * **NSRR blocked HTML responses** — Norfolk Southern responses that return a block page are now handled cleanly instead of failing the tracker run, with retries handled upstream * **Container event ordering** — Improved event ordering in container status updates, so derived statuses follow the true chronology of vessel, terminal, and rail events * **Stripe 35-day period cap** — Fixed a Stripe timestamp issue that could cap subscription period dates at 35 days, restoring accurate end-of-period timing on PAYG accounts ### Pickup & Delivery dashboard A new **Pickup & Delivery** workspace is available in the Terminal49 dashboard, organizing containers by their pickup-and-delivery phase. View phase-grouped tables, edit pickup and delivery event datetimes inline, and sort by delivery appointment to focus on what needs to move next. *** ### Bulk party (role) assignment Shipment and container party roles can now be assigned and updated **in bulk** from the dashboard, including via CSV upload alongside other shipment and container fields. Bulk-assign consignee, shipper, notify party, and other roles across many records in a single import. *** ### Event-driven calculated demurrage LFD The Smart LFD calculation engine now runs **event-by-event** rather than on a periodic backfill. When a tariff input changes — a new arrival event, a hold release, or a tariff update — the calculated Last Free Day refreshes immediately and emits a transport event with `t49_calculation` as its source. View the container resource *** ### Refreshed ONE, CMA CGM, and Evergreen tariffs Re-synthesized the demurrage tariff rule datasets for **ONE (ONEY)**, **CMA CGM (CMDU)**, and **Evergreen (EGLV)** with new effective-date periods and pipeline improvements. Calculated LFD values for these carriers now reflect their latest published tariffs. *** ### Updates * **`t49_calculation` source on the shipment timeline** — events created by the Smart LFD calculation engine now surface a `t49_calculation` source tag in the dashboard event timeline, so you can tell at a glance which LFD updates came from a tariff calculation * **Manual milestone edits flow through the event pipeline** — user-submitted empty-in, full-out, and delivery-appointment events are now created as proper transport events. They update container attributes, fire `container.transport_event` webhooks, and appear consistently in the timeline like carrier-sourced events * **Updated Wayfair arrival notice extraction** — published a new versioned extraction schema for Wayfair arrival notices with refreshed gold datasets and classifier prompts, improving field-level accuracy on Wayfair documents * **Sort container dashboard by shipment number** — added Shipment Number as a sortable column on the container dashboard * **Sort container dashboard by delivery appointment** — added delivery appointment as a sortable field on the containers API and dashboard * **PAYG billing clarification** — the billing settings page now clearly explains how active free containers are counted on Pay-As-You-Go plans * **Bookings removed from the left nav** — the Bookings preview entry has been removed from the dashboard navigation while the workspace continues to evolve *** ### Bug fixes * **Tracking slot limit no longer blocks paying customers** — Fixed an issue where the active tracking slot limit could incorrectly block paying accounts from creating additional tracking requests. The limit and its warning now apply only to free plans * **CMDU ambiguous port events** — Rejected ambiguous CMDU port events that could otherwise be attributed to the wrong leg, and excluded pre-POD events from the destination-port heuristic so inland destinations are inferred more accurately * **YMLU POD selection** — Fixed port-of-discharge selection for YMLU shipments where only container-level data was available, so the correct POD is chosen * **NSRR top-level ETA** — Fixed parsing of Norfolk Southern's top-level ETA so rail ETAs populate when the carrier returns them at the response root * **WMC unavailable terminal status** — Where's My Container responses that report a container as unavailable are now handled cleanly instead of failing the terminal scrape * **Removed sort custom field columns are forgotten** — Deleting a custom field used as the active sort no longer leaves the dashboard stuck on an unknown sort column * **Map route Pylon widget** — Fixed the Pylon help bubble showing on the global vessel map; it now hides and re-shows correctly when navigating in and out of the map ### Global vessel map is now generally available The **global vessel map** is now enabled for all accounts in the Terminal49 dashboard. Live vessel positions, voyage details, and the shipments on board are available without a feature flag, with realtime updates and analytics tracking built in. *** ### Wayfair document extraction improvements Released new versioned extraction schemas for three Wayfair document types — **arrival notice**, **carrier delivery order**, and **cargo control document**. Highlights: * Arrival notice now captures **place of delivery** (name, address, phone, available date, last free day, FIRMS code, and sublocation) * FIRMS code routing distinguishes rail-ramp destinations from the port of discharge, with both fields populated when both codes appear * **Hapag-Lloyd Canada** "Advice Note / Avis d'Expédition" documents are now correctly classified as arrival notices (previously misclassified as sea waybills) * Tighter `I` vs `1` disambiguation across carriers How document linking works *** ### BL submissions without containers You can now submit **bill of lading shipments before container numbers are known**. Tracking requests for BL submissions accept shipments with no containers, so you can initiate tracking earlier in your workflow and have containers attach as they're released by the carrier. *** ### New contract products Added two new contract products available through your account team: * **Lite** — a lower-tier subscription for smaller-volume customers * **Integration service fee** — a separate line item for one-time integration work Contact your account team for pricing and availability. *** ### Updates * **Pending tracking requests count as one slot** — Pending tracking requests are now counted against a single tracking slot rather than per-container, giving free and metered plans more headroom while requests are awaiting manifest * **HDMU mobile LFD locations** — Last Free Day events from the HDMU mobile tracker now infer the correct terminal location, fixing missing per-terminal LFD displays for Hyundai (HDMU) containers * **SSL overview no longer overwrites terminal data** — Shipping line overview responses no longer overwrite terminal-sourced fields on containers, so terminal availability, holds, and fees stay authoritative when both sources are present * **Empty-In can override terminal status** — A reported empty-in event now correctly overrides a stale terminal status, so containers returned empty are reflected promptly in the dashboard and API * **Transport event location fallbacks removed** — Removed legacy fallbacks that could fill in incorrect locations on transport events. Event locations now reflect the source data directly, with no implicit substitutions *** ### Bug fixes * **ZIM destination ETA no longer auto-promotes to ATA** — Fixed an issue where ZIM destination ETAs could be promoted to an actual arrival time prematurely, causing inland arrival dates to flip without a real event * **Document linking via bridge shipments** — Documents no longer create an intermediate link when more than one shipment is involved in a reference bridge, reducing incorrect cross-shipment links * **Dashboard custom field date filters** — Fixed an error where the *not present* operator failed on date and datetime custom field filters, and resolved a related stale sort issue * **Harbor filter search** — Restored search inside the dashboard's chip-style filter dropdowns after the underlying picker upgrade * **Trawler `no_event_history` with containers** — Allowed the `no_event_history` tracker state to coexist with container records, preventing dropped updates for shipments whose carriers temporarily return no event history ### Tracking request custom fields API [You can now manage **custom field values on tracking requests** via the API, in addition to shipments and containers. Use the new endpoints to attach predefined custom fields when a tracking request is created — values flow through to the resulting shipment and containers once tracking starts.](/docs/api-docs/api-reference/tracking-requests/auto-detect-carrier) List values on a tracking request Attach a value to a tracking request *** ### KMTC steamship tracker Added a direct integration for **KMTC (KORP)** shipments. Track KMTC containers, bills of lading, and bookings using their native SCAC, with full container, event, and route data. *** ### New terminal integrations * **Termont (Montreal)** — added a WordPress-based scraper for Termont container availability, status, and rail-state data * **APM Terminals — alternative scrapers** — two new APM scraper integrations (`apmweb_cffi` and `apmweb_firecrawl`) are available alongside the existing APM source, so terminals can be served by whichever endpoint performs best per site *** ### Container status in CSV export The container dashboard CSV export now includes a **Current Status** column and supports filtering by current status before export, so exported files reflect exactly what you see on screen. *** ### Report Issue from the container side panel The container details side panel now includes a **Report Issue** button so you can flag a data problem directly from the container you're viewing, without leaving the dashboard. *** ### Multi-period tariff history for Smart LFD The Smart LFD tariff engine now tracks **multiple historical tariff periods** per carrier, so Last Free Day calculations on older containers use the tariff rules that were in effect at the time. Tariff rules now also carry `tariff_number` and `tariff_rule_number` for traceability back to the source document, and `synthesis_warnings` surface any issues detected when a tariff was synthesized. View the container resource *** ### Updates * **Document linking by custom fields** — documents can now be linked to shipments and containers using custom field values, in addition to PO numbers, BOLs, and container numbers * **PO number matching is more forgiving** — additional common typo patterns are now matched when linking documents via PO number * **Free plan tracking slot limits** — Free accounts now meter against active tracking slots with clearer in-app usage copy, and unknown-container slot handling has been tightened * **Inline custom field option creation** — single- and multi-select custom fields now let you create new options inline from the value picker, with several follow-up polish improvements * **Custom field filter-out option** — container filters now support filtering for records where a custom field is *not* set * **Quick link to shipment/container** — added a button to copy a direct link to a shipment or container from the dashboard * **EGLV Last Free Day events now include location** — EGLV LFD events now carry the inland destination location so per-terminal LFD displays correctly * **Arrival notice classification** — fixed an issue where some arrival notices were misclassified as sea waybills during document processing * **Hapag-Lloyd FIRMS code routing** — clarified FIRMS code handling on arrival notice extraction so terminal routing is more reliable *** ### Bug fixes * **TRKU future events** — TRKU events dated in the future are now classified correctly so container timelines no longer skip past upcoming events * **HMM mobile error pages** — HMM (HDMU) mobile responses that return an HTML error page are now handled cleanly instead of failing the tracker run * **MAEU tracking** — Migrated remaining MAEU traffic off the legacy tracker to MaeusynergyTracker for more reliable Maersk container data * **Document linking edge cases** — documents with both an explicit and implicit link no longer fail to link, child packets now inherit only direct links from their parent, and legacy `.xls` files using the `roo-xls` path are processed correctly again * **Container state filters** — removed the unused "loaded" and "dropped" values from the current-status filter so the dropdown only shows real states * **Dashboard search focus** — fixed an issue where selecting a recent search left the input out of focus, and tightened the global click listener that drives dropdowns * **Disabled integrations** — disabled integrations no longer appear in the credentials list * **Unknown account handling** — webhook and ingestion paths no longer raise when an unknown account is referenced * **Sentry instrumentation** — fixed a missing region on the Sentry cassette job and removed a path that could cause Sentry error reporting itself to loop * **Knock in-app guides** — improved rendering of the Knock guide banner and moved free-plan Knock syncing to commit-time so guides display reliably ### `link.created` webhook A new `link.created` webhook event fires whenever a document is linked to a shipment or container. Subscribe to it to react in real time when reference extraction connects an inbound document to your cargo — no polling required. Browse all webhook events *** ### Bookings (preview) A new **Bookings** workspace is available as an early-access preview. Bookings ingests carrier schedules, surfaces gate-in cutoffs and vessel phase, and automatically initiates tracking once a booking has the schedule context it needs (POL, vessel, ETD). Contact your account team to enable the preview on your account. *** ### New terminal integrations * **Port Houston (Barbours Cut, S787)** — added a direct API integration for container status, holds, fees, and last free day at Port Houston * **Port of Wilmington (L194)** — added a Voyager-based scraper for Port of Wilmington container data *** ### Per-account terminal scraper selection Accounts can now opt into specific terminal scraper integrations independently, so you can pilot new data sources or stay on the existing one on a per-account basis. *** ### `value_timestamp` on shipping line LFD transport events The `value` and `value_timestamp` fields on transport events are now always returned in the API and on webhook payloads, including for shipping line Last Free Day events. Previously these were gated by a rail data permission; you can now see when an SSL LFD was reported without extra configuration. Transport events API reference *** ### Draft demurrage tariff rules for ONE, Evergreen, and CMA CGM Added draft demurrage tariff rule datasets for **ONE (ONEY)**, **Evergreen (EGLV)**, and **CMA CGM (CMDU)** to the Smart LFD tariff engine. Tariff rules now also carry `charge_type` (demurrage vs. detention) and `day_count_type` so calculated Last Free Day values reflect the carrier's specific tariff structure. View the container resource *** ### Expanded CSV bulk update The container dashboard's CSV bulk update now supports more fields: * **Shipment tags and reference numbers** can be updated in the same CSV alongside container fields * **Tracking request custom fields** can be created and updated in bulk for tracking requests that are still pending or awaiting manifest *** ### Manual refresh feedback When you trigger a manual refresh on a shipment, its containers now display a refreshing state in the dashboard so you can tell which records are still updating. *** ### Custom fields moved to Data Management Custom fields now live under **Data Management** in the dashboard, and tags share the same look-and-feel as custom fields for a more consistent experience when configuring shipment metadata. Custom Fields API reference *** ### Daily email digest pacing Daily email digests are now spaced out across the delivery window instead of being scheduled in a single burst, improving deliverability and reducing duplicate-send risk for large accounts. *** ### Bug fixes * **Rail LFD on container pickup** — Rail Last Free Day no longer fails to update once a container has been picked up * **APM containers on rail** — Fixed an error that could occur for APM containers on rail with no equipment moves, allowing container data to load * **HDMU LFD events** — HDMU Last Free Day events now include the correct location for accurate per-terminal LFD display * **Swire ETA fallback** — Swire shipments now fall back to the tracking sheet for ETA when the primary source is missing, and route locations are extracted more reliably * **SMLU schedule port calls** — Fixed a NoMethodError affecting SMLU schedule lookups so shipments load consistently * **POL/POD location extraction** — Improved extraction of port-of-loading and port-of-discharge locations for **SSPH**, **ZIM (ZIMU)**, **TJFH**, **TXZJ/TXSJ**, and **YMLU** shipments, reducing missing or incorrect route locations * **Terminal scrape error handling** — Failures during terminal scrape context resolution are now handled cleanly so they no longer pollute terminal-health metrics * **Steamship tracker rate limits** — Expected rate-limit errors from upstream carriers are now silenced and vessel-event extraction is instrumented for better visibility into ingestion health ### Global vessel map The Terminal49 dashboard now includes a **global vessel map** showing live positions for vessels carrying your shipments. Click any vessel to see its current voyage, route, and the shipments on board. The map streams updates over a websocket connection so positions stay current without manual refresh. *** ### Search by custom field values Shipments and containers can now be searched by **custom field values** alongside built-in identifiers. Use the same search you already use for container numbers or PO numbers to find records by any custom field you've defined. Custom Fields API reference *** ### Sensitive custom fields Custom field definitions now support a `sensitive` flag. Mark a field as sensitive to restrict its visibility to authorized users — useful for fields containing internal references, financial data, or PII. *** ### Configurable duplicate document handling Accounts can now configure how duplicate documents are handled at upload time. Choose whether to reject duplicates, replace the existing document, or accept both, so document intake matches your team's workflow. How document linking works *** ### xlsm spreadsheet support Macro-enabled Excel files (`.xlsm`) are now accepted by the document upload pipeline and converted to CSV for reference extraction. Previously only `.xls` and `.xlsx` were processed; mixed Excel uploads now flow through end-to-end without manual conversion. *** ### Destination ETA for non-US/CA containers without rail data Containers routed to inland destinations outside the US and Canada now return a destination ETA even when no rail tracking data is available, expanding ETA coverage for international inland moves. View the container resource *** ### New terminal scrapers * **Felixstowe** — added a mobile-site-based scraper as an alternative data source, improving reliability for containers moving through Felixstowe * **DP World Vancouver (3380)** — added a public-API scraper for terminal data at DP World Vancouver *** ### Promo codes and coupon links for Pay-As-You-Go checkout Pay-As-You-Go customers can now enter a promo code at Stripe Checkout, or follow a coupon link that pre-applies the discount during signup. *** ### Bug fixes * **YMLU transshipment events** — Fixed spurious full-out events generated at transshipment ports and corrected port-of-discharge mismatches for YMLU shipments * **MSC inland voyage numbers** — Stabilized voyage number assignment for MSC inland feeder and barge legs so the same leg no longer appears under different voyage numbers across updates * **ONE event timestamps** — Rail unloaded and other ONE events now use port-local time consistently, fixing display and ordering issues for ONE-tracked containers * **Bulk email CSV uploads** — Fixed an issue where rows using alternative SCACs were silently dropped from CSVs submitted by email * **Rail carrier lookup** — Rail carrier resolution now considers neighboring ports, improving carrier assignment for containers transferring between adjacent rail-served facilities * **Tideworks terminal sessions** — A 403 on the CSV endpoint now clears the cached session immediately, preventing a cascade of failed terminal lookups across containers * **Steamship tracker reliability** — Proxy and firewall blocks at carrier websites are now classified as `TrackerBlocked` errors, surfacing a clearer failure reason and enabling targeted retries * **POMTOC (L239)** — Routed POMTOC Tideworks requests through a residential proxy and added required Referer/Origin headers to restore data collection * **Free plan container limits** — Free plan accounts now meter against active container concurrency rather than monthly cumulative counts, so picked-up and delivered containers no longer count toward the limit * **Duplicate tags** — Eliminated a race condition that could cause duplicate tag creation to fail with a unique constraint error * **Postmark email intake** — Recipient email lookups are now case-insensitive, fixing missed matches on inbound document emails * **Pay-As-You-Go cancellation** — Fixed handling of Stripe subscription period dates so the cancel response returns accurate end-of-period timing for newer subscription structures * **Daily digest jobs** — Database errors during daily digest delivery now release the cache lock cleanly so the next run can proceed ### Booking number support in Infer Tracking Number The [Infer Tracking Number](/docs/api-docs/api-reference/tracking-requests/auto-detect-carrier) endpoint now predicts the carrier and number type for **booking numbers**, in addition to container numbers and bills of lading. Send a booking number you aren't sure how to route, and Terminal49 returns a confidence-scored decision so you can create the tracking request with the correct VOCC SCAC and `request_type`. How to use Infer Tracking Number in your workflow *** ### Delivered date filter and column on the container dashboard The container dashboard now includes a **Delivered at** column and date-range filter, plus delivered-date sorting. Quickly find recently delivered containers, sort by delivery date, or filter to a specific window for reporting and reconciliation. *** ### Fuzzy search for documents Document search is now fuzzy across shipment, container, and tracking request reference numbers. Minor typos, format differences, or partial reference numbers still return the right document, reducing time spent hunting for paperwork. *** ### Hold descriptions on container holds Each hold returned on a container now includes a plain-language description explaining what the hold means and what's typically required to clear it. The descriptions are surfaced wherever holds appear so your team — and your customers — can act without looking up terminology. How to work with holds and fees data *** ### Reference number flag on custom field definitions Custom field definitions now support a `reference_number` flag. Mark a custom field as a reference number and Terminal49 will treat its value as a searchable identifier alongside built-in references like PO numbers and BOLs. Custom Fields API reference *** ### PO number normalization for document linking Document-to-shipment linking via PO numbers now normalizes common format variants (separators, prefixes, and casing differences), so documents match shipments even when the PO is written slightly differently across systems. *** ### Bug fixes * **Containers requiring attention** — Picked-up, on-rail, empty-returned, and delivered containers no longer appear under the "needs attention" LFD view. Only containers still active at a terminal show up * **Tideworks fees** — Containers reported as on a vessel or on rail no longer return stale terminal fees from a previous move * **MSC Port of Discharge events** — Improved criteria for selecting the correct POD arrival event on MSC shipments * **LBCT inland transfers** — Fixed handling of on-dock-to-inland transfer events at LBCT, so rail moves out of the terminal are tracked correctly * **COSCO vessel departure** — Improved logic for determining when a COSCO vessel has departed the port of loading * **Vessel event deduplication** — Fixed duplicate vessel events on the shipment timeline coming from carriers and from the CSE invalidation pipeline * **Document linking race condition** — Eliminated a race condition that could leave a document briefly unlinked when references arrived from multiple sources at once * **Webhook notifications page** — Fixed an exception that could occur when listing webhook notifications containing document processing events * **Carrier integrations** — Fixes to MAEU auth handling (now treats 403 correctly), APM Empty In OAuth requests, BNSF rail scraping, NSRR equipment requests, Hapag-Lloyd booking sweeps for shipments with no containers, and Voyager terminal request headers * **Terminal data** — Updated headers for SSAMX and Holt agent terminals, and added external location mapping for PCIU * **Document email intake** — Tightened sender-email rules so unrelated inbox messages are filtered out, and fixed an edge case where a single email recipient was not handled as an array ### Smarter document-to-shipment linking Documents are now matched to shipments using **indirect references** such as purchase order (PO) numbers. Previously, a document had to contain a direct identifier like a bill of lading or container number. Now, if a document references a PO number that appears on another already-linked document, Terminal49 can connect the two — reducing unlinked documents and manual work. How document linking works *** ### Improved document extraction accuracy Document data extraction now uses the original filename as additional context, improving field-level accuracy when the filename contains shipment identifiers or document type hints. *** ### Bug fixes * **Mark as delivered** — Fixed an issue where marking a container as delivered from the dashboard event viewer could fail. User-submitted delivery events are now accepted correctly * **Stale terminal LFD dates** — Fixed a bug where outdated Last Free Day dates from a previous cycle could appear on containers still in transit. Terminal LFD events are now rejected until the container arrives at the port * **MSC departure events** — Corrected incorrect departure-from-POD events being reported for certain MSC containers * **Hapag-Lloyd tracking** — Fixed an error that could occur when Hapag-Lloyd returns an empty shipment overview, preventing container data from loading * **Norfolk Southern rail LFD timezone** — Fixed incorrect timezone parsing for Norfolk Southern rail Last Free Day dates, which could shift the date by a day * **Dashboard event viewer** — Fixed display issues in the unified event viewer for user-submitted events * **Terminal data collection** — Resolved connectivity and data retrieval issues at Goport, Maher, and Packer Avenue terminals, restoring terminal-sourced hold, fee, and availability data for containers at those facilities ### Dashboard deep linking You can now link directly to any shipment or container in the Terminal49 dashboard using a container number, bill of lading, booking number, or reference number — no internal IDs needed. Use the URL pattern: ```text theme={null} https://app.terminal49.com/shipments/find?q={identifier} ``` The link automatically resolves to the correct tracking page, making it easy to link from a TMS, ERP, spreadsheet, or automated notification. How to build direct links to shipments and containers *** ### Calculated Last Free Day The Smart LFD system now includes a **calculated Last Free Day** derived from carrier demurrage tariff rules. When a shipping line or terminal hasn't reported an LFD, Terminal49 computes one using the applicable tariff — including free days, start events, holidays, and business-day rules. Calculated values appear as a fallback in the `import_deadlines` field on the container API. If a reported LFD is available from the carrier or terminal, it takes priority. View the container resource *** ### Smart LFD dashboard columns The container dashboard now includes two new columns for accounts with Smart LFD enabled: * **Calc. Line LFD** — the calculated Last Free Day derived from carrier tariff rules * **Smart LFD** — the best available LFD with its source displayed, so you can see at a glance whether the date comes from the shipping line, terminal, or a tariff calculation Click the Smart LFD value to open a detail modal showing the full calculation breakdown, including tariff inputs, free days, and the day-by-day calendar. *** ### Bug fixes * **Hapag-Lloyd booking tracking** — Fixed an issue where certain Hapag-Lloyd bookings could return an invalid number error, preventing container data from loading ### Smart Last Free Day The container API now returns a detailed breakdown of **Last Free Day (LFD)** data within the `import_deadlines` field. Instead of a single date, you get separate LFD values from the shipping line and the terminal facility — at both the port of discharge and the inland destination. Each source includes: * **Original** — the first LFD reported * **Current** — the latest LFD reported * **Calculated** — a derived value when the raw date needs adjustment A **unified** selection shows which source Terminal49 considers the best current LFD and why. View the container resource *** ### Inland destination ETA source transparency Containers now include an `inland_destination_eta_source_summary` field that tells you exactly where the displayed inland destination ETA comes from. Possible sources are: * **Shipping line** — ETA provided by the carrier * **Rail** — ETA from the rail carrier * **T49 operations team** — manually verified ETA * **T49 prediction engine** — machine-learning-based estimate The Terminal49 dashboard also shows the active ETA source, and hovering over it displays all available estimates so you can compare. *** ### Expanded shipping line LFD coverage Shipping line Last Free Day data is now collected from additional SSA terminals, improving LFD accuracy for containers moving through those facilities. DataSync containers reference *** ### Bug fixes * **Hapag-Lloyd booking tracking** — Fixed an error that could prevent booking equipment data from being returned for Hapag-Lloyd shipments * **Dashboard copy/paste** — Copy and paste keyboard shortcuts in the container dashboard no longer interfere with text input fields * **Webhook notification links** — The dashboard webhook notifications page now correctly displays reference links for events with multiple related resources ### TypeScript SDK Released the **Terminal49 TypeScript SDK** (`@terminal49/sdk`), a typed client for Node.js 18+ with built-in retry logic, error handling, and JSON:API deserialization. * Track containers, retrieve shipments, and manage tracking requests with type-safe methods * Automatic retry with exponential backoff for rate limits and server errors * Flexible response formats: raw JSON:API, mapped objects, or both ```bash theme={null} npm install @terminal49/sdk ``` Get started with the TypeScript SDK *** ### MCP server Launched the **Terminal49 MCP server**, letting you query live container and shipment data from Claude Desktop, Cursor, or any MCP-compatible AI client. * **10 tools** — search, track, get container details, shipment routing, transport events, and more * **3 prompts** — pre-built workflows for tracking, demurrage analysis, and delay root cause analysis * Connect with a single config block — no custom code required Connect your AI client to Terminal49 *** ### Custom Fields API You can now create and manage **custom fields** on shipments and containers via the API. Define your own field schemas (text, number, date, single-select, multi-select), create option lists, and attach values to individual shipments or containers. * **Custom Field Definitions** — create, update, list, and delete field schemas scoped to shipments or containers * **Custom Field Options** — manage allowed values for select-type fields * **Custom Fields** — assign values to specific shipments or containers View the Custom Fields API reference *** ### Webhook trigger endpoint Added `POST /webhooks/trigger` to send a one-time test webhook delivery to any HTTPS URL — without creating a webhook endpoint first. Use it to validate your webhook handler before going live. API reference for the trigger endpoint *** ### DataSync improvements * **Shipping line Last Free Day** — new `ssl_last_free_day_on` and `ssl_last_free_day_on_local` columns on both the `containers` and `containers_rail` tables, so you can distinguish the shipping line's LFD from the terminal's LFD * **Pending hold status** — individual hold columns (`freight_hold`, `customs_hold`, `usda_hold`, `tmf_hold`, `other_hold`) now support a `"Pending"` value in addition to `"Hold"` and blank, giving you earlier visibility into holds before they become active DataSync containers reference How to work with holds and fees data ### Container Holds, Fees, and Release Readiness Published a comprehensive guide for working with holds and fees data in the Terminal49 API. The guide covers: * **Pickup readiness logic** — a decision flowchart and code example showing how to combine `available_for_pickup` with `holds_at_pod_terminal` to determine if a container can be picked up * **Hold and fee enum references** — quick-reference tables for all hold names and fee types, with expandable details for each value * **Webhook changeset examples** — how to subscribe to `container.updated` and react to hold/fee changes in real time * **Rail and inland destinations** — clarification that the same fields apply at inland rail terminals, not just the port of discharge * **FAQ** — answers to common integration questions like double-counting fees, sync lag between holds and availability, and case-sensitive hold names Container Holds, Fees, and Release Readiness ### Infer Tracking Number (Beta) Added the **Infer Tracking Number** endpoint to help you predict: * The **VOCC SCAC** to use for tracking * The **number type** (container, bill of lading, booking) * A confidence-driven **decision** (`auto_select`, `needs_confirmation`, `no_prediction`) Infer Tracking Number endpoint details How to use Infer Tracking Number in your workflow Use Infer Tracking Number when you have a valid tracking number but don’t know the correct **VOCC SCAC**. ### Documentation improvements Added an **Updates** section to publish API/DataSync changes. Added cross-links from relevant docs to Infer Tracking Number.