Start in sixty seconds
Signing up and getting a key is self-serve.Create an account
Generate an API key
Make your first request
The architectural shift
Gnosis’s CLM platform (tracking engine “Marlo”) and Terminal49 both integrate with ocean carriers, terminals, and rail. The difference is in how you fetch the result and how much of the operational picture arrives already computed for you.Before: Gnosis Freight
After: Terminal49
Quick comparison
Authentication
Gnosis uses an OAuth2 password grant: exchange a username and password for a bearer token, then send that token on every request. Terminal49 uses a static API key sent as a token, with no separate token-exchange step.Bearer. Terminal49 uses Token.Request parameter mapping
POST /api/v1/tracking_requests/ accepts multiple MBL numbers in a single call and returns one tracking request per number internally. Terminal49’s POST /v2/tracking_requests is one call per bill of lading, booking, or container. Track by BOL and we return every container on that bill of lading as related container resources.Response field mapping
Terminal49 is JSON:API compliant, so relationships between shipments, containers, ports, and terminals are explicit rather than something you reassemble from ID references. Gnosis returns a flat, paginated container list fromGET /api/v1/containers/ ({metadata, containers: [...]}); Terminal49 splits the same information across shipment, container, port, and terminal resources.
Use the include parameter to sideload related resources in one call instead of chasing IDs.
Shipment level
Container level
Locations, facilities, and vessels
Milestone and event mapping
Gnosis exposes milestones as dated fields on the container object (loaded_on_vessel_dt, discharged_dt, and so on) plus a separate webhook schema endpoint. Terminal49 exposes the same milestones as normalized transport events and pushes each one to your webhook.
- Empty picked up / returned:
container.transport.empty_outand.empty_in - Full gated in / out:
container.transport.full_inand.full_out - Vessel berthed:
container.transport.vessel_berthed - Transshipment: arrived, discharged, loaded, departed
- Feeder vessel and barge: arrived, discharged, loaded, departed
- ETA changes:
container.transport.estimated.vessel_arrived,shipment.estimated.arrival
Registering a webhook
Gnosis publishes its webhook shape as a schema dictionary atGET /api/v1/webhooks/containers/webhook_schema, which you inspect and subscribe to out of band. Terminal49 webhooks are self-service: register a URL and a list of named events directly.
What you gain
This is the part worth reading even if the rest is mechanical. Terminal49 integrates with terminals directly, and normalizes holds, fees, and last free day into a fixed shape you don’t have to reconcile yourself.Holds
holds_at_pod_terminal is an array of active holds blocking pickup:
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.
Gnosis’s holds object is a dictionary of hold types where a true value means the container is held by that party. Terminal49’s shape is an array of objects, one per active hold, each carrying a name, a status, and a description. Rewrite any code that checks holds.customs === true to instead check whether an entry with name: "customs" exists in the array.
Fees
fees_at_pod_terminal carries type, amount, and currency:
demurrage, extended_dwell_time, exam, total, and other.
Gnosis’s demurrage_amount maps to a demurrage entry in fees_at_pod_terminal. Gnosis also publishes predictive fields — gnosis_estimated_demurrage_amount, gnosis_estimated_detention_amount, and gnosis_estimated_next_day_demurrage_amount — that project charges before the terminal reports them. Terminal49 has no equivalent: fees_at_pod_terminal only reports amounts the terminal has actually posted. If your workflow depends on predictive fee amounts, budget time to either drop that logic or replace it with your own estimate.
Last free day
pickup_lfd is a coalesced value that follows a fixed source priority: shipping line, then terminal, then rail. It does not pick the earliest date. The individual sources are available separately on import_deadlines:
pickup_lfd_line: the shipping line’s LFD (per diem deadline) — the Terminal49 equivalent of Gnosis’slast_free_detention_day_dtpickup_lfd_terminal: the terminal’s LFD (demurrage deadline) — the equivalent of Gnosis’slast_free_demurrage_day_dtpickup_lfd_rail: the rail carrier’s LFD at the inland destination
gnosis_estimated_last_free_demurrage_day_dt is a predictive estimate; Terminal49 has no equivalent, since import_deadlines only carries dates the terminal or line has actually published.
Release readiness
Two fields answer “can I pick this up?”available_for_pickup and the holds array:
available_for_pickup field is a straight match — same name, same boolean. The difference is what you check alongside it: swap a dictionary lookup on holds for an array scan on holds_at_pod_terminal.
Full detail in Holds, Fees, and Release Readiness.
Gotchas that will bite you
MBL arrays become one tracking request per number
MBL arrays become one tracking request per number
POST /api/v1/tracking_requests/ accepts an array of mbl_numbers in a single call. Terminal49’s POST /v2/tracking_requests takes one request_number per call. If you currently batch MBLs, loop over the array and fire one request per bill of lading; store the returned tracking_request.id per number, not per batch.OAuth2 token exchange goes away
OAuth2 token exchange goes away
Authorization: Token YOUR_API_KEY, and delete the token-refresh logic entirely.JSON:API response shape
JSON:API response shape
{metadata, containers: [...]} list. Terminal49 relationships are ID references into an included array. Use a JSON:API client library, or use include to sideload exactly what you need. Parsing raw JSON works but you will write more code than you expect.Holds go from a boolean map to an array
Holds go from a boolean map to an array
holds object marks each hold type true or false. Terminal49’s holds_at_pod_terminal is an array containing only the holds currently active, each with a name, status, and description. Rewrite holds.customs === true checks as array-membership checks.Timestamps are UTC with a separate timezone field
Timestamps are UTC with a separate timezone field
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.container.updated carries a changeset
container.updated carries a changeset
container.updated with a changeset showing old value first, new value second. Use it instead of diffing state yourself, the way you might diff two GET /api/v1/containers/ polls today.Async lifecycle
Async lifecycle
POST /tracking_requests returns immediately with a pending status. The shipment appears once the carrier responds. Subscribe to tracking_request.succeeded, tracking_request.failed, and tracking_request.awaiting_manifest rather than expecting shipment data in the creation response. See Tracking Request Lifecycle.Error handling
Gnosis returns a422 HTTPValidationError with an array of validation details (location, message, type) for malformed requests. Terminal49 uses standard HTTP status codes consistently across every endpoint.
AuthenticationError, ValidationError, RateLimitError, UpstreamError, FeatureNotEnabledError, AuthorizationError, NotFoundError) and retries rate-limit and server errors automatically with exponential backoff.
Where we are narrower than Gnosis Freight
Worth knowing before you commit. No air cargo tracking. Gnosis’s CLM platform covers air cargo alongside ocean and rail. Terminal49 tracks ocean containers and their rail legs only. If your integration depends on air shipment visibility, this migration handles only the ocean and rail portion. No drayage execution features. Gnosis is built as an execution platform with import drayage workflows (import_drayage). Terminal49 is a tracking and visibility API — it tells you what happened and what’s next, but it does not book, dispatch, or execute drayage moves.
No customs milestone tracking. Gnosis exposes customs milestones and a customs_clearance_dt field directly. Terminal49 has no direct equivalent beyond the customs entry on holds_at_pod_terminal, which tells you whether a customs hold is currently blocking pickup, not a full customs event timeline.
No predictive fee or ETA estimates. Gnosis publishes gnosis_estimated_* fields for demurrage, detention, next-day demurrage, and LFD, plus a gnosis_vessel_eta_dt predictive ETA. Terminal49 reports what carriers and terminals have actually posted — pod_eta_at, fees_at_pod_terminal, and import_deadlines — and does not predict future values.
Carrier count. Terminal49 integrates directly with 36 ocean carriers, plus 2 more enabled on request, as of 14 August 2026. Check your carrier mix against the ocean carrier list before cutover, and read the known issues section there. We publish the per-carrier field gaps.
Terminal data is North America. Holds, fees, LFD, and availability come from direct terminal integrations concentrated in the US and Canada, with European ports expanding. Ocean milestones work globally; terminal-level operational data does not yet.
Some fields are source-dependent. Seal number, container weight, and departure or arrival events vary by carrier. The field availability reference 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
Get a key
Switch authentication
Authorization: Token, a single static key.Check your carrier mix
Unbatch your MBL arrays
mbl_numbers with one POST /tracking_requests per bill of lading, booking, or container.Handle the async lifecycle
succeeded, failed, and awaiting_manifest rather than expecting data on creation.Update response parsing
Update error handling
422 HTTPValidationError envelope parsing with HTTP status-code checks.Backfill active shipments
Decide what to do about predictive fields
gnosis_estimated_* amounts and ETA have no Terminal49 equivalent. Decide whether to drop that logic or replace it with your own estimate before cutover.Then pick one
- Webhook path
- Polling path
Expose an HTTPS endpoint
Register a webhook
Verify HMAC signatures
Whitelist our IPs
Trigger a test delivery
Retire your polling job
GET /api/v1/containers/ on a schedule.Migrate with an AI coding agent
If you use Cursor, Claude Code, Windsurf, Copilot, or another AI coding assistant, hand it the prompt below. It is written to run a side-by-side migration: the agent stands up a Terminal49 client next to your existing Gnosis Freight code, shadows every Gnosis call with a Terminal49 call, diffs the responses, and only cuts over once parity is proven.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 (Gnosis 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 existingGnosisClient, sharing the same interface where possible. - A shadow-mode wrapper that calls both providers and logs response diffs without changing behavior.
- A parity report per shipment: matched fields, diverged fields, and Terminal49-only fields (holds, fees, LFD).
- A feature-flagged cutover: route reads to Terminal49, keep Gnosis as fallback until you flip the flag off.
- A webhook receiver with HMAC verification, or a polling scheduler, depending on which path you pick.
- A cleanup PR that removes Gnosis code, env vars, the OAuth2 token-refresh logic, dependencies, and dedupe logic.