Migrating from the JS reference client¶
Code-by-code translation of the patterns the JS client made you write by hand into the SDK's equivalent. Ctrl-F any JS idiom and find the SDK answer.
Login¶
Before (JS)¶
const r = await axios.post('/api/v2/auth/log_in.json', {
email: 'qa@example.com',
password: 'hunter2',
});
const token = r.data.data.token;
localStorage.setItem('USER_TOKEN', token);
axios.defaults.headers.common['X-User-Token'] = token;
axios.defaults.headers.common['X-User-Email'] = r.data.data.email;
After (SDK)¶
from rtls_sdk import RtlsClient
client = RtlsClient.from_env() # reads RTLS_USERNAME / RTLS_PASSWORD / RTLS_BASE_URL
Why this is better: No global axios mutation, no manual token plumbing, no token persistence. Lazy: the actual login fires on the first API call.
401 handling¶
Before (JS)¶
axios.interceptors.response.use(null, async err => {
if (err.response?.status === 401 && !err.config.__isRetry) {
err.config.__isRetry = true;
const token = await refreshToken();
err.config.headers['X-User-Token'] = token;
return axios(err.config);
}
throw err;
});
After (SDK)¶
Nothing. The SDK handles 401 internally — on a 401 it
re-authenticates with the stored credentials and replays the request.
The caller sees the eventual success or an AuthenticationError.
Why this is better: No interceptor maintenance, no
failedRequestsQueue-style coalescing logic, no shared mutable state
to debug.
Compound: tag create + zone attach + groups bind¶
Before (JS)¶
const tag = await axios.post('/api/v2/trackable_objects', {
name: 'Forklift 9',
// ⚠ JS sent mac_address here but the server's createTrackObject schema
// silently strips unknown keys — the MAC never actually bound. The
// intended binding is via /trackable_objects_associations.
});
await axios.put(`/api/v2/trackable_objects/${tag.data.uid}/zone`, {
zone_uid: 'z-warehouse-3',
});
await axios.put(`/api/v2/trackable_objects/${tag.data.uid}/groups`, [
'g-fleet', 'g-priority',
]);
// If any step failed, the half-created tag is now orphaned — caller's
// problem to clean up.
After (SDK)¶
tag = client.tags.create(
name="Forklift 9",
attached_zone={"shape": "circle", "geometry": {"radius": 3000}},
groups=["g-fleet", "g-priority"],
)
# Bind hardware separately (tag.create has no mac_address parameter):
client.nodes.create("aabbccddeeff")
client.tag_associations.create("aabbccddeeff", tag.uid)
Why this is better: One call (per saga) and one return. On partial
failure the SDK rolls back the created tag and raises
PartialFailureError with step="attach_zone" so a retry knows where
to resume. No orphaned records. And the MAC binding is no longer
silent — the SDK exposes the three primitives the server actually
implements.
Scope juggling — per-call project switch¶
Before (JS)¶
function setProjectUidToHeader(uid) {
axios.defaults.headers.common['X-User-Project'] = uid;
}
function removeProjectFromHeader() {
delete axios.defaults.headers.common['X-User-Project'];
}
setProjectUidToHeader('p-target');
try {
await axios.post('/api/v2/nodes_associations/release', {...});
// If this throws, the header is leaked to every subsequent call
// because the catch path doesn't run cleanup.
} finally {
removeProjectFromHeader();
}
After (SDK)¶
Why this is better: Scope override lives on a per-thread
threading.local, applied per-request. Header restoration is
automatic and exception-safe. Two threads doing parallel scope
switches don't race each other.
Pagination — following nextPage¶
Before (JS)¶
const events = [];
let url = '/api/v2/external_events?...';
while (url) {
const r = await axios.get(url);
events.push(...r.data.events);
url = r.data.nextPage; // null when done
}
After (SDK)¶
Why this is better: The SDK handles all three server pagination
styles (nextPage body URL, content-next-positions-page header
cursor, bare page+limit) behind one iterator. Memory stays bounded
even for year-long windows.
Array query params (Rails-style)¶
Before (JS)¶
// hand-built — easy to forget the brackets
const qs = trackUids.map(u => `track_uid[]=${u}`).join('&');
await axios.get(`/api/v2/external_events?${qs}&...`);
After (SDK)¶
Why this is better: Pass a list. The SDK encodes track_uid[]=t-1&track_uid[]=t-2 correctly.
Envelope-unwrap fallback¶
Before (JS)¶
After (SDK)¶
Why this is better: Typed return. The SDK knows which envelope each endpoint uses and unwraps consistently. Unknown shapes raise loudly instead of silently returning the wrong type.
Timestamps¶
Before (JS)¶
// alarms — uses epoch ms
await axios.get('/api/v2/alarms', { params: { from: Date.now() - 86400e3, to: Date.now() } });
// reports — uses ISO-8601
await axios.get('/api/v2/report', { params: { from: '2024-01-01T00:00:00Z', to: '2024-01-02T00:00:00Z' } });
After (SDK)¶
from datetime import datetime, timezone
start = datetime(2024, 1, 1, tzinfo=timezone.utc)
end = datetime(2024, 1, 2, tzinfo=timezone.utc)
client.alarms.list(start=start, end=end) # SDK sends epoch ms
client.reports.list(start=start, end=end, site_uid="s-1", type=ReportType.HEATMAP) # SDK sends ISO-8601
Why this is better: Always a tz-aware datetime. The SDK picks
the right wire format per endpoint.
Token storage¶
Before (JS)¶
After (SDK)¶
Nothing — tokens are instance-scoped and never persisted. Construct
a new RtlsClient per identity. When the process exits, the token is
gone.
Swallowed errors¶
Before (JS)¶
async function createTrackableAssociations(pairs) {
try {
await axios.post('/api/v2/associations/bulk', pairs);
} catch (e) {
console.error(e);
return null; // caller can't distinguish "no associations" from "all failed"
}
}
After (SDK)¶
result = client.tag_associations.bulk_create(pairs)
# result.successes and result.failures both populated
Why this is better: Typed BulkResult lists which items succeeded
and which raised, so a partial batch doesn't lose state. No
return-null-on-error.
Refresh failure UX¶
Before (JS)¶
After (SDK)¶
Globals to avoid¶
The JS client tracks several pieces of state on the window or
axios.defaults. None of them have an SDK analogue:
| JS | SDK answer |
|---|---|
axios.defaults.baseURL = ... |
RtlsClient(base_url=...) per instance. |
axios.defaults.headers.common['X-User-Token'] |
httpx.Auth injects per-request, no client state. |
_getHostName() reading localStorage |
base_url is a constructor arg. |
Browser-only target_server URL-param routing |
Not ported (server team confirmed). |
setInterval(..., 3min) widget polls |
Out of scope for v1 — write your own loop. |
| Client-side zone-type filtering by role | Not ported. The SDK returns what the server returns; filter in your application code. |
A complete migration¶
The shortest before-and-after — typical dashboard bootstrap:
Before (JS)¶
await login(email, password);
const sites = await fetchSites();
const tags = await fetchTags();
const users = await fetchUsers();
const me = await fetchMe();
// 17 more list calls
After (SDK)¶
See Context for the full field list.