> ## Documentation Index
> Fetch the complete documentation index at: https://terminal49.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrating from ShipsGo

> Map ShipsGo container tracking API fields, parameters, and errors to their Terminal49 equivalents. Includes webhook setup, carrier coverage, widget replacement, and a migration checklist.

If you have the ShipsGo container tracking API in production, this page maps it onto Terminal49 field by field, so you can cut over without reverse-engineering our schema.

There is no compatibility shim. You will change your request code and your response parsing. For most integrations that is an afternoon.

## Start in sixty seconds

You do not need to talk to anyone to try this.

1. [Create an account](https://app.terminal49.com) — the free Developer Key tracks up to 10 active containers.
2. Generate a key at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys).
3. Run this:

```bash theme={null}
curl -X POST https://api.terminal49.com/v2/tracking_requests \
  -H "Content-Type: application/vnd.api+json" \
  -H "Authorization: Token YOUR_API_KEY" \
  -d '{
    "data": {
      "type": "tracking_request",
      "attributes": {
        "request_number": "YOUR_BOL_NUMBER",
        "request_type": "bill_of_lading",
        "scac": "MAEU"
      }
    }
  }'
```

<Warning>
  The full API key is shown once, right after you create it. Copy it before you navigate away — after that it is masked and cannot be revealed. If you miss it, create a new key and delete the old one.
</Warning>

If you want to test without a live shipment, use the [test tracking numbers](/docs/api-docs/useful-info/test-numbers), which simulate success, failure, and edge-case outcomes.

## The architectural shift

ShipsGo gives you endpoints to create, list, and retrieve shipments. You call them on demand, get current state back, and own the schedule, the cache, and the deduplication.

Terminal49 splits this in two. You register a tracking request once. We keep it updated and push changes to your webhook.

<CardGroup cols={2}>
  <Card title="Before — ShipsGo" icon="rotate">
    Cron every few hours -> GET or POST to your shipment endpoints -> diff against your cache -> dedupe events -> write to your database. Every call spends credits. Freshness is capped by your polling interval.
  </Card>

  <Card title="After — Terminal49" icon="webhook">
    `POST /tracking_requests` once -> Terminal49 polls carriers, terminals, and rail -> we POST to your endpoint as things change -> write to your database. No cache layer, no dedupe logic.
  </Card>
</CardGroup>

You can keep polling if you prefer — point your existing scheduler at `GET /v2/shipments` or `GET /v2/containers`. But webhooks are the reason the API is shaped this way, and terminal data (holds, fees, last free day) changes on a cadence that polling tends to miss.

## Quick comparison

|                             | ShipsGo                              | Terminal49                                                   |
| --------------------------- | ------------------------------------ | ------------------------------------------------------------ |
| Tracking model              | Poll on demand                       | Register once, then push or poll                             |
| Authentication              | `X-Shipsgo-User-Token` header        | `Authorization: Token` header                                |
| Base URL                    | `https://api.shipsgo.com/v2`         | `https://api.terminal49.com/v2`                              |
| Content type                | `application/json`                   | `application/vnd.api+json`                                   |
| Response format             | Custom JSON                          | JSON:API                                                     |
| Webhooks                    | Supported for shipment updates       | 30+ events, HMAC-signed                                      |
| Carrier identification      | `carrier` (SCAC)                     | `scac`, or omit and use Infer                                |
| Credit per tracking request | Yes, one credit per creation         | No per-track credits                                         |
| Rate limit                  | 100 requests per minute              | See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) |
| Terminal holds and fees     | Not available                        | Included on the container object                             |
| Last free day               | Not available                        | Included, with per-source breakdown                          |
| Rail milestones             | Limited                              | North American Class I and short-line                        |
| Embeddable widget           | Open lookup, any container; live map | Your tracked shipments only (add-on)                         |
| Getting an API key          | Self-serve in dashboard              | Self-serve in developer portal                               |

## Authentication

Switch from ShipsGo's token header to Terminal49's token header.

