npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tractive-client

v1.0.0

Published

TypeScript client for Tractive's unofficial REST API (graph.tractive.com). Zero runtime dependencies.

Readme

Tractive Client

A small TypeScript client library for Tractive's unofficial REST API (graph.tractive.com). Tractive has no official public API, so this talks to the same backend their web dashboard (my.tractive.com) uses.

This is a plain importable library - no server, no framework required. You bring your own credentials and wire it into your own application. Zero runtime dependencies.

Install

npm install tractive-client

Setup (for local development on this package itself)

npm install     # also builds dist/ via the prepare script
npm run build   # rebuild after making changes
npm run typecheck
npm test

Usage

import { createTractiveClient } from "tractive-client";

const client = createTractiveClient({
  email: "[email protected]",
  password: "your-tractive-password",
  // Both optional - default to the values from tractive that are known working.
  // Override only if you have your own X-Tractive-Client value or Tractive
  // changes their API base URL (e.g. a version bump from /4/ to /5/).
  clientId: "your-x-tractive-client-header-value",
  baseUrl: "https://graph.tractive.com/4/",
});

const trackers = await client.getTrackers();
const location = await client.getTrackerLocation(trackers[0]);

Config

| Field | Required | Description | |---|---|---| | email | yes | Tractive account email | | password | yes | Tractive account password | | clientId | no | The X-Tractive-Client header value Tractive's API requires alongside the token. Defaults to a confirmed-working value | | baseUrl | no | Base URL of Tractive's API. Defaults to https://graph.tractive.com/4/ |

A note on authentication

Every method calls authenticate() internally before making a request:

  1. If there's a valid cached token in memory (on this specific client instance), reuse it - no network call.
  2. Otherwise log in fresh against POST {baseUrl}/auth/token using email/password, cache the result in memory, and use that.

A token is treated as expired 60 seconds before its real expires_at, so a request never straddles an expiry mid-flight. You never need to call authenticate() yourself first - every other method does it on demand. The cache is a plain closure variable private to that one client - it's not a module-level singleton, so multiple createTractiveClient() instances (e.g. multiple accounts) never interfere with each other.

A note on security

Tractive only supports email/password authentication so these credentials will always be required. It is highly advised to not use this package in any frontend application that bundles code to the client.

API

Returned by createTractiveClient(config). Full parameter and return types ship in the package's .d.ts files, so your editor will show them on hover - this section documents behavior the types alone don't capture: what each call does under the hood, when it throws, and any caching/fallback logic.

Auth

authenticate()

Returns: Promise<TractiveAuth> - { user_id, client_id, expires_at, access_token }

Logs in (or reuses a cached token) and returns the raw auth response. Every other method calls this internally, so you only need it yourself if you want the raw token data. See How auth works.

isAuthenticated()

Returns: boolean

True if there's a still-valid cached token in memory. Never makes a network call.

Account

getMe()

Returns: Promise<TractiveMe> - { userId, email, firstName, lastName }

Curated, not a raw passthrough of Tractive's /user/{id} (which also returns phone number, home address, etc.).

Trackers

getTrackers()

Returns: Promise<string[]>

Tracker IDs associated with the account.

getTrackerDetails(trackerId)

Returns: Promise<TractiveTracker> - model, firmware, battery/charging state, capabilities.

Throws: TrackerNotFoundError if the tracker doesn't exist or isn't accessible.

getTrackerGeofences(trackerId)

Returns: Promise<unknown[]>

Raw geofence data. Not yet typed - TODO.

Location & history

getTrackerLocation(trackerId)

Returns: Promise<TractiveLocation> - latest GPS fix.

Falls back to the last successfully fetched value for that tracker if the live call fails.

Throws: TrackerNotFoundError only if nothing's cached yet.

getTrackerPositionRange(trackerId)

Returns: Promise<TractivePositionRange> - { first, last }, unix-second bounds of available history.

getTrackerPositionHistory(trackerId, timeFrom, timeTo)

Returns: Promise<TractivePositionHistoryPoint[][]>

GPS track as segments - a new segment marks a gap in tracking. timeFrom/timeTo are unix seconds.

reverseGeocode(latitude, longitude)

Returns: Promise<TractiveAddress> - { street, house_number, zip_code, city, country, full_address }

Hardware

getTrackerHardware(trackerId)

Returns: Promise<TractiveHardware>

Same stale-fallback behavior as getTrackerLocation.

Throws: TrackerNotFoundError only if nothing's cached yet.

getTrackerBattery(trackerId)

Returns: Promise<number> - 0-100.

Device commands (untested against real hardware)

setTrackerLED(trackerId, on)

setTrackerBuzzer(trackerId, on)

