REST API — v1

API Reference

GuestNetworks REST API — v1. All endpoints are served from https://api.guestnetworks.com.

AUTHENTICATION
Authorization: Bearer gn_live_sk_xxxxxxxxxxxxxxxxxxxx

Required on all endpoints except /health and /api/status. Obtain your key from the Dashboard.

Rate Limits

EndpointLimit
POST /ingest100 req / min per operator
GET /api/*1,000 req / min per operator
POST /api/*1,000 req / min per operator
DELETE /api/*1,000 req / min per operator
GET /api/venues/:id/telemetry1,000 req / min per operator
GET /api/dashboard/*1,000 req / min per operator
WS /ws10 concurrent connections per operator
/health, /api/statusUnlimited (public)

Rate limits are enforced per operator via Upstash Redis. Exceeded requests return 429 RATE_LIMITED with a Retry-After header.

Endpoints

POST/ingestBearer token

Ingest raw events from a hardware connector. Accepts up to 1,000 events per request. Events are written to the TimescaleDB hypertable and fanned out to the Redis stream.

Request Schema
{
  "venue_id":       "uuid — must belong to authenticated operator",
  "connector_type": "'meraki' | 'aruba' | 'unifi' | 'mywifi' | 'square' | 'density' | 'verkada'",
  "events": [
    {
      "type":      "string — e.g. 'device_seen' | 'entry' | 'exit'",
      "timestamp": "ISO 8601 string",
      "payload":   "object — connector-specific fields"
    }
  ]
}
Response Example
// 202 Accepted
{
  "accepted": 10,
  "rejected": 0,
  "errors":   []
}

// Partial rejection
{
  "accepted": 8,
  "rejected": 2,
  "errors": [
    { "index": 3, "reason": "timestamp must be ISO 8601" },
    { "index": 7, "reason": "unknown event type" }
  ]
}
cURL
curl -X POST https://api.guestnetworks.com/ingest \
  -H "Authorization: Bearer gn_live_sk_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "venue_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "connector_type": "meraki",
    "events": [
      {
        "type": "device_seen",
        "timestamp": "2026-03-19T12:00:00.000Z",
        "payload": { "mac": "aa:bb:cc:dd:ee:ff", "rssi": -65 }
      }
    ]
  }'
JavaScript
import { GuestNetworksClient } from '@gn/sdk';

const client = new GuestNetworksClient({ apiKey: process.env.GN_API_KEY });

const result = await client.ingest({
  venueId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  connectorType: 'meraki',
  events: [
    {
      type: 'device_seen',
      timestamp: new Date().toISOString(),
      payload: { mac: 'aa:bb:cc:dd:ee:ff', rssi: -65 },
    },
  ],
});

console.log(`Accepted: ${result.accepted}`);
POST/api/operatorsX-Admin-Key

Create a new operator account. Admin-only — requires the X-Admin-Key header. Returns the API key once; it is never retrievable again.

Request Schema
{
  "name":  "string — operator or company name",
  "email": "string — contact email",
  "plan":  "'free' | 'starter' | 'pro'  (default: 'free')"
}
Response Example
// 201 Created
{
  "operator_id":    "uuid",
  "api_key":        "gn_live_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "api_key_prefix": "gn_live_"
}

// Store api_key immediately — it is shown only once.
cURL
curl -X POST https://api.guestnetworks.com/api/operators \
  -H "X-Admin-Key: ${GN_ADMIN_SECRET}" \
  -H "Content-Type: application/json" \
  -d '{
    "name":  "Acme Hotels",
    "email": "ops@acme.com",
    "plan":  "starter"
  }'
JavaScript
const res = await fetch('https://api.guestnetworks.com/api/operators', {
  method: 'POST',
  headers: {
    'X-Admin-Key': process.env.GN_ADMIN_SECRET,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Acme Hotels',
    email: 'ops@acme.com',
    plan: 'starter',
  }),
});
const { operator_id, api_key } = await res.json();
GET/api/venuesBearer token

List all venues belonging to the authenticated operator. Includes zone count and last event timestamp per venue. Supports cursor-based pagination.

Request Schema
// Query parameters
?limit=50   // default 50, max 200
?offset=0   // default 0
Response Example
// 200 OK
{
  "data": [
    {
      "id":               "uuid",
      "name":             "Downtown Café",
      "timezone":         "America/Chicago",
      "address":          "123 Main St, Chicago, IL",
      "is_active":        true,
      "zone_count":       4,
      "last_event_at":    "2026-03-19T11:55:00.000Z",
      "created_at":       "2026-01-15T08:00:00.000Z"
    }
  ],
  "total":  12,
  "limit":  50,
  "offset": 0
}
cURL
curl https://api.guestnetworks.com/api/venues?limit=10 \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
const res = await fetch('https://api.guestnetworks.com/api/venues?limit=10', {
  headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
const { data: venues, total } = await res.json();
POST/api/venuesBearer token

Create a new venue under the authenticated operator. Timezone must be a valid IANA tz string (e.g. "America/New_York").

Request Schema
{
  "name":          "string — required",
  "timezone":      "IANA timezone string — required, e.g. 'America/New_York'",
  "address":       "string — optional",
  "lat":           "number — optional, WGS84 latitude",
  "lng":           "number — optional, WGS84 longitude",
  "hardware_type": "'meraki' | 'aruba' | 'unifi' | 'mywifi' | ...  optional"
}
Response Example
// 201 Created
{
  "id":          "uuid",
  "name":        "Lobby Bar",
  "timezone":    "America/New_York",
  "is_active":   true,
  "operator_id": "uuid",
  "created_at":  "2026-03-19T12:00:00.000Z"
}
cURL
curl -X POST https://api.guestnetworks.com/api/venues \
  -H "Authorization: Bearer gn_live_sk_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name":     "Lobby Bar",
    "timezone": "America/New_York",
    "address":  "50 Park Ave, New York, NY"
  }'
JavaScript
const res = await fetch('https://api.guestnetworks.com/api/venues', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.GN_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Lobby Bar',
    timezone: 'America/New_York',
  }),
});
const venue = await res.json();
GET/api/venues/:idBearer token

Retrieve a single venue by ID, including its zones array and integration list.

Response Example
// 200 OK
{
  "id":          "uuid",
  "name":        "Lobby Bar",
  "timezone":    "America/New_York",
  "address":     "50 Park Ave, New York, NY",
  "lat":         40.7517,
  "lng":         -73.9754,
  "is_active":   true,
  "zones": [
    { "id": "uuid", "slug": "main-floor", "name": "Main Floor", "type": "area", "capacity": 120 }
  ],
  "integrations": [
    { "id": "uuid", "connector_type": "meraki", "is_active": true, "created_at": "..." }
  ],
  "created_at":  "2026-03-19T12:00:00.000Z"
}
cURL
curl https://api.guestnetworks.com/api/venues/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
const res = await fetch(`https://api.guestnetworks.com/api/venues/${venueId}`, {
  headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
const venue = await res.json();
PUT/api/venues/:idBearer token

Partially update a venue. Accepts any subset of mutable fields. operator_id cannot be changed.

Request Schema
// All fields optional — send only what you want to change
{
  "name":          "string",
  "timezone":      "IANA timezone string",
  "address":       "string",
  "lat":           "number",
  "lng":           "number",
  "hardware_type": "string"
}
Response Example
// 200 OK — full updated venue object
{
  "id":        "uuid",
  "name":      "Lobby Bar (Updated)",
  "timezone":  "America/Chicago",
  "is_active": true,
  "updated_at":"2026-03-19T13:00:00.000Z"
}
cURL
curl -X PUT https://api.guestnetworks.com/api/venues/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "Authorization: Bearer gn_live_sk_xxxx" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Lobby Bar (Renovated)" }'
JavaScript
const res = await fetch(`https://api.guestnetworks.com/api/venues/${venueId}`, {
  method: 'PUT',
  headers: {
    'Authorization': `Bearer ${process.env.GN_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name: 'Lobby Bar (Renovated)' }),
});
DELETE/api/venues/:idBearer token

Soft-delete a venue by setting is_active = false. No data is destroyed. The venue will no longer appear in list results but historical data is preserved.

Response Example
// 200 OK
{
  "id":        "uuid",
  "is_active": false,
  "deleted_at":"2026-03-19T14:00:00.000Z"
}
cURL
curl -X DELETE https://api.guestnetworks.com/api/venues/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
await fetch(`https://api.guestnetworks.com/api/venues/${venueId}`, {
  method: 'DELETE',
  headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
GET/api/venues/:id/zonesBearer token

List all zones for a venue. The full ownership chain (zone → venue → operator) is validated.

Response Example
// 200 OK
{
  "data": [
    {
      "id":       "uuid",
      "slug":     "main-floor",
      "name":     "Main Floor",
      "type":     "area",
      "capacity": 120,
      "is_active":true
    },
    {
      "id":       "uuid",
      "slug":     "entrance",
      "name":     "Front Entrance",
      "type":     "entrance",
      "capacity": null,
      "is_active":true
    }
  ],
  "total": 2
}
cURL
curl https://api.guestnetworks.com/api/venues/a1b2c3d4-e5f6-7890-abcd-ef1234567890/zones \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
const res = await fetch(`https://api.guestnetworks.com/api/venues/${venueId}/zones`, {
  headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
const { data: zones } = await res.json();
POST/api/venues/:id/zonesBearer token

Create a new zone within a venue. Zones are sub-areas used to segment occupancy and flow data.

Request Schema
{
  "slug":     "string — URL-safe identifier, e.g. 'main-floor'",
  "name":     "string — display name",
  "type":     "'area' | 'entrance' | 'exit' | 'counter'",
  "capacity": "number — optional, max occupancy"
}
Response Example
// 201 Created
{
  "id":        "uuid",
  "venue_id":  "uuid",
  "slug":      "main-floor",
  "name":      "Main Floor",
  "type":      "area",
  "capacity":  120,
  "is_active": true,
  "created_at":"2026-03-19T12:00:00.000Z"
}
cURL
curl -X POST https://api.guestnetworks.com/api/venues/a1b2c3d4-e5f6-7890-abcd-ef1234567890/zones \
  -H "Authorization: Bearer gn_live_sk_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "slug":     "main-floor",
    "name":     "Main Floor",
    "type":     "area",
    "capacity": 120
  }'
JavaScript
const res = await fetch(`https://api.guestnetworks.com/api/venues/${venueId}/zones`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.GN_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    slug: 'main-floor',
    name: 'Main Floor',
    type: 'area',
    capacity: 120,
  }),
});

List all hardware integrations connected to a venue. Sensitive config fields (API keys, passwords) are redacted in the response.

Response Example
// 200 OK
{
  "data": [
    {
      "id":             "uuid",
      "venue_id":       "uuid",
      "connector_type": "meraki",
      "is_active":      true,
      "config": {
        "api_key":        "gn_redacted",
        "org_id":         "123456",
        "webhook_secret": "gn_redacted"
      },
      "created_at":     "2026-01-15T08:00:00.000Z"
    }
  ],
  "total": 1
}
cURL
curl https://api.guestnetworks.com/api/venues/a1b2c3d4-e5f6-7890-abcd-ef1234567890/integrations \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
const res = await fetch(`https://api.guestnetworks.com/api/venues/${venueId}/integrations`, {
  headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
const { data: integrations } = await res.json();

Connect a hardware system to a venue. Config is validated against the connector-specific schema and sensitive fields are encrypted at rest using AES-256-GCM.

Request Schema
// Discriminated union on connector_type

// Meraki
{
  "connector_type": "meraki",
  "config": {
    "api_key":        "string — required",
    "org_id":         "string — required",
    "webhook_secret": "string — optional"
  }
}

// UniFi
{
  "connector_type": "unifi",
  "config": {
    "controller_url": "https://... — required",
    "username":       "string — required",
    "password":       "string — required",
    "site":           "string — optional, default 'default'"
  }
}

// Aruba / MyWiFi / Square / Density / Verkada — similar structure.
// See /docs/connectors for per-connector field reference.
Response Example
// 201 Created
{
  "id":             "uuid",
  "venue_id":       "uuid",
  "connector_type": "meraki",
  "is_active":      true,
  "created_at":     "2026-03-19T12:00:00.000Z"
}
cURL
curl -X POST https://api.guestnetworks.com/api/venues/a1b2c3d4-e5f6-7890-abcd-ef1234567890/integrations \
  -H "Authorization: Bearer gn_live_sk_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "connector_type": "meraki",
    "config": {
      "api_key": "your-meraki-api-key",
      "org_id":  "123456"
    }
  }'
JavaScript
const res = await fetch(`https://api.guestnetworks.com/api/venues/${venueId}/integrations`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.GN_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    connector_type: 'meraki',
    config: { api_key: process.env.MERAKI_API_KEY, org_id: '123456' },
  }),
});
GET/healthPublic

Health check endpoint — no authentication required. Returns database and Redis latency, plus aggregate platform stats. Used by uptime monitors.

Response Example
// 200 OK
{
  "status": "healthy",
  "db":    { "connected": true, "latencyMs": 12 },
  "redis": { "connected": true, "latencyMs": 3 },
  "stats": {
    "lastEventAt":    "2026-03-19T11:59:50.000Z",
    "operatorCount":  42,
    "venueCount":     187,
    "eventsPerMin":   340
  },
  "uptime": 86400
}
cURL
curl https://api.guestnetworks.com/health
JavaScript
const res = await fetch('https://api.guestnetworks.com/health');
const { status, db, redis, stats } = await res.json();
console.log(`DB latency: ${db.latencyMs}ms, Redis: ${redis.latencyMs}ms`);
GET/api/statusPublic

Public platform status endpoint, safe to embed in status pages. Subset of /health — no infrastructure internals exposed. Auto-refreshed every 30s on the GuestNetworks status page.

Response Example
// 200 OK
{
  "status":       "operational",
  "eventsPerMin": 340,
  "venueCount":   187,
  "uptime":       86400,
  "checkedAt":    "2026-03-19T12:00:00.000Z"
}
cURL
curl https://api.guestnetworks.com/api/status
JavaScript
const res = await fetch('https://api.guestnetworks.com/api/status');
const { status, eventsPerMin } = await res.json();

Query time-series telemetry data for a venue. Automatically selects the best aggregate table (1-minute, 1-hour, or 1-day) based on the requested interval. Supports multiple keys in a single request.

Request Schema
// Query parameters — all required except interval and agg
?keys=occupancy,temperature,co2   // comma-separated: occupancy | temperature | co2 | humidity | noise | entries | exits | devices
&startTs=1710806400000             // epoch milliseconds — start of range
&endTs=1710892800000               // epoch milliseconds — end of range
&interval=3600000                  // bucket size in ms (default 3600000 = 1hr). <=60s → 1min table, <=1hr → 1hr table, else 1day
&agg=AVG                           // aggregation: AVG | MAX | MIN | SUM | COUNT (default AVG)
Response Example
// 200 OK — keyed by telemetry key, each an array of { ts, value }
{
  "occupancy": [
    { "ts": 1710806400000, "value": 42 },
    { "ts": 1710810000000, "value": 58 },
    { "ts": 1710813600000, "value": 35 }
  ],
  "temperature": [
    { "ts": 1710806400000, "value": 22.3 },
    { "ts": 1710810000000, "value": 23.1 },
    { "ts": 1710813600000, "value": 21.8 }
  ]
}
cURL
curl "https://api.guestnetworks.com/api/venues/${VENUE_ID}/telemetry?keys=occupancy,co2&startTs=1710806400000&endTs=1710892800000&interval=3600000&agg=AVG" \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
const params = new URLSearchParams({
  keys: 'occupancy,temperature',
  startTs: String(Date.now() - 86400000),
  endTs: String(Date.now()),
  interval: '3600000',
  agg: 'AVG',
});

const res = await fetch(`https://api.guestnetworks.com/api/venues/${venueId}/telemetry?${params}`, {
  headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
const data = await res.json();
// data.occupancy → [{ ts, value }, ...]
GET/api/dashboard/statsBearer token

Aggregated KPI summary for the authenticated operator. Returns venue count, 24-hour event count, active integration count, and average dwell time. Powers the dashboard overview cards.

Response Example
// 200 OK
{
  "venueCount":          12,
  "events24h":           8420,
  "activeIntegrations":  7,
  "avgDwellSeconds":     1847
}
cURL
curl https://api.guestnetworks.com/api/dashboard/stats \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
const res = await fetch('https://api.guestnetworks.com/api/dashboard/stats', {
  headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
const { venueCount, events24h, activeIntegrations, avgDwellSeconds } = await res.json();
GET/api/dashboard/eventsBearer token

Time-series event data for dashboard charts. Returns bucketed event counts, entries, and exits across all operator venues. Uses 1-hour buckets for 24h range and 1-day buckets for longer ranges.

Request Schema
// Query parameters
?range=7d   // '24h' | '7d' | '30d' | '90d'  (default '7d')
Response Example
// 200 OK
{
  "events": [
    { "ts": 1710720000000, "events": 1240, "entries": 680, "exits": 560 },
    { "ts": 1710806400000, "events": 1580, "entries": 820, "exits": 760 },
    { "ts": 1710892800000, "events": 950,  "entries": 510, "exits": 440 }
  ],
  "range": "7d"
}
cURL
curl "https://api.guestnetworks.com/api/dashboard/events?range=7d" \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
const res = await fetch('https://api.guestnetworks.com/api/dashboard/events?range=7d', {
  headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
const { events, range } = await res.json();
// events → [{ ts, events, entries, exits }, ...]

Connector health summary across all operator venues. Groups active integrations by type and health status with last event timestamp. Used to render the connector status grid on the dashboard.

Response Example
// 200 OK
{
  "connectors": [
    { "type": "meraki",  "status": "healthy",  "count": 5, "lastEventAt": "2026-03-20T11:55:00.000Z" },
    { "type": "unifi",   "status": "healthy",  "count": 3, "lastEventAt": "2026-03-20T11:50:00.000Z" },
    { "type": "density", "status": "degraded", "count": 1, "lastEventAt": "2026-03-20T10:30:00.000Z" }
  ]
}
cURL
curl https://api.guestnetworks.com/api/dashboard/connectors \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
const res = await fetch('https://api.guestnetworks.com/api/dashboard/connectors', {
  headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
const { connectors } = await res.json();
// connectors → [{ type, status, count, lastEventAt }, ...]
GET/api/alarmsBearer token

List alarms for the authenticated operator. Supports filtering by status, severity, and venue. Paginated with limit/offset. Alarms follow a state machine: ACTIVE_UNACK -> ACTIVE_ACK -> CLEARED_ACK (or ACTIVE_UNACK -> CLEARED_UNACK -> CLEARED_ACK).

Request Schema
// Query parameters — all optional
?status=ACTIVE_UNACK     // 'ACTIVE_UNACK' | 'ACTIVE_ACK' | 'CLEARED_UNACK' | 'CLEARED_ACK'
&severity=CRITICAL        // 'CRITICAL' | 'MAJOR' | 'MINOR' | 'WARNING'
&venue_id=uuid            // filter by specific venue
&limit=50                 // default 50, max 200
&offset=0                 // default 0
Response Example
// 200 OK
{
  "alarms": [
    {
      "id":             "uuid",
      "venueId":        "uuid",
      "zoneId":         "uuid",
      "type":           "occupancy_threshold",
      "severity":       "CRITICAL",
      "status":         "ACTIVE_UNACK",
      "message":        "Lobby occupancy exceeded 95% capacity",
      "details":        { "current": 114, "capacity": 120, "threshold": 0.95 },
      "createdAt":      "2026-03-20T11:55:00.000Z",
      "acknowledgedAt": null,
      "clearedAt":      null
    }
  ],
  "total":  42,
  "limit":  50,
  "offset": 0
}
cURL
curl "https://api.guestnetworks.com/api/alarms?status=ACTIVE_UNACK&severity=CRITICAL&limit=10" \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
const res = await fetch('https://api.guestnetworks.com/api/alarms?status=ACTIVE_UNACK&limit=10', {
  headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
const { alarms, total } = await res.json();
POST/api/alarms/:id/ackBearer token

Acknowledge an active alarm. Transitions ACTIVE_UNACK to ACTIVE_ACK, or CLEARED_UNACK to CLEARED_ACK. Returns 409 if the alarm is already acknowledged.

Response Example
// 200 OK
{
  "id":             "uuid",
  "status":         "ACTIVE_ACK",
  "acknowledgedAt": "2026-03-20T12:05:00.000Z"
}

// 409 Conflict
{
  "error": "Alarm already acknowledged (status: ACTIVE_ACK)",
  "code":  "ALREADY_ACKNOWLEDGED"
}
cURL
curl -X POST https://api.guestnetworks.com/api/alarms/${ALARM_ID}/ack \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
const res = await fetch(`https://api.guestnetworks.com/api/alarms/${alarmId}/ack`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
const { status, acknowledgedAt } = await res.json();
POST/api/alarms/:id/clearBearer token

Clear an active alarm. Transitions ACTIVE_UNACK to CLEARED_UNACK, or ACTIVE_ACK to CLEARED_ACK. Returns 409 if the alarm is already cleared.

Response Example
// 200 OK
{
  "id":        "uuid",
  "status":    "CLEARED_ACK",
  "clearedAt": "2026-03-20T12:10:00.000Z"
}

// 409 Conflict
{
  "error": "Alarm already cleared (status: CLEARED_ACK)",
  "code":  "ALREADY_CLEARED"
}
cURL
curl -X POST https://api.guestnetworks.com/api/alarms/${ALARM_ID}/clear \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
const res = await fetch(`https://api.guestnetworks.com/api/alarms/${alarmId}/clear`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
const { status, clearedAt } = await res.json();

Visitor loyalty segmentation for a venue. Groups unique devices into New (1 visit), Returning (2-3), Regular (4-7), and Loyal (8+) segments based on distinct visit days within the date range.

Request Schema
// Query parameters
?range=30d   // '7d' | '30d' | '90d'  (default '30d')
Response Example
// 200 OK
{
  "segments": [
    { "name": "New",       "count": 3420, "percentage": 62.15 },
    { "name": "Returning", "count": 1280, "percentage": 23.25 },
    { "name": "Regular",   "count": 580,  "percentage": 10.53 },
    { "name": "Loyal",     "count": 224,  "percentage": 4.07 }
  ],
  "total": 5504,
  "range": "30d"
}
cURL
curl "https://api.guestnetworks.com/api/venues/${VENUE_ID}/visitors/scoring?range=30d" \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
const res = await fetch(`https://api.guestnetworks.com/api/venues/${venueId}/visitors/scoring?range=30d`, {
  headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
const { segments, total } = await res.json();
// segments → [{ name: 'New', count: 3420, percentage: 62.15 }, ...]
GET/api/venues/:id/dwellBearer token

Dwell time distribution for a venue, broken down by zone. Returns histogram buckets (0-5m, 5-15m, 15-30m, 30-60m, 60m+) showing how long visitors stay in each area.

Request Schema
// Query parameters
?range=7d   // '24h' | '7d' | '30d' | '90d'  (default '7d')
Response Example
// 200 OK
{
  "zones": [
    {
      "zone_id":   "main-floor",
      "zone_name": "Main Floor",
      "buckets": [
        { "range": "0-5m",   "count": 1240 },
        { "range": "5-15m",  "count": 820 },
        { "range": "15-30m", "count": 340 },
        { "range": "30-60m", "count": 95 },
        { "range": "60m+",   "count": 22 }
      ]
    },
    {
      "zone_id":   "default",
      "zone_name": "Entire Venue",
      "buckets": [
        { "range": "0-5m",   "count": 2100 },
        { "range": "5-15m",  "count": 1560 },
        { "range": "15-30m", "count": 680 },
        { "range": "30-60m", "count": 210 },
        { "range": "60m+",   "count": 48 }
      ]
    }
  ]
}
cURL
curl "https://api.guestnetworks.com/api/venues/${VENUE_ID}/dwell?range=7d" \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
const res = await fetch(`https://api.guestnetworks.com/api/venues/${venueId}/dwell?range=7d`, {
  headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
const { zones } = await res.json();
// zones → [{ zone_id, zone_name, buckets: [{ range, count }] }]

Occupancy prediction for a specific day-of-week and hour. Uses the last 28 days of hourly aggregate data to compute mean, 25th, 75th, and 95th percentile occupancy values. Useful for staffing and capacity planning.

Request Schema
// Query parameters — both required
?dayOfWeek=1   // 0 (Sunday) through 6 (Saturday)
&hour=14       // 0 through 23
Response Example
// 200 OK
{
  "dayOfWeek":  1,
  "hour":       14,
  "mean":       67.25,
  "p25":        48.0,
  "p75":        82.5,
  "p95":        108.0,
  "sampleSize": 4
}
cURL
curl "https://api.guestnetworks.com/api/venues/${VENUE_ID}/predictions/occupancy?dayOfWeek=1&hour=14" \
  -H "Authorization: Bearer gn_live_sk_xxxx"
JavaScript
const res = await fetch(
  `https://api.guestnetworks.com/api/venues/${venueId}/predictions/occupancy?dayOfWeek=1&hour=14`,
  { headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` } },
);
const { mean, p25, p75, p95, sampleSize } = await res.json();
WS/wsBearer token

Real-time WebSocket event stream. Authenticates via query parameter token. Subscribe to venue-level event channels using ThingsBoard-compatible tsSubCmds/tsUnsubCmds protocol. Events are pushed from the Redis pub/sub bridge as they arrive.

Request Schema
// Connect with token as query param
wss://api.guestnetworks.com/ws?token=gn_live_sk_xxxx

// Subscribe to venue events (send as JSON)
{
  "tsSubCmds": [
    { "cmdId": 1, "entityId": "<venue-uuid>", "keys": ["occupancy", "entries"] }
  ]
}

// Unsubscribe
{
  "tsUnsubCmds": [
    { "entityId": "<venue-uuid>" }
  ]
}
Response Example
// Connection acknowledgment
{
  "type":       "connected",
  "operatorId": "uuid",
  "timestamp":  "2026-03-20T12:00:00.000Z"
}

// Incoming event data (pushed per subscription)
{
  "cmdId": 1,
  "data": {
    "venueId":   "uuid",
    "eventType": "presence",
    "metrics":   { "occupancy": 67, "rssi": -58 },
    "time":      "2026-03-20T12:00:05.000Z"
  }
}
cURL
# WebSocket connections require a WebSocket client
# Using websocat (https://github.com/vi/websocat):
websocat "wss://api.guestnetworks.com/ws?token=gn_live_sk_xxxx"

# Then send subscription:
{"tsSubCmds":[{"cmdId":1,"entityId":"<venue-uuid>","keys":["occupancy"]}]}
JavaScript
const ws = new WebSocket(
  `wss://api.guestnetworks.com/ws?token=${process.env.GN_API_KEY}`
);

ws.onopen = () => {
  ws.send(JSON.stringify({
    tsSubCmds: [{ cmdId: 1, entityId: venueId, keys: ['occupancy', 'entries'] }],
  }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === 'connected') {
    console.log(`Connected as ${msg.operatorId}`);
  } else {
    console.log(`[cmd ${msg.cmdId}] `, msg.data);
  }
};

Error Codes

All errors use a consistent envelope: { "error": string, "code": string, "details"?: unknown }

CodeStatusDescription
UNAUTHORIZED401Missing or invalid API key in Authorization header.
FORBIDDEN403Valid key but you do not own the requested resource.
NOT_FOUND404Resource does not exist or has been soft-deleted.
VALIDATION_ERROR400Request body or query params failed Zod validation. See details array.
RATE_LIMITED429Too many requests. Back off and retry after the Retry-After header value.
INTERNAL_ERROR500Unexpected server error. Retryable after a short delay.
SDK

@gn/sdk — TypeScript Client

The official typed SDK wraps every endpoint, handles auth, and provides retry logic. Works in Node.js and the browser.

Install
npm install @gn/sdk
Basic Usage
import { GuestNetworksClient } from '@gn/sdk';

const client = new GuestNetworksClient({
  apiKey: process.env.GN_API_KEY,   // gn_live_sk_...
  baseUrl: 'https://api.guestnetworks.com',           // optional, defaults to production
});

// List venues
const { data: venues } = await client.venues.list({ limit: 10 });

// Ingest events
await client.ingest({
  venueId: venues[0].id,
  connectorType: 'meraki',
  events: [
    { type: 'device_seen', timestamp: new Date().toISOString(), payload: { mac: 'aa:bb:cc:dd:ee:ff' } },
  ],
});

// Get platform health
const health = await client.health();
console.log(`Status: ${health.status}, DB: ${health.db.latencyMs}ms`);