Start in sixty seconds
Signing up and getting a key is self-serve.- Create an account — the free plan tracks up to 10 active containers.
- Generate a key at app.terminal49.com/developers/api-keys.
- Run this:
The free plan tracks up to 10 active containers. Creating tracking requests through the API works right away; reading tracking data back through the API requires a free 7-day API trial (same 10-container limit) — contact us via in-app chat or support@terminal49.com and we enable it.
Migration offer: sign up now and the Vessels API — vessel schedules, AIS positions, and projected routes — is free for your first month.
The architectural shift
ShipsGo gives you endpoints to create, list, and retrieve shipments. You call them on demand, get current state back, and own the schedule, the cache, and the deduplication. Terminal49 splits this in two. You register a tracking request once. We keep it updated and push changes to your webhook.Before — ShipsGo
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.
After — Terminal49
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.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
Authentication
Switch from ShipsGo’s token header to Terminal49’s token header.Token prefix. It is not Bearer.
Request parameter mapping
ShipsGo allows creating a shipment and then retrieving it by its generated ID. Terminal49 also returns an ID, but the canonical identifier for ocean tracking is the combination of
request_number and request_type. You will track by BOL, booking, or container number directly rather than maintaining a separate shipment ID mapping.Response field mapping
Terminal49 is JSON:API compliant, so relationships between shipments, containers, ports, and terminals are explicit rather than something you reassemble from ID references. Use theinclude parameter to sideload related resources in one call instead of chasing IDs.
Shipment level
Container level
ShipsGo returns a combined container type or ISO code. Terminal49 splits this into three normalized fields: type (dry, reefer, open top, flat rack, bulk, tank), length (10, 20, 40, 45), and height (standard, high cube). If you were parsing ISO codes yourself, you can delete that code.
Locations, facilities, and vessels
Milestone and event mapping
ShipsGo returns milestones in a flatevents[] 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 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.availableand.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
Registering a webhook
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:
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.
Fees
fees_at_pod_terminal carries type, amount, and currency:
demurrage, extended_dwell_time, exam, total, and other.
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
Release readiness
Two fields answer “can I pick this up?” —available_for_pickup and the holds array:
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 for the features that do require account enablement — Routing Data (container map and vessel positions), rail LFD, container refresh, and the embeddable map and widget are the gated ones.
Replacing the ShipsGo widget
If you embedded the ShipsGo tracking widget or live map on your own website, read this before you swap in ours. They behave differently and the difference matters. The ShipsGo widget and map are open lookups. Any visitor types any container number and gets a result, whether or not that shipment has anything to do with you. The Terminal49 widget is a customer portal. It resolves only shipments and containers already tracked in your Terminal49 account. A visitor entering a container you are not tracking gets nothing back. For most freight forwarders this is the behaviour you actually want — your customers see their shipments, and you are not running a free public lookup service on your own domain. But if you were relying on open lookup, this is a real change and you should plan for it. The embed is two lines:yourcompany.com/track.
The widget is an add-on, priced against container volume, and the publishable key comes from support@terminal49.com rather than the developer portal. Email us and we will get you set up.
Gotchas that will bite you
Timestamps are UTC with a separate timezone field
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.Tracking requests are asynchronous
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.Empty arrays are the normal state
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.A fee amount of 0 is valid
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.
JSON:API responses are verbose
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.container.updated carries a changeset
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.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.
Rough equivalence for the ShipsGo errors you are handling today:
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.
AuthenticationError, ValidationError, RateLimitError, UpstreamError, FeatureNotEnabledError, AuthorizationError, NotFoundError) and retries rate-limit and server errors automatically with exponential backoff.
Where we are narrower than ShipsGo
Worth knowing before you commit. Carrier count. Terminal49 integrates directly with 36 ocean carriers, plus 2 more enabled on request — as of 14 August 2026. ShipsGo lists substantially more. Ours are direct integrations covering the lines that move volume into North America, and each is normalized into one schema. Check your carrier mix against the ocean carrier list 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 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
1
Get a key
Self-serve at app.terminal49.com/developers/api-keys. Copy it immediately — it is shown once.
2
Switch authentication
Replace
X-Shipsgo-User-Token with Authorization: Token. Note the Token prefix.3
Check your carrier mix
Compare your ShipsGo carrier codes against the carrier list. Flag anything missing before you cut over.
4
Create tracking requests
One
POST /tracking_requests per BOL, booking, or container, replacing the per-request lookup.5
Handle the async lifecycle
Tracking requests start pending. Handle
succeeded, failed, and awaiting_manifest rather than expecting data on creation.6
Update response parsing
JSON:API structure, split equipment fields, UTC timestamps with a separate timezone.
7
Update error handling
Replace credit-exhaustion and duplicate checks with standard HTTP status codes. Remove credit budget logic.
8
Backfill active shipments
Submit tracking requests for everything currently in transit. Send us the list if it is large and we will load it.
9
Add the terminal fields
Holds, fees, and LFD are the reason to do this properly rather than porting like for like.
Then pick one
- Webhook path
- Polling path
- Expose an HTTPS endpoint that accepts our POST payloads.
- Register a webhook and subscribe only to events you act on.
- Verify HMAC signatures.
- Whitelist our IPs if your firewall restricts inbound traffic.
- Trigger a test delivery before going live.
- Retire your polling job and your dedupe layer.
Migrate with an AI coding agent
If you use Claude Code, Cursor, Codex, Windsurf, Copilot, or another AI coding assistant, hand it the prompt below. It is written to run a side-by-side migration: the agent stands up a Terminal49 client next to your existing ShipsGo code, shadows every ShipsGo call with a Terminal49 call, diffs the responses, and only cuts over once parity is proven.How to use this prompt
How to use this prompt
- Open your repo in your AI coding tool.
- Add this page as a documentation source, or paste its URL into the chat.
- Copy the prompt below into a new chat and send it.
- Answer the agent’s discovery questions (ShipsGo client location, env var names, carrier mix).
- Review each PR the agent opens. It should ship in small, reviewable steps: client, shadow, parity harness, cutover, cleanup.
What the agent will produce
What the agent will produce
- A
Terminal49Clientalongside your existing ShipsGo client, sharing the same interface where possible. - A shadow-mode wrapper that calls both providers and logs response diffs without changing behavior.
- A parity report per shipment: matched fields, diverged fields, and Terminal49-only fields (holds, fees, LFD).
- A feature-flagged cutover: route reads to Terminal49, keep ShipsGo as fallback until you flip the flag off.
- A webhook receiver with HMAC verification, or a polling scheduler, depending on which path you pick.
- A cleanup PR that removes ShipsGo code, env vars, dependencies, and dedupe logic.
The prompt
Copy this into your agent. Replace the bracketed placeholders in the Repo context block before sending.Terminal49 migration agent prompt
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.API reference
Every endpoint, with request and response schemas
TypeScript SDK
Typed client with retries and pagination built in
Coverage
Carriers, terminals, rail, and field availability
Test numbers
Simulate success and failure outcomes