Integrate the container tracking widget
GET https://your-site.example/api/tracking/ocean — the endpoint you host and hand to the container tracking widget as apiEndpoint. The path is yours to choose; /api/tracking/ocean is only the example used throughout this page.
Forward the parameters below to GET /v1/shipments/ocean with your Bearer token attached, and return the upstream body and status code unchanged. What the parameters mean and what comes back is documented once, in the API reference — this page only says which of them the widget sends.
What the widget sends
| Parameter | Required | Notes |
|---|---|---|
referenceNumber | yes | Whatever the visitor typed — a container, bill of lading or booking number. Forward verbatim; rewriting it changes which shipment is billed. |
carrierCode | no | Sent only when the visitor picked a carrier. Omit it when the widget omits it: an empty carrierCode is not the same as no carrierCode. |
routeDetails | no | Always true from the widget — it is what fills the map. Forward as received. |
What you return
| Status | Meaning |
|---|---|
200 | The upstream payload, forwarded unchanged. Do not unwrap, rename or re-encode it — the widget parses the document the API reference describes. |
401 | Missing or invalid Bearer token — your server did not attach the key, or it was revoked. |
402 | No credits left for ocean tracking. |
429 | Rate limit reached. |
502 | Your proxy could not reach the tracking API. |
Embed the widget
Drop the pair of tags where the tracker should appear. The <div> is the mount point — its id is what the bundle looks for, so keep it exactly as written, and point apiEndpoint at your own proxy.
<div
id="container-tracking"
data-app='{"apiEndpoint": "https://your-site.example/api/tracking/ocean"}'
></div>
<script src="https://tracking.one/widget/container-tracking" async></script>
Note the single quotes around data-app: the value is a JSON object, and its own double quotes have to survive HTML parsing. data-app={"apiEndpoint": ""} is parsed as three separate attributes and the widget starts with no endpoint at all.
apiEndpoint is the only required setting — an embedded widget with no endpoint has nowhere to send the lookup. Two more are optional:
height— a CSS height for the widget,"600px"and the like, withoverflow: autoalongside it: a tall shipment scrolls inside that box instead of stretching your page. Without it the widget grows with its content.disabledSearch—truehides the widget's own search form, leaving the status line and the result card — for a page that keeps its own search field and passes the number in the URL (below).
Styling is not among the options and does not need to be: the widget renders into a shadow root, so it neither inherits your stylesheet nor leaks its own.
Driving the widget from the page URL
On mount the widget reads the query string of your page and runs the lookup itself if it finds referenceNumber there, plus carrierCode when you know the carrier:
https://your-site.example/tracking?referenceNumber=UETU7838717&carrierCode=MAEU
It is read once, on mount — a page that rewrites the query in place (a client-side router, history.pushState) leaves the widget on the previous shipment.
Host the proxy
The proxy exists for one reason: an API key in a page's HTML is a public API key. Keep it on the server, and let the browser talk only to you. It is a passthrough, not a translation layer:
- Take the query parameters the widget sent —
referenceNumber,carrierCodeandrouteDetails. - Call
GET /v1/shipments/oceanonapi.tracking.onewith those parameters unchanged. - Add
Authorization: Bearer <your API key>. - Return the response body and the status code exactly as they came back.
The four samples below are deliberately literal — the key is written straight into the code so you can see exactly where yours goes. Replace trk_********** with your own key from the dashboard; this file runs on your server, never in the browser.
Node (Express)
const FORWARD = ["referenceNumber", "carrierCode", "routeDetails"];
app.get("/api/tracking/ocean", async (req, res) => {
const params = new URLSearchParams();
// Only what the widget sent, and only when it sent it: an empty
// carrierCode is not the same as no carrierCode.
for (const key of FORWARD) {
if (req.query[key]) params.set(key, String(req.query[key]));
}
const upstream = await fetch(
`https://api.tracking.one/v1/shipments/ocean?${params}`,
{ headers: { Authorization: "Bearer trk_**********" } }
);
// Status and body pass through untouched.
res.status(upstream.status).json(await upstream.json());
});
Python (FastAPI)
import httpx
from fastapi import FastAPI, Request, Response
app = FastAPI()
FORWARD = ("referenceNumber", "carrierCode", "routeDetails")
@app.get("/api/tracking/ocean")
async def ocean(request: Request) -> Response:
# Only what the widget sent, and only when it sent it: an empty
# carrierCode is not the same as no carrierCode.
params = {k: v for k, v in request.query_params.items() if k in FORWARD and v}
async with httpx.AsyncClient(timeout=30) as client:
upstream = await client.get(
"https://api.tracking.one/v1/shipments/ocean",
params=params,
headers={"Authorization": "Bearer trk_**********"},
)
# A bare Response, not a response_model: the bytes and the status
# go back as they came, with nothing validated or re-encoded.
return Response(
content=upstream.content,
status_code=upstream.status_code,
media_type="application/json",
)
WordPress
Register one REST route — in a small plugin, or in your theme's functions.php. The endpoint is then https://your-site.example/wp-json/tracking/v1/ocean, and that is what goes in apiEndpoint.
add_action('rest_api_init', function () {
register_rest_route('tracking/v1', '/ocean', [
'methods' => 'GET',
// The widget is public, so the route is too. The API key
// stays server-side either way.
'permission_callback' => '__return_true',
'callback' => function (WP_REST_Request $request) {
$params = array_filter([
'referenceNumber' => $request->get_param('referenceNumber'),
'carrierCode' => $request->get_param('carrierCode'),
'routeDetails' => $request->get_param('routeDetails'),
], static fn ($value) => $value !== null && $value !== '');
$response = wp_remote_get(
add_query_arg($params, 'https://api.tracking.one/v1/shipments/ocean'),
[
'headers' => ['Authorization' => 'Bearer trk_**********'],
'timeout' => 30,
]
);
if (is_wp_error($response)) {
return new WP_Error('upstream', 'Tracking API unreachable', ['status' => 502]);
}
// Body and status both pass through untouched.
return new WP_REST_Response(
json_decode(wp_remote_retrieve_body($response), true),
wp_remote_retrieve_response_code($response)
);
},
]);
});
PHP (Laravel)
Route::get('/api/tracking/ocean', function (Request $request) {
// array_filter drops the parameters the widget left out.
$params = array_filter($request->only([
'referenceNumber', 'carrierCode', 'routeDetails',
]), static fn ($value) => $value !== '' && $value !== null);
$response = Http::withToken('trk_**********')
->get('https://api.tracking.one/v1/shipments/ocean', $params);
return response($response->body(), $response->status())
->header('Content-Type', 'application/json');
});