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
Beacon gives you a container-centric API. You register a container number, poll for current state, 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: Beacon
After: Terminal49
POST /tracking_requests once. Terminal49 polls carriers, terminals, and rail, then POSTs to your endpoint as things change. 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
Beacon issues a short-lived Bearer token from a login call and expects you to refresh it before it expires. Terminal49 uses a single static API key, so this entire flow goes away.Token prefix. It is not Bearer.Request parameter mapping
Beacon’sPOST /v1/containers takes an array of container objects in one call. Terminal49’s POST /tracking_requests takes one identifier per request, but that identifier can be a container number, a BOL, or a booking number.
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.
Beacon’s GET /v1/containers/{containerNumber} response is container-centric — it does not return a separate shipment object. Below, the Beacon column reflects the real field names from that response.
Shipment level
Container level
container.attributes.equipment_type, equipment_length (10, 20, 40, 45), and equipment_height (standard, high cube).Locations, facilities, and vessels
Milestone and event mapping
Beacon returns each milestone as its own date object withestimated_date and/or actual_date, rather than a single flat events array. Terminal49 exposes the same milestones as normalized transport events and pushes each one to your webhook.
- 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 Beacon 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:
Gotchas that will bite you
Scope is ocean only, not air or orders
Scope is ocean only, not air or orders
No more token refresh loop
No more token refresh loop
401s from an expired token. Terminal49’s API key is static — delete all of that. There is no login call, no refresh endpoint, and no expiry to track.Usage accounting works differently
Usage accounting works differently
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, and we retry automatically. See Tracking Request Lifecycle.JSON:API structure
JSON:API structure
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.Timestamps are UTC with a separate timezone field
Timestamps are UTC with a separate timezone field
...Z UTC timestamp if no zone information is available, or a date with no time at all if no time information is available — so parsing has to branch on which shape you got. Terminal49 always stores event timestamps in UTC and returns the matching IANA timezone alongside as a separate field. Convert for display rather than assuming local time. See Event Timestamps.Polling cadence vs. webhooks
Polling cadence vs. webhooks
GET /v1/containers/{containerNumber} no more than once every 2 hours. If your Beacon integration is polling on a cron, you can either keep the same cadence against GET /v2/containers, or drop polling entirely and let Terminal49 push webhook events as they happen — freshness is no longer capped by how often you ask.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.Error handling
Beacon returns HTTP status codes with an error body containingtimestamp, error, and sub_error fields. Terminal49 also uses standard HTTP status codes. Replace any Beacon-specific error parsing with status-code checks.
AuthenticationError, ValidationError, RateLimitError, UpstreamError, FeatureNotEnabledError, AuthorizationError, NotFoundError) and retries rate-limit and server errors automatically with exponential backoff.
Where we are narrower than Beacon
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. Beacon advertises coverage across 160+ ocean and air carriers combined. 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 tracking. Beacon also tracks air waybills (AWBs). Terminal49 tracks ocean only. If your integration covers air freight, keep it on Beacon or move it elsewhere — this migration handles only the ocean portion. No order management. Beacon lets you create and update orders and link an order to a shipment. Terminal49 does not model orders — it tracks shipments and containers. If your Beacon integration uses order management, keep Beacon for that or replace it separately. 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. Note the Token prefix. Set Content-Type: application/vnd.api+json.Check your carrier mix
Create tracking requests
POST /tracking_requests per BOL, booking, or container, replacing the per-request lookup.Handle the async lifecycle
succeeded, failed, and awaiting_manifest rather than expecting data on creation.Update response parsing
Update error handling
Backfill active shipments
Add the terminal fields
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
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 Beacon code, shadows every Beacon 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 (Beacon 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 existingBeaconClient, 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 Beacon 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 Beacon code, its login/token-refresh flow, env vars, dependencies, and dedupe logic.