setTrackerLiveTracking(trackerId, on)

Returns: Promise<unknown> - Tractive's command-state object as-is, for all three: { active, started_at, timeout, remaining, pending }.

Known open issue: in testing, setTrackerLED(id, true) returned { pending: true, active: false, remaining: 0, ... } and the physical LED did not light up. Two live theories, neither confirmed:

  1. The tracker was in a power-saving state and hadn't checked in with Tractive's servers to pick up the queued command yet.
  2. The clientId in use has read access but may not have permission to issue device commands - Tractive may scope command-issuing to the official app's exact client ID differently from read-only data access.

Errors

  • NotAuthenticatedError - thrown if a valid access token can't be obtained
  • TrackerNotFoundError - thrown if a tracker has no data available (cached or live)

Architecture

The library is split into one folder per domain, each folder owning its own implementation, types, and test file together, rather than one large client file (or a flat src/ with same-named files sitting loose next to each other):

src/
  index.ts                    - main entry point (barrel export)
  account/
    account.ts                - getMe()
    account.test.ts
  auth/
    auth.ts                   - TractiveAuth type + createAuthContext(): token cache, login flow
    auth.test.ts
  client/
    client.ts                 - createTractiveClient(config): composition root
    client.test.ts
  commands/
    commands.ts                - setTrackerLED(), setTrackerBuzzer(), setTrackerLiveTracking()
    commands.test.ts
  errors/
    errors.ts                  - NotAuthenticatedError, TrackerNotFoundError (shared, not owned by any one domain)
  geocode/
    geocode.ts                 - reverseGeocode()
    geocode.test.ts
  hardware/
    hardware.ts                - getTrackerHardware(), getTrackerBattery(), stale-hardware fallback cache
    hardware.test.ts
  http/
    http.ts                    - TractiveContext: get()/post() request client every domain module uses
    http.test.ts
  location/
    location.ts                - getTrackerLocation(), getTrackerPositionRange(), getTrackerPositionHistory(), stale-location fallback cache
    location.test.ts
  trackers/
    trackers.ts                 - getTrackers(), getTrackerDetails(), getTrackerGeofences()
    trackers.test.ts
  test/
    test-support.ts             - fakeContext()/fakeAuth() shared by every domain's tests

http.ts's TractiveContext is built on native fetch (no HTTP library dependency) and handles query-param building, JSON parsing, and throwing on non-2xx responses in one place, so no domain module talks to fetch directly. auth.ts's createAuthContext() is what every other domain depends on for requireAccessToken(). errors.ts and test/test-support.ts are the two files that don't belong to a single domain - they're cross-cutting, used by several folders, which is why they get their own folders rather than living inside one domain's.

Cross-domain imports go up and back down (../auth/auth.js, ../errors/errors.js, etc.); within a domain, the module imports its own test support with a plain ./ (e.g. auth.test.ts imports createAuthContext from ./auth.js).

  • dist/ - build output (gitignored, regenerated by npm run build / the prepare script), mirroring this same folder structure (dist/auth/auth.js, etc.) except test/ and every *.test.ts, both excluded via tsconfig.build.json. This is what package.json's main/types/exports point to.
  • tsconfig.json / tsconfig.build.json - two configs: the base one is what tsc --noEmit uses for typechecking (sees everything, including tests); .build.json extends it excluding tests (src/**/*.test.ts, src/test/**) - that's what actually ships to dist/. Tests don't go through either config at runtime - see "Testing" above for why tsx runs them directly instead.

Each domain module is a small factory - createTrackersApi(ctx, auth), createLocationApi(ctx, auth), etc. - taking the shared HTTP context and auth context as its only dependencies, and returning just the methods for that domain. createTractiveClient is the only place that knows about all of them at once; adding a new domain (say, geofence management) means adding one new folder and one line in client/client.ts, not touching anything else. The public API - every method name on the object createTractiveClient() returns - is unchanged by this reorganization.

Endpoints discovered but not wired in

Found while reverse-engineering the dashboard's network traffic and JS bundle, deliberately left out:

  • GET /user/{userId} (full profile - phone, home address; getMe() gives a curated subset instead)
  • GET /user/{userId}/subscriptions, /invoices, /shop_orders, /payment/roaming, /user/{userId}/shares, /share/{id} - billing/sharing data, sensitive, no real use case for a tracker client
  • GET /user/push_notification_settings, /pull-notifications - low value
  • GET /pet/{petId} and GET /weight_activity_history/{petId} - real pet profile/weight data confirmed working, but there's no known way to list pet IDs from the account or tracker (/user/{id}/pets, /tracker/{id}/pet are both dead ends); the pet ID we have was read manually out of a dashboard URL