<CodeGroup>
  ```bash ShipsGo theme={null}
  curl -X GET https://api.shipsgo.com/v2/ocean/shipments \
    -H "X-Shipsgo-User-Token: YOUR_SHIPSGO_TOKEN" \
    -H "Accept: application/json"
  ```

  ```bash Terminal49 theme={null}
  curl -X POST https://api.terminal49.com/v2/tracking_requests \
    -H "Content-Type: application/vnd.api+json" \
    -H "Authorization: Token YOUR_T49_KEY" \
    -d '{"data":{"type":"tracking_request","attributes":{
         "request_number":"MRKU9465770",
         "request_type":"container",
         "scac":"MAEU"}}}'
  ```
</CodeGroup>

Note the `Token` prefix. It is not `Bearer`.

## Request parameter mapping

| ShipsGo parameter   | Terminal49 equivalent                                                                       | Notes                                                   |
| ------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `containerNumber`   | `request_number` with `request_type: "container"`                                           |                                                         |
| `bookingNumber`     | `request_number` with `request_type: "booking_number"`                                      |                                                         |
| `blNumber`          | `request_number` with `request_type: "bill_of_lading"`                                      | Master or house BOL                                     |
| `carrier`           | `scac`                                                                                      | Same SCAC values for most carriers                      |
| `carrier: "OTHERS"` | Omit `scac`, or call [Infer Tracking Number](/docs/api-docs/in-depth-guides/auto-detect-carrier) | Returns the predicted SCAC and number type              |
| `reference`         | Shipment reference tag                                                                      | Set on the shipment or container after creation         |
| `followers`, `tags` | Shipment reference or your own metadata                                                     | Store in your system against the Terminal49 shipment ID |
| `filters[status]`   | `GET /v2/shipments` or `GET /v2/containers` query filters                                   |                                                         |
| `skip` / `take`     | Pagination via JSON:API `page` params                                                       | Use the SDK for automatic pagination                    |
| `mapPoint=true`     | Vessel endpoints and container GeoJSON                                                      | Not inline on the tracking response                     |
| `extended=true`     | Always included in Terminal49 responses                                                     | No toggle needed                                        |

<Note>
  ShipsGo allows creating a shipment and then retrieving it by its generated ID. Terminal49 also returns an ID, but the canonical identifier for ocean tracking is the combination of `request_number` and `request_type`. You will track by BOL, booking, or container number directly rather than maintaining a separate shipment ID mapping.
</Note>

## Response field mapping

Terminal49 is JSON:API compliant, so relationships between shipments, containers, ports, and terminals are explicit rather than something you reassemble from ID references.

Use the [`include` parameter](/docs/api-docs/in-depth-guides/including-resources) to sideload related resources in one call instead of chasing IDs.

### Shipment level

| ShipsGo (v2)                   | Terminal49                                                                                  |
| ------------------------------ | ------------------------------------------------------------------------------------------- |
| `shipment.reference`           | Your own metadata tag; store against the shipment                                           |
| `shipment.carrier.code`        | `shipment.attributes.shipping_line_scac`                                                    |
| `shipment.carrier.name`        | `shipment.attributes.shipping_line_name`                                                    |
| `shipment.status`              | Derived from container status and milestones                                                |
| `shipment.pol`                 | `shipment.relationships.port_of_lading`                                                     |
| `shipment.pod`                 | `shipment.relationships.port_of_discharge`                                                  |
| `shipment.eta`                 | `shipment.attributes.pod_eta_at`                                                            |
| `shipment.etd`                 | Derived from vessel departure events                                                        |
| `shipment.voyage.vesselName`   | `shipment.attributes.pod_vessel_name`                                                       |
| `shipment.voyage.voyageNumber` | Available via the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo) |
| `shipment.transitTime`         | Calculate from departure and arrival timestamps                                             |
| `shipment.co2Emissions`        | Not returned                                                                                |
| `shipment.createdAt`           | `tracking_request.created_at`                                                               |
| `shipment.updatedAt`           | `container.updated` or `tracking_request.succeeded`                                         |

