Skip to main content
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:
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:
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}
{
  "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:
FieldTypeDescription
namestringThe canonical hold type (see table below)
statusstring"hold" or "pending"
descriptionstring | nullRaw 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 nameDescriptionWho resolves it
freightCarrier freight charges unpaidShipping line or freight forwarder
customsCBP hold — docs, exam, or inspectionLicensed customs broker
USDAUSDA phytosanitary inspectionCustoms broker or USDA compliance team
VACISNon-intrusive X-ray / gamma-ray scanCustoms broker
TMFTerminal management fee (pier pass)Pay terminal directly
otherUnmapped hold — check descriptionTerminal 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.
{
  "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.
{
  "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.
{
  "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.
{
  "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.
{
  "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.
{
  "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:
FieldTypeDescription
typestringThe canonical fee type (see table below)
amountnumberFee amount in local currency
currency_codestringISO 4217 currency code, typically "USD"
Fee typeDescriptionCharged by
demurrageDaily charge after carrier free time expiresOcean carrier
extended_dwell_timeTerminal charge for prolonged dwellTerminal
examCBP/USDA inspection costTerminal or exam facility
totalCombined total of all fees (may overlap with line items)See individual items
otherUnmapped fee typeVaries
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.
{
  "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.
{
  "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.
{
  "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.
{
  "type": "total",
  "amount": 2000.00,
  "currency_code": "USD"
}
A fee that Terminal49 could not map to a specific type.
{
  "type": "other",
  "amount": 75.00,
  "currency_code": "USD"
}

Full example: container with multiple holds and fees

{
  "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.
A customs hold appeared on the container:
{
  "changeset": {
    "holds_at_pod_terminal": [
      [],
      [
        {
          "name": "customs",
          "status": "hold",
          "description": "CBP HOLD"
        }
      ]
    ]
  }
}

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:
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 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:
const lineItems = container.fees_at_pod_terminal.filter(f => f.type !== 'total');
const totalAmount = lineItems.reduce((acc, f) => acc + f.amount, 0);