Terminal49 Documentation Audit
The Mintlify-style docs at terminal49.com/docs cover a real, working JSON:API platform with thoughtful guides — but they coexist with two other doc surfaces, contradict themselves on auth, rate limits, and event names, point readers at 404s, and ship a stale changelog and sales-gated features without self-serve paths.
1. Three competing doc surfaces with no canonical pointer (critical)
Location: terminal49.com/docs, developers.terminal49.com, help.terminal49.com, www2.terminal49.com
Problem: Google still indexes a legacy ReadMe-style site at developers.terminal49.com (returns 403 to anonymous traffic), an end-user help center at help.terminal49.com with its own "Developer & API" category (3 articles, partially overlapping the /docs reference), and search-indexed URLs on www2.terminal49.com/docs/api-docs/... that return ERR_TLS_CERT_ALTNAME_INVALID because the cert doesn't cover that host. Compounding this, the /docs landing page's own internal linkmap repeatedly uses /api-docs/* paths off the bare host (e.g., /api-docs/getting-started/start-here), but those return 404 — the canonical path is /docs/api-docs/*. The current Mintlify docs at terminal49.com/docs link to none of the external hosts and do not declare itself canonical.
Consequence: Developers (and AI agents) following Google or stale internal links land on 403s, broken-cert pages, 404s on the bare host, or stub help-center articles, and have no signal that the canonical reference lives elsewhere. Some users will integrate against outdated specs from developers.terminal49.com; agents will index the wrong surface entirely.
The fix: Pick one canonical host and one canonical path prefix. 301-redirect developers.terminal49.com and www2.terminal49.com/docs/* to the equivalent terminal49.com/docs/* path, fix the www2 cert, and either redirect /api-docs/* to /docs/api-docs/* or rewrite the docs landing linkmap to use the canonical prefix. Add a "Looking for our legacy developer site?" banner on any URL still serving content there.
2. Authentication scheme described three different ways (critical)
Location: /docs/api-docs/api-reference/introduction, /docs/api-docs/api-reference/containers/get-a-container, /docs/api-docs/getting-started/list-shipments-and-containers/
Problem: The introduction and most pages say Authorization: Token YOUR_API_KEY. The Get-a-Container reference page says auth uses an "API key and secret … sent via the Authorization request header" — there is no secret anywhere else in the docs. The "List Tracked Shipments and Containers" page calls it "HTTP Bearer Token authentication," which is a different RFC scheme (Authorization: Bearer …).
Consequence: Anyone reading "Bearer Token" in a getting-started page and using a standard HTTP client's Bearer helper (axios, requests' HTTPBearerAuth, fetch wrappers, OpenAPI clients) will send Authorization: Bearer … and get 401s. Anyone reading "API key and secret" will hunt the dashboard for a missing secret. Agents that pattern-match "Bearer" auth will fail silently on every call.
The fix: Pick one description — "Token-prefixed API key in the Authorization header" — and replace every other phrasing globally. Show the literal header Authorization: Token <key> once at the top of the introduction, and remove all references to "Bearer" and "secret".
3. Rate limit contradicts itself by 2x (critical)
Location: /docs/api-docs/api-reference/introduction vs /docs/api-docs/in-depth-guides/auto-detect-carrier
Problem: The API Reference introduction says "100 requests per minute" per API key on a rolling 60-second window. The Infer / auto-detect-carrier page says "Rate limits allow 200 requests per minute." Neither page acknowledges the other or scopes the difference (e.g., per-endpoint vs per-key vs per-account).
Consequence: Developers building backoff and concurrency limits will pick one number and either over-throttle (cutting throughput in half) or under-throttle and trip 429s in production. Agents reading the intro page will refuse to dispatch parallel Infer calls that the platform actually permits.
The fix: State the global per-key limit once on the introduction page, then on each endpoint that has its own limit (Infer at 200/min, Create Tracking Request at 100/min) repeat the number with a clear "this endpoint has its own bucket" note. Add a RateLimit-* header reference and the exact Retry-After semantics.
4. Webhook event names disagree across pages, including events for a feature with no docs (critical)
Location: /docs/api-docs/api-reference/webhooks/create-a-webhook vs /docs/api-docs/webhooks/event-catalog, /docs/api-docs/in-depth-guides/rail-integration-guide, /docs/api-docs/webhooks/use-cases/milestone-tracking
Problem: The Create-a-Webhook reference lists transport events as bare names — vessel_arrived, rail_loaded, vessel_berthed, empty_out, etc. Every other page (event catalog, rail guide, milestone-tracking guide) names the same events with the container.transport. prefix — e.g., container.transport.vessel_arrived. The rail guide also documents container.pickup_lfd_rail.changed, which does not appear in the Create-a-Webhook event list at all. Separately, Create-a-Webhook lists document.extracted and document.extraction_failed as subscribable events — but no Documents API endpoints, no extraction guide, and no payload examples for these events appear anywhere reachable in the docs.
Consequence: A developer copying the bare names into the events array on POST /v2/webhooks will subscribe to event identifiers that may not match what's actually delivered, or will silently miss events. A rail customer following the LFD-alerts use case will not subscribe to container.pickup_lfd_rail.changed because the create endpoint never lists it. Anyone subscribing to document.extracted will receive payloads for a feature with no schema reference. Agents auto-generating subscriptions from the OpenAPI spec will get the wrong set.
The fix: Pick one naming convention (the container.transport.* form everyone else uses), update the Create-a-Webhook reference to match, and add container.pickup_lfd_rail.changed and any other rail-specific events to the canonical list. Either ship the Documents API reference and an extraction-events payload page, or remove document.extracted/document.extraction_failed from the public event enum until that surface exists. Verify the OpenAPI/Postman spec uses the same strings.
5. Custom-fields endpoints disagree about the entity type (critical)
Location: /docs/api-docs/api-reference/custom-fields/create-a-custom-field-definition vs /docs/api-docs/api-reference/custom-fields/create-a-custom-field
Problem: create-a-custom-field-definition says entity_type "Must be either Shipment or Cargo." The sibling create-a-custom-field says entity is a "Polymorphic relationship to Shipment or Container." The two endpoints use different identifiers (Cargo vs Container) for the same target. Separately, the definition endpoint accepts data_type: reference, but the field-creation endpoint does not list reference among its supported data types — so a definition can be created for a value endpoint that doesn't document it.
Consequence: Developers will guess at whether to send Cargo or Container, get 422s, and have no way to tell which is correct. A definition created with data_type: reference has no documented way to set values. Agents writing custom-field tooling will flip-flop between the two strings.
The fix: Use one canonical entity name (most likely Container, since that's what the rest of the API exposes) on both endpoints. Either add reference to the supported data types on create-a-custom-field with a worked example, or remove it from the definition endpoint until the value side is implemented.
6. llms.txt lives at a non-standard path; llms-full.txt missing entirely (significant)
Location: terminal49.com/llms.txt, terminal49.com/llms-full.txt, terminal49.com/docs/llms-full.txt, terminal49.com/docs/llms.txt
Problem: The standard agent-discovery paths /llms.txt and /llms-full.txt at the root return 404. The only working file is at /docs/llms.txt. There is no llms-full.txt anywhere — neither at root nor under /docs/. The /docs landing page directly tells readers "A complete documentation index is available at https://terminal49.com/docs/llms.txt."
Consequence: Agents (Claude Code, Cursor, Windsurf, Copilot, Perplexity scrapers) check /llms.txt at the root by convention. They will not find Terminal49's index, and will fall back to crawling Mintlify HTML — slower, less complete, and unlikely to discover the deep api-reference/* paths or the in-depth-guides/webhooks security page. There is no llms-full equivalent at all, so agents that want the full bundled context have nothing to fetch.
The fix: Serve llms.txt and llms-full.txt at the root host (terminal49.com/llms.txt, terminal49.com/llms-full.txt). Generate llms-full.txt containing the concatenated text of the docs. Keep /docs/llms.txt redirecting or duplicated, but add the canonical paths the convention expects.
7. Webhook security and retry policy buried under "in-depth-guides" rather than the webhooks tree (significant)
Location: /docs/api-docs/webhooks/overview, /docs/api-docs/getting-started/receive-status-updates/, /docs/api-docs/in-depth-guides/webhooks
Problem: The conceptual "Why Webhooks" page and the "Receive Status Updates" getting-started page contain zero security content — no HMAC, no X-T49-Webhook-Signature, no IP allowlist, no retry semantics. All of that lives at /docs/api-docs/in-depth-guides/webhooks, which is not in the webhooks navigation tree and is not linked from either page above. The retry policy on that page reads "up to a dozen attempts" with no backoff schedule, no dead-letter behavior, and no signature versioning. The signature example is Ruby-only — no JS/Python/Go.
Consequence: A beginner integrating webhooks lands on the obvious page, ships an unverified endpoint, and is now vulnerable to spoofed POSTs from anyone who finds the URL. Production reliability work (idempotency, retry windows) cannot be implemented without first stumbling into the in-depth-guides surface. Most webhook users build in Node/Python/Go and have no signature sample to copy.
The fix: Move signature verification, IP allowlist, and retry policy into the canonical webhooks/overview page. Add JS, Python, and Go snippets next to the Ruby one. Replace "up to a dozen attempts" with the actual count, the backoff curve, and the dead-letter rule. Document a signature version header so future rotations don't break consumers.
8. Event catalog points readers to a payloads page that 404s (significant)
Location: /docs/api-docs/webhooks/event-catalog → /docs/api-docs/webhooks/payloads
Problem: The event catalog tells readers "For complete payload examples and schemas, the source directs readers to a separate payloads reference page." That page does not exist — /docs/api-docs/webhooks/payloads is a dead route.
Consequence: Anyone wanting the canonical payload schema for the 30+ events has nowhere to land. They have to cobble payloads together from the in-depth-guides webhooks page (which only shows four examples) and the use-case pages. Agents auto-generating webhook handlers cannot resolve the schema reference.
The fix: Either publish the payloads reference page (one section per event with a JSON example and the included array contents), or remove the cross-reference and inline payload examples next to each event in the catalog.
9. Marketing coverage numbers don't match what the docs enumerate (significant)
Location: /docs landing vs /docs/api-docs/useful-info/api-data-sources-availability
Problem: The /docs landing page claims "100+ integrations covering over 98% of global container volume," "100+ ocean carriers," and "150+ terminal operators." The single page these claims link to lists ~13 ocean carriers (CMA-CGM, Maersk, Hamburg Süd, MSC, Hapag Lloyd, Evergreen, COSCO, OOCL, ONE, Yang-Ming, Hyundai, ZIM, Westwood) and ~25 ports. SCAC codes for the rest are punted to an external Google Sheets link.
Consequence: Buyers and developers cannot answer "is my carrier supported?" from the docs. The 7-10x gap between the marketing claim and the enumerated list erodes trust and forces a sales conversation for what should be a self-serve question. Agents asked "does Terminal49 cover Wan Hai?" cannot answer from the docs.
The fix: Publish a complete carrier and terminal coverage table in the docs (not in Google Sheets), with per-carrier limitations attached. If "100+" is true, list them; if not, correct the marketing copy. Treat coverage as part of the API surface, not marketing copy.
10. Changelog has been silent for 23 months and contradicts another doc page (significant)
Location: feedback.terminal49.com/changelog vs /docs/api-docs/useful-info/api-data-sources-availability
Problem: As of 2026-05-09, the most recent changelog entry is dated 2024-06-21: an active "AIS Degradation" notice saying Vessel Departed/Arrived/Berthed events are degraded. There are no entries for the past ~23 months. Meanwhile, the api-data-sources-availability page lists AIS as "coming soon" — framing it as future work, not a regression. The docs site itself does not publish a versioned changelog; the Canny-style feedback.terminal49.com is the only source.
Consequence: A reader cannot tell whether AIS is degraded, fixed, or never shipped. A reader cannot tell what's been built since June 2024. This is the worst-case shape: a stale, alarming top entry plus a silent two-year gap. Customers will assume the platform is unmaintained or that AIS-dependent flows are still broken in production.
The fix: Either resume changelog publication or replace feedback.terminal49.com with a proper changelog inside the Mintlify docs. Resolve or update the AIS Degradation entry. Reconcile "coming soon" vs "degraded" — they cannot both be true.
11. DataSync overview is a sales page, not a reference (significant)
Location: /docs/datasync/overview
Problem: DataSync is one of four headline surfaces on the /docs landing page ("Data Integration") and gets repeated front-door links. The overview page lists three core tables (containers (rail), shipments, tracking_requests) by name only — no schema, no field list, no data types, no enumerated destinations beyond "almost any database, data warehouse, or object store, as well as to Google Sheets." The setup process is a "1-hour configuration call with Terminal49." The "containers (rail)" naming is itself confusing — it implies the container table is rail-only, which it isn't.
Consequence: A developer evaluating DataSync cannot answer "what fields will I get?" or "does this support Snowflake / BigQuery / Redshift / Postgres?" without a sales call. Agents asked to map DataSync output to a downstream warehouse have nothing to map. Procurement teams cannot scope a DataSync integration on docs alone.
The fix: Publish the actual schema for each table (fields, types, nullability) and the enumerated destination list. If the schema is generated, link to the generated artifact. Rename containers (rail) to whatever it actually is. Treat DataSync as a documented product surface, not a sales lead.
12. Sales-gated paid features inline-mentioned with no upgrade path documented (significant)
Location: /docs/api-docs/in-depth-guides/routing, /docs/api-docs/in-depth-guides/terminal49-map, /docs/api-docs/api-reference/vessels/*, /docs/mcp/home
Problem: Routing data, Vessel positions, and the publishable map API key are gated by emails to sales@terminal49.com or support@terminal49.com. The MCP page describes get_container_route as "paid feature" inline. None of these pages explain what error a non-entitled user actually receives (the vessel pages do say 403 with "Routing data feature is not enabled" — others don't), what the entitlement is called in the dashboard, or whether the entitlement applies to all three (Routing/Vessels/Map) together or separately.
Consequence: Developers prototyping a map embed or routing flow will hit silent failures, sales emails, and ambiguity about what exactly is gated. Agents asked to "add the Terminal49 map" will not realize they can't get a publishable key without a sales touch.
The fix: Add one "Entitlements" page enumerating the paid surfaces (routing, vessel positions, map publishable key, MCP route tool), the SKU name, the dashboard location, and the exact error code/body returned to non-entitled callers. Cross-link from each affected reference page.
13. Pricing for the embeddable widget hidden inside a code-snippet guide (significant)
Location: /docs/api-docs/in-depth-guides/terminal49-widget
Problem: The widget guide casually notes a "Monthly fee of $500 (unlimited visitors and requests included)" inside an "Important Limitations" bullet. The docs site has no central pricing page, and this $500/month figure is the only place pricing appears in any scraped doc.
Consequence: A developer evaluating whether to embed the widget will not look for pricing inside an integration guide. Procurement and product teams will be surprised. The figure could also drift from sales reality without anyone noticing.
The fix: Move pricing to a single Pricing page (or sales-gated mention with a clear pointer), and on the widget guide link out to it instead of inlining a number that may be stale.
14. Vessel future-positions endpoint requires two undocumented parameters (significant)
Location: /docs/api-docs/api-reference/vessels/get-vessel-future-positions
Problem: The GET /v2/vessels/{id}/future_positions endpoint requires both port_id and previous_port_id as query parameters, but neither the vessels overview nor any higher-level routing/vessels guide tells a developer they need both UUIDs in hand before calling this endpoint. There is no documented flow for obtaining the right previous_port_id for a given vessel state.
Consequence: A developer following the routing or vessel-tracking guide will call the endpoint with just id (or with only the destination port) and get a 422, with no breadcrumb on which parameter is missing or how to derive it. Agents generating client code from the reference will not surface the dependency until runtime.
The fix: Document the resolution flow ("how to get the right previous_port_id for a vessel") on a vessels overview page, and cross-link from the future-positions reference. Mark both parameters required at the top of the endpoint, not buried in the query-parameter table.
15. MCP client config is macOS-only (significant)
Location: /docs/mcp/home
Problem: The MCP page documents the Claude Desktop config path as ~/Library/Application Support/Claude/claude_desktop_config.json only — no Windows (%APPDATA%\Claude\…) or Linux paths. It mentions Cursor IDE but no other MCP-capable clients (ChatGPT, Continue, Cline, Zed). Auth is bearer-token only; no OAuth flow is documented.
Consequence: Windows and Linux developers — a large share of the agent-tooling audience — cannot follow the setup steps without translating paths from elsewhere. Agents asked to wire up Terminal49's MCP server in a non-Claude client have no template to follow. Given the audit's emphasis on agents as primary doc consumers, this is a notable gap on the page that ought to be the most agent-friendly in the docs.
The fix: List config paths for macOS, Windows, and Linux side-by-side. Add a generic "any MCP client" section showing the server URL and bearer-token shape so non-Claude/non-Cursor clients have a starting point. State whether OAuth is on the roadmap.
16. SDK package not discoverable on npm or GitHub (significant)
Location: /docs/sdk/introduction
Problem: The introduction tells readers to npm install @terminal49/sdk. The docs do not link to the npm package page, do not link to a GitHub repo, do not state a license, and do not describe a versioning or deprecation policy. As of audit date 2026-05-09, a public npm and GitHub search for @terminal49/sdk did not surface a published package or repository (this is observation outside the scrape, included to give the company a reproducible check). Node.js 18+ is required, but Deno/Bun/browser support is not addressed.
Consequence: A developer who runs the install command and wants to read the source, file an issue, or audit the package before shipping it to production has no link to follow. Agents that resolve npm metadata before recommending a package will refuse to install something they can't verify exists. If the package is private, the docs should say so; if it's public, it should be discoverable.
The fix: Link to the npm package page and a public GitHub repo. State the license, the support window for old SDK versions, and which JS runtimes are supported. If the package is intentionally unlisted/private, explain how to obtain it.
17. SDK quickstart references three sub-pages that do not exist (minor)
Location: /docs/sdk/quickstart
Problem: The quickstart references "filtering techniques, pagination strategies, and webhook implementation for real-time updates" sub-pages, but only /docs/sdk/methods and /docs/sdk/introduction are reachable — there are no /docs/sdk/filters, /docs/sdk/pagination, or /docs/sdk/webhooks pages.
Consequence: Readers following the "next steps" pointers hit dead navigation or have to grep methods.md for the relevant patterns themselves.
The fix: Either publish the three sub-pages or remove the references and inline the missing content into methods.md.
18. Pagination defaults inconsistent across list endpoints (minor)
Location: /docs/api-docs/api-reference/shipments/list-shipments vs /docs/api-docs/api-reference/parties/list-parties
Problem: list-shipments defaults page[size] to 30. list-parties defaults to 25. No global pagination doc explains the per-resource defaults or the maximum page size.
Consequence: A developer writing a generic JSON:API list helper will under- or over-fetch on one of the two resources. Agents writing pagination loops will compute totals against the wrong assumed page size.
The fix: Either standardize the default across resources or publish a single "Pagination" reference page that lists the default and max per endpoint.
19. q parameter on list-shipments deprecated with no sunset (minor)
Location: /docs/api-docs/api-reference/shipments/list-shipments
Problem: The q query parameter ("Search by master bill of lading, reference number, or container") is explicitly tagged "(deprecated)" in the parameter table. There is no replacement parameter named, no removal date, no migration guidance, and no link to a deprecations page.
Consequence: Existing integrations using q cannot tell whether they have weeks, months, or years to migrate, and don't know what to migrate to. New developers reading the page will avoid q and may not realize there's a search story at all.
The fix: State the sunset date, the replacement parameter (or endpoint), and link to a single "Deprecations" page that lists every deprecated field across the API with timelines.
20. Container-status enum publishes two values marked "not actively used" (minor)
Location: /docs/api-docs/in-depth-guides/container-statuses
Problem: The status reference lists dropped and loaded and explicitly tags both as "Defined but not actively used."
Consequence: Any consumer writing an exhaustive switch on current_status will write dead branches, and any consumer omitting them risks future surprise if Terminal49 starts emitting them without notice. The "defined but not used" framing tells developers nothing about whether to handle them.
The fix: Either remove them from the public enum entirely, or document the conditions under which they will start being emitted and the migration policy for consumers. "Defined but not used" is not a contract.
21. Map widget and tracking widget load third-party scripts with no SRI hashes (minor)
Location: /docs/api-docs/in-depth-guides/terminal49-widget, /docs/api-docs/in-depth-guides/terminal49-map
Problem: Both embed snippets load JS without subresource integrity hashes:
<script src="https://kit.fontawesome.com/cd34e860ca.js" crossorigin="anonymous"></script><script src="https://widget.terminal49.com/app.bundle.js"></script><script src="https://map.terminal49.com/bundle.js"></script>
The Font Awesome script is loaded from a third-party domain under a kit ID, so its contents can change without site-owner notification.
Consequence: Customers embedding the widget on pages that handle shipment data or accept user input have no integrity guarantee on the loaded script. The Font Awesome kit dependency adds a third-party supply-chain surface that some customers' CSP/security review will reject.
The fix: Publish integrity hashes for app.bundle.js and map.terminal49.com/bundle.js and document a versioned URL pattern. Make the Font Awesome dependency optional or self-host the icons used.
What they do well
- The webhook conceptual model (overview + use-cases for ETA / LFD / milestone) is genuinely good — clear "why webhooks beat polling" framing and concrete
included-array parsing patterns. - The
holds-and-feesguide gives explicit, machine-usable readiness rules (available_for_pickup === true && no holds with status: "hold"), case-sensitivity callouts, and a "filter out thetotalfee type" gotcha that agents would otherwise miss. - The tracking-request lifecycle and failed-reason taxonomy are documented with retry counts, time windows, and the temporary-vs-permanent distinction — uncommon clarity for a status enum.
Top 3 recommendations
- Pick one canonical doc surface, one canonical path prefix, and one canonical auth phrasing. Redirect
developers.terminal49.comandwww2.terminal49.com/docs/*toterminal49.com/docs/*, fix the www2 cert, rewrite/api-docs/*linkmap entries to/docs/api-docs/*, and globally replace "Bearer Token" / "API key and secret" withAuthorization: Token <key>. - Reconcile the cross-page contradictions agents will trip over. Webhook event names (
vessel_arrivedvscontainer.transport.vessel_arrived), custom-field entity (CargovsContainer), rate limits (100 vs 200/min), anddocument.*events without a Documents API must each have one answer in one place. - Restore the agent and changelog surfaces. Serve
llms.txtandllms-full.txtat the root, publish the missing/webhooks/payloadsreference page, document Windows/Linux MCP config paths, and either resume changelog entries inside the docs or close out the stale "AIS Degradation" notice that has been sitting at the top since June 2024.