# 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.
## 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
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 |
| ---------- | -------- | ------------------------------------------------------------- |
| `api_slug` | Yes | The slug of the custom field definition |
| `value` | Yes | The value to set (type depends on the definition's data type) |
## 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
# 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}
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 a container
# Get a container
Source: https://terminal49.com/docs/api-docs/api-reference/containers/get-a-container
get /containers/{id}
Retrieves the details of a container.
# 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
#### Deprecation warning
The `raw_events` endpoint is provided as-is.
For past events we recommend consuming `transport_events`.
---
Get a list of past and future (estimated) milestones for a container as reported by the carrier. Some of the data is normalized even though the API is called raw_events.
Normalized attributes: `event` and `timestamp` timestamp. Not all of the `event` values have been normalized. You can expect the the events related to container movements to be normalized but there are cases where events are not normalized.
For past historical events we recommend consuming `transport_events`. Although there are fewer events here those events go through additional vetting and normalization to avoid false positives and get you correct data.
# Get 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
Get a list of past transport events (canonical) for a container. All data has been normalized across all carriers. These are a verified subset of the raw events may also be sent as Webhook Notifications to a webhook endpoint.
This does not provide any estimated future events. See `container/:id/raw_events` endpoint for that.
# Get container map GeoJSON
Source: https://terminal49.com/docs/api-docs/api-reference/containers/get-container-map-geojson
get /containers/{id}/map_geojson
Returns a GeoJSON FeatureCollection containing all map-related data for a container, including port locations, current vessel position (if at sea), past vessel paths, and estimated future routes. The response can be directly used with most mapping libraries (Leaflet, Mapbox GL, Google Maps, etc.). This is a paid feature. Please contact sales@terminal49.com.
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](/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
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
Returns a list of container. The containers are returned sorted by creation date, with the most recently refreshed containers appearing first.
This API will return all containers associated with the account.
# Refresh container
Source: https://terminal49.com/docs/api-docs/api-reference/containers/refresh-container
patch /containers/{id}/refresh
Schedules the container to be refreshed immediately from all relevant sources.
To be alerted of updates you should subscribe to the [relevant webhooks](/api-docs/in-depth-guides/webhooks). This endpoint is limited to 10 requests per minute.This is a paid feature. Please contact sales@terminal49.com.
# 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}
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
Use this endpoint to create a custom field value on a shipment or container. The field must reference an existing custom field definition.
## Request body
| Parameter | Required | Description |
| ---------- | -------- | ------------------------------------------------------- |
| `entity` | Yes | Polymorphic relationship to a Shipment or Container |
| `api_slug` | Yes | The slug of the custom field definition |
| `value` | Yes | The field value (must match the definition's data type) |
## 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 |
## Validation
* Values are validated against the definition's data type
* Enum values must match one of the definition's configured options
* The `api_slug` must reference a definition belonging to your account or a Terminal49 template
# 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 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 `Cargo`) |
| `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`) |
| `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
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}
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 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}
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}
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}
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 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 all custom field definitions available to your account.
## Query filters
| Filter | Description |
| ---------------------- | --------------------------------------------- |
| `filter[entity_type]` | Filter by entity type (`Shipment` or `Cargo`) |
| `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 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
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 `Cargo`) |
| `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}
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 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 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 how document representation resources are returned through includes and webhook payloads.
**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.extracted`, `document.extraction_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}`](/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}
**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-deletes (discards) the document.
**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}
Updates manual extraction and classification fields.
**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}
**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
Returns a presigned URL for downloading/viewing the current document file.
**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
Returns account-scoped allowed document types and labels.
**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
Returns documents for the authenticated account. Supports filters, sorting, includes, and pagination.
**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
Triggers asynchronous classification for the document.
**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
Triggers asynchronous extraction for the document.
**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-runs reference linking for the document.
**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
Queues a rotation update for image document types only. Non-image documents are not rotatable.
**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`](/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
Creates a document record. Provide an ActiveStorage signed blob id in `attached_document`.
**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}
# List email submissions
Source: https://terminal49.com/docs/api-docs/api-reference/email-submissions/list-email-submissions
get /email_submissions
Returns email submissions for the authenticated account.
# Get a metro area using the un/locode or the id
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}
Return the details of a single metro area.
# Create a party
Source: https://terminal49.com/docs/api-docs/api-reference/parties/create-a-party
post /parties
Creates a new party
# Edit a party
Source: https://terminal49.com/docs/api-docs/api-reference/parties/edit-a-party
patch /parties/{id}
Updates a party
# Get a party
Source: https://terminal49.com/docs/api-docs/api-reference/parties/get-a-party
get /parties/{id}
Returns a party by it's given identifier
# List parties
Source: https://terminal49.com/docs/api-docs/api-reference/parties/list-parties
get /parties
Get a list of parties
# Get a port using the locode or the id
Source: https://terminal49.com/docs/api-docs/api-reference/ports/get-a-port-using-the-locode-or-the-id
get /ports/{id}
Return the details of a single port.
# Create shipment custom field
Source: https://terminal49.com/docs/api-docs/api-reference/shipments/create-shipment-custom-field
post /shipments/{shipment_id}/custom_fields
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 |
| ---------- | -------- | ------------------------------------------------------------- |
| `api_slug` | Yes | The slug of the custom field definition |
| `value` | Yes | The value to set (type depends on the definition's data type) |
## 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
# 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}
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 a shipment
# Get a shipment
Source: https://terminal49.com/docs/api-docs/api-reference/shipments/get-a-shipment
get /shipments/{id}
Retrieves the details of an existing shipment. You need only supply the unique shipment `id` that was returned upon `tracking_request` creation.
# List shipment custom fields
Source: https://terminal49.com/docs/api-docs/api-reference/shipments/list-shipment-custom-fields
get /shipments/{shipment_id}/custom_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
Returns a list of your shipments. The shipments are returned sorted by creation date, with the most recent shipments appearing first.
This api will return all shipments associated with the account. Shipments created via the `tracking_request` API aswell as the ones added via the dashboard will be retuned via this endpoint.
# Resume tracking a shipment
Source: https://terminal49.com/docs/api-docs/api-reference/shipments/resume-tracking-shipment
patch /shipments/{id}/resume_tracking
Resume tracking a shipment. Keep in mind that some information is only made available by our data sources at specific times, so a stopped and resumed shipment may have some information missing.
# Stop tracking a shipment
Source: https://terminal49.com/docs/api-docs/api-reference/shipments/stop-tracking-shipment
patch /shipments/{id}/stop_tracking
We'll stop tracking the shipment, which means that there will be no more updates. You can still access the shipment's previously-collected information via the API or dashboard.
You can resume tracking a shipment by calling the `resume_tracking` endpoint, but keep in mind that some information is only made available by our data sources at specific times, so a stopped and resumed shipment may have some information missing.
# 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}
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}
Return the details of a single shipping line.
# Shipping Lines
Source: https://terminal49.com/docs/api-docs/api-reference/shipping-lines/shipping-lines
get /shipping_lines
Return a list of shipping lines supported by Terminal49.
N.B. There is no pagination for this endpoint.
# Get a terminal using the id
Source: https://terminal49.com/docs/api-docs/api-reference/terminals/get-a-terminal-using-the-id
get /terminals/{id}
Return the details of a single terminal.
# 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 (VOCC) and number type from a tracking number. Provide a container number, bill of lading number, or booking number and receive the predicted carrier with confidence and a decision value. Use this to auto-populate carrier fields before creating a tracking request.
**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
Give us a tracking number (container, bill of lading, or booking). We return:
* The **predicted VOCC carrier SCAC** to use for tracking
* The **predicted number type**
* A confidence-driven **decision** (`auto_select`, `needs_confirmation`, `no_prediction`)
We use machine learning prediction across container, bill of lading, and
booking numbers. For container numbers, we leverage 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 is rate limited to 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
To track an ocean shipment, you create a new tracking request.
Two attributes are required to track a shipment. A `bill of lading/booking number` and a shipping line `SCAC`.
Once a tracking request is created we will attempt to fetch the shipment details and it's related containers from the shipping line. If the attempt is successful we will create in new shipment object including any related container objects. We will send a `tracking_request.succeeded` webhook notification to your webhooks.
If the attempt to fetch fails then we will send a `tracking_request.failed` webhook notification to your `webhooks`.
A `tracking_request.succeeded` or `tracking_request.failed` webhook notificaiton will only be sent if you have atleast one active webhook.
This endpoint is limited to 100 tracking requests per minute.
**Don't know the SCAC?** Call [Auto-Detect
Carrier](/api-docs/api-reference/tracking-requests/auto-detect-carrier) first
to identify the shipping line from your tracking number.
# Edit a tracking request
Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/edit-a-tracking-request
patch /tracking_requests/{id}
Update a tracking request
# Get a single tracking request
Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/get-a-single-tracking-request
get /tracking_requests/{id}
Get the details and status of an existing tracking request.
# List tracking requests
Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/list-tracking-requests
get /tracking_requests
Returns a list of your tracking requests. The tracking requests are returned sorted by creation date, with the most recent tracking request appearing first.
# Get a vessel using the id
Source: https://terminal49.com/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id
get /vessels/{id}
Returns a vessel by id. `show_positions` is a paid feature. Please contact sales@terminal49.com.
# Get a vessel using the imo
Source: https://terminal49.com/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo
get /vessels/{imo}
Returns a vessel by the given IMO number. `show_positions` is a paid feature. Please contact sales@terminal49.com.
# Get vessel future positions
Source: https://terminal49.com/docs/api-docs/api-reference/vessels/get-vessel-future-positions
get /vessels/{id}/future_positions
Returns the estimated route between two ports for a given vessel. The timestamp of the positions has fixed spacing of one minute.This is a paid feature. Please contact sales@terminal49.com.
# Get 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
Returns the estimated route between two ports for a given vessel from a set of coordinates. The timestamp of the positions has fixed spacing of one minute.This is a paid feature. Please contact sales@terminal49.com.
# Get a single webhook notification
Source: https://terminal49.com/docs/api-docs/api-reference/webhook-notifications/get-a-single-webhook-notification
get /webhook_notifications/{id}
# 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
Returns an example payload as it would be sent to a webhook endpoint for the provided `event`
# List webhook notifications
Source: https://terminal49.com/docs/api-docs/api-reference/webhook-notifications/list-webhook-notifications
get /webhook_notifications
Return the list of webhook notifications. This can be useful for reconciling your data if your endpoint has been down.
# Create a webhook
Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/create-a-webhook
post /webhooks
You can configure a webhook via the API to be notified about events that happen in your Terminal49 account. These events can be realted to tracking_requests, shipments and containers.
This is the recommended way tracking shipments and containers via the API. You should use this instead of polling our the API periodically.
# Delete a webhook
Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/delete-a-webhook
delete /webhooks/{id}
Delete a webhook
# Edit a webhook
Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/edit-a-webhook
patch /webhooks/{id}
Update a single webhook
# Get single webhook
Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/get-single-webhook
get /webhooks/{id}
Get the details of a single webhook
# List webhook IPs
Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/list-webhook-ips
get /webhooks/ips
Return the list of IPs used for sending webhook notifications. This can be useful for whitelisting the IPs on the firewall.
# List webhooks
Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/list-webhooks
get /webhooks
Get a list of all the webhooks
# 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 payload to a target HTTPS URL without creating a webhook endpoint.
# 3. List Your Shipments & Containers
Source: https://terminal49.com/docs/api-docs/getting-started/list-shipments-and-containers
## Shipment and Container Data in Terminal49
After you've successfully made a tracking request, Terminal49 will begin to track shipments and store relevant information about that shipment on your behalf.
The initial tracking request starts this process, collecting available data from Carriers and Terminals. Then, Terminal49 periodically checks for new updates adn pulls data from the carriers and terminals to keep the data we store up to date.
You can access data about shipments and containers on your tracked shipments any time. We will introduce the basics of this method below.
Keep in mind, however, that apart from initialization code, you would not usually access shipment data in this way. You would use Webhooks (described in the next section). A Webhook is another name for a web-based callback URL, or a HTTP Push API. They provide a method for an API to post a notification to your service. Specifically, a webhook is simply a URL that can receive HTTP Post Requests from the Terminal49 API.
## List all your Tracked Shipments
If your tracking request was successful, you will now be able to list your tracked shipments.
**Try it below. Click "Headers" and replace YOUR\_API\_KEY 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.
**We suggest copy and pasting the response returned into a text editor so you can examine it while continuing the tutorial.**
```json http theme={null}
{
"method": "get",
"url": "https://api.terminal49.com/v2/shipments",
"headers": {
"Content-Type": "application/vnd.api+json",
"Authorization": "Token YOUR_API_KEY"
}
}
```
> ### Why so much JSON? (A note on JSON API)
>
> The Terminal49 API is JSON API compliant, which means that there are nifty libraries which can translate JSON into a fully fledged object model that can be used with an ORM. This is very powerful, but it also requires a larger, more structured payload to power the framework. The tradeoff, therefore, is that it's less convenient if you're parsing the JSON directly. Ultimately we strongly recommend you set yourself up with a good library to use JSON API to its fullest extent. But for the purposes of understanding the API's fundamentals and getting your feet wet, we'll work with the data directly.
## Authentication
The API uses HTTP Bearer Token authentication.
This means you send your API Key as your token in every request.
Webhooks are associated with API tokens, and this is how the Terminal49 knows who to return relevant shipment information to.
## Anatomy of Shipments JSON Response
Here's what you'll see come back after you get the /shipments endpoint.
Note that for clarity I've deleted some of the data that is less useful right now, and replaced them with ellipses (...). Bolded areas are also mine to point out important data.
The **Data** attribute contains an array of objects. Each object is of type "shipment" and includes attributes such as bill of lading number, the port of lading, and so forth. Each Shipment object also has Relationships to structured data objects, for example, Ports and Terminals, as well as a list of Containers which are on this shipment.
You can write code to access these structured elements from the API. The advantage of this approach is that Terminal49 cleans and enhances the data that is provided from the steamship line, meaning that you can access a pre-defined object definition for a specific port in Los Angeles.
```jsx 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 Shipment into 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, we need to parse the JSON directly, making this example an ideal real world application of the API.
```jsx 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](/api-docs/in-depth-guides/holds-and-fees).
**Try it below. Click "Headers" and replace YOUR\_API\_KEY with your API key.**
**We suggest copy and pasting the response returned into a text editor so you can examine it while continuing the tutorial.**
```json http theme={null}
{
"method": "get",
"url": "https://api.terminal49.com/v2/containers",
"headers": {
"Content-Type": "application/vnd.api+json",
"Authorization": "Token YOUR_API_KEY"
}
}
```
## Anatomy of Containers JSON Response
Now that you've got a list of containers, let's examine the response you've received.
```jsx 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"
}
},
...
}
},
...
```
# 4. How to Receive Status Updates
Source: https://terminal49.com/docs/api-docs/getting-started/receive-status-updates
## Using Webhooks to Receive Status Updates
Terminal49 posts status updates to a webhook that you register with us.
A Webhook is another name for a web-based callback URL, or a HTTP Push API. They provide a method for an API to post a notification to your service. Specifically, a webhook is simply a URL that can receive HTTP Post Requests from the Terminal49 API.
The HTTP Post request from Terminal49 has a JSON payload which you can parse to extract the relevant information.
## How do I use a Webhook with Terminal49?
First, you need to register a webhook. You can register as many webhooks as you like. Webhooks are associated with your account. All updates relating to that account are sent to the Webhook associated with it.
You can setup a new webhook by visiting [https://app.terminal49.com/developers/webhooks](https://app.terminal49.com/developers/webhooks) and clicking the 'Create Webhook Endpoint' button.

## Authentication
The API uses HTTP Bearer Token authentication.
This means you send your API Key as your token in every request.
Webhooks are associated with API tokens, and this is how the Terminal49 knows who to return relevant shipment information to.
## Anatomy of a Webhook Notification
Here's what you'll see in a Webhook Notification, which arrives as a POST request to your designated URL.
For more information, refer to the Webhook In Depth guide.
Note that for clarity I've deleted some of the data that is less useful right now, and replaced them with ellipses (...). Bolded areas are also mine to point out important data.
Note that there are two main sections:
**Data.** The core information being returned.
**Included**. Included are relevant objects that you are included for convenience.
```jsx theme={null}
{
"data": {
"id": "87d4f5e3-df7b-4725-85a3-b80acc572e5d",
"type": "webhook_notification",
"attributes": {
"id": "87d4f5e3-df7b-4725-85a3-b80acc572e5d",
"event": "tracking_request.succeeded",
"delivery_status": "pending",
"created_at": "2020-09-13 14:46:37 UTC"
},
"relationships": {
...
}
},
"included":[
{
"id": "90873f19-f9e8-462d-b129-37e3d3b64c82",
"type": "tracking_request",
"attributes": {
"request_number": "MEDUNXXXXXX",
...
},
...
},
{
"id": "66db1d2a-eaa1-4f22-ba8d-0c41b051c411",
"type": "shipment",
"attributes": {
"created_at": "2020-09-13 14:46:36 UTC",
"bill_of_lading_number": "MEDUNXXXXXX",
"ref_numbers":[
null
],
"shipping_line_scac": "MSCU",
"shipping_line_name": "Mediterranean Shipping Company",
"port_of_lading_locode": "PLGDY",
"port_of_lading_name": "Gdynia",
....
},
"relationships": {
...
},
"links": {
"self": "/v2/shipments/66db1d2a-eaa1-4f22-ba8d-0c41b051c411"
}
},
{
"id": "4d556105-015e-4c75-94a9-59cb8c272148",
"type": "container",
"attributes": {
"number": "CRLUYYYYYY",
"seal_number": null,
"created_at": "2020-09-13 14:46:36 UTC",
"equipment_type": "reefer",
"equipment_length": 40,
"equipment_height": "high_cube",
...
},
"relationships": {
....
}
},
{
"id": "129b695c-c52f-48a0-9949-e2821813690e",
"type": "transport_event",
"attributes": {
"event": "container.transport.vessel_loaded",
"created_at": "2020-09-13 14:46:36 UTC",
"voyage_number": "032A",
"timestamp": "2020-08-07 06:57:00 UTC",
"location_locode": "PLGDY",
"timezone": "Europe/Warsaw"
},
...
}
]
}
```
> ### Why so much JSON? (A note on JSON API)
>
> The Terminal49 API is JSON API compliant, which means that there are nifty libraries which can translate JSON into a fully fledged object model that can be used with an ORM. This is very powerful, but it also requires a larger, more structured payload to power the framework. The tradeoff, therefore, is that it's less convenient if you're parsing the JSON directly. Ultimately we strongly recommend you set yourself up with a good library to use JSON API to its fullest extent. But for the purposes of understanding the API's fundamentals and getting your feet wet, we'll work with the data directly.
### What type of webhook event is this?
This is the first question you need to answer so your code can handle the webhook.
The type of update can be found in \["data"]\["attributes"].
The most common Webhook notifications are status updates on tracking requests, like **tracking\_request.succeeded** and updates on ETAs, shipment milestone, and terminal availability.
You can find what type of event you have received by looking at the "attributes", "event".
```jsx theme={null}
"data" : {
...
"attributes": {
"id": "87d4f5e3-df7b-4725-85a3-b80acc572e5d",
"event": "tracking_request.succeeded",
"delivery_status": "pending",
"created_at": "2020-09-13 14:46:37 UTC"
},
}
```
### Inclusions: Tracking Requests & Shipment Data
When a tracking request has succeeded, the webhook event **includes** information about the shipment, the containers in the shipment, and the milestones for that container, so your app can present this information to your end users without making further queries to the API.
In the payload below (again, truncated by ellipses for clarity) you'll see a list of JSON objects in the "included" section. Each object has a **type** and **attributes**. The type tells you what the object is. The attributes tell you the data that the object carries.
Some objects have **relationships**. These are simply links to another object. The most essential objects in relationships are often included, but objects that don't change very often, for example an object that describes a teminal, are not included - once you query these, you should consider caching them locally.
```jsx theme={null}
"included":[
{
"id": "90873f19-f9e8-462d-b129-37e3d3b64c82",
"type": "tracking_request",
"attributes" : {
...
}
},
{
"id": "66db1d2a-eaa1-4f22-ba8d-0c41b051c411",
"type": "shipment",
"attributes": {
"created_at": "2020-09-13 14:46:36 UTC",
"bill_of_lading_number": "MEDUNXXXXXX",
"ref_numbers":[
null
],
"shipping_line_scac": "MSCU",
"shipping_line_name": "Mediterranean Shipping Company",
"port_of_lading_locode": "PLGDY",
"port_of_lading_name": "Gdynia",
....
},
"relationships": {
...
},
"links": {
"self": "/v2/shipments/66db1d2a-eaa1-4f22-ba8d-0c41b051c411"
}
},
{
"id": "4d556105-015e-4c75-94a9-59cb8c272148",
"type": "container",
"attributes": {
"number": "CRLUYYYYYY",
"seal_number": null,
"created_at": "2020-09-13 14:46:36 UTC",
"equipment_type": "reefer",
"equipment_length": 40,
"equipment_height": "high_cube",
...
},
"relationships": {
....
}
},
{
"id": "129b695c-c52f-48a0-9949-e2821813690e",
"type": "transport_event",
"attributes": {
"event": "container.transport.vessel_loaded",
"created_at": "2020-09-13 14:46:36 UTC",
"voyage_number": "032A",
"timestamp": "2020-08-07 06:57:00 UTC",
"location_locode": "PLGDY",
"timezone": "Europe/Warsaw"
},
...
}
]
```
## Code Examples
### Registering a Webhook
```jsx theme={null}
function registerWebhook(){
// Make a POST request with a JSON payload.
options = {
"method" : "POST"
"headers" : {
"content-type": "application/vnd.api+json",
"authorization" : "Token YOUR_API_KEY"
},
"payload" : {
"data": {
"type": "webhook",
"attributes": {
"url": "http://yourwebhookurl.com/webhook",
"active": true,
"events": ["tracking_request.succeeded"]
}
}
}
};
options.payload = JSON.stringify(data)
var response = UrlFetchApp.fetch('https://api.terminal49.com/v2/webhooks', options);
}
```
### Receiving a Post Webhook
Here's an example of some Javascript code that receives a Post request and parses out some of the desired data.
```
function receiveWebhook(postReq) {
try {
var json = postReq.postData.contents;
var webhook_raw = JSON.parse(json);
var webhook_data = webhook_raw["data"]
var notif_string = "";
if (webhook_data["type"] == "webhook_notification"){
if (webhook_data["attributes"]["event"] == "shipment.estimated.arrival"){
/* the webhook "event" attribute tell us what event we are being notified
* about. You will want to write a code path for each event type. */
var webhook_included = webhook_raw["included"];
// from the list of included objects, extract the information about the ETA update. This should be singleton.
var etas = webhook_included.filter(isEstimatedEvent);
// from the same list, extract the tracking Request information. This should be singleton.
var trackingReqs = webhook_included.filter(isTrackingRequest);
if(etas.length > 0 && trackingReqs.length > 0){
// therethis is an ETA updated for a specific tracking request.
notif_string = "Estimated Event Update: " + etas[0]["attributes"]["event"] + " New Time: " + etas[0]["attributes"]["estimated_timestamp"];
notif_string += " for Tracking Request: " + trackingReqs[0]["attributes"]["request_number"] + " Status: " + trackingReqs[0]["attributes"]["status"];
} else {
// this is a webhook type we haven't written handling code for.
notif_string = "Error. Webhook Returned Unexpected Data.";
}
if (webhook_data["attributes"]["event"] == "shipment.estimated.arrival"){
}
}
return HtmlService.createHtmlOutput(notf_string);
} catch (error){
return HtmlService.createHtmlOutput("Webhook failed: " + error);
}
}
// JS helper functions to filter events of certain types.
function isEstimatedEvent(item){
return item["type"] == "estimated_event";
}
function isTrackingRequest(item){
return item["type"] == "tracking_request";
}
```
## Try It Out & See More Sample Code
Update your API key below, and register a simple Webhook.
View the "Code Generation" button to see sample code.
```json http theme={null}
{
"method": "post",
"url": "https://api.terminal49.com/v2/webhooks",
"headers": {
"Content-Type": "application/vnd.api+json",
"Authorization": "Token YOUR_API_KEY"
},
"body": "{\r\n \"data\": {\r\n \"type\": \"webhook\",\r\n \"attributes\": {\r\n \"url\": \"https:\/\/webhook.site\/\",\r\n \"active\": true,\r\n \"events\": [\r\n \"tracking_request.succeeded\"\r\n ]\r\n }\r\n }\r\n}"
}
```
# SDK Quickstart (TypeScript)
Source: https://terminal49.com/docs/api-docs/getting-started/sdk-quickstart
The SDK documentation has moved to the top-level SDK Docs section.
* [Open the SDK quickstart](/sdk/quickstart)
# 1. Start Here
Source: https://terminal49.com/docs/api-docs/getting-started/start-here
So you want to start tracking your ocean shipments and containers and you have a few BL numbers. Follow the guide.
Our API responses use [JSONAPI](https://jsonapi.org/) schema. There are [client libraries](https://jsonapi.org/implementations/#client-libraries) available in almost every language. Our API should work with these libs out of the box.
Our APIs can be used with any HTTP client; choose your favorite! We love Postman, it's a friendly graphical interface to a powerful cross-platform HTTP client. Best of all it has support for the OpenAPI specs that we publish with all our APIs. We have created a collection of requests for you to easily test the API endpoints with your API Key. Link to the collection below.
**Run in Postman**
***
## Get an API Key
Sign in to your Terminal49 account and go to your [developer portal](https://app.terminal49.com/developers/api-keys) page to get your API key.
### Authentication
When passing your API key it should be prefixed with `Token`. For example, if your API Key is 'ABC123' then your Authorization header would look like:
```
"Authorization": "Token ABC123"
```
# 2. Tracking Shipments & Containers
Source: https://terminal49.com/docs/api-docs/getting-started/tracking-shipments-and-containers
Submitting a tracking request is how you tell Terminal49 to track a shipment for you.
## What is a Tracking Request?
Your tracking request includes two pieces of data:
* Your Bill of Lading, Booking number, or container number from the carrier.
* The SCAC code for that carrier.
**Don't know the SCAC?** Use the [Auto-Detect
Carrier](/api-docs/in-depth-guides/auto-detect-carrier) endpoint to
automatically identify the shipping line from your tracking number.
You can see a complete list of supported SCACs in row 2 of the Carrier Data Matrix.
## What sort of numbers can I track?
**Supported numbers**
1. Master Bill of Lading from the carrier (recommended)
2. Booking number from the carrier
3. Container number
* Container number tracking support across ocean carriers is sometimes more limited. Please refer to the Carrier Data Matrix to see which SCACs are compatible with Container number tracking.
**Unsupported numbers**
* House Bill of Lading numbers (HBOL)
* Customs entry numbers
* Seal numbers
* Internally generated numbers, for example PO numbers or customer reference numbers.
## How do I use Tracking Requests?
Terminal49 is an event-based API, which means that the API can be used asynchronously. In general the data flow is:
1. You send a tracking request to the API with your Bill of Lading number and SCAC.
2. The API will respond that it has successfully received your Tracking Request and return the Shipment's data that is available at that time.
3. After you have submitted a tracking request, the shipment and all of the shipments containers are tracked automatically by Terminal49.
4. You will be updated when anything changes or more data becomes available. Terminal49 sends updates relating to your shipment via posts to the webhook you have registered. Generally speaking, updates occur when containers reach milestones. ETA updates can happen at any time. As the ship approaches port, you will begin to receive Terminal Availability data, Last Free day, and so forth.
5. At any time, you can directly request a list of shipments and containers from Terminal49, and the API will return current statuses and information. This is covered in a different guide.
## How do you send me the data relating to the tracking request?
You have two options. First, you can poll for updates. This is the way we'll show you first.
You can poll the `GET /tracking_request/{id}` endpoint to see the status of your request. You just need to track the ID of your tracking request, which is returned to you by the API.
Second option is that you can register a webhook and the API will post updates when they happen. This is more efficient and therefore preferred. But it also requires some work to set up.
A Webhook is another name for a web-based callback URL, or a HTTP Push API. Webhooks provide a method for an API to post a notification to your service. Specifically, a webhook is simply a URL that can receive HTTP Post Requests from the Terminal49 API.
When we successfully lookup the Bill of Lading with the Carrier's SCAC, we will create a shipment, and send the event `tracking_request.succeeded` to your webhook endpoint with the associated record.
If we encounter a problem we'll send the event `tracking_request.failed`.
## Authentication
The API uses Bearer Token style authentication. This means you send your API Key as your token in every request.
To get your API token to Terminal49 and go to your [account API settings](https://app.terminal49.com/settings/api)
The token should be sent with each API request in the Authentication header:
Support [dev@terminal49.com](dev@terminal49.com)
```
Authorization: Token YOUR_API_KEY
```
## How to Create a Tracking Request
Here is javascript code that demonstates sending a tracking request
```json theme={null}
fetch("https://api.terminal49.com/v2/tracking_requests", {
"method": "POST",
"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"
}
}
})
.then(response => {
console.log(response);
})
.catch(err => {
console.error(err);
});
```
## Anatomy of a Tracking Request Response
Here's what you'll see in a Response to a tracking request.
```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, 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, which means that there are nifty libraries which can translate JSON into a fully fledged object model that can be used with an ORM. This is very powerful, but it also requires a larger, more structured payload to power the framework. The tradeoff, therefore, is that it's less convenient if you're parsing the JSON directly. Ultimately we strongly recommend you set yourself up with a good library to use JSON API to its fullest extent. But for the purposes of understanding the API's fundamentals and getting your feet wet, we'll work with the data directly.
## Try It: Make a Tracking Request
Try it using the request maker below!
1. Enter your API token in the autorization header value.
2. Enter a value for the `request_number` and `scac`. The request number has to be a shipping line booking or master bill of lading number. The SCAC has to be a shipping line scac (see data sources to get a list of valid SCACs)
Note that you can also access sample code in multiple languages by clicking the "Code Generation" below.
**Tracking Request Troubleshooting**
The most common issue people encounter is that they are entering the wrong number.
Please check that you are entering the Bill of Lading number, booking number, or container number and not internal reference at your company or by your frieght forwarder. You can the number you are supplying by going to a carrier's website and using their tools to track your shipment using the request number. If this works, and if the SCAC is supported by T49, you should able to track it with us.
If you're unsure of the correct SCAC, try the [Auto-Detect Carrier](/api-docs/api-reference/tracking-requests/auto-detect-carrier) endpoint first.
It is entirely possible that's neither us nor you but the shipping line is giving us a headache. Temporary network problems, not populated manifest and other things happen! You can read on how are we handling them in the [Tracking Request Retrying](/api-docs/useful-info/tracking-request-retrying) section.
Rate limiting: You can create up to 100 tracking requests per minute.
You can always email us at [support@terminal49.com](mailto:support@terminal49.com) if you have persistent
issues.
```json 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": "{\r\n \"data\": {\r\n \"attributes\": {\r\n \"request_type\": \"bill_of_lading\",\r\n \"request_number\": \"\",\r\n \"scac\": \"\"\r\n },\r\n \"type\": \"tracking_request\"\r\n }\r\n}"
}
```
## Try It: List Your Active Tracking Requests
We have not yet set up a webook to receive status updates from the Terminal49 API, so we will need to manually poll to check if the Tracking Request has succeeded or failed.
**Try it below. Click "Headers" and replace `` with your API key.**
```json theme={null}
{
"method": "get",
"url": "https://api.terminal49.com/v2/tracking_requests",
"headers": {
"Content-Type": "application/vnd.api+json",
"Authorization": "Token YOUR_API_KEY"
}
}
```
## Next Up: Get your Shipments
Now that you've made a tracking request, let's see how you can list your shipments and retrieve the relevant data.
Go to this
[page](https://help.terminal49.com/en/articles/8074102-how-to-initiate-shipment-tracking-on-terminal49)
to see different ways of initiating shipment tracking on Terminal49.
# How to add a Customer to a Tracking Request?
Source: https://terminal49.com/docs/api-docs/in-depth-guides/adding-customer
## Why you would want to add a party to a tracking request?
Adding a party to a tracking request allows you to associate customer information with the tracking request. The customer added to the tracking request will be assigned to the shipment when it is created, just like reference numbers and tags. This can help in organizing and managing 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.
## Listing all parties
You can list all parties associated with your account through the [API](/api-docs/api-reference/parties/list-parties).
Endpoint: **GET** - [https://api.terminal49.com/v2/parties](/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.
## How to add party to tracking request if you have the party ID?
To add a customer to a tracking request, you need to add the party to the tracking request as a customer relationship while being created. **Note** that a party cannot be added to a tracking request that has already been created.
Endpoint: **POST** - [https://api.terminal49.com/v2/tracking\_requests](/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"
}
}
}
}
}
```
After you send a **POST** request to create a tracking request, you will receive a response with the Tracking Request ID and customer relationship. You can use this tracking request ID to track the shipment.
```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": {
"id": "PARTY_ID",
"type": "party"
}
}
},
"links": {
"self": "/v2/tracking_requests/TRACKING_REQUEST_ID"
}
}
}
```
## Adding party for a customer
For adding a customer to a tracking request, you need to create a party first. You can create a party through the [API](/api-docs/api-reference/parties/create-a-party).
Endpoint: **POST** - [https://api.terminal49.com/v2/parties](/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"
}
}
}
```
## Editing a party
You can update existing parties through the [API](/api-docs/api-reference/parties/edit-a-party).
Endpoint: **PATCH** - [https://api.terminal49.com/v2/parties/PARTY\_ID](/api-docs/api-reference/parties/edit-a-party)
## Reading a party
You can retrieve the details of an existing party through the [API](/api-docs/api-reference/parties/get-a-party).
Endpoint: **GET** - [https://api.terminal49.com/v2/parties/PARTY\_ID](/api-docs/api-reference/parties/get-a-party)
# Identify Your Carrier with the Infer API
Source: https://terminal49.com/docs/api-docs/in-depth-guides/auto-detect-carrier
Don't know the carrier SCAC for your tracking number? The Infer API identifies it automatically, helping you create successful tracking requests.
**Beta Feature** β This guide covers the [Infer Number
API](/api-docs/api-reference/tracking-requests/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 the Infer API comes in.
You've seen this feature in action β when you enter a number, we auto-suggest
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) we use here is the **shipping line SCAC for tracking** β i.e., the carrier operating the move weβ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. The Infer API predicts the shipping line SCAC + number type to
increase the likelihood your tracking request succeeds.
## How the Infer API 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
Our system 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).
Our system 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, we use 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](/api-docs/api-reference/tracking-requests/auto-detect-carrier) for complete schema details.
## Integration Guide
For developers 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 |
| Rate limit header | `Retry-After` (seconds) |
Rate limit errors return HTTP 429 with a `Retry-After` header. Respect this
header to avoid extended throttling.
## 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 we support and their data availability
Learn what happens after you submit a tracking request
# Container Statuses
Source: https://terminal49.com/docs/api-docs/in-depth-guides/container-statuses
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.
## Available Status Values
The `current_status` field can have one of the following values:
### new
**Default state** - The container is tracked but we don't yet have enough information to determine its actual status. This is the initial state when tracking begins.
### on\_ship
**In transit by vessel** - The container is on a ship at any point prior to arrival at the Port of Discharge (POD). This status can apply at multiple stages:
* During the ocean voyage from origin to destination
* Before vessel departure from the Port of Lading
* Any time the container is loaded on a vessel
### available
**Ready for pickup** - The container has arrived at the POD or inland destination and is confirmed available for pickup. You can proceed with arranging pickup when you see this status. For a definitive readiness check that combines `available_for_pickup` with hold data, see [Container Holds, Fees, and Release Readiness](/api-docs/in-depth-guides/holds-and-fees).
### not\_available
**Arrived but not ready** - The container is at the POD or inland destination but is not yet available for pickup. This could be due to:
* Customs holds
* Terminal holds
* Documentation requirements
* Other restrictions
See [Container Holds, Fees, and Release Readiness](/api-docs/in-depth-guides/holds-and-fees) for details on specific hold types and how to determine when the container is released.
### grounded
**Availability unknown** - The container is physically at the POD, but we don't currently know whether it's available for pickup or not. This typically means the terminal isn't providing real-time availability data.
### awaiting\_inland\_transfer
**Moving inland** - The container is either:
* Still at the POD waiting to be loaded onto rail for an inland move
* In transit between the POD and a rail terminal
* At a rail terminal but not yet loaded onto a rail car
This status is specific to shipments with inland rail movements.
### on\_rail
**In transit by rail** - The container has been loaded onto a rail car and is being transported to its inland destination.
### picked\_up
**Out for delivery** - The container has been picked up from the terminal or facility for delivery.
### off\_dock
**At alternative facility** - The container has been moved to a different terminal or off-dock storage facility for pickup. You may need to go to this alternative location rather than the original POD terminal.
### delivered
**Delivery confirmed** - This status is only shown when delivery has been [manually marked as delivered](https://help.terminal49.com/articles/4713318249-how-to-mark-containers-as-delivered) through the Terminal49 dashboard.
### empty\_returned
**Container returned empty** - The container has been emptied and returned to the shipping line or designated return location.
### dropped
**Not currently used** - This status value is defined but not actively used in the system.
### loaded
**Not currently used** - This status value is defined but not actively used in the system.
## 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 business decisions, we recommend:
* Cross-referencing with 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` if holds are placed and removed
* The status may remain as `new` for some time if data from the shipping line is delayed
### API Usage
To get the current status of a container, you can 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 Upload for Documents
Source: https://terminal49.com/docs/api-docs/in-depth-guides/direct-upload-documents
Upload files with the direct upload flow, then create Terminal49 documents with the returned signed_id.
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`](/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 documents by email or API, then consume extraction results via webhooks.
This guide is for first-time integrators building document automation.
You can submit documents in two ways:
* Email attachments to your unique account docs alias
* API endpoint
Both follow the same customer-facing lifecycle: submit -> classify -> extract -> webhook result.
## Workflow Diagrams
```mermaid theme={null}
flowchart LR
A[User emails document attachments] --> B[Upload File] --> C[API /POST Documents]
C --> D[Terminal49 receives and parses documents]
D --> E[Terminal49 classifies documents]
E --> F[Terminal49 extracts structured data]
F --> G[Terminal49 sends webhook result]
```
## Workflow: Step-by-Step
Use email (attachments to your docs alias) or Upload directly by API
Terminal49 classifies and extracts data asynchronously.
You receive `document.extracted` or `document.extraction_failed`.
If the same file content already exists for your account, it is treated as duplicate and not processed again.
POST document through endpoint. Used by emailΒ
Treat submission as asynchronous. Do not assume extraction is complete immediately after upload/email.
## Technical Implementation (One End-to-End Example)
Example scenario: user uploads one file, `invoice.pdf`.
### 1) Submit the document
`POST /documents`
Before creating the document, complete the direct upload flow and get a `signed_id`: [`Direct Upload for Documents`](/api-docs/in-depth-guides/direct-upload-documents).
Note: `attached_document` is the S3 `signed_id` from the file direct upload.
```json theme={null}
{
"data": {
"type": "document",
"attributes": {
"name": "invoice.pdf",
"attached_document": "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBLi4u"
}
}
}
```
Response (`201`):
```json theme={null}
{
"data": {
"id": "5ab53a5e-0d68-4a8f-8d3d-1d24555d20cb",
"type": "document",
"attributes": {
"name": "invoice.pdf",
"document_type": null,
"file_name": "invoice.pdf",
"file_content_type": "application/pdf",
"file_size_bytes": 248193,
"created_at": "2026-03-26T08:14:24Z",
"updated_at": "2026-03-26T08:14:24Z"
}
}
}
```
### 2) Receive extraction webhook
`document.extracted` example:
```json theme={null}
{
"data": {
"id": "40cb28de-63ee-4542-909e-a19efe46904d",
"type": "webhook_notification",
"attributes": {
"event": "document.extracted",
"delivery_status": "pending",
"created_at": "2026-03-26T08:17:06Z",
"version": "2026-03-10"
},
"relationships": {
"document": {
"data": {
"id": "5ab53a5e-0d68-4a8f-8d3d-1d24555d20cb",
"type": "document"
}
}
}
},
"included": [
{
"id": "5ab53a5e-0d68-4a8f-8d3d-1d24555d20cb",
"type": "document",
"attributes": {
"name": "invoice.pdf",
"document_type": "commercial_invoice",
"source": "upload",
"file_name": "invoice.pdf",
"file_content_type": "application/pdf",
"file_size_bytes": 248193,
"created_at": "2026-03-26T08:14:24Z",
"updated_at": "2026-03-26T08:17:05Z"
},
"relationships": {
"account": {
"data": {
"id": "91d5b7cc-6d3f-4c87-b8bb-f5f31ed866f4",
"type": "account"
}
},
"user": {
"data": {
"id": "fa25bdf2-0692-48f9-a8a7-cad8eb99527f",
"type": "user"
}
},
"email_submission": {
"data": {
"id": "7de2c356-5d2a-4d6e-99f4-6f0d2d63e357",
"type": "email_submission"
}
},
"last_document_representation": {
"data": {
"id": "ab1fba20-6b7b-4d5f-95e8-e2c55a7a8f89",
"type": "document_representation"
}
}
},
"links": {
"self": "/documents/5ab53a5e-0d68-4a8f-8d3d-1d24555d20cb",
"download": "/documents/5ab53a5e-0d68-4a8f-8d3d-1d24555d20cb/download_url"
}
},
{
"id": "7de2c356-5d2a-4d6e-99f4-6f0d2d63e357",
"type": "email_submission",
"attributes": {
"subject": "Invoice #INV-10027",
"body_preview": "Please find attached invoice INV-10027.",
"from": [
"ap@acme-manufacturing.com"
],
"to": [
"docs+account@terminal49.com"
],
"cc": [],
"message_id": "",
"sent_at": "2026-03-26T08:14:11Z",
"created_at": "2026-03-26T08:14:12Z",
"updated_at": "2026-03-26T08:14:12Z"
}
},
{
"id": "ab1fba20-6b7b-4d5f-95e8-e2c55a7a8f89",
"type": "document_representation",
"attributes": {
"schema_version": "2026-03-23",
"payload": {
"invoice_number": "INV-10027",
"invoice_date": "2026-03-25",
"supplier_name": "Acme Manufacturing Ltd",
"total_amount": "12450.00",
"currency": "USD"
},
"created_at": "2026-03-26T08:17:05Z",
"updated_at": "2026-03-26T08:17:05Z"
}
}
]
}
```
### 3) Persist extracted outcome in your system
Use the webhook `event` and included `document` payload to update your internal record for that document.
## Webhooks You Should Handle
| Event | Meaning |
| ---------------------------- | --------------------------------- |
| `document.extracted` | Extraction completed successfully |
| `document.extraction_failed` | Extraction did not complete |
## Use these endpoints while integrating
* [`GET /webhook_notifications/examples`](/api-docs/api-reference/webhook-notifications/get-webhook-notification-payload-examples)
* [`POST /webhooks/trigger`](/api-docs/api-reference/webhooks/trigger-a-webhook)
Webhook event availability depends on your account configuration. If you are not receiving expected events, contact Terminal49 support.
## APIs Involved
* [`POST /documents`](/api-docs/api-reference/documents/upload-a-document)
* [`GET /documents`](/api-docs/api-reference/documents/list-documents)
* [`GET /documents/{id}`](/api-docs/api-reference/documents/get-a-document)
* [`GET /documents/{id}/download_url`](/api-docs/api-reference/documents/get-a-document-download-url)
* [`GET /email_submissions`](/api-docs/api-reference/email-submissions/list-email-submissions)
* [`GET /email_submissions/{id}`](/api-docs/api-reference/email-submissions/get-an-email-submission)
* [`GET /document_schemas/{id}`](/api-docs/api-reference/document-schemas/get-a-document-schema)
* [`Document representations resource`](/api-docs/api-reference/document-representations/document-representations-resource)
## Planned (Not Live Yet)
* `email_submission.created` webhook event after inbound email acceptance.
# Event Timestamps
Source: https://terminal49.com/docs/api-docs/in-depth-guides/event-timestamps
Through the typical container lifecycle events occur across multiple timezones. Wheverever you see a timestamp for some kind of transporation event, there should be a corresponding [IANA tz](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 there is event that occurs where Terminal49 cannot determine the location (and therefore the timezone) of the event the system is unable to store the event in true UTC.
In this scenario we take timestamp as given from the source and parse 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)
## 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 TimeZone.
# 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, and availability data.
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 across 150+ 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}`](/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](/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.
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
# Including Resources
Source: https://terminal49.com/docs/api-docs/in-depth-guides/including-resources
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](/api/4c6091811c4e0-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
```
# MCP Server Quickstart
Source: https://terminal49.com/docs/api-docs/in-depth-guides/mcp
Full setup guide for the Terminal49 MCP server
# MCP Server Quickstart
This guide covers everything you need to connect Claude or Cursor to Terminal49's container tracking data via MCP.
Just want to get started fast? See [MCP Overview](/mcp/home) for a 5-minute setup.
## Prerequisites
Before you begin, make sure you have:
A Terminal49 account with API access
A `T49_API_TOKEN` from the [dashboard](https://app.terminal49.com/developers/api-keys)
Required if running the MCP server locally
Claude Desktop or Cursor IDE
**Technical Details:**
* **MCP SDK**: `@modelcontextprotocol/sdk ^1.26.0`
* **TypeScript SDK**: `@terminal49/sdk`
* **Runtime**: Node.js 18+
***
## Transports
| Transport | Endpoint | Best For |
| ----------------- | ------------------------------ | -------------------------------- |
| HTTP (streamable) | `POST /api/mcp` or `POST /mcp` | Serverless, short-lived requests |
**Authentication**: API token only (OAuth not required for this release).\
Pass `Authorization: Bearer ` or `Authorization: Token `.\
Server falls back to `T49_API_TOKEN` environment variable.
Claude Desktop and Cursor use the HTTP transport. For hosted production usage, use Streamable HTTP at `/mcp`.
For hosted OAuth implementation planning, see [Hosted HTTP OAuth Requirements](/mcp/hosted-http-oauth-requirements) and [Hosted HTTP OAuth Test Plan](/mcp/hosted-http-oauth-test-plan).
***
## Configure Your MCP Client
### Claude Desktop
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json theme={null}
{
"mcpServers": {
"terminal49": {
"url": "https://mcp.terminal49.com/mcp",
"headers": {
"Authorization": "Bearer "
}
}
}
}
```
Edit `%APPDATA%\Claude\claude_desktop_config.json`:
```json theme={null}
{
"mcpServers": {
"terminal49": {
"url": "https://mcp.terminal49.com/mcp",
"headers": {
"Authorization": "Bearer "
}
}
}
}
```
Edit `~/.config/Claude/claude_desktop_config.json`:
```json theme={null}
{
"mcpServers": {
"terminal49": {
"url": "https://mcp.terminal49.com/mcp",
"headers": {
"Authorization": "Bearer "
}
}
}
}
```
### Cursor IDE
Add to your Cursor settings:
```json theme={null}
{
"mcp": {
"servers": {
"terminal49": {
"url": "https://mcp.terminal49.com/mcp",
"headers": {
"Authorization": "Bearer "
}
}
}
}
}
```
### 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_token_here"
}
}
}
}
```
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](/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/mcp` and config file path matches your OS |
| `401 Unauthorized` | Missing or invalid token | Create a new API token in [dashboard](https://app.terminal49.com/developers/api-keys); ensure `Authorization: Bearer ` header is set |
| `429 Too Many Requests` | Rate limit exceeded | See [Rate Limiting](/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_token npm run mcp:stdio 2>&1 | head -20
```
***
## MCP Capabilities
### Tools (10)
| 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 (3)
| 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 (2)
| 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](/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:
```json theme={null}
{
"buildCommand": "cd packages/mcp && npm ci && npm run build",
"functions": {
"api/mcp.ts": { "maxDuration": 30, "memory": 1024 }
},
"rewrites": [
{ "source": "/mcp", "destination": "/api/mcp" }
]
}
```
### Environment Variables
| Variable | Required | Description |
| ------------------------- | -------- | -------------------------------------------------------------- |
| `T49_API_TOKEN` | Yes | Terminal49 API token |
| `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 |
***
## 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_token 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_token npm run mcp:stdio
# Test the hosted endpoint
curl -X POST https://mcp.terminal49.com/mcp \
-H "Authorization: Bearer $T49_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
```
***
## Related Guides
* [MCP Overview](/mcp/home) β Quick start and tools reference
* [Rate Limiting](/api-docs/in-depth-guides/rate-limiting) β API limits (same for MCP)
* [Test Numbers](/api-docs/useful-info/test-numbers) β Containers for testing
* [Webhooks](/api-docs/in-depth-guides/webhooks) β Real-time updates
* [API Data Sources](/api-docs/useful-info/api-data-sources-availability) β Data freshness and coverage
# Quick Start Guide
Source: https://terminal49.com/docs/api-docs/in-depth-guides/quickstart
## Before You Begin
You'll need a 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) to get your API key.
Not sure which SCAC to use? The [Auto-Detect
Carrier](/api-docs/in-depth-guides/auto-detect-carrier) API can identify it
from your tracking number.
## Track a Shipment
You can try this using the embedded request maker below, or using Postman.
1. Try it below. Click "Headers" and replace YOUR\_API\_KEY with your API key. In the authorization header value.
2. Enter a value for theΒ `request_number`Β andΒ `scac`. The request number has to be a shipping line booking or master bill of lading number. The SCAC has to be a shipping line scac (see data sources to get a list of valid SCACs)
Note that you can also access sample code, include a cURL template, by clicking the "Code Generation" tab in the Request Maker.
```json http 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": "{\r\n \"data\": {\r\n \"attributes\": {\r\n \"request_type\": \"bill_of_lading\",\r\n \"request_number\": \"\",\r\n \"scac\": \"\"\r\n },\r\n \"type\": \"tracking_request\"\r\n }\r\n}"
}
```
## Check Your Tracking Request Succeeded
We have not yet set up a webook to receive status updates from the Terminal49 API, so we will need to manually poll to check if the Tracking Request has succeeded or failed.
> ### Tracking Request Troubleshooting
>
> The most common issue people encounter is that they are entering the wrong number.
>
> Please check that you are entering the Bill of Lading number, booking number, or container number and not internal reference at your company or by your frieght forwarder. You can the number you are supplying by going to a carrier's website and using their tools to track your shipment using the request number. If this works, and if the SCAC is supported by T49, you should able to track it with us.
>
> You can always email us at [support@terminal49.com](mailto:support@terminal49.com) if you have persistent issues.
\*\* Try it below. Click "Headers" and replace `` with your API key.\*\*
```json http 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.
**Try it below. Click "Headers" and replace YOUR\_API\_KEY 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 http 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.
Try it after replacing `` with your API key.
```json http 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 true power of Terminal49's API is that it is asynchronous. You can register a Webhook, which is essentially a callback URL that our systems HTTP Post to when there are updates.
To try this, you will need to first set up a URL on the open web to receive POST requests. Once you have done this, you'll be able to receive status updates from containers and shipments as they happen, which means you don't need to poll us for updates; we'll notify you.
\*\* Try it below. Click "Headers" and replace YOUR\_API\_KEY with your API key.\*\*
Once this is done, any changes to shipments and containers you're tracking in step 2 will now be sent to your webhook URL as Http POST Requests.
View the "Code Generation" button to see sample code.
```json http theme={null}
{
"method": "post",
"url": "https://api.terminal49.com/v2/webhooks",
"headers": {
"Content-Type": "application/vnd.api+json",
"Authorization": "Token YOUR_API_KEY"
},
"body": "{\r\n \"data\": {\r\n \"type\": \"webhook\",\r\n \"attributes\": {\r\n \"url\": \"https:\/\/webhook.site\/\",\r\n \"active\": true,\r\n \"events\": [\r\n \"*\"\r\n ]\r\n }\r\n }\r\n}"
}
```
# Integrate Rail Container Tracking Data
Source: https://terminal49.com/docs/api-docs/in-depth-guides/rail-integration-guide
This guide provides a comprehensive, step-by-step approach for integrating North American Class-1 rail container tracking data into your systems. Whether you are a shipper or a logistics service provider, this guide will help you track all your rail containers via a single API.
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 our 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 container tracking platform integrates with all North American Class-1 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.
We provide 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 be notified by webhook whenever these events occur.
We also provide a set of attributes [on the container model](/api-docs/api-reference/containers/get-a-container) that let you know the current status of your container at any given time, as well as useful information such as 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](/api-docs/in-depth-guides/holds-and-fees).
### Rail-Specific Transport Events
There are several core Transport Events that 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 allow you to 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 |
| Rail Departed | `container.transport.rail_departed` | The container departs on the railcar (not always from port of discharge). | Example |
| Rail Arrived | `container.transport.rail_arrived` | The container arrives at a rail terminal (not always at the destination terminal). | Example |
| Arrived At Inland Destination | `container.transport.arrived_at_inland_destination` | The container arrives at the destination terminal. | Example |
| Rail Unloaded | `container.transport.rail_unloaded` | The container is unloaded from a railcar. | Example |
There's also a set of events that are triggered when the status of the container at the destination rail terminal changes. For containers without rail, they would have been triggered at the ocean terminal.
| Transport Event | Webhook Notification | Description | Example |
| --------------- | ------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| Full Out | `container.transport.full_out` | The full container leaves the rail terminal. | Example |
| Empty In | `container.transport.empty_in` | The empty container is returned to the terminal. | Example |
Finally, we have a webhook notifications 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 |
Integrate these notifications by subscribing to the webhooks and handling the incoming data to update your systems.
#### Rail Container Attributes
The following are new attributes that are specific to rail container tracking.
* **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.
* **ind\_ata\_at**: Actual Time of Arrival at the inland destination.
* **ind\_rail\_unloaded\_at**: Time when the container is unloaded from rail at the inland destination.
* **ind\_facility\_lfd\_on**: Last Free Day for demurrage charges at the inland destination terminal.
* **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.
These attributes can be found on [container objects](/api-docs/api-reference/containers/get-a-container).
## 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](/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 will be in the same tables alongside the ocean terminal and carrier data.
[Learn more about DataSync](/datasync/overview)
# Rate Limiting
Source: https://terminal49.com/docs/api-docs/in-depth-guides/rate-limiting
## Overview
Terminal49 API implements rate limiting to ensure fair usage and maintain service quality for all users.
## Rate Limit Details
* **Limit**: 100 requests per minute per account
* **Scope**: Applied per API key/account
* **Window**: Rolling 60-second window
## 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
**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](/api-docs/api-reference/webhooks) section for setup instructions
### 2. Implement Exponential Backoff
If you receive a `429` response:
1. Check the `Retry-After` header
2. Wait for the specified number of seconds
3. Implement exponential backoff for subsequent failures
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](/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:
```python theme={null}
import time
import requests
def make_request_with_retry(url, headers, max_retries=3):
"""
Make an API request with automatic retry on rate limit.
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:
# Get the retry-after value from header (default to 60 seconds)
retry_after = int(response.headers.get('Retry-After', 60))
print(f"Rate limited. Waiting {retry_after} seconds...")
time.sleep(retry_after)
continue
# Return response for any other status code
return response
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
* **Monitor rate limit headers**: Some APIs provide headers indicating remaining quota (check our response headers)
# Vessel and Container Route Data
Source: https://terminal49.com/docs/api-docs/in-depth-guides/routing
This guide explains how to access detailed container routes and vessel positions data (historical and future positions) using Terminal49 APIs.
This is a technical article describing how to use our 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. If you are interested in using these APIs, please contact [sales@terminal49.com](mailto:sales@terminal49.com).
## 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
## 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_TOKEN"
```
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 |
**Note:** 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](/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_TOKEN'
}
})
.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), we recommend refreshing 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
**Q: How up-to-date is the vessel position data?**
A: 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.
**Q: How accurate are the future predictions?**
A: 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.
**Q: What if a vessel deviates from the predicted path?**
A: 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.
**Q: Why don't I see a `current_vessel` feature for my container?**
A: 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
**Q: Can I get map data for multiple containers at once?**
A: 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.
**Q: What coordinate system is used?**
A: All coordinates follow the GeoJSON standard: `[longitude, latitude]` in WGS84 (EPSG:4326) format.
**Q: Are the LineString coordinates simplified?**
A: 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
The Terminal49 Map allows you to embed real-time visualized container tracking on your website with just a few lines of code.
### Prerequisites
* A Terminal49 account.
* A Publishable API key, you can get one by reaching out to us at [support@terminal49.com](mailto:support@terminal49.com).
* Familiarity with our [Shipments API](/api-docs/api-reference/shipments/list-shipments) and [Containers API](/api-docs/api-reference/containers/list-containers).
In the following examples we'll be passing a `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.
### How do I embed the map on my website?
Once you have the API Key, you can embed the map on your website.
1. 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 `