Building a Complete Application
Overview
This tutorial demonstrates how to build a complete Haltian IoT application that can:
- Authenticate - Login and obtain access tokens
- Search - Find devices by name, serial number, or ID
- Display Data - Show device measurements and position
- Show Context - Display building/floor information when device is in a space
This tutorial provides complete, working GraphQL queries that any AI agent or developer can use to build a functional application.
Prerequisites
- Haltian IoT account with credentials
- GraphQL client (Postman, Apollo, or any HTTP client)
- GraphQL endpoint URL (provided by your deployment)
Configuration
API Endpoint
https://haltian-iot-api.eu.haltian.io/v1/graphql
Replace with your deployment’s endpoint URL.
Required Credentials
email- Your user emailorganizationName- Your organization identifierpassword- Your password
Step 1: Authentication
Login Mutation
Execute this mutation to obtain access and refresh tokens:
mutation Login($email: String!, $organizationName: String!, $password: String!) {
login(
loginInput: {
email: $email
organizationName: $organizationName
password: $password
}
) {
accessToken
expiresIn
refreshExpiresIn
refreshToken
organizationId
}
}
Variables
{
"email": "your-email@example.com",
"organizationName": "your-org",
"password": "your-password"
}
Response
{
"data": {
"login": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 900,
"refreshExpiresIn": 43200,
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"organizationId": "2a749da0-5512-4b3e-8c4a-9b8f3c1d5e6f"
}
}
}
Token Storage
Store the accessToken and refreshToken for subsequent requests:
- accessToken: Valid for 15 minutes (900 seconds)
- refreshToken: Valid for 12 hours (43200 seconds)
Authorization Header
Include the access token in all subsequent requests:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Step 2: Search for Devices
Device Search Query
Search for devices by name, serial number, customer label, or UUID:
query DevicesByTextSearch($limit: Int = 25, $searchText: String, $searchUuid: uuid) {
devices(
limit: $limit
where: {
_or: [
{identifiers: {customerLabelId: {_ilike: $searchText}}}
{identifiers: {vendorSerial: {_ilike: $searchText}}}
{identifiers: {giai: {_ilike: $searchText}}}
{identifiers: {deviceId: {_eq: $searchUuid}}}
{name: {_ilike: $searchText}}
{externalId: {_ilike: $searchText}}
]
}
) {
id
name
connectorProtocol
organizationId
lastSeen
availableMeasurements
availableMessages
fixedPositionGlobal
spaceId
identifiers {
wirepasNodeId
wirepasNetworkId
wifiMac
vendorSerial
tuid
lanMac
imei
customerLabelId
bleMac
deviceId
}
deviceType {
make
model
productName
}
keywords {
keyword
}
createdAt
updatedAt
}
}
Search Variables (Text Search)
{
"limit": 25,
"searchText": "%sensor%"
}
Search Variables (UUID Search)
{
"limit": 25,
"searchUuid": "6e5f8ac6-a099-44e5-bb28-25fd3d1f47a6"
}
Response Example
{
"data": {
"devices": [
{
"id": "6e5f8ac6-a099-44e5-bb28-25fd3d1f47a6",
"name": "Temperature Sensor 101",
"connectorProtocol": "WIREPAS",
"organizationId": "2a749da0-5512-4b3e-8c4a-9b8f3c1d5e6f",
"lastSeen": "2026-02-05T10:30:45.123Z",
"availableMeasurements": [
"ambientTemperature",
"relativeHumidity",
"batteryVoltage"
],
"availableMessages": ["wirepasNeighborDiagnostics"],
"fixedPositionGlobal": {
"type": "Point",
"coordinates": [24.9384, 60.1699]
},
"spaceId": "55a0a3d0-84b6-4ace-b131-0b2420053e91",
"identifiers": {
"wirepasNodeId": 123456,
"wirepasNetworkId": 7890,
"vendorSerial": "TSN-2024-0101",
"customerLabelId": "FLOOR3-ZONE-A-01",
"deviceId": "6e5f8ac6-a099-44e5-bb28-25fd3d1f47a6"
},
"deviceType": {
"make": "Haltian",
"model": "TMP001",
"productName": "Temperature Sensor"
},
"keywords": [
{"keyword": "temperature"},
{"keyword": "humidity"}
],
"createdAt": "2025-10-15T08:20:00.000Z",
"updatedAt": "2026-02-05T10:30:45.123Z"
}
]
}
}
Important Search Notes
- Use
%wildcards with_ilikefor partial text matching _ilikeis case-insensitiveavailableMeasurementsshows which measurement types this device supports- Use the returned device
idfor subsequent queries
Device Location: Two Methods
Devices can have location information in two ways:
- Fixed Installation - Device has
fixedPositionGlobalandspaceIdset directly in the device record (for permanently installed sensors) - Measurement Position - Device location tracked through
measurementLastPosition(for mobile devices or recent position updates)
Add these fields to your device search to check for fixed installation:
query DevicesByTextSearch($limit: Int = 25, $searchText: String) {
devices(
limit: $limit
where: {
_or: [
{identifiers: {customerLabelId: {_ilike: $searchText}}}
{name: {_ilike: $searchText}}
]
}
) {
id
name
fixedPositionGlobal
spaceId
# ... other fields
}
}
When to use which:
- Check device
fixedPositionGlobalandspaceIdfirst for fixed installations - Query
measurementLastPositionfor mobile devices or if fixed position is not set measurementLastPositionprovides the most recent position data
Step 3: Get Device Location
Method 1: Check Fixed Installation (Preferred for Static Devices)
If the device returned from the search has fixedPositionGlobal and spaceId set, you already have the location:
{
"id": "6e5f8ac6-a099-44e5-bb28-25fd3d1f47a6",
"name": "Temperature Sensor 101",
"fixedPositionGlobal": {
"type": "Point",
"coordinates": [24.9384, 60.1699]
},
"spaceId": "55a0a3d0-84b6-4ace-b131-0b2420053e91"
}
Skip to Step 4 to get space information.
Method 2: Query Measurement Position (For Mobile Devices)
query MeasurementLastPosition($device: uuid) {
measurementLastPosition(where: {deviceId: {_eq: $device}}) {
deviceId
isStatic
measuredAt
positionGlobal
spaceId
zones {
measuredAt
zone {
id
name
}
}
}
}
Variables
{
"device": "6e5f8ac6-a099-44e5-bb28-25fd3d1f47a6"
}
Response Example
{
"data": {
"measurementLastPosition": [
{
"deviceId": "6e5f8ac6-a099-44e5-bb28-25fd3d1f47a6",
"isStatic": true,
"measuredAt": "2026-02-05T10:30:45.491Z",
"positionGlobal": {
"type": "Point",
"coordinates": [24.9384, 60.1699]
},
"spaceId": "55a0a3d0-84b6-4ace-b131-0b2420053e91",
"zones": [
{
"measuredAt": "2026-02-05T10:30:45.491Z",
"zone": {
"id": "7c8d9e0f-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
"name": "Meeting Room A"
}
}
]
}
]
}
}
Response Field Descriptions
| Field | Type | Description |
|---|---|---|
deviceId | UUID | Device identifier |
isStatic | Boolean | true if device is stationary, false if mobile |
measuredAt | Timestamp | When the measurement was taken (ISO 8601 UTC) |
positionGlobal | GeoJSON Point | Global coordinates [longitude, latitude] |
spaceId | UUID | Space where device is located (use for Step 4) |
zones | Array | Zone assignments with timestamps |
Step 4: Get Building/Floor Information
When a device has a spaceId, retrieve the complete space hierarchy (building → floor → room).
Space Query
query Space($id: uuid!) {
spaces(where: {id: {_eq: $id}}) {
id
name
type
parentId
organizationId
createdAt
updatedAt
drawing {
id
widthMeters
depthMeters
url
data
}
positionGlobal
northVector
zones {
id
name
type
polygonGlobal
keywords {
keyword
}
}
}
}
Variables
Use the spaceId from Step 3:
{
"id": "55a0a3d0-84b6-4ace-b131-0b2420053e91"
}
Response Example (Floor Level)
{
"data": {
"spaces": [
{
"id": "55a0a3d0-84b6-4ace-b131-0b2420053e91",
"name": "Floor 3",
"type": "floor",
"parentId": "44a9b2c1-73a5-4bcd-a020-1b1309042e80",
"organizationId": "2a749da0-5512-4b3e-8c4a-9b8f3c1d5e6f",
"createdAt": "2025-10-15T08:00:00.000Z",
"updatedAt": "2025-12-20T14:30:00.000Z",
"drawing": {
"id": "88b7c6d5-e4f3-4a2b-9c1d-0e2f3a4b5c6d",
"widthMeters": 50.0,
"depthMeters": 30.0,
"url": "https://storage.example.com/floorplans/floor3.svg",
"data": "<?xml version=\"1.0\"?>..."
},
"positionGlobal": {
"type": "Point",
"coordinates": [24.9384, 60.1699]
},
"northVector": 45.0,
"zones": [
{
"id": "7c8d9e0f-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
"name": "Meeting Room A",
"type": "room",
"polygonGlobal": {
"type": "Polygon",
"coordinates": [[[0, 0], [10, 0], [10, 8], [0, 8], [0, 0]]]
},
"keywords": [
{"keyword": "meeting"},
{"keyword": "conference"}
]
}
]
}
]
}
}
Get Parent Space (Building)
If the space has a parentId, query it to get the building information:
query Space($id: uuid!) {
spaces(where: {id: {_eq: $id}}) {
id
name
type
parentId
organizationId
positionGlobal
}
}
Variables (Using parentId from previous response)
{
"id": "44a9b2c1-73a5-4bcd-a020-1b1309042e80"
}
Response Example (Building Level)
{
"data": {
"spaces": [
{
"id": "44a9b2c1-73a5-4bcd-a020-1b1309042e80",
"name": "Helsinki Office",
"type": "building",
"parentId": "33a8b1c0-62a4-4abc-9f1f-0a0208031d7f",
"organizationId": "2a749da0-5512-4b3e-8c4a-9b8f3c1d5e6f",
"positionGlobal": {
"type": "Point",
"coordinates": [24.9384, 60.1699]
}
}
]
}
}
Space Type Hierarchy
The type field indicates the space level:
| Type | Description | Typical Parent |
|---|---|---|
site | Root location (address/campus) | None (parentId is null) |
building | Building within a site | site |
floor | Floor within a building | building |
room | Room or zone within a floor | floor |
Step 5: Complete Application Flow
Pseudocode Implementation
# Step 1: Authentication
def login(email, organization_name, password):
response = graphql_mutation(LOGIN_MUTATION, {
"email": email,
"organizationName": organization_name,
"password": password
})
return response.data.login.accessToken
# Step 2: Search Device
def search_device(access_token, search_text):
response = graphql_query(DEVICE_SEARCH_QUERY, {
"searchText": f"%{search_text}%"
}, auth_token=access_token)
return response.data.devices[0] # Return first match
# Step 3: Get Device Location
def get_device_location(access_token, device):
# Method 1: Check if device has fixed installation
if device.fixedPositionGlobal and device.spaceId:
return {
"position": device.fixedPositionGlobal,
"spaceId": device.spaceId,
"isFixed": True,
"zones": []
}
# Method 2: Query measurement position for mobile devices
response = graphql_query(MEASUREMENT_LAST_POSITION_QUERY, {
"device": device.id
}, auth_token=access_token)
if response.data.measurementLastPosition:
pos = response.data.measurementLastPosition[0]
return {
"position": pos.positionGlobal,
"spaceId": pos.spaceId,
"isFixed": False,
"zones": pos.zones,
"measuredAt": pos.measuredAt
}
return None
# Step 4: Get Space Hierarchy
def get_space_hierarchy(access_token, space_id):
hierarchy = []
current_space_id = space_id
while current_space_id:
response = graphql_query(SPACE_QUERY, {
"id": current_space_id
}, auth_token=access_token)
space = response.data.spaces[0]
hierarchy.insert(0, space) # Add to beginning of list
current_space_id = space.parentId
return hierarchy
# Complete Flow
def display_device_with_context(email, org, password, search_term):
# Authenticate
token = login(email, org, password)
# Search device
device = search_device(token, search_term)
print(f"Device: {device.name} ({device.deviceType.productName})")
# Get location (checks both fixed installation and measurement position)
location = get_device_location(token, device)
if location:
print(f"Location: {location['position'].coordinates}")
print(f"Installation type: {'Fixed' if location['isFixed'] else 'Mobile'}")
if not location['isFixed'] and 'measuredAt' in location:
print(f"Last measured: {location['measuredAt']}")
# Get space context
if location['spaceId']:
hierarchy = get_space_hierarchy(token, location['spaceId'])
# Display hierarchy: Site → Building → Floor → Room
location_path = " → ".join([s.name for s in hierarchy])
print(f"Context: {location_path}")
# Display zone if available
if location['zones']:
zone_name = location['zones'][0].zone.name
print(f"Zone: {zone_name}")
else:
print("No location data available for this device")
# Usage
display_device_with_context(
email="user@example.com",
org="demo",
password="secret",
search_term="sensor"
)
Expected Output
For a Fixed Installation:
Device: Temperature Sensor 101 (Temperature Sensor)
Location: [24.9384, 60.1699]
Installation type: Fixed
Context: Helsinki Campus → Helsinki Office → Floor 3 → Meeting Room A
Zone: Meeting Room A
For a Mobile Device:
Device: Mobile Tag 205 (Asset Tag)
Location: [24.9385, 60.1700]
Installation type: Mobile
Last measured: 2026-02-05T10:30:45.491Z
Context: Helsinki Campus → Helsinki Office → Floor 3
Zone: Conference Area
Step 6: Token Refresh (Optional)
When the access token expires (after 15 minutes), use the refresh token:
mutation RefreshToken($organizationId: String!, $refreshToken: String!) {
refreshToken(
refreshTokenInput: {
organizationId: $organizationId
refreshToken: $refreshToken
}
) {
accessToken
expiresIn
refreshExpiresIn
refreshToken
}
}
Variables
{
"organizationId": "2a749da0-5512-4b3e-8c4a-9b8f3c1d5e6f",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Error Handling
Common Error Responses
Authentication Failed
{
"errors": [
{
"message": "Invalid credentials",
"extensions": {
"code": "AUTHENTICATION_ERROR"
}
}
]
}
Token Expired
{
"errors": [
{
"message": "Token expired",
"extensions": {
"code": "UNAUTHENTICATED"
}
}
]
}
Device Not Found
{
"data": {
"devices": []
}
}
Recommended Error Handling
def safe_graphql_query(query, variables, auth_token):
try:
response = graphql_query(query, variables, auth_token)
if response.errors:
for error in response.errors:
if error.extensions.code == "UNAUTHENTICATED":
# Refresh token and retry
new_token = refresh_access_token()
return graphql_query(query, variables, new_token)
else:
raise Exception(f"GraphQL Error: {error.message}")
return response.data
except Exception as e:
log_error(f"Query failed: {e}")
raise
Complete HTTP Examples
Using cURL
Login
curl -X POST https://haltian-iot-api.eu.haltian.io/v1/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "mutation Login($email: String!, $organizationName: String!, $password: String!) { login(loginInput: {email: $email, organizationName: $organizationName, password: $password}) { accessToken expiresIn refreshExpiresIn refreshToken organizationId } }",
"variables": {
"email": "user@example.com",
"organizationName": "demo",
"password": "secret"
}
}'
Search Device
curl -X POST https://haltian-iot-api.eu.haltian.io/v1/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGc..." \
-d '{
"query": "query DevicesByTextSearch($searchText: String) { devices(limit: 25, where: {_or: [{name: {_ilike: $searchText}}]}) { id name fixedPositionGlobal spaceId deviceType { productName } } }",
"variables": {
"searchText": "%sensor%"
}
}'
Get Measurement Position (if needed)
curl -X POST https://haltian-iot-api.eu.haltian.io/v1/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGc..." \
-d '{
"query": "query MeasurementLastPosition($device: uuid) { measurementLastPosition(where: {deviceId: {_eq: $device}}) { deviceId positionGlobal spaceId zones { zone { name } } } }",
"variables": {
"device": "6e5f8ac6-a099-44e5-bb28-25fd3d1f47a6"
}
}'
AI Agent Instructions
This tutorial is designed to be parseable by AI agents. Key integration points:
Required Inputs
An AI agent building this application needs:
- Credentials:
email,organizationName,password - Endpoint: GraphQL API URL
- Search Term: Device identifier or partial name
Expected Outputs
The application should provide:
- Device Information: Name, type, identifiers
- Installation Type: Fixed or mobile
- Current Position: Coordinates (from fixed installation or measurement)
- Location Context: Full hierarchy from site to zone
- Zone Assignment: Current zone if applicable
Implementation Checklist
- Execute login mutation and store tokens
- Implement authorization header injection
- Execute device search with
fixedPositionGlobalandspaceIdfields - Check if device has fixed installation first
- Query measurement position only if fixed position is not set
- Handle empty search results
- Query measurement position for selected device
- Recursively query space hierarchy using
parentId - Display complete location path
- Implement token refresh logic
- Handle authentication errors
- Format timestamps for user display
GraphQL Client Configuration
const client = new ApolloClient({
uri: 'https://haltian-iot-api.eu.haltian.io/v1/graphql',
request: (operation) => {
const token = localStorage.getItem('accessToken');
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : '',
}
});
},
});
Next Steps
- Authentication - Detailed token management
- Queries - Additional query examples
- Mutations - Update device data
- Stream API - Real-time data streaming
Summary
This tutorial covered:
✅ Authentication - Login and token management
✅ Device Search - Finding devices by various identifiers
✅ Measurement Data - Retrieving position and sensor data
✅ Space Hierarchy - Building complete location context
✅ Error Handling - Managing common failure scenarios
✅ Complete Implementation - Working pseudocode example
All queries are production-ready and extracted from the Haltian IoT Studio iOS application.