API Reference
GuestNetworks REST API — v1. All endpoints are served from https://api.guestnetworks.com.
Authorization: Bearer gn_live_sk_xxxxxxxxxxxxxxxxxxxxRequired on all endpoints except /health and /api/status. Obtain your key from the Dashboard.
Rate Limits
| Endpoint | Limit |
|---|---|
POST /ingest | 100 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/telemetry | 1,000 req / min per operator |
GET /api/dashboard/* | 1,000 req / min per operator |
WS /ws | 10 concurrent connections per operator |
/health, /api/status | Unlimited (public) |
Rate limits are enforced per operator via Upstash Redis. Exceeded requests return 429 RATE_LIMITED with a Retry-After header.
Endpoints
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.
{
"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"
}
]
}// 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 -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 }
}
]
}'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}`);Create a new operator account. Admin-only — requires the X-Admin-Key header. Returns the API key once; it is never retrievable again.
{
"name": "string — operator or company name",
"email": "string — contact email",
"plan": "'free' | 'starter' | 'pro' (default: 'free')"
}// 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 -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"
}'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();List all venues belonging to the authenticated operator. Includes zone count and last event timestamp per venue. Supports cursor-based pagination.
// Query parameters ?limit=50 // default 50, max 200 ?offset=0 // default 0
// 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 https://api.guestnetworks.com/api/venues?limit=10 \ -H "Authorization: Bearer gn_live_sk_xxxx"
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();Create a new venue under the authenticated operator. Timezone must be a valid IANA tz string (e.g. "America/New_York").
{
"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"
}// 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 -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"
}'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();Retrieve a single venue by ID, including its zones array and integration list.
// 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 https://api.guestnetworks.com/api/venues/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer gn_live_sk_xxxx"
const res = await fetch(`https://api.guestnetworks.com/api/venues/${venueId}`, {
headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});
const venue = await res.json();Partially update a venue. Accepts any subset of mutable fields. operator_id cannot be changed.
// 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"
}// 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 -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)" }'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)' }),
});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.
// 200 OK
{
"id": "uuid",
"is_active": false,
"deleted_at":"2026-03-19T14:00:00.000Z"
}curl -X DELETE https://api.guestnetworks.com/api/venues/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer gn_live_sk_xxxx"
await fetch(`https://api.guestnetworks.com/api/venues/${venueId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${process.env.GN_API_KEY}` },
});List all zones for a venue. The full ownership chain (zone → venue → operator) is validated.
// 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 https://api.guestnetworks.com/api/venues/a1b2c3d4-e5f6-7890-abcd-ef1234567890/zones \ -H "Authorization: Bearer gn_live_sk_xxxx"
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();Create a new zone within a venue. Zones are sub-areas used to segment occupancy and flow data.
{
"slug": "string — URL-safe identifier, e.g. 'main-floor'",
"name": "string — display name",
"type": "'area' | 'entrance' | 'exit' | 'counter'",
"capacity": "number — optional, max occupancy"
}// 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 -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
}'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.
// 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 https://api.guestnetworks.com/api/venues/a1b2c3d4-e5f6-7890-abcd-ef1234567890/integrations \ -H "Authorization: Bearer gn_live_sk_xxxx"
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.
// 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.// 201 Created
{
"id": "uuid",
"venue_id": "uuid",
"connector_type": "meraki",
"is_active": true,
"created_at": "2026-03-19T12:00:00.000Z"
}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"
}
}'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' },
}),
});Health check endpoint — no authentication required. Returns database and Redis latency, plus aggregate platform stats. Used by uptime monitors.
// 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 https://api.guestnetworks.com/health
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`);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.
// 200 OK
{
"status": "operational",
"eventsPerMin": 340,
"venueCount": 187,
"uptime": 86400,
"checkedAt": "2026-03-19T12:00:00.000Z"
}curl https://api.guestnetworks.com/api/status
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.
// 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)
// 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 "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"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 }, ...]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.
// 200 OK
{
"venueCount": 12,
"events24h": 8420,
"activeIntegrations": 7,
"avgDwellSeconds": 1847
}curl https://api.guestnetworks.com/api/dashboard/stats \ -H "Authorization: Bearer gn_live_sk_xxxx"
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();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.
// Query parameters ?range=7d // '24h' | '7d' | '30d' | '90d' (default '7d')
// 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 "https://api.guestnetworks.com/api/dashboard/events?range=7d" \ -H "Authorization: Bearer gn_live_sk_xxxx"
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.
// 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 https://api.guestnetworks.com/api/dashboard/connectors \ -H "Authorization: Bearer gn_live_sk_xxxx"
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 }, ...]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).
// 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
// 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 "https://api.guestnetworks.com/api/alarms?status=ACTIVE_UNACK&severity=CRITICAL&limit=10" \ -H "Authorization: Bearer gn_live_sk_xxxx"
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();Acknowledge an active alarm. Transitions ACTIVE_UNACK to ACTIVE_ACK, or CLEARED_UNACK to CLEARED_ACK. Returns 409 if the alarm is already acknowledged.
// 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 -X POST https://api.guestnetworks.com/api/alarms/${ALARM_ID}/ack \
-H "Authorization: Bearer gn_live_sk_xxxx"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();Clear an active alarm. Transitions ACTIVE_UNACK to CLEARED_UNACK, or ACTIVE_ACK to CLEARED_ACK. Returns 409 if the alarm is already cleared.
// 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 -X POST https://api.guestnetworks.com/api/alarms/${ALARM_ID}/clear \
-H "Authorization: Bearer gn_live_sk_xxxx"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.
// Query parameters ?range=30d // '7d' | '30d' | '90d' (default '30d')
// 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 "https://api.guestnetworks.com/api/venues/${VENUE_ID}/visitors/scoring?range=30d" \
-H "Authorization: Bearer gn_live_sk_xxxx"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 }, ...]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.
// Query parameters ?range=7d // '24h' | '7d' | '30d' | '90d' (default '7d')
// 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 "https://api.guestnetworks.com/api/venues/${VENUE_ID}/dwell?range=7d" \
-H "Authorization: Bearer gn_live_sk_xxxx"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.
// Query parameters — both required ?dayOfWeek=1 // 0 (Sunday) through 6 (Saturday) &hour=14 // 0 through 23
// 200 OK
{
"dayOfWeek": 1,
"hour": 14,
"mean": 67.25,
"p25": 48.0,
"p75": 82.5,
"p95": 108.0,
"sampleSize": 4
}curl "https://api.guestnetworks.com/api/venues/${VENUE_ID}/predictions/occupancy?dayOfWeek=1&hour=14" \
-H "Authorization: Bearer gn_live_sk_xxxx"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();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.
// 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>" }
]
}// 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"
}
}# 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"]}]}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 }
| Code | Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid API key in Authorization header. |
FORBIDDEN | 403 | Valid key but you do not own the requested resource. |
NOT_FOUND | 404 | Resource does not exist or has been soft-deleted. |
VALIDATION_ERROR | 400 | Request body or query params failed Zod validation. See details array. |
RATE_LIMITED | 429 | Too many requests. Back off and retry after the Retry-After header value. |
INTERNAL_ERROR | 500 | Unexpected server error. Retryable after a short delay. |
@gn/sdk — TypeScript Client
The official typed SDK wraps every endpoint, handles auth, and provides retry logic. Works in Node.js and the browser.
npm install @gn/sdk
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`);