### Container level

| ShipsGo (v2)         | Terminal49                                                                                                                         |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `container.number`   | `container.attributes.number`                                                                                                      |
| `container.type`     | `container.attributes.equipment_type`                                                                                              |
| `container.size`     | `container.attributes.equipment_length`                                                                                            |
| `container.height`   | `container.attributes.equipment_height`                                                                                            |
| `container.status`   | `container.attributes.current_status`                                                                                              |
| `container.events[]` | Container timestamps, [transport events](/docs/api-docs/api-reference/containers/get-a-containers-transport-events), and webhook events |

<Note>
  ShipsGo returns a combined container type or ISO code. Terminal49 splits this into three normalized fields: type (dry, reefer, open top, flat rack, tank, hard top), length (20, 40, 45, 50), and height (standard, high cube). If you were parsing ISO codes yourself, you can delete that code.
</Note>

### Locations, facilities, and vessels

| ShipsGo (v2)                     | Terminal49                                                                                  |
| -------------------------------- | ------------------------------------------------------------------------------------------- |
| `port.name`                      | `port.attributes.name`                                                                      |
| `port.code`                      | `port.attributes.code`                                                                      |
| `port.country`                   | `port.attributes.country_code`                                                              |
| `port.latitude` / `.longitude`   | `port.attributes.latitude` / `.longitude`                                                   |
| `port.timezone`                  | `port.attributes.time_zone`                                                                 |
| `terminal.name`                  | `terminal.attributes.name`                                                                  |
| `terminal.smdgCode`              | `terminal.attributes.smdg_code`                                                             |
| `vessel.name`                    | `shipment.attributes.pod_vessel_name`                                                       |
| `vessel.imo`                     | `shipment.attributes.pod_vessel_imo`                                                        |
| `vessel.mmsi`                    | Available via the [Vessels API](/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo) |
| `vessel.latitude` / `.longitude` | Available via the Vessels API                                                               |

## Milestone and event mapping

ShipsGo returns milestones in a flat `events[]` array with names like "Loaded", "Sailing", and "Discharged". Terminal49 exposes the same milestones as normalized transport events and pushes each one to your webhook.

| ShipsGo event name      | Milestone                     | Terminal49 event                        |
| ----------------------- | ----------------------------- | --------------------------------------- |
| `Booked`                | Booking confirmed             | `container.transport.booking_confirmed` |
| `Loaded`                | Loaded on vessel at origin    | `container.transport.vessel_loaded`     |
| `Sailing`               | Vessel departed origin        | `container.transport.vessel_departed`   |
| `Arrived`               | Vessel arrived at destination | `container.transport.vessel_arrived`    |
| `Discharged`            | Discharged from vessel        | `container.transport.vessel_discharged` |
| `Gate out confirmation` | Gated out at destination      | `container.transport.full_out`          |
| —                       | Gated in at origin            | `container.transport.full_in`           |
| —                       | Empty picked up at origin     | `container.transport.empty_out`         |
| —                       | Empty returned at destination | `container.transport.empty_in`          |

ShipsGo also emits a "Not Released" status, which maps to active holds present in `container.attributes.holds_at_pod_terminal`.

Terminal49 also emits milestones ShipsGo has no equivalent for:

* **Vessel berthed** — `container.transport.vessel_berthed`
* **Available for pickup** — `container.transport.available` and `.not_available`
* **Transshipment** — arrived, discharged, loaded, departed
* **Feeder vessel and barge** — arrived, discharged, loaded, departed
* **Rail** — loaded, departed, arrived, unloaded, plus `arrived_at_inland_destination`

See the full [event catalog](/docs/api-docs/webhooks/event-catalog).

### Registering a webhook

```bash theme={null}
curl -X POST https://api.terminal49.com/v2/webhooks \
  -H "Content-Type: application/vnd.api+json" \
  -H "Authorization: Token YOUR_API_KEY" \
  -d '{
    "data": {
      "type": "webhook",
      "attributes": {
        "url": "https://your-endpoint.example.com/t49",
        "active": true,
        "events": [
          "container.transport.vessel_discharged",
          "container.transport.available",
          "container.pickup_lfd.changed"
        ]
      }
    }
  }'
```

