Integrate the air tracking widget
GET https://your-site.example/api/tracking/air — the endpoint you host and hand to the air tracking widget as apiEndpoint.
Same contract as the ocean proxy, against GET /v1/shipments/air. Forward the parameters below with your Bearer token attached, and return the upstream body and status code unchanged.
What the widget sends
| Parameter | Required | Notes |
|---|---|---|
number | yes | The air waybill number the visitor typed. Its three-digit prefix identifies the airline, which is why the air widget has no carrier field and sends no carrier parameter. |
routePath | 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 — the document the API reference describes. |
401 | Missing or invalid Bearer token. |
402 | No credits left for air 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="air-tracking"
data-app='{"apiEndpoint": "https://your-site.example/api/tracking/air"}'
></div>
<script src="https://tracking.one/widget/air-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 number there — one parameter, since the airline is in the waybill's own three-digit prefix:
https://your-site.example/tracking?number=176-12345675
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 —
numberandroutePath. - Call
GET /v1/shipments/aironapi.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 = ["number", "routePath"];
app.get("/api/tracking/air", async (req, res) => {
const params = new URLSearchParams();
// Only what the widget sent: routePath is optional, and an empty
// value is not the same as the flag being absent.
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/air?${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 = ("number", "routePath")
@app.get("/api/tracking/air")
async def air(request: Request) -> Response:
# Only what the widget sent: routePath is optional, and an empty
# value is not the same as the flag being absent.
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/air",
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/air, and that is what goes in apiEndpoint.
add_action('rest_api_init', function () {
register_rest_route('tracking/v1', '/air', [
'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([
'number' => $request->get_param('number'),
'routePath' => $request->get_param('routePath'),
], static fn ($value) => $value !== null && $value !== '');
$response = wp_remote_get(
add_query_arg($params, 'https://api.tracking.one/v1/shipments/air'),
[
'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/air', function (Request $request) {
// array_filter drops the parameters the widget left out.
$params = array_filter($request->only([
'number', 'routePath',
]), static fn ($value) => $value !== '' && $value !== null);
$response = Http::withToken('trk_**********')
->get('https://api.tracking.one/v1/shipments/air', $params);
return response($response->body(), $response->status())
->header('Content-Type', 'application/json');
});