JSON Exports API for WiFi data
Before you start exporting WiFi data, it’s critical to understand that in 2024, the vast majority of WiFi probe requests contain randomized MAC addresses.
Less than 10% of probe requests are not being randomized.
This is severly impacting the quality of passive WiFi analytics, regardless of your choice of network equipment or analytics provider.
We strongly recommend to interpret WiFi data as very high level trends and to invest in reliable, privacy respecting solutions like smart cameras.
While the GraphQL API exposes a number of interesting metrics, it wasn’t designed to export raw data efficiently.
This endpoint addresses this use case and allows exporting raw WiFi data.
The endpoint offers 3 variants:
- Version 1 Accepts batches of location IDs (or reference numbers) and rejects queries containing unauthorized IDs.
- Version 2 Accepts batches of location IDs (or reference numbers) and allows partial responses.
- Version 3 Implements a cursor approach. You only need to specify a brand ID and locations will be returned in batches automatically.
Authentication
The authentication workflow is the same as the GraphQL API. Using your client ID and client secret, the following HTTP query will return your token:
curl --request POST \
--url https://tokens.analytiks.ai \
--header 'content-type: application/json' \
--data '{"client_id":"__YOUR_CLIENT_ID__","client_secret":"__YOUR_CLIENT_SECRET__","audience":"https://gql.analytiks.ai","grant_type":"client_credentials"}'
On success the API call will return a structure like this one:
{"access_token":"YOUR_TOKEN","expires_in":86400,"token_type":"Bearer"}
Usage
Once you have your token, you can send POST
requests to https://exports.analytiks.ai/wifi/json/v1
.
We are expecting the following headers:
Content-Type
set toapplication/json
Authorization
set toBearer YOUR_TOKEN
Request body
Here’s an examplre request body.
{
"startDate": "2023-06-08T11:30:00.000Z",
"endDate": "2023-06-08T23:45:00.000Z",
"locations": [1, 2, 3]
}
Dates are ISO 8601 and can be sent either as UTC or local time. A few notes regarding dates:
- If dates are requested in UTC (eg.
2023-06-08T11:30:00.000Z
, note the finalZ
), dates in the response will also be UTC. - If dates are requested in local time (eg.
2023-06-08T09:00:00.000
, no finalZ
), dates in the response will also be local time. - Both
startDate
andendDate
must be in the same format. - A maximum of 5 days can be requested at once.
A maximum of 50 locations can be requested at once.
Use the GraphQL API to figure out what location IDs you can use in your queries.
In this version of the endpoint, requesting locations you are not allowed to access will cause the whole request to be rejected with a status 401.
A v2 of the endpoint that allows partial results is described at the bottom of the page.
Requests can also be made by reference numbers (which you can set to your internal IDs)
{
"startDate": "2023-06-08T11:30:00.000Z",
"endDate": "2023-06-08T23:45:00.000Z",
"brandId": 1234,
"referenceNumbers": ["US0658", "FR2561"]
}
The same rules apply, except here we expect a brand ID (which does not change) and a list of reference numbers.
In this mode, the response will contain a referenceNumber
number fields for each requested location.
Response body
The API can return data for the following metrics. All metrics are Maps from a UTC date to Natural numbers:
visits
: People who entered the location (excludesbounces
, see below).dwellTime
: How long people spent inside the location, in ms.bounces
: People who entered, but have not been observed long enough.allVisits
: All visitors who entered the location (visits
+bounces
).walkBys
: People who walked past the location, observed with a signal strength too weak to be counted as visitors.newVisitors
: People we’re seeing for the first time.returningVisitors
: People we’ve seen at least once before.under15Mins
: Visitors we’ve observed for less than 15 minutes.from15To30Mins
: Visitors we’ve observed for more than 15 minutes, but less than 30.from30To45Mins
: Visitors we’ve observed for more than 30 minutes, but less than 45.from45To60Mins
: Visitors we’ve observed for more than 45 minutes, but less than an hour.over60Mins
: Visitors we’ve observed for an hour or more.
Again, all dates are UTC. A typical response looks like this:
[
{
"locationId": 1,
"visits": {
"2023-06-08T08:00:00Z": 3,
"2023-06-08T09:00:00Z": 6
},
"dwellTime": {
"2023-06-08T14:00:00Z": 111111,
"2023-06-08T15:00:00Z": 111111
},
"newVisitors": {
"2023-06-08T08:00:00Z": 1,
"2023-06-08T09:00:00Z": 2
},
"returningVisitors": {
"2023-06-08T08:00:00Z": 2,
"2023-06-08T09:00:00Z": 4
},
"bounces": {
"2023-06-08T14:00:00Z": 3,
"2023-06-08T15:00:00Z": 9
},
"under15Mins": {
"2023-06-08T14:00:00Z": 10,
"2023-06-08T15:00:00Z": 12
},
"over60Mins": {
"2023-06-08T08:00:00Z": 1
},
}
]
Exports endpoint V2
A variant of the endpoint is available at https://exports.analytiks.ai/wifi/json/v2
.
In this version, requesting unauthorized locations by mistake will not cause the whole request to be rejected.
Instead, you’ll get a partial response. To achieve this, the structure of the response must be slightly different:
{
"successes": [...], // Same structure as in V1
"failures": {
"description": "You are not authorized to query these locations",
"locations": [__LOC_ID_1__, ...]
}
}
Some additional information:
- If all requested locations are authorized, the
failures
object will just be empty. - In case of full or partial response, the HTTP status will be set to 200
- If all requested locations are rejected, the HTTP status will be set to 401
Exports endpoint V3
A third variant of the endpoint is available at https://exports.analytiks.ai/wifi/json/v3
.
In this version, batches are done automatically using cursors. You only need to specify a brandId
and the endpoint will return batches of data with an optional cursor.
When a cursor is returned in the response, you can include it in your next request to get the next batch.
A cursor set to null
means eveything was fetched.
Request body:
{
"startDate": "2023-06-08T11:30:00.000",
"endDate": "2023-06-08T23:45:00.000",
"brandId": 1234,
"cursor": null // Can be null or missing for the first batch
}
Response body:
{
"cursor": "some_opaque_string" // or null when everything has been fetched
"data": [...], // Same structure as in V1
}