Payloads are HMAC-signed. See [webhook setup](/docs/api-docs/in-depth-guides/webhooks) for signature verification, and [List webhook IPs](/docs/api-docs/api-reference/webhooks/list-webhook-ips) if your firewall restricts inbound traffic.

## What you gain

This is the part worth reading even if the rest is mechanical. Terminal49 integrates with terminals directly, not only carriers, so the container object carries operational data that has no ShipsGo equivalent.

### Holds

`holds_at_pod_terminal` is an array of active holds blocking pickup:

```json theme={null}
{
  "holds_at_pod_terminal": [
    { "name": "customs", "status": "hold", "description": "CBP HOLD" },
    { "name": "freight", "status": "hold", "description": null }
  ]
}
```

Hold names are `freight`, `customs`, `USDA`, `VACIS`, `TMF`, and `other`. Status is `hold` or `pending`. When a hold clears, the object is removed from the array — there is no released state.

<Warning>
  Hold names are case-sensitive. `USDA`, `VACIS`, and `TMF` are uppercase; `freight`, `customs`, and `other` are lowercase. Match exactly.
</Warning>

### Fees

`fees_at_pod_terminal` carries type, amount, and currency:

```json theme={null}
{
  "fees_at_pod_terminal": [
    { "type": "demurrage", "amount": 850.00, "currency_code": "USD" },
    { "type": "exam", "amount": 450.00, "currency_code": "USD" }
  ]
}
```

Fee types are `demurrage`, `extended_dwell_time`, `exam`, `total`, and `other`.

<Warning>
  Some terminals report a `total` line item alongside individual fees. Filter it out before summing or you will double-count.
</Warning>

### Last free day

`pickup_lfd` is a coalesced value that follows a fixed source priority: shipping line, then terminal, then rail. It does not pick the earliest date. The individual sources are available separately on `import_deadlines`:

* `pickup_lfd_line` — the shipping line's LFD (per diem deadline)
* `pickup_lfd_terminal` — the terminal's LFD (demurrage deadline)
* `pickup_lfd_rail` — the rail carrier's LFD at the inland destination

Each has its own webhook event, so you can alert on whichever source your operation cares about.

### Release readiness

Two fields answer "can I pick this up?" — `available_for_pickup` and the holds array:

```javascript theme={null}
function isReadyForPickup(container) {
  const { available_for_pickup, holds_at_pod_terminal } = container.attributes;
  const hasActiveHolds = holds_at_pod_terminal.some(h => h.status === 'hold');
  return available_for_pickup === true && !hasActiveHolds;
}
```

Full detail in [Holds, Fees, and Release Readiness](/docs/api-docs/in-depth-guides/holds-and-fees).

<Info>
  Holds, fees, LFD, and availability come back on the container object wherever the terminal is a supported source. They are not a paid add-on and they do not require a sales conversation. See [Entitlements](/docs/api-docs/useful-info/entitlements) for the features that do require account enablement — rail LFD and the embeddable widget are the main ones.
</Info>

## Replacing the ShipsGo widget

If you embedded the ShipsGo tracking widget or live map on your own website, read this before you swap in ours. They behave differently and the difference matters.

**The ShipsGo widget and map are open lookups.** Any visitor types any container number and gets a result, whether or not that shipment has anything to do with you.

**The Terminal49 widget is a customer portal.** It resolves only shipments and containers already tracked in your Terminal49 account. A visitor entering a container you are not tracking gets nothing back.

For most freight forwarders this is the behaviour you actually want — your customers see their shipments, and you are not running a free public lookup service on your own domain. But if you were relying on open lookup, this is a real change and you should plan for it.

The embed is two lines:

