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

@airqo-packages/network-coverage

v2.0.0

Published

Node.js client for the AirQo Network Coverage API — typed wrappers for all monitor and registry endpoints

Downloads

436

Readme

@airqo-packages/network-coverage

Node.js client for the AirQo Network Coverage API — typed wrappers for all monitor and registry endpoints that power the "Where We Monitor" map.

Requires Node.js ≥ 18 (uses native fetch). No runtime dependencies.


Installation

npm install @airqo-packages/network-coverage

Migrating from an earlier version? Replace @airqo/network-coverage with @airqo-packages/network-coverage in your package.json and all import statements.


Authentication

To call protected endpoints (registry write operations), you need an AirQo access token.

  1. Create an account at analytics.airqo.net
  2. Go to Account Settings → API tab → Register a client
  3. Generate an access token from the registered client
  4. Pass the token to the client constructor — it will be appended as ?token= on every request automatically
const client = new NetworkCoverageClient({
  token: "YOUR_ACCESS_TOKEN",
});

Public endpoints (list, getMonitor, getCountryMonitors, exportCsv) do not require a token. Only upsertRegistry and deleteRegistry require authentication.


Quick start

import { NetworkCoverageClient } from "@airqo-packages/network-coverage";

const client = new NetworkCoverageClient({
  baseUrl: "https://api.airqo.net/api/v2/devices",
  defaultTenant: "airqo",
});

// List all monitors grouped by country
const data = await client.list();
console.log(data.meta);      // { total, active, inactive, countries }
console.log(data.countries); // CountrySummary[]
console.log(data.monitors);  // MonitorListItem[]

Configuration

const client = new NetworkCoverageClient({
  /** Base API URL — no trailing slash (default: AirQo production) */
  baseUrl: "https://api.airqo.net/api/v2/devices",

  /** Default tenant for all requests (default: "airqo") */
  defaultTenant: "airqo",

  /** Request timeout in ms (default: 30 000) */
  timeoutMs: 15_000,

  /** AirQo access token — appended as ?token= on every request */
  token: "YOUR_ACCESS_TOKEN",

  /** Additional headers sent with every request */
  headers: {
    "X-Custom-Header": "value",
  },
});

API

client.list(params?)

Returns all monitors and per-country aggregates.

const result = await client.list({
  search: "kampala",         // partial name / city / country match
  activeOnly: true,          // only active monitors
  types: ["Reference", "LCS"], // filter by type
});

Returns: NetworkCoverageListResponse

{
  success: true,
  message: "...",
  meta: { total: 120, active: 98, inactive: 22, countries: 11 },
  countries: [
    { countryId: "uganda", country: "Uganda", iso2: "UG", total: 45, active: 40, inactive: 5 },
    // ...
  ],
  monitors: [ /* MonitorListItem[] */ ],
}

client.getMonitor(monitorId, options?)

Fetches a single monitor by its _id.

const result = await client.getMonitor("64a1f2b3c4d5e6f7a8b9c0d1");
// or with a tenant override (public endpoint — no token needed):
const resultWithTenant = await client.getMonitor("64a1f2b3c4d5e6f7a8b9c0d1", { tenant: "airqo" });
console.log(result.data); // MonitorListItem

client.getCountryMonitors(countryId, params?)

Returns all monitors for a country identified by its URL slug.

const result = await client.getCountryMonitors("cote-divoire", {
  activeOnly: true,
});
console.log(result.monitors); // MonitorListItem[]

Country slugs are lowercase, accent-normalised, and hyphenated:

| Country | Slug | |---|---| | Uganda | uganda | | Ghana | ghana | | Côte d'Ivoire | cote-divoire | | South Africa | south-africa |


client.exportCsv(params?)

Returns the raw CSV string (UTF-8 with BOM). Write it to a file or stream it.

import fs from "fs";

const csv = await client.exportCsv({ countryId: "kenya", activeOnly: true });
fs.writeFileSync("kenya-monitors.csv", csv);

client.upsertRegistry(payload)

Creates or updates a registry entry. Returns 201 for new records, 200 for updates.

Shape A — AirQo-site enrichment (provide site_id):

await client.upsertRegistry({
  site_id: "64a1f2b3c4d5e6f7a8b9c0d1",
  equipment: "AirQo Binos",
  calibrationMethod: "Colocation",
  uptime30d: "96%",
  publicData: "Yes",
});

Shape B — Standalone external entry (omit site_id):

await client.upsertRegistry({
  name: "Makerere KCCA Station",
  country: "Uganda",
  city: "Kampala",
  latitude: 0.3476,
  longitude: 32.5825,
  type: "Reference",
  network: "KCCA",
  operator: "Kampala Capital City Authority",
  pollutants: ["PM2.5", "PM10", "NO2"],
});

client.deleteRegistry(registryId, options?)

Removes a registry entry by document _id. Requires a valid access token — ensure the client was constructed with one or pass it explicitly via options.token.

// client must have been constructed with a token:
// const client = new NetworkCoverageClient({ token: "YOUR_TOKEN" });
await client.deleteRegistry("64a1f2b3c4d5e6f7a8b9c0d1");
// or pass token explicitly per-call:
await client.deleteRegistry("64a1f2b3c4d5e6f7a8b9c0d1", { token: "YOUR_TOKEN" });

Error handling

All methods throw NetworkCoverageError on non-2xx responses or timeouts.

import { NetworkCoverageClient, NetworkCoverageError } from "@airqo-packages/network-coverage";

try {
  await client.getMonitor("nonexistent-id");
} catch (err) {
  if (err instanceof NetworkCoverageError) {
    console.error(err.message);    // API error message
    console.error(err.statusCode); // HTTP status code (e.g. 404)
    console.error(err.body);       // Raw response body
  }
}

| Status | Meaning | |--------|---------| | 400 | Validation error — check your payload | | 404 | Monitor or registry entry not found | | 409 | Duplicate registry entry for a site | | 408 | Request timed out (client-side) | | 500 | Internal server error |


TypeScript types

All types are exported from the package root:

import type {
  MonitorListItem,
  CountrySummary,
  NetworkCoverageMeta,
  NetworkCoverageListResponse,
  MonitorDetailResponse,
  CountryMonitorsResponse,
  RegistryUpsertPayload,
  RegistryUpsertResponse,
  MonitorType,
  MonitorStatus,
  MonitorSource,
  ListParams,
  ExportCsvParams,
  NetworkCoverageClientOptions,
} from "@airqo-packages/network-coverage";

License

MIT