```html theme={null}
<div id="terminal49-tnt-widget"
     data-token="REPLACE_WITH_PUBLISHABLE_KEY"
     data-number="REPLACE_WITH_NUMBER_TO_QUERY"></div>
<script src="https://widget.terminal49.com/app.bundle.js"></script>
```

Your customers can search by master bill of lading, container number, or any reference number you have tagged a shipment with. We suggest a dedicated page at `yourcompany.com/track`.

<Note>
  The widget is an add-on, priced against container volume, and the publishable key comes from [support@terminal49.com](mailto:support@terminal49.com) rather than the developer portal. Email us and we will get you set up.
</Note>

If you want a map rather than a lookup form, see the [Map Embed Guide](/docs/api-docs/in-depth-guides/terminal49-map) — same publishable key, renders live vessel positions and routes.

## Gotchas that will bite you

<AccordionGroup>
  <Accordion title="Timestamps are UTC with a separate timezone field">
    ShipsGo returns local time in `YYYY-MM-DD HH:MM:SS` format. Terminal49 stores event timestamps in UTC and returns the matching IANA timezone alongside. Convert for display rather than assuming local time. See [Event Timestamps](/docs/api-docs/in-depth-guides/event-timestamps).
  </Accordion>

  <Accordion title="Tracking requests are asynchronous">
    `POST /tracking_requests` returns immediately with a pending status. The shipment appears once the carrier responds. Subscribe to `tracking_request.succeeded` and `tracking_request.failed` rather than expecting shipment data in the creation response. A request may also land in `awaiting_manifest` if the carrier has not manifested the shipment yet — we retry automatically. See [Tracking Request Lifecycle](/docs/api-docs/in-depth-guides/tracking-request-lifecycle).
  </Accordion>

  <Accordion title="Empty arrays are the normal state">
    `holds_at_pod_terminal: []` and `fees_at_pod_terminal: []` mean no active holds or fees. This is the common case. Do not treat it as missing data.
  </Accordion>

  <Accordion title="A fee amount of 0 is valid">
    It means the terminal reported the fee type but has not posted an amount yet. Common for demurrage in the first day or two after discharge.
  </Accordion>

  <Accordion title="JSON:API responses are verbose">
    Relationships are ID references into an `included` array. Use a JSON:API client library, or use `include` to sideload exactly what you need. Parsing raw JSON works but you will write more code than you expect.
  </Accordion>

  <Accordion title="container.updated carries a changeset">
    Terminal changes (fees, holds, LFD, appointment, availability) arrive on `container.updated` with a `changeset` showing old value first, new value second. Use it instead of diffing state yourself.
  </Accordion>
</AccordionGroup>

## Error handling

ShipsGo returns standard HTTP status codes alongside a JSON error body. Terminal49 also uses standard HTTP status codes. Replace any body-level error parsing with status-code checks.

| Status | Meaning                                                                    |
| ------ | -------------------------------------------------------------------------- |
| 400    | Malformed request or failed validation                                     |
| 401    | Missing or invalid API key                                                 |
| 403    | Key lacks permission, or the feature is not enabled on your plan           |
| 404    | Resource does not exist                                                    |
| 422    | Valid syntax, rejected content — for example a malformed container number  |
| 429    | Rate limited. See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) |
| 5xx    | Terminal49 or an upstream carrier or terminal is unavailable               |

Rough equivalence for the ShipsGo errors you are handling today:

| ShipsGo error                        | Terminal49                                                                           |
| ------------------------------------ | ------------------------------------------------------------------------------------ |
| HTTP 401 (Unauthorized)              | HTTP 401                                                                             |
| HTTP 402 (Payment Required)          | Not applicable; no per-track credits                                                 |
| HTTP 403 (Forbidden)                 | HTTP 403                                                                             |
| HTTP 404 (Not Found)                 | HTTP 404                                                                             |
| HTTP 409 (Conflict)                  | Duplicates handled by idempotency; see note below                                    |
| HTTP 422 (Unprocessable Content)     | HTTP 422                                                                             |
| HTTP 429 (Too Many Requests)         | HTTP 429                                                                             |
| Rate limit headers (`X-RateLimit-*`) | See [Rate Limiting](/docs/api-docs/in-depth-guides/rate-limiting) for current headers     |
| Duplicate shipment (409)             | Submitting the same tracking request returns the existing tracking request; no error |
| Credit exhausted (402)               | Not applicable; does not apply to Terminal49                                         |

<Note>
  Terminal49 does not charge per-track credits. If you were managing credit budgets and duplicate checks in ShipsGo to avoid burning credits, you can remove that logic.
</Note>

The [TypeScript SDK](/docs/sdk/introduction) maps these to typed errors (`AuthenticationError`, `ValidationError`, `RateLimitError`, `UpstreamError`, `FeatureNotEnabledError`) and retries rate-limit and server errors automatically with exponential backoff.

## Where we are narrower than ShipsGo

Worth knowing before you commit.

**Carrier count.** Terminal49 integrates directly with 36 ocean carriers, plus 2 more enabled on request — as of 14 August 2026. ShipsGo lists substantially more. Ours are direct integrations covering the lines that move volume into North America, and each is normalized into one schema. Check your carrier mix against the [ocean carrier list](/docs/coverage/ocean-carriers) before cutover, and read the known issues section there — we publish the per-carrier field gaps.

**Terminal data is North America.** Holds, fees, LFD, and availability come from direct terminal integrations concentrated in the US and Canada, with European ports expanding. Ocean milestones work globally; terminal-level operational data does not yet.

**No air, parcel, or road tracking.** If your ShipsGo integration covers air cargo, this migration handles only the ocean portion.

**No freight rates or sailing schedules.** We do not offer a rate calculator, rate index, or schedule search.

**Some fields are source-dependent.** Seal number, container weight, and departure or arrival events vary by carrier. The [field availability reference](/docs/coverage/fields) says which fields are always present and which depend on the carrier, terminal, or journey.

## Migration checklist

Everyone does the base path. Then pick a branch.

### Base path

<Steps>
  <Step title="Get a key">
    Self-serve at [app.terminal49.com/developers/api-keys](https://app.terminal49.com/developers/api-keys). Copy it immediately — it is shown once.
  </Step>

  <Step title="Switch authentication">
    Replace `X-Shipsgo-User-Token` with `Authorization: Token`. Note the `Token` prefix.
  </Step>

  <Step title="Check your carrier mix">
    Compare your ShipsGo carrier codes against the [carrier list](/docs/coverage/ocean-carriers). Flag anything missing before you cut over.
  </Step>

  <Step title="Create tracking requests">
    One `POST /tracking_requests` per BOL, booking, or container, replacing the per-request lookup.
  </Step>

  <Step title="Handle the async lifecycle">
    Tracking requests start pending. Handle `succeeded`, `failed`, and `awaiting_manifest` rather than expecting data on creation.
  </Step>

  <Step title="Update response parsing">
    JSON:API structure, split equipment fields, UTC timestamps with a separate timezone.
  </Step>

  <Step title="Update error handling">
    Replace credit-exhaustion and duplicate checks with standard HTTP status codes. Remove credit budget logic.
  </Step>

  <Step title="Backfill active shipments">
    Submit tracking requests for everything currently in transit. Send us the list if it is large and we will load it.
  </Step>

  <Step title="Add the terminal fields">
    Holds, fees, and LFD are the reason to do this properly rather than porting like for like.
  </Step>
</Steps>

### Then pick one

<Tabs>
  <Tab title="Webhook path">
    1. Expose an HTTPS endpoint that accepts our POST payloads.
    2. Register a webhook and subscribe only to events you act on.
    3. Verify HMAC signatures.
    4. Whitelist our IPs if your firewall restricts inbound traffic.
    5. Trigger a test delivery before going live.
    6. Retire your polling job and your dedupe layer.

    See [webhook best practices](/docs/api-docs/webhooks/best-practices) for retries and idempotency.
  </Tab>

  <Tab title="Polling path">
    1. Store the tracking request ID and shipment ID from the creation response.
    2. Repoint your existing scheduler at `GET /v2/shipments` or `GET /v2/containers`.
    3. Keep your existing cadence.

    Skipped: endpoint setup, signature verification, IP whitelisting, delivery testing.

    Terminal data changes on a cadence polling tends to miss. If you only adopt webhooks for one thing, make it `container.updated` and `container.pickup_lfd.changed`.
  </Tab>
</Tabs>

## Migrate with an AI coding agent

```markdown Terminal49 migration agent prompt expandable icon=robot wrap theme={null}

You are migrating this codebase from the ShipsGo tracking API to Terminal49.

Context you have:
- Environment variables `SHIPSGO_USER_TOKEN` (old) and `TERMINAL49_API_KEY` (new).
- A list of tracked shipments in the old system.

Rules:
1. Do not change business logic. Keep the same scheduling, alerting, and display behavior.
2. Replace ShipsGo request building with Terminal49 request building.
   - Auth: `Authorization: Token ${process.env.TERMINAL49_API_KEY}`.
   - Content type: `application/vnd.api+json`.
   - Base URL: `https://api.terminal49.com/v2`.
   - Endpoints: `POST /tracking_requests`, `GET /v2/shipments`, `GET /v2/containers`.
   - Parameter mapping:
     - `containerNumber` -> `request_number` with `request_type: "container"`
     - `bookingNumber` -> `request_number` with `request_type: "booking_number"`
     - `blNumber` -> `request_number` with `request_type: "bill_of_lading"`
     - `carrier` -> `scac` (or omit for Infer)
3. Replace response parsing with JSON:API parsing.
   - Use a JSON:API client library if the project does not already have one.
   - Map shipment fields: `shipping_line_scac`, `shipping_line_name`, `bill_of_lading_number`, `pod_eta_at`, `pod_vessel_name`, `pod_vessel_imo`.
   - Map container fields: `number`, `equipment_type`, `equipment_length`, `equipment_height`, `current_status`.
   - Map port fields: `name`, `code`, `country_code`, `latitude`, `longitude`, `time_zone`.
4. Add webhook handling if the old code polled.
   - Recommended events: `container.transport.vessel_discharged`, `container.transport.available`, `container.pickup_lfd.changed`.
   - Verify HMAC signatures on inbound POSTs.
5. Update error handling.
   - Replace ShipsGo HTTP status and credit-exhaustion checks with Terminal49 status codes.
   - Use typed errors if the codebase uses the TypeScript SDK.
6. Add Terminal49-specific fields where they improve the UI.
   - `holds_at_pod_terminal`: show as red badges if any `status === "hold"`.
   - `fees_at_pod_terminal`: show amount and currency; filter out `type === "total"` before summing.
   - `pickup_lfd`: show as a date; subscribe to `container.pickup_lfd.changed` for alerts.
   - `available_for_pickup`: green indicator when true and no active holds.
7. Do not delete old code. Comment it out with a `// TODO: remove after cutover` so we can roll back.
8. Add a feature flag `USE_TERMINAL49` so both paths can run in parallel during the transition.
9. Output: the exact file changes, inline diff style, ready for review.
```

## Getting help

Send us your list of active container and bill of lading numbers and we will load them rather than making you script the backfill.

If something in this mapping is wrong or incomplete, tell us. We would rather fix the page than have you work around it.

<CardGroup cols={2}>
  <Card title="API reference" icon="code" href="/docs/api-docs/api-reference/introduction">
    Every endpoint, with request and response schemas
  </Card>

  <Card title="TypeScript SDK" icon="rectangle-terminal" href="/docs/sdk/introduction">
    Typed client with retries and pagination built in
  </Card>

  <Card title="Coverage" icon="ship" href="/docs/coverage/home">
    Carriers, terminals, rail, and field availability
  </Card>

  <Card title="Test numbers" icon="flask" href="/docs/api-docs/useful-info/test-numbers">
    Simulate success, failure, and edge cases
  </Card>
</CardGroup>
