Xylolabs API Reference¶
Version: v1 Last Updated: 2026-03-22
Table of Contents¶
- Overview
- Authentication
- Facilities
- Users
- API Keys
- Devices
- Audio Upload
- Audio Streaming
- Transcode Jobs
- Tags
- Metadata Ingest
- Metadata Query
- System Configuration
- Dashboard Stats
- Health
- XMBP Protocol Reference
- RBAC Reference
- Error Responses
- Data Models
- Pagination
- Example Workflows
- LTE Cat-M1 Integration Guide
1. Overview¶
What is Xylolabs API?¶
Xylolabs API handles audio recording uploads and real-time sensor metadata from IoT and embedded devices. It gives you:
- Audio Upload and Transcoding -- Upload high-resolution audio files (WAV, FLAC, etc.) and they're automatically transcoded into web-playable formats (Opus/WebM, AAC/MP4).
- Real-time Metadata Streaming -- Stream sensor data from embedded devices using the compact XMBP binary protocol over HTTP or WebSocket.
- Multi-tenant Facility Isolation -- All data is scoped to facilities with role-based access control.
- Admin Dashboard API -- Full CRUD for facilities, users, API keys, devices, and system configuration.
Quick Start¶
Get up and running in five steps:
- Log in with
POST /api/auth/loginto get a JWT token. - Create a facility with
POST /api/v1/facilities(Super Admin only). - Generate an API key with
POST /api/v1/api-keysfor your devices. - Upload audio from a device with
POST /api/v1/uploadsusing the API key. - Query data through the dashboard endpoints using your JWT.
If you're deploying over Cat-M1 cellular, read Section 22 first for bandwidth planning.
Architecture¶
Embedded Device (Pico, ESP32, Cat-M1 modem, etc.)
|
+-- Audio Upload (multipart HTTP) ---> S3 (original) ---> Auto-transcode ---> S3 (web copy)
|
+-- Metadata Streaming (XMBP) -------> IngestManager ---> PostgreSQL + S3 chunks
LTE Cat-M1 Device (nRF9160, BG770A, etc.)
|
+-- HTTP POST (XMBP batches) ---------> IngestManager ---> PostgreSQL + S3 chunks
| (~37 KB/s uplink budget)
+-- Audio Upload (chunked) -----------> S3 (original) ---> Auto-transcode
(large files sent in background)
Admin Dashboard (Web)
|
+-- JWT Auth ------> REST API --------> PostgreSQL / S3
Base URLs¶
| Environment | Base URL |
|---|---|
| Production API | https://api.xylolabs.com/api |
| Admin Dashboard | https://admin.api.xylolabs.com |
| Development | http://localhost:3000/api |
Authentication Methods¶
| Method | Header | Used By | Endpoints |
|---|---|---|---|
| JWT Bearer Token | Authorization: Bearer <token> |
Dashboard / Web clients | All /api/v1/* protected routes |
| API Key | X-Api-Key: xk_... |
Embedded devices | Upload, Ingest |
Cat-M1 devices typically use API key authentication since they don't need dashboard access.
2. Authentication¶
POST /api/auth/login¶
Authenticate with email and password. Returns a JWT access token and refresh token pair.
Auth: None Role: None
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | User email address |
password |
string | Yes | User password |
Response 200 OK:
{
// Short-lived token for API calls (expires in 15 minutes)
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
// Long-lived token to get new access tokens without re-entering credentials
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "a3c1e7b2-4f8d-4a12-b6e9-1d5f3a7c9e20",
"email": "minseok.jeon@xylolabs.com",
"display_name": "전민석",
"role": "super_admin",
"facility_id": null // null because super_admins aren't tied to one facility
}
}
Error Cases:
| Status | Condition |
|---|---|
401 Unauthorized |
Invalid email or password |
401 Unauthorized |
Account is inactive |
curl:
# Log in and capture the access token for later use
curl -X POST https://api.xylolabs.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "minseok.jeon@xylolabs.com",
"password": "your-password"
}'
POST /api/auth/refresh¶
Trade a valid refresh token for a fresh access token and a rotated refresh token. Call this when your access token expires instead of asking the user to log in again.
Auth: None Role: None
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
refresh_token |
string | Yes | The refresh token from login |
Response 200 OK:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Error Cases:
| Status | Condition |
|---|---|
401 Unauthorized |
Invalid or expired refresh token |
401 Unauthorized |
User not found or inactive |
curl:
# Silently refresh the token before it expires
curl -X POST https://api.xylolabs.com/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token":"eyJhbGciOiJIUzI1NiIs..."}'
GET /api/auth/me¶
Returns the profile of whoever owns the current JWT.
Auth: JWT Bearer Token Role: Any authenticated user
Response 200 OK:
{
"id": "a3c1e7b2-4f8d-4a12-b6e9-1d5f3a7c9e20",
"email": "minseok.jeon@xylolabs.com",
"display_name": "전민석",
"role": "super_admin",
"facility_id": null
}
Error Cases:
| Status | Condition |
|---|---|
401 Unauthorized |
Missing or invalid JWT |
curl:
# Check which user the current token belongs to
curl https://api.xylolabs.com/api/auth/me \
-H "Authorization: Bearer $TOKEN"
3. Facilities¶
Facilities are the top-level organizational unit. All data -- users, devices, uploads, sessions -- belongs to a facility. Only Super Admins can create or manage facilities.
GET /api/v1/facilities¶
Returns every facility in the system.
Auth: JWT Bearer Token
Role: super_admin
Response 200 OK:
[
{
"id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"name": "페리지에어로스페이스 옥천공장",
"slug": "perigee-okcheon",
"description": "옥천 생산공장 음향 모니터링",
"is_active": true,
"created_at": "2025-01-15T09:00:00Z",
"updated_at": "2025-03-22T14:30:00Z"
},
{
"id": "c9e2a3b5-7d1f-4c68-b4a7-2e8f6d9c1b35",
"name": "페리지에어로스페이스 미래기술연구소",
"slug": "perigee-futuretech",
"description": "미래기술연구소 센서 데이터 수집",
"is_active": true,
"created_at": "2025-02-10T11:00:00Z",
"updated_at": "2025-02-10T11:00:00Z"
}
]
curl:
# List all facilities (super_admin only)
curl https://api.xylolabs.com/api/v1/facilities \
-H "Authorization: Bearer $TOKEN"
POST /api/v1/facilities¶
Creates a new facility.
Auth: JWT Bearer Token
Role: super_admin
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Facility display name |
slug |
string | Yes | Unique URL-friendly identifier |
description |
string | No | Optional description |
Response 200 OK:
{
"id": "d5f8c2e1-3a9b-4d72-8c6e-4b1a7f9d3e56",
"name": "페리지에어로스페이스 옥천공장",
"slug": "perigee-okcheon",
"description": "옥천 생산공장 음향 모니터링",
"is_active": true,
"created_at": "2025-06-15T10:00:00Z",
"updated_at": "2025-06-15T10:00:00Z"
}
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
User is not a Super Admin |
409 Conflict |
Facility with the same slug already exists |
curl:
# Create a new facility
curl -X POST https://api.xylolabs.com/api/v1/facilities \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "페리지에어로스페이스 옥천공장",
"slug": "perigee-okcheon",
"description": "옥천 생산공장 음향 모니터링"
}'
GET /api/v1/facilities/{id}¶
Fetches a single facility by its ID.
Auth: JWT Bearer Token
Role: super_admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Facility ID |
Response 200 OK:
{
"id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"name": "페리지에어로스페이스 옥천공장",
"slug": "perigee-okcheon",
"description": "옥천 생산공장 음향 모니터링",
"is_active": true,
"created_at": "2025-01-15T09:00:00Z",
"updated_at": "2025-03-22T14:30:00Z"
}
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
User is not a Super Admin |
404 Not Found |
Facility not found |
curl:
curl https://api.xylolabs.com/api/v1/facilities/b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14 \
-H "Authorization: Bearer $TOKEN"
PATCH /api/v1/facilities/{id}¶
Updates a facility. Only include the fields you want to change.
Auth: JWT Bearer Token
Role: super_admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Facility ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | No | New facility name |
description |
string | No | New description |
is_active |
boolean | No | Enable/disable facility |
Response 200 OK: Updated facility object (same shape as GET).
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
User is not a Super Admin |
404 Not Found |
Facility not found |
curl:
# Update facility name and description
curl -X PATCH https://api.xylolabs.com/api/v1/facilities/b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"페리지에어로스페이스 옥천공장"}'
DELETE /api/v1/facilities/{id}¶
Deletes a facility and all its associated data.
Auth: JWT Bearer Token
Role: super_admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Facility ID |
Response 204 No Content
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
User is not a Super Admin |
404 Not Found |
Facility not found |
curl:
# Permanently delete a facility -- this can't be undone
curl -X DELETE https://api.xylolabs.com/api/v1/facilities/d5f8c2e1-3a9b-4d72-8c6e-4b1a7f9d3e56 \
-H "Authorization: Bearer $TOKEN"
4. Users¶
Users belong to a facility and have roles that control what they can see and do. Facility Admins manage users within their own facility. Super Admins can manage everyone.
GET /api/v1/users¶
Returns a list of users. Facility Admins only see users in their own facility; Super Admins see all users across the system.
Auth: JWT Bearer Token
Role: facility_admin or higher
Response 200 OK:
[
{
"id": "e4b9c6d2-1f3a-4e87-9b5c-8d2a6f4e1c73",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"email": "minseok.jeon@xylolabs.com",
"display_name": "전민석",
"role": "facility_admin",
"is_active": true,
"last_login_at": "2025-06-15T08:30:00Z",
"created_at": "2025-01-15T09:00:00Z",
"updated_at": "2025-06-15T08:30:00Z"
},
{
"id": "f7a2d8e5-4c6b-4f19-a3d7-5e9c1b8a2f46",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"email": "dongyun.shin@xylolabs.com",
"display_name": "신동윤",
"role": "user",
"is_active": true,
"last_login_at": "2025-06-14T17:45:00Z",
"created_at": "2025-03-01T10:00:00Z",
"updated_at": "2025-06-14T17:45:00Z"
}
]
curl:
POST /api/v1/users¶
Creates a new user account.
Auth: JWT Bearer Token
Role: facility_admin or higher
Request Body:
{
"email": "minseok.jeon@xylolabs.com",
"password": "Kj8#mP2x!vNq",
"display_name": "전민석",
"role": "user",
"facility_id": "c9e2a3b5-7d1f-4c68-b4a7-2e8f6d9c1b35"
}
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Unique email address |
password |
string | Yes | User password (hashed with Argon2) |
display_name |
string | Yes | Display name |
role |
string | Yes | One of: super_admin, facility_admin, user |
facility_id |
UUID | No | Facility to assign (Super Admin only; Facility Admins auto-assign their facility) |
Response 200 OK: The created user object (same shape as the list response).
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Insufficient role |
409 Conflict |
Email already exists |
curl:
# Add a read-only user to the facility
curl -X POST https://api.xylolabs.com/api/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "dongyun.shin@xylolabs.com",
"password": "Kj8#mP2x!vNq",
"display_name": "신동윤",
"role": "user",
"facility_id": "c9e2a3b5-7d1f-4c68-b4a7-2e8f6d9c1b35"
}'
GET /api/v1/users/{id}¶
Fetches a single user by ID.
Auth: JWT Bearer Token
Role: facility_admin or higher
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | User ID |
Response 200 OK: User object.
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Insufficient role or cross-facility access |
404 Not Found |
User not found |
curl:
curl https://api.xylolabs.com/api/v1/users/e4b9c6d2-1f3a-4e87-9b5c-8d2a6f4e1c73 \
-H "Authorization: Bearer $TOKEN"
PATCH /api/v1/users/{id}¶
Updates a user. Only include the fields you want to change.
Auth: JWT Bearer Token
Role: facility_admin or higher
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | User ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
display_name |
string | No | New display name |
role |
string | No | New role |
is_active |
boolean | No | Enable/disable user |
Response 200 OK: Updated user object.
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Insufficient role or cross-facility access |
404 Not Found |
User not found |
curl:
# Promote user to facility admin
curl -X PATCH https://api.xylolabs.com/api/v1/users/e4b9c6d2-1f3a-4e87-9b5c-8d2a6f4e1c73 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"role":"facility_admin"}'
DELETE /api/v1/users/{id}¶
Soft-deletes a user by setting is_active to false. The account record stays in the database but the user can no longer log in.
Auth: JWT Bearer Token
Role: facility_admin or higher
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | User ID |
Response 204 No Content
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Insufficient role or cross-facility access |
404 Not Found |
User not found |
curl:
# Deactivate a user (soft delete -- can be re-enabled via PATCH)
curl -X DELETE https://api.xylolabs.com/api/v1/users/f7a2d8e5-4c6b-4f19-a3d7-5e9c1b8a2f46 \
-H "Authorization: Bearer $TOKEN"
5. API Keys¶
API keys let embedded devices authenticate for audio uploads and metadata ingestion. Each key is scoped to a single facility and can have optional permission scopes and an expiration date. The plaintext key is shown only once at creation, is not stored retrievably, and must be saved immediately.
GET /api/v1/api-keys¶
Lists all API keys for the current user's facility.
Auth: JWT Bearer Token
Role: facility_admin or higher
Response 200 OK:
[
{
"id": "1a8b3c5d-7e2f-4a69-b1c4-3d5e7f9a2b48",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"key_prefix": "xk_t4r7",
"name": "Turbine Hall Sensor Array",
"scopes": ["upload", "ingest"],
"is_active": true,
"expires_at": "2026-01-01T00:00:00Z",
"last_used_at": "2025-06-15T10:30:00Z",
"created_by": "e4b9c6d2-1f3a-4e87-9b5c-8d2a6f4e1c73",
"created_at": "2025-06-01T09:00:00Z"
},
{
"id": "2b9c4d6e-8f3a-4b71-c2d5-4e6f8a1b3c59",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"key_prefix": "xk_m9p2",
"name": "Rooftop Weather Station",
"scopes": ["ingest"],
"is_active": true,
"expires_at": null,
"last_used_at": "2025-06-15T09:15:00Z",
"created_by": "e4b9c6d2-1f3a-4e87-9b5c-8d2a6f4e1c73",
"created_at": "2025-05-20T14:00:00Z"
}
]
curl:
# See all API keys for your facility (only the key prefix is shown here)
curl https://api.xylolabs.com/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN"
POST /api/v1/api-keys¶
Creates a new API key. The plaintext key appears only in this response -- store it in your device firmware or a secrets manager right away. You won't be able to retrieve it later.
Auth: JWT Bearer Token
Role: facility_admin or higher
Request Body:
{
"name": "Underwater Hydrophone #3",
"scopes": ["upload", "ingest"],
"expires_at": "2026-06-01T00:00:00Z"
}
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Descriptive name for the key |
scopes |
string[] | No | Permission scopes (defaults to all) |
expires_at |
ISO 8601 | No | Expiration date (null = never expires) |
Response 200 OK:
{
"id": "3c1d5e7f-9a4b-4c82-d3e6-5f7a9b2c4d61",
"facility_id": "c9e2a3b5-7d1f-4c68-b4a7-2e8f6d9c1b35",
"key_prefix": "xk_h3yd",
"name": "Underwater Hydrophone #3",
"scopes": ["upload", "ingest"],
"expires_at": "2026-06-01T00:00:00Z",
"created_at": "2025-06-15T10:00:00Z",
// IMPORTANT: This is the only time the full key is returned. Copy it now.
"key": "xk_h3yd8k2m5p7r9t1v3x5z7b9d1f3h5j7l9n1q3s5u7w9y1a3c5e7g9i1k3m5o7q9"
}
curl:
# Create an API key for the Busan hydrophone and save the key to a file
curl -X POST https://api.xylolabs.com/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Underwater Hydrophone #3",
"scopes": ["upload", "ingest"],
"expires_at": "2026-06-01T00:00:00Z"
}' | tee api_key_response.json | jq -r '.key'
DELETE /api/v1/api-keys/{id}¶
Revokes an API key. Any device still using it will start getting 401 errors on its next request.
Auth: JWT Bearer Token
Role: facility_admin or higher
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | API Key ID |
Response 204 No Content
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Insufficient role or cross-facility access |
curl:
# Revoke a compromised API key
curl -X DELETE https://api.xylolabs.com/api/v1/api-keys/3c1d5e7f-9a4b-4c82-d3e6-5f7a9b2c4d61 \
-H "Authorization: Bearer $TOKEN"
6. Devices¶
Devices represent physical hardware (RPi Pico 2, ESP32, nRF9160, etc.) within a facility. Each device has a facility-scoped device_uid (integer). You can register devices manually through the dashboard, or they'll be auto-created when an ingest session references a new device_uid.
GET /api/v1/devices¶
Lists all devices in the current user's facility.
Auth: JWT Bearer Token Role: Any authenticated user
Response 200 OK:
[
{
"id": "4d2e6f8a-1b3c-4d93-e4f7-6a8b1c3d5e72",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"device_uid": 1,
"name": "Turbine Hall Sensor Array",
"hardware_version": "rp2350",
"firmware_version": "2.1.0",
"is_active": true,
"last_seen_at": "2025-06-15T10:30:00Z",
"created_at": "2025-06-01T09:00:00Z"
},
{
"id": "5e3f7a9b-2c4d-4e14-f5a8-7b9c2d4e6f83",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"device_uid": 2,
"name": "Rooftop Wind Noise Monitor",
"hardware_version": "nrf9160-dk",
"firmware_version": "1.0.3",
"is_active": true,
"last_seen_at": "2025-06-15T10:28:00Z",
"created_at": "2025-05-15T11:00:00Z"
}
]
curl:
POST /api/v1/devices¶
Registers a new device. The device_uid must be unique within the facility.
Auth: JWT Bearer Token
Role: facility_admin or higher
Request Body:
{
"device_uid": 3,
"name": "Bird Call Recorder",
"hardware_version": "esp32-s3",
"firmware_version": "1.4.2"
}
| Field | Type | Required | Description |
|---|---|---|---|
device_uid |
u32 | Yes | Unique device number within facility |
name |
string | Yes | Device display name |
hardware_version |
string | No | Hardware version identifier |
firmware_version |
string | No | Firmware version identifier |
Response 201 Created:
{
"id": "6f4a8b1c-3d5e-4f25-a6b9-8c1d3e5f7a94",
"facility_id": "d5f8c2e1-3a9b-4d72-8c6e-4b1a7f9d3e56",
"device_uid": 3,
"name": "Bird Call Recorder",
"hardware_version": "esp32-s3",
"firmware_version": "1.4.2",
"is_active": true,
"last_seen_at": null, // hasn't sent any data yet
"created_at": "2025-06-15T10:00:00Z"
}
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Insufficient role |
409 Conflict |
Device with same device_uid already exists in this facility |
curl:
# Register the new sensor device at the facility
curl -X POST https://api.xylolabs.com/api/v1/devices \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"device_uid": 3,
"name": "Bird Call Recorder",
"hardware_version": "esp32-s3",
"firmware_version": "1.4.2"
}'
GET /api/v1/devices/{id}¶
Fetches a single device by ID.
Auth: JWT Bearer Token Role: Any authenticated user
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Device ID |
Response 200 OK: Device object.
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Cross-facility access |
404 Not Found |
Device not found |
curl:
curl https://api.xylolabs.com/api/v1/devices/4d2e6f8a-1b3c-4d93-e4f7-6a8b1c3d5e72 \
-H "Authorization: Bearer $TOKEN"
PATCH /api/v1/devices/{id}¶
Updates a device. Only include the fields you want to change.
Auth: JWT Bearer Token
Role: facility_admin or higher
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Device ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | No | New device name |
hardware_version |
string | No | New hardware version |
firmware_version |
string | No | New firmware version |
is_active |
boolean | No | Enable/disable device |
Response 200 OK: Updated device object.
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Insufficient role or cross-facility access |
404 Not Found |
Device not found |
curl:
# Update the firmware version after an OTA update
curl -X PATCH https://api.xylolabs.com/api/v1/devices/4d2e6f8a-1b3c-4d93-e4f7-6a8b1c3d5e72 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"firmware_version":"2.2.0"}'
7. Audio Upload¶
POST /api/v1/uploads¶
Upload an audio file as multipart form data. The original file is stored in S3 as-is, and a transcode job is automatically created to produce a web-playable copy (Opus/WebM by default).
Auth: API Key (X-Api-Key header)
Role: N/A (API Key scoped to facility)
Content-Type: multipart/form-data
Multipart Fields:
| Field | Type | Required | Description |
|---|---|---|---|
file |
file | Yes | Audio file (WAV, FLAC, MP3, etc.) |
metadata |
text (JSON) | No | Arbitrary metadata JSON |
tags |
text (JSON) | No | Array of tag UUIDs |
Response 200 OK:
{
"id": "7a5b9c2d-4e6f-4a36-b7c1-9d2e4f6a8b15",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"api_key_id": "1a8b3c5d-7e2f-4a69-b1c4-3d5e7f9a2b48",
"uploaded_by": null,
"original_filename": "turbine_hall_2025-06-15_14h30.wav",
"original_format": "wav",
"original_size_bytes": 115200000,
"duration_ms": null, // populated after transcoding finishes
"sample_rate_hz": null,
"channels": null,
"bit_depth": null,
"codec": null,
"metadata": {
"source": "turbine-array",
"location": "Hall B, Section 3",
"sample_rate": 384000,
"bit_depth": 24
},
"status": "pending", // will change to "processing" then "completed"
"tags": [],
"created_at": "2025-06-15T14:30:00Z",
"updated_at": "2025-06-15T14:30:00Z"
}
Upload statuses: pending, processing, completed, failed.
If you're uploading over Cat-M1, keep in mind that a 100 MB WAV file takes roughly 45 minutes at the ~37 KB/s Cat-M1 uplink rate. Consider compressing to FLAC first (typically 50-60% of the original size) or scheduling uploads during off-peak hours. See Section 22 for more Cat-M1 tips.
curl:
# Upload a high-resolution turbine hall recording with metadata
curl -X POST https://api.xylolabs.com/api/v1/uploads \
-H "X-Api-Key: $API_KEY" \
-F "file=@turbine_hall_2025-06-15_14h30.wav" \
-F 'metadata={"source":"turbine-array","location":"Hall B, Section 3","sample_rate":384000,"bit_depth":24}' \
-F 'tags=["a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"]'
Python example:
import requests
api_key = "xk_t4r78k2m5p7r9t1v3x5z7b9d1f3h5j7l9n1q3s5u7w9y1a3c5e7g9i1k3m5o7q9"
url = "https://api.xylolabs.com/api/v1/uploads"
with open("turbine_hall_2025-06-15_14h30.wav", "rb") as f:
response = requests.post(
url,
headers={"X-Api-Key": api_key},
files={"file": ("turbine_hall_2025-06-15_14h30.wav", f, "audio/wav")},
data={
"metadata": '{"source":"turbine-array","location":"Hall B, Section 3"}',
"tags": '["a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"]',
},
)
print(response.json())
# {'id': '7a5b9c2d-...', 'status': 'pending', ...}
Error Cases:
| Status | Condition |
|---|---|
400 Bad Request |
Missing file field or invalid multipart |
401 Unauthorized |
Invalid or missing API key |
Storage Flow:
Device --> HTTP POST --> Server --> S3 (original preserved)
|
Auto-transcode
|
S3 (web-playable copy)
GET /api/v1/uploads¶
Lists uploads for the current user's facility, with pagination.
Auth: JWT Bearer Token Role: Any authenticated user
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
integer | 1 | Page number (1-based) |
per_page |
integer | 20 | Items per page (1-100) |
status |
string | -- | Filter by status: pending, processing, completed, failed |
Response 200 OK:
{
"items": [
{
"id": "7a5b9c2d-4e6f-4a36-b7c1-9d2e4f6a8b15",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"original_filename": "turbine_hall_2025-06-15_14h30.wav",
"original_format": "wav",
"original_size_bytes": 115200000,
"status": "completed",
"tags": [
{
"id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"name": "Turbine Monitoring",
"color": "#FF5722",
"created_at": "2025-06-01T09:00:00Z"
}
],
"created_at": "2025-06-15T14:30:00Z",
"updated_at": "2025-06-15T14:35:00Z"
}
],
"total": 42,
"page": 1,
"per_page": 20
}
curl:
# Get the first page of completed uploads
curl "https://api.xylolabs.com/api/v1/uploads?page=1&per_page=10&status=completed" \
-H "Authorization: Bearer $TOKEN"
GET /api/v1/uploads/{id}¶
Fetches a single upload by ID.
Auth: JWT Bearer Token Role: Any authenticated user
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Upload ID |
Response 200 OK: Full upload object (same schema as list item).
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Cross-facility access |
404 Not Found |
Upload not found |
curl:
curl https://api.xylolabs.com/api/v1/uploads/7a5b9c2d-4e6f-4a36-b7c1-9d2e4f6a8b15 \
-H "Authorization: Bearer $TOKEN"
PATCH /api/v1/uploads/{id}¶
Updates upload metadata and/or tags. Metadata is a full replacement, not a merge -- include all the fields you want to keep.
Auth: JWT Bearer Token Role: Any authenticated user
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Upload ID |
Request Body:
{
"metadata": {
"source": "turbine-array",
"location": "Hall B, Section 3",
"notes": "Anomalous low-frequency hum detected at ~120 Hz"
},
"tags": ["a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"]
}
| Field | Type | Required | Description |
|---|---|---|---|
metadata |
object | No | Replace metadata (full replace) |
tags |
UUID[] | No | Replace tags (removes existing, adds new) |
Response 200 OK: Updated upload object.
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Cross-facility access |
404 Not Found |
Upload not found |
curl:
# Add a note about an anomaly found during review
curl -X PATCH https://api.xylolabs.com/api/v1/uploads/7a5b9c2d-4e6f-4a36-b7c1-9d2e4f6a8b15 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"metadata":{"source":"turbine-array","notes":"Anomalous 120 Hz hum"}}'
DELETE /api/v1/uploads/{id}¶
Deletes an upload. This removes the file from S3 and cascade-deletes all associated transcode jobs and tag associations.
Auth: JWT Bearer Token Role: Any authenticated user
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Upload ID |
Response 204 No Content
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Cross-facility access |
404 Not Found |
Upload not found |
curl:
# Permanently delete an upload and its transcoded copies
curl -X DELETE https://api.xylolabs.com/api/v1/uploads/7a5b9c2d-4e6f-4a36-b7c1-9d2e4f6a8b15 \
-H "Authorization: Bearer $TOKEN"
POST /api/v1/uploads/{id}/retranscode¶
Kicks off a new transcode job for an existing upload using the current default transcode settings. Useful after changing the default format in system configuration.
Auth: JWT Bearer Token Role: Any authenticated user
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Upload ID |
Response 200 OK:
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Cross-facility access |
404 Not Found |
Upload not found |
curl:
# Re-transcode after switching the default format from Opus to AAC
curl -X POST https://api.xylolabs.com/api/v1/uploads/7a5b9c2d-4e6f-4a36-b7c1-9d2e4f6a8b15/retranscode \
-H "Authorization: Bearer $TOKEN"
8. Audio Streaming¶
GET /api/v1/streams/{id}¶
Streams a transcoded audio file by proxying the latest completed transcode result through the backend. This avoids exposing internal or browser-unreachable S3 endpoints.
Auth: JWT Bearer Token Role: Any authenticated user
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Upload ID |
Response 200 OK:
Headers:
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Cross-facility access |
404 Not Found |
Upload not found or no completed transcode job |
curl:
# Download the transcoded audio (curl follows the redirect with -L)
curl -L https://api.xylolabs.com/api/v1/streams/7a5b9c2d-4e6f-4a36-b7c1-9d2e4f6a8b15 \
-H "Authorization: Bearer $TOKEN" \
--output turbine_hall_transcoded.webm
9. Transcode Jobs¶
GET /api/v1/transcode-jobs¶
Lists transcode jobs for the current user's facility, with pagination.
Auth: JWT Bearer Token
Role: user or higher
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
integer | 1 | Page number |
per_page |
integer | 20 | Items per page (1-100) |
status |
string | -- | Filter: queued, running, completed, failed, cancelled |
Response 200 OK:
{
"items": [
{
"id": "8b6c1d3e-5f7a-4b47-c8d2-1e3f5a7b9c26",
"upload_id": "7a5b9c2d-4e6f-4a36-b7c1-9d2e4f6a8b15",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"target_format": "opus_webm",
"target_codec": "libopus",
"target_container": "webm",
"target_bitrate": "128k",
"target_sample_rate": null,
"output_size_bytes": 2048000,
"output_duration_ms": 120000,
"status": "completed",
"error_message": null,
"attempts": 1,
"max_attempts": 3,
"queued_at": "2025-06-15T14:30:00Z",
"started_at": "2025-06-15T14:30:05Z",
"completed_at": "2025-06-15T14:30:15Z",
"worker_id": "worker-01"
}
],
"total": 10,
"page": 1,
"per_page": 20
}
curl:
# Check which transcode jobs are still running
curl "https://api.xylolabs.com/api/v1/transcode-jobs?status=running" \
-H "Authorization: Bearer $TOKEN"
GET /api/v1/transcode-jobs/{id}¶
Fetches a single transcode job by ID.
Auth: JWT Bearer Token
Role: user or higher
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Transcode Job ID |
Response 200 OK: Full transcode job object (same schema as list item).
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Cross-facility access |
404 Not Found |
Job not found |
curl:
curl https://api.xylolabs.com/api/v1/transcode-jobs/8b6c1d3e-5f7a-4b47-c8d2-1e3f5a7b9c26 \
-H "Authorization: Bearer $TOKEN"
POST /api/v1/transcode-jobs/{id}/cancel¶
Cancels a queued or running transcode job.
Auth: JWT Bearer Token
Role: facility_admin or higher
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Transcode Job ID |
Response 204 No Content
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Insufficient role or cross-facility access |
404 Not Found |
Job not found |
curl:
# Cancel a stuck transcode job
curl -X POST https://api.xylolabs.com/api/v1/transcode-jobs/8b6c1d3e-5f7a-4b47-c8d2-1e3f5a7b9c26/cancel \
-H "Authorization: Bearer $TOKEN"
10. Tags¶
Tags are labels you can attach to uploads for organization and filtering. They're scoped to a facility, so different facilities can have tags with the same name without colliding.
GET /api/v1/tags¶
Lists all tags for the current user's facility.
Auth: JWT Bearer Token
Role: user or higher
Response 200 OK:
[
{
"id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"name": "Turbine Monitoring",
"color": "#FF5722",
"created_at": "2025-06-01T09:00:00Z"
},
{
"id": "b2c3d4e5-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"name": "Night Recording",
"color": "#3F51B5",
"created_at": "2025-06-03T14:00:00Z"
}
]
curl:
POST /api/v1/tags¶
Creates a new tag.
Auth: JWT Bearer Token
Role: facility_admin or higher
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Tag name |
color |
string | No | Hex color for display (e.g. #F44336) |
Response 200 OK: Created tag object.
curl:
# Create a red tag for flagging anomalies
curl -X POST https://api.xylolabs.com/api/v1/tags \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Anomaly Detected","color":"#F44336"}'
DELETE /api/v1/tags/{id}¶
Deletes a tag. Any uploads that had this tag will have it removed.
Auth: JWT Bearer Token
Role: facility_admin or higher
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Tag ID |
Response 204 No Content
curl:
curl -X DELETE https://api.xylolabs.com/api/v1/tags/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d \
-H "Authorization: Bearer $TOKEN"
11. Metadata Ingest¶
The Metadata Ingest subsystem lets embedded devices stream sensor data to the server in real-time using the XMBP (Xylolabs Metadata Binary Protocol). This section covers the device-facing ingest API.
Developer Tips¶
Session lifecycle: A session represents one continuous recording period from a device. Create it when the device powers on or starts recording, send data during operation, and close it when you're done. If the device reboots unexpectedly, the server will eventually mark the session as error after a timeout.
HTTP POST vs WebSocket: For Cat-M1 devices and intermittent senders, use HTTP POST. WebSocket is better for WiFi-connected devices with continuous high-frequency streams. Cat-M1 modems have limited socket support, and HTTP POST with keep-alive works more reliably with cellular networks.
Batch sizing: Don't send one sample at a time -- batch them up. For a 100 Hz stream, 250 samples per batch (one POST every 2.5 seconds) is a good starting point. See the bandwidth calculations in Section 16 and Section 22 for Cat-M1 constraints.
Ingest Flow¶
1. Create Session (POST /ingest/sessions)
--> Session ID + stream definitions returned
2. Send Data (repeat)
|-- HTTP: POST /ingest/sessions/{id}/data (XMBP binary body)
|-- WebSocket: /ingest/ws (XMBP binary frames)
3. Close Session (POST /ingest/sessions/{id}/close)
POST /api/v1/ingest/sessions¶
Creates a new ingest session with stream definitions. If the device_uid doesn't match an existing device, one is automatically created.
Auth: API Key (X-Api-Key header)
Role: N/A (API Key scoped to facility)
Request Body:
{
"device_uid": 1,
"name": "Turbine Hall Continuous Monitoring",
"streams": [
{
"stream_index": 0,
"name": "vibration_x",
"value_type": "f32",
"unit": "g",
"sample_rate_hz": 500.0,
"description": "X-axis accelerometer on turbine bearing"
},
{
"stream_index": 1,
"name": "vibration_y",
"value_type": "f32",
"unit": "g",
"sample_rate_hz": 500.0,
"description": "Y-axis accelerometer on turbine bearing"
},
{
"stream_index": 2,
"name": "temperature",
"value_type": "f32",
"unit": "celsius",
"sample_rate_hz": 10.0,
"description": "Bearing temperature (slower rate, less critical)"
},
{
"stream_index": 3,
"name": "rpm",
"value_type": "f32",
"unit": "rpm",
"sample_rate_hz": 10.0,
"description": "Turbine shaft RPM from tachometer"
}
],
"metadata": {
"location": "Hall B, Turbine #7",
"firmware": "2.1.0",
"calibration_date": "2025-06-01"
}
}
CreateIngestSessionRequest fields:
| Field | Type | Required | Description |
|---|---|---|---|
device_uid |
u32 | Yes | Facility-scoped unique device number |
name |
string | No | Session name |
streams |
StreamDefinition[] | Yes | Stream definitions |
metadata |
object | No | Session metadata |
StreamDefinition fields:
| Field | Type | Required | Description |
|---|---|---|---|
stream_index |
u16 | Yes | Stream index (matches XMBP stream_id) |
name |
string | Yes | Stream name |
value_type |
string | Yes | Value type (f32, f64, i32, i64, bool, string, bytes, f64_array, f32_array, i32_array, json, i16, i8) |
unit |
string | No | Unit label (display only) |
sample_rate_hz |
f32 | No | Sampling rate (display only) |
description |
string | No | Description |
Response 201 Created:
{
"id": "9c7d2e4f-6a8b-4c58-d9e3-2f4a6b8c1d37",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"device_id": "4d2e6f8a-1b3c-4d93-e4f7-6a8b1c3d5e72",
"status": "active",
"started_at": "2025-06-15T10:00:00Z",
"total_samples": 0,
"total_bytes": 0,
"missed_batches": 0,
"streams": [
{
"id": "1e4f7a2b-5c8d-4e69-a1b4-7c2d5e8f1a3b",
"stream_index": 0,
"name": "vibration_x",
"value_type": "f32",
"unit": "g",
"sample_rate_hz": 500.0
},
{
"id": "2f5a8b3c-6d9e-4f71-b2c5-8d3e6f9a2b4c",
"stream_index": 1,
"name": "vibration_y",
"value_type": "f32",
"unit": "g",
"sample_rate_hz": 500.0
}
// ... remaining streams
]
}
curl:
# Start a monitoring session with 4 sensor channels
curl -X POST https://api.xylolabs.com/api/v1/ingest/sessions \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"device_uid": 1,
"name": "Turbine Hall Continuous Monitoring",
"streams": [
{"stream_index":0,"name":"vibration_x","value_type":"f32","unit":"g","sample_rate_hz":500},
{"stream_index":1,"name":"vibration_y","value_type":"f32","unit":"g","sample_rate_hz":500},
{"stream_index":2,"name":"temperature","value_type":"f32","unit":"celsius","sample_rate_hz":10},
{"stream_index":3,"name":"rpm","value_type":"f32","unit":"rpm","sample_rate_hz":10}
],
"metadata": {"location":"Hall B, Turbine #7","firmware":"2.1.0"}
}'
POST /api/v1/ingest/sessions/{id}/data¶
Sends an XMBP binary batch via HTTP POST.
Auth: API Key (X-Api-Key header)
Role: N/A (API Key scoped to facility)
Content-Type: application/octet-stream
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Session ID |
Request Body: Raw XMBP binary batch (see XMBP Protocol Reference).
Response 200 OK:
{
"accepted_samples": 1000, // running total of samples received in this session
"missed_batches": 0 // non-zero if the server detected gaps in batch_seq
}
| Field | Type | Description |
|---|---|---|
accepted_samples |
u32 | Total samples received so far |
missed_batches |
u32 | Number of missed batches (based on batch sequence numbers) |
Error Cases:
| Status | Condition |
|---|---|
400 Bad Request |
XMBP decode error |
401 Unauthorized |
Invalid API key |
404 Not Found |
Session not found or facility mismatch |
curl:
# Send a pre-built XMBP binary batch
curl -X POST https://api.xylolabs.com/api/v1/ingest/sessions/9c7d2e4f-6a8b-4c58-d9e3-2f4a6b8c1d37/data \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @batch.bin
POST /api/v1/ingest/sessions/{id}/close¶
Closes an ingest session. The server flushes any buffered data and changes the session status to closed.
Auth: API Key (X-Api-Key header)
Role: N/A (API Key scoped to facility)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Session ID |
Response 200 OK: Final session state with totals.
{
"id": "9c7d2e4f-6a8b-4c58-d9e3-2f4a6b8c1d37",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"device_id": "4d2e6f8a-1b3c-4d93-e4f7-6a8b1c3d5e72",
"status": "closed",
"started_at": "2025-06-15T10:00:00Z",
"total_samples": 1200000, // ~40 minutes of 4-channel data at 500 Hz
"total_bytes": 9600000, // ~9.6 MB transferred
"missed_batches": 2, // 2 batches lost (cellular dropout, perhaps)
"streams": [...]
}
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Session belongs to another facility |
404 Not Found |
Session not found |
curl:
# Close the session when the device is shutting down
curl -X POST https://api.xylolabs.com/api/v1/ingest/sessions/9c7d2e4f-6a8b-4c58-d9e3-2f4a6b8c1d37/close \
-H "X-Api-Key: $API_KEY"
GET /api/v1/ingest/ws¶
WebSocket upgrade endpoint for real-time binary data streaming. Best for WiFi-connected devices with continuous high-frequency data. For Cat-M1 devices, prefer HTTP POST.
Auth: X-Api-Key header required
Role: N/A
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
session_id |
UUID | Yes | Target session ID |
The WebSocket endpoint rejects API keys passed via query parameters because URLs are commonly logged by proxies, CDNs, and browsers.
Connection:
GET /api/v1/ingest/ws?session_id=9c7d2e4f-...
Connection: Upgrade
Upgrade: websocket
X-Api-Key: xk_t4r7...
Protocol:
| Direction | Frame Type | Content |
|---|---|---|
| Client --> Server | Binary | XMBP batch |
| Server --> Client | Binary | ACK: 4-byte u32 big-endian (accepted_samples) |
| Server --> Client | Text | Error message (connection closed after) |
Behavior: - Idle Timeout: The server closes the connection after 60 seconds of inactivity. - Ping/Pong: The server automatically responds to Ping frames with Pong. - Error: On XMBP decode failure, the server sends a Text frame with the error and may close the connection. - Max frame size: 64 KB - Max message size: 256 KB
ACK Processing (C pseudocode):
uint8_t ack_buf[4];
ws_recv(ack_buf, 4);
uint32_t accepted = (ack_buf[0] << 24) | (ack_buf[1] << 16)
| (ack_buf[2] << 8) | ack_buf[3];
// 'accepted' is the running total of samples the server has stored
WebSocket vs HTTP Comparison:
| Aspect | HTTP POST | WebSocket |
|---|---|---|
| Connection overhead | New connection per batch (or keep-alive) | Single long-lived connection |
| Header overhead | ~200 bytes per request | 2-14 bytes per frame |
| ACK format | JSON response | 4-byte binary |
| Best for | Intermittent sends, Cat-M1 | Continuous streaming, WiFi |
| Reconnection | Not needed | Must implement auto-reconnect |
12. Metadata Query¶
These endpoints let you query previously ingested sensor data. All require JWT authentication.
GET /api/v1/metadata/sessions¶
Lists ingest sessions for the current user's facility.
Auth: JWT Bearer Token Role: Any authenticated user
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
integer | 1 | Page number |
per_page |
integer | 20 | Items per page (1-100) |
status |
string | -- | Filter: active, closing, closed, error |
device_id |
UUID | -- | Filter by device |
Response 200 OK:
{
"items": [
{
"id": "9c7d2e4f-6a8b-4c58-d9e3-2f4a6b8c1d37",
"facility_id": "b7d4f1a8-2e6c-4b39-a8d1-9f3e5c7b2a14",
"device_id": "4d2e6f8a-1b3c-4d93-e4f7-6a8b1c3d5e72",
"status": "closed",
"started_at": "2025-06-15T10:00:00Z",
"total_samples": 1200000,
"total_bytes": 9600000,
"missed_batches": 2,
"streams": [
{
"id": "1e4f7a2b-5c8d-4e69-a1b4-7c2d5e8f1a3b",
"stream_index": 0,
"name": "vibration_x",
"value_type": "f32",
"unit": "g",
"sample_rate_hz": 500.0
}
]
}
],
"total": 5,
"page": 1,
"per_page": 20
}
curl:
# List all closed sessions for a specific device
curl "https://api.xylolabs.com/api/v1/metadata/sessions?status=closed&device_id=4d2e6f8a-1b3c-4d93-e4f7-6a8b1c3d5e72" \
-H "Authorization: Bearer $TOKEN"
GET /api/v1/metadata/sessions/{id}¶
Fetches a single ingest session with its stream definitions.
Auth: JWT Bearer Token Role: Any authenticated user
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Session ID |
Response 200 OK: Session object (same schema as list item).
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
Cross-facility access |
404 Not Found |
Session not found |
curl:
curl https://api.xylolabs.com/api/v1/metadata/sessions/9c7d2e4f-6a8b-4c58-d9e3-2f4a6b8c1d37 \
-H "Authorization: Bearer $TOKEN"
GET /api/v1/metadata/sessions/{id}/streams/{stream_id}/data¶
Queries data samples for a specific stream. You can filter by time range and request server-side downsampling when fetching large datasets for visualization.
Auth: JWT Bearer Token Role: Any authenticated user
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Session ID |
stream_id |
UUID | Stream ID |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
start_us |
i64 | 0 | Start timestamp in microseconds |
end_us |
i64 | MAX | End timestamp in microseconds |
downsample |
u32 | -- | Target number of samples (server-side downsampling) |
Response 200 OK:
{
"stream_id": "1e4f7a2b-5c8d-4e69-a1b4-7c2d5e8f1a3b",
"stream_name": "vibration_x",
"value_type": "f32",
"sample_count": 1000,
"timestamps_us": [1718442000000000, 1718442002000, 1718442004000],
"values": [0.042, 0.051, 0.038]
}
curl:
# Get 1000 downsampled vibration readings for a chart
curl "https://api.xylolabs.com/api/v1/metadata/sessions/9c7d2e4f-.../streams/1e4f7a2b-.../data?start_us=0&downsample=1000" \
-H "Authorization: Bearer $TOKEN"
GET /api/v1/metadata/sessions/{id}/export¶
Exports all session data as a CSV or JSON file download.
Auth: JWT Bearer Token Role: Any authenticated user
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Session ID |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
format |
string | csv |
Export format: csv or json |
Response 200 OK:
For CSV:
Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="session_9c7d2e4f-....csv"
timestamp_us,vibration_x,vibration_y,temperature,rpm
1718442000000000,0.042,0.039,72.3,1485
1718442002000,0.051,0.044,72.3,1486
For JSON:
{
"session_id": "9c7d2e4f-6a8b-4c58-d9e3-2f4a6b8c1d37",
"streams": [
{
"stream_id": "1e4f7a2b-5c8d-4e69-a1b4-7c2d5e8f1a3b",
"stream_name": "vibration_x",
"value_type": "f32",
"sample_count": 600000,
"timestamps_us": [1718442000000000, 1718442002000],
"values": [0.042, 0.051]
}
]
}
Error Cases:
| Status | Condition |
|---|---|
400 Bad Request |
Unsupported export format |
403 Forbidden |
Cross-facility access |
404 Not Found |
Session not found |
curl:
# Download a full session as CSV for analysis in Python/R/MATLAB
curl "https://api.xylolabs.com/api/v1/metadata/sessions/9c7d2e4f-.../export?format=csv" \
-H "Authorization: Bearer $TOKEN" \
--output turbine7_session_2025-06-15.csv
GET /api/v1/metadata/sessions/{id}/live¶
Subscribe to live sensor data from an active ingest session via Server-Sent Events (SSE). This is how the dashboard shows real-time charts.
Auth: JWT Bearer Token Role: Any authenticated user
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Session ID (must be active) |
Response: SSE stream
event: sample
data: {"stream_index":0,"timestamp_us":1718442000000000,"value":0.042}
event: sample
data: {"stream_index":1,"timestamp_us":1718442000000000,"value":0.039}
event: lag
data: {"skipped":10}
Events:
| Event | Description |
|---|---|
sample |
A new data sample arrived from the device |
lag |
Your client is falling behind; some events were skipped |
curl:
# Watch live vibration data (Ctrl+C to stop)
curl -N "https://api.xylolabs.com/api/v1/metadata/sessions/9c7d2e4f-.../live" \
-H "Authorization: Bearer $TOKEN"
13. System Configuration¶
Runtime system settings managed by Super Admins. Values are stored in the database and cached in memory, so changes take effect immediately without a restart.
GET /api/v1/config¶
Returns all configuration entries grouped by category.
Auth: JWT Bearer Token
Role: super_admin
Response 200 OK:
{
"categories": [
{
"category": "transcode",
"entries": [
{
"key": "transcode.default_format",
"value": "opus_webm",
"category": "transcode",
"value_type": "string",
"description": "Default transcode output format",
"updated_at": "2025-06-15T10:00:00Z"
},
{
"key": "transcode.default_bitrate",
"value": "128k",
"category": "transcode",
"value_type": "string",
"description": "Default transcode bitrate",
"updated_at": "2025-06-15T10:00:00Z"
}
]
}
]
}
curl:
# View all system configuration (super_admin only)
curl https://api.xylolabs.com/api/v1/config \
-H "Authorization: Bearer $TOKEN"
GET /api/v1/config/{key}¶
Fetches a single configuration entry.
Auth: JWT Bearer Token
Role: super_admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
key |
string | Configuration key (e.g. transcode.default_format) |
Response 200 OK:
{
"key": "transcode.default_format",
"value": "opus_webm",
"category": "transcode",
"value_type": "string",
"description": "Default transcode output format",
"updated_at": "2025-06-15T10:00:00Z"
}
Error Cases:
| Status | Condition |
|---|---|
403 Forbidden |
User is not a Super Admin |
404 Not Found |
Config key not found |
curl:
curl https://api.xylolabs.com/api/v1/config/transcode.default_format \
-H "Authorization: Bearer $TOKEN"
PUT /api/v1/config/{key}¶
Updates a configuration value.
Auth: JWT Bearer Token
Role: super_admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
key |
string | Configuration key |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
value |
any (JSON) | Yes | New value (type must match value_type of the config entry) |
Response 200 OK: Updated config entry object.
curl:
# Switch default transcode format from Opus/WebM to AAC/MP4
curl -X PUT https://api.xylolabs.com/api/v1/config/transcode.default_format \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"value":"aac_mp4"}'
POST /api/v1/config/reset/{key}¶
Resets a configuration value to its default.
Auth: JWT Bearer Token
Role: super_admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
key |
string | Configuration key |
Response 200 OK: Reset config entry object with default value.
curl:
# Oops, revert the format change back to the default
curl -X POST https://api.xylolabs.com/api/v1/config/reset/transcode.default_format \
-H "Authorization: Bearer $TOKEN"
14. Dashboard Stats¶
GET /api/v1/stats/overview¶
Returns a summary of system statistics for the dashboard home page.
Auth: JWT Bearer Token Role: Any authenticated user
Response 200 OK:
For Facility Admin / User (scoped to their facility):
For Super Admin (system-wide totals, including facility count):
curl:
15. Health¶
GET /api/health¶
Basic liveness check. Returns 200 OK if the server process is running. This endpoint doesn't verify database or storage connectivity.
Auth: None Role: None
Response 200 OK:
curl:
GET /api/health/ready¶
Readiness check that verifies both database and S3 connectivity. Use this for load balancer health checks so traffic doesn't get routed to an instance that can't actually serve requests.
Auth: None Role: None
Response 200 OK:
If S3 is unreachable (the server is still running but uploads will fail):
curl:
GET/POST /api/v1/ping¶
API key validation and connectivity test. Verifies the X-Api-Key header is valid and returns the key name and associated facility. Accepts an optional message field that gets echoed back.
Use this from MCU firmware to confirm the API key works before starting an ingest session.
Auth: API Key (X-Api-Key header)
Request body (optional, POST only):
Response 200 OK:
{
"pong": true,
"message": "hello from pico",
"api_key_name": "production-sensor-01",
"facility_id": "b666b8c3-bbc2-4675-a3ac-8b6a1e6dad9f"
}
If no body is sent, message defaults to "pong".
Errors:
- 401 — missing or invalid API key
curl:
# GET — quick key validation
curl -H "X-Api-Key: xk_your_key_here" https://api.xylolabs.com/api/v1/ping
# POST — with echo message
curl -X POST -H "X-Api-Key: xk_your_key_here" \
-H "Content-Type: application/json" \
-d '{"message":"test"}' \
https://api.xylolabs.com/api/v1/ping
16. XMBP Protocol Reference¶
XMBP (Xylolabs Metadata Binary Protocol) is a compact binary protocol for transmitting sensor data over constrained networks like LTE Cat-M1. All multi-byte numbers are big-endian.
Batch Envelope¶
Top-level structure contained in each HTTP POST body or WebSocket binary frame.
+--------+------+--------------------------------------+
| Offset | Size | Field |
+--------+------+--------------------------------------+
| 0 | 4 | magic (0x584D4250 = "XMBP") |
| 4 | 1 | version (1) |
| 5 | 1 | flags |
| 6 | 2 | batch_seq (u16) |
| 8 | 4 | device_id (u32, conditional) |
| 8/12 | 2 | stream_count (u16) |
| 10/14 | ... | StreamBlock[stream_count] |
+--------+------+--------------------------------------+
Flags:
| Bit | Name | Description |
|---|---|---|
| 0 (0x01) | HAS_DEVICE_ID |
When set, device_id field (4 bytes) is present at offset 8 |
| 1 (0x02) | ZSTD_COMPRESSED |
Reserved for future zstd compression |
| 2-7 | -- | Reserved |
When HAS_DEVICE_ID is set, stream_count starts at offset 12; otherwise at offset 8.
StreamBlock¶
Data block for a single stream (channel).
+--------+------+--------------------------------------+
| Offset | Size | Field |
+--------+------+--------------------------------------+
| 0 | 2 | stream_id (u16) |
| 2 | 1 | value_type (u8) |
| 3 | 2 | sample_count (u16) |
| 5 | ... | Sample[sample_count] |
+--------+------+--------------------------------------+
Sample Format¶
Fixed-size types (f32, f64, i32, i64, bool):
+--------+------+--------------------------------------+
| Offset | Size | Field |
+--------+------+--------------------------------------+
| 0 | 8 | timestamp_us (u64, microseconds) |
| 8 | N | value (type-dependent size) |
+--------+------+--------------------------------------+
Variable-size types (string, bytes, arrays, json):
+--------+------+--------------------------------------+
| Offset | Size | Field |
+--------+------+--------------------------------------+
| 0 | 8 | timestamp_us (u64, microseconds) |
| 8 | 2 | length (u16, bytes/elements) |
| 10 | ... | data (variable) |
+--------+------+--------------------------------------+
Value Type Tags¶
| Tag | Type | Size | Description |
|---|---|---|---|
| 0 | f64 |
8 bytes | 64-bit IEEE 754 float |
| 1 | f32 |
4 bytes | 32-bit IEEE 754 float |
| 2 | i64 |
8 bytes | 64-bit signed integer |
| 3 | i32 |
4 bytes | 32-bit signed integer |
| 4 | bool |
1 byte | 0 = false, non-zero = true |
| 5 | string |
variable | u16 length + UTF-8 bytes |
| 6 | bytes |
variable | u16 length + raw bytes |
| 7 | f64_array |
variable | u16 element count + f64[] |
| 8 | f32_array |
variable | u16 element count + f32[] |
| 9 | i32_array |
variable | u16 element count + i32[] |
| 10 | json |
variable | u32 length + UTF-8 JSON string |
| 11 | i16 |
2 bytes | 16-bit signed integer, big-endian |
| 12 | i8 |
1 byte | 8-bit signed integer (stored as u8 cast to i8) |
Building an XMBP Batch Step by Step¶
Here's how to construct a batch containing 2 samples of f32 data on stream 0, without a device_id in the header.
Step 1: Write the header
Bytes: 58 4D 42 50 01 00 00 07 00 01
^^^^^^^^^^^ ^^ ^^ ^^^^^ ^^^^^
magic XMBP v1 flags=0 seq=7 1 stream
flags = 0x00: NoHAS_DEVICE_ID, no compression.batch_seq = 7: This is the 8th batch (0-indexed).stream_count = 1: One stream block follows.
Step 2: Write the StreamBlock header
Step 3: Write each sample (timestamp + value)
Sample 1: timestamp = 1718442000000000 us (2025-06-15T10:00:00Z), value = 23.5 C
Bytes: 00 06 1B 7E 7C 4E 80 00 41 BC 00 00
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^
timestamp (u64 BE) 23.5 as f32 BE
Sample 2: timestamp = 1718442000002000 us (2 ms later), value = 23.6 C
Bytes: 00 06 1B 7E 7C 4E 87 D0 41 BC CC CD
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^
timestamp (u64 BE) 23.6 as f32 BE
Complete batch (34 bytes):
58 4D 42 50 01 00 00 07 00 01 -- header (10 bytes)
00 00 01 00 02 -- stream block header (5 bytes)
00 06 1B 7E 7C 4E 80 00 41 BC 00 00 -- sample 1: t=..., v=23.5 (12 bytes)
00 06 1B 7E 7C 4E 87 D0 41 BC CC CD -- sample 2: t=..., v=23.6 (12 bytes)
You can verify this with a hex dump after writing:
xxd batch.bin
# 00000000: 584d 4250 0100 0007 0001 0000 0100 02 XMBP............
# 00000010: 0006 1b7e 7c4e 8000 41bc 0000 0006 1b7e ...~|N..A......~
# 00000020: 7c4e 87d0 41bc cccd |N..A...
Batch Size Calculation¶
For fixed-size types, you can pre-compute the batch size:
Header size:
base = 4 (magic) + 1 (version) + 1 (flags) + 2 (batch_seq) + 2 (stream_count) = 10
with device_id: +4 = 14
StreamBlock size (fixed-size types):
block_header = 2 (stream_id) + 1 (value_type) + 2 (sample_count) = 5
sample_size = 8 (timestamp) + value_size
Total batch size:
header_size + SUM(5 + sample_count * (8 + value_size)) for each stream
Example: 4 channels of f32, 250 samples/batch, with device_id:
14 + 4 * (5 + 250 * (8 + 4))
= 14 + 4 * (5 + 250 * 12)
= 14 + 4 * 3005
= 14 + 12020
= 12,034 bytes (~12 KB)
Cat-M1 Optimization¶
On Cat-M1 with ~37 KB/s practical uplink, a 12 KB batch takes about 0.32 seconds to transmit. With 4 channels of f32 at 100 Hz, you produce 4 * 100 * 12 = 4,800 bytes/second of raw XMBP data. That fits comfortably in the Cat-M1 bandwidth, leaving room for HTTP overhead.
For higher sample rates, consider these strategies: - Use fewer channels per device (split across multiple physical devices). - Lower the sample rate for less-critical streams (e.g., temperature at 10 Hz instead of 100 Hz). - Batch more aggressively: 500 samples/batch at 100 Hz = one POST every 5 seconds.
17. RBAC Reference¶
Roles¶
| Role | Level | Description |
|---|---|---|
super_admin |
3 | Full system access across all facilities |
facility_admin |
2 | Manage users, keys, devices, and data within their facility |
user |
1 | Read access to data within their facility |
Role checks use >= comparison, so a super_admin passes any role check.
Permission Matrix¶
| Endpoint | super_admin |
facility_admin |
user |
|---|---|---|---|
| Facilities CRUD | Yes | -- | -- |
| Users CRUD | Yes | Own facility | -- |
| API Keys CRUD | Yes | Own facility | -- |
| Devices List/Get | Yes | Yes | Yes |
| Devices Create/Update | Yes | Own facility | -- |
| Uploads List/Get | Yes | Yes | Yes |
| Uploads Create (API Key) | Via API Key | Via API Key | Via API Key |
| Uploads Update/Delete | Yes | Yes | Yes |
| Uploads Retranscode | Yes | Yes | Yes |
| Streams Get | Yes | Yes | Yes |
| Transcode Jobs List/Get | Yes | Yes | Yes |
| Transcode Jobs Cancel | Yes | Own facility | -- |
| Tags List | Yes | Yes | Yes |
| Tags Create/Delete | Yes | Own facility | -- |
| Ingest (API Key) | Via API Key | Via API Key | Via API Key |
| Metadata Query | Yes | Yes | Yes |
| Config CRUD | Yes | -- | -- |
| Stats Overview | Yes | Yes | Yes |
Facility Isolation¶
- Non-Super Admin users can only access data within their assigned facility.
- API Keys are scoped to a single facility.
- Cross-facility access attempts return
403 Forbidden.
18. Error Responses¶
All errors follow a standard JSON format:
HTTP Status Codes¶
| Status | Meaning | Common Causes |
|---|---|---|
400 Bad Request |
Invalid request | Missing fields, invalid JSON, XMBP decode error |
401 Unauthorized |
Authentication failed | Invalid/missing JWT or API key, expired token |
403 Forbidden |
Insufficient permissions | Wrong role, cross-facility access |
404 Not Found |
Resource not found | Invalid ID, deleted resource |
409 Conflict |
Resource conflict | Duplicate email, duplicate slug, duplicate device_uid |
500 Internal Server Error |
Server error | Database error, S3 error, unexpected failure |
Example Error Responses¶
401 -- Invalid credentials:
403 -- Wrong role:
404 -- Missing resource:
409 -- Name collision:
19. Data Models¶
Facility¶
{
"id": "UUID",
"name": "string",
"slug": "string",
"description": "string | null",
"is_active": "boolean",
"created_at": "ISO 8601",
"updated_at": "ISO 8601"
}
User¶
{
"id": "UUID",
"facility_id": "UUID | null",
"email": "string",
"display_name": "string",
"role": "super_admin | facility_admin | user",
"is_active": "boolean",
"last_login_at": "ISO 8601 | null",
"created_at": "ISO 8601",
"updated_at": "ISO 8601"
}
ApiKey¶
{
"id": "UUID",
"facility_id": "UUID",
"key_prefix": "string",
"name": "string",
"scopes": ["string"],
"is_active": "boolean",
"expires_at": "ISO 8601 | null",
"last_used_at": "ISO 8601 | null",
"created_by": "UUID | null",
"created_at": "ISO 8601"
}
Device¶
{
"id": "UUID",
"facility_id": "UUID",
"device_uid": "integer",
"name": "string",
"hardware_version": "string | null",
"firmware_version": "string | null",
"is_active": "boolean",
"last_seen_at": "ISO 8601 | null",
"created_at": "ISO 8601"
}
Upload¶
{
"id": "UUID",
"facility_id": "UUID",
"api_key_id": "UUID | null",
"uploaded_by": "UUID | null",
"original_filename": "string",
"original_format": "string",
"original_size_bytes": "integer",
"duration_ms": "integer | null",
"sample_rate_hz": "integer | null",
"channels": "integer | null",
"bit_depth": "integer | null",
"codec": "string | null",
"metadata": "object",
"status": "pending | processing | completed | failed",
"tags": ["Tag"],
"created_at": "ISO 8601",
"updated_at": "ISO 8601"
}
TranscodeJob¶
{
"id": "UUID",
"upload_id": "UUID",
"facility_id": "UUID",
"target_format": "string",
"target_codec": "string",
"target_container": "string",
"target_bitrate": "string | null",
"target_sample_rate": "integer | null",
"output_size_bytes": "integer | null",
"output_duration_ms": "integer | null",
"status": "queued | running | completed | failed | cancelled",
"error_message": "string | null",
"attempts": "integer",
"max_attempts": "integer",
"queued_at": "ISO 8601",
"started_at": "ISO 8601 | null",
"completed_at": "ISO 8601 | null",
"worker_id": "string | null"
}
Tag¶
{
"id": "UUID",
"facility_id": "UUID",
"name": "string",
"color": "string | null",
"created_at": "ISO 8601"
}
IngestSession¶
{
"id": "UUID",
"facility_id": "UUID",
"device_id": "UUID",
"status": "active | closing | closed | error",
"started_at": "ISO 8601",
"total_samples": "integer",
"total_bytes": "integer",
"missed_batches": "integer",
"streams": ["MetadataStream"]
}
MetadataStream¶
{
"id": "UUID",
"stream_index": "integer",
"name": "string",
"value_type": "f32 | f64 | i32 | i64 | bool | string | bytes | f64_array | f32_array | i32_array | json | i16 | i8",
"unit": "string | null",
"sample_rate_hz": "float | null"
}
ConfigEntry¶
{
"key": "string",
"value": "any (JSON)",
"category": "string",
"value_type": "string",
"description": "string | null",
"updated_at": "ISO 8601"
}
20. Pagination¶
Paginated endpoints use consistent query parameters and response format.
Query Parameters¶
| Parameter | Type | Default | Range | Description |
|---|---|---|---|---|
page |
integer | 1 | >= 1 | Page number (1-based) |
per_page |
integer | 20 | 1-100 | Items per page |
Response Format¶
| Field | Type | Description |
|---|---|---|
items |
array | Data items for the current page |
total |
integer | Total number of items across all pages |
page |
integer | Current page number |
per_page |
integer | Items per page |
Paginated Endpoints¶
GET /api/v1/uploadsGET /api/v1/transcode-jobsGET /api/v1/metadata/sessions
21. Example Workflows¶
Complete Setup Flow¶
Set up a new facility from scratch: create the facility, add a user, generate an API key, and upload the first recording.
# 1. Log in as Super Admin to get a JWT
TOKEN=$(curl -s -X POST https://api.xylolabs.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"minseok.jeon@xylolabs.com","password":"admin-password"}' \
| jq -r '.access_token')
# 2. Create the facility
FACILITY_ID=$(curl -s -X POST https://api.xylolabs.com/api/v1/facilities \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "페리지에어로스페이스 옥천공장",
"slug": "perigee-okcheon",
"description": "옥천 생산공장 음향 모니터링"
}' | jq -r '.id')
echo "Created facility: $FACILITY_ID"
# 3. Create a facility admin who'll manage day-to-day operations
USER_ID=$(curl -s -X POST https://api.xylolabs.com/api/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"email\": \"minseok.jeon@xylolabs.com\",
\"password\": \"Kj8mP2xvNq!\",
\"display_name\": \"전민석\",
\"role\": \"facility_admin\",
\"facility_id\": \"$FACILITY_ID\"
}" | jq -r '.id')
echo "Created user: $USER_ID"
# 4. Log in as the new facility admin
FA_TOKEN=$(curl -s -X POST https://api.xylolabs.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"minseok.jeon@xylolabs.com","password":"Kj8mP2xvNq!"}' \
| jq -r '.access_token')
# 5. Generate an API key for the sensor device in the field
API_KEY=$(curl -s -X POST https://api.xylolabs.com/api/v1/api-keys \
-H "Authorization: Bearer $FA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"옥천공장 센서 유닛 #1","scopes":["upload","ingest"]}' \
| jq -r '.key')
echo "API key (save this!): $API_KEY"
# 6. Upload the first field recording using the API key
curl -X POST https://api.xylolabs.com/api/v1/uploads \
-H "X-Api-Key: $API_KEY" \
-F "file=@okcheon_factory_2025-06-15.flac" \
-F 'metadata={"source":"sensor-array","location":"옥천공장 생산라인 A","facility":"perigee-okcheon"}'
# 7. Check that the upload showed up and transcoding started
curl -s "https://api.xylolabs.com/api/v1/uploads?page=1" \
-H "Authorization: Bearer $FA_TOKEN" | jq '.items[0] | {status, original_filename}'
Device Metadata Streaming Flow¶
Create an ingest session, send XMBP sensor data, close the session, and export the results.
# 1. Start a monitoring session with vibration and temperature streams
SESSION_ID=$(curl -s -X POST https://api.xylolabs.com/api/v1/ingest/sessions \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"device_uid": 1,
"name": "Turbine #7 morning run 2025-06-16",
"streams": [
{"stream_index":0,"name":"vibration_x","value_type":"f32","unit":"g","sample_rate_hz":500},
{"stream_index":1,"name":"vibration_y","value_type":"f32","unit":"g","sample_rate_hz":500},
{"stream_index":2,"name":"bearing_temp","value_type":"f32","unit":"celsius","sample_rate_hz":10}
],
"metadata": {"location":"Hall B, Turbine #7","firmware":"2.1.0"}
}' | jq -r '.id')
echo "Session started: $SESSION_ID"
# 2. Send XMBP binary batches in a loop (your device firmware does this)
# Each batch contains 250 samples at 500 Hz = one batch every 0.5 seconds
for i in $(seq 1 100); do
curl -s -X POST "https://api.xylolabs.com/api/v1/ingest/sessions/$SESSION_ID/data" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @batch_${i}.bin > /dev/null
done
# 3. Close the session when the monitoring period ends
curl -s -X POST "https://api.xylolabs.com/api/v1/ingest/sessions/$SESSION_ID/close" \
-H "X-Api-Key: $API_KEY" | jq '{status, total_samples, total_bytes, missed_batches}'
# 4. Review the session from the dashboard
curl -s "https://api.xylolabs.com/api/v1/metadata/sessions/$SESSION_ID" \
-H "Authorization: Bearer $TOKEN" | jq '.streams[] | {name, sample_rate_hz}'
# 5. Export all session data as CSV for analysis
curl "https://api.xylolabs.com/api/v1/metadata/sessions/$SESSION_ID/export?format=csv" \
-H "Authorization: Bearer $TOKEN" \
--output turbine7_2025-06-16.csv
echo "Exported to turbine7_2025-06-16.csv"
Admin Configuration Flow¶
Check current transcode settings, change them, verify, and revert if needed.
# 1. Log in as Super Admin
TOKEN=$(curl -s -X POST https://api.xylolabs.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"minseok.jeon@xylolabs.com","password":"admin-password"}' \
| jq -r '.access_token')
# 2. See what format uploads are being transcoded to
curl -s https://api.xylolabs.com/api/v1/config/transcode.default_format \
-H "Authorization: Bearer $TOKEN" | jq '.value'
# "opus_webm"
# 3. Switch to AAC/MP4 for better Safari compatibility
curl -s -X PUT https://api.xylolabs.com/api/v1/config/transcode.default_format \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"value":"aac_mp4"}' | jq '.value'
# "aac_mp4"
# 4. Changed your mind? Reset to the default
curl -s -X POST https://api.xylolabs.com/api/v1/config/reset/transcode.default_format \
-H "Authorization: Bearer $TOKEN" | jq '.value'
# "opus_webm"
# 5. Check the dashboard stats while you're here
curl -s https://api.xylolabs.com/api/v1/stats/overview \
-H "Authorization: Bearer $TOKEN"
# {"users":47,"uploads":1284,"facilities":5}
Field Deployment with Cat-M1 Devices¶
End-to-end workflow for deploying Cat-M1-connected sensors at a remote field station. This covers provisioning, firmware configuration, and data retrieval.
# === PROVISIONING (done from your laptop before going to the field) ===
# 1. Log in
TOKEN=$(curl -s -X POST https://api.xylolabs.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"minseok.jeon@xylolabs.com","password":"admin-password"}' \
| jq -r '.access_token')
# 2. Create the remote facility
FACILITY_ID=$(curl -s -X POST https://api.xylolabs.com/api/v1/facilities \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "페리지에어로스페이스 미래기술연구소",
"slug": "perigee-futuretech",
"description": "미래기술연구소 센서 데이터 수집"
}' | jq -r '.id')
# 3. Create a facility admin for the field team
curl -s -X POST https://api.xylolabs.com/api/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"email\": \"minseok.jeon@xylolabs.com\",
\"password\": \"FieldWork2025!\",
\"display_name\": \"전민석\",
\"role\": \"facility_admin\",
\"facility_id\": \"$FACILITY_ID\"
}" > /dev/null
# 4. Log in as the field team lead
FA_TOKEN=$(curl -s -X POST https://api.xylolabs.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"minseok.jeon@xylolabs.com","password":"FieldWork2025!"}' \
| jq -r '.access_token')
# 5. Generate API keys for three Cat-M1 sensor units
for i in 1 2 3; do
KEY=$(curl -s -X POST https://api.xylolabs.com/api/v1/api-keys \
-H "Authorization: Bearer $FA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"Cat-M1 Unit #${i}\",\"scopes\":[\"upload\",\"ingest\"]}" \
| jq -r '.key')
echo "Unit #${i} API key: $KEY"
# Flash this key into each unit's firmware config
done
# 6. Pre-register the devices with hardware info
curl -s -X POST https://api.xylolabs.com/api/v1/devices \
-H "Authorization: Bearer $FA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"device_uid":1,"name":"Corridor North Recorder","hardware_version":"nrf9160-dk","firmware_version":"1.0.3"}' > /dev/null
curl -s -X POST https://api.xylolabs.com/api/v1/devices \
-H "Authorization: Bearer $FA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"device_uid":2,"name":"Stream Crossing Hydrophone","hardware_version":"bg96-shield","firmware_version":"0.9.1"}' > /dev/null
curl -s -X POST https://api.xylolabs.com/api/v1/devices \
-H "Authorization: Bearer $FA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"device_uid":3,"name":"Hillside Bat Detector","hardware_version":"nrf9160-dk","firmware_version":"1.0.3"}' > /dev/null
# === WHAT THE DEVICE FIRMWARE DOES (Cat-M1 units in the field) ===
# Each unit powers on, attaches to the Cat-M1 network, and runs this cycle:
#
# a) POST /api/v1/ingest/sessions -- create session (once at boot)
# b) Build XMBP batch (250 samples of f32 at 100 Hz = every 2.5 seconds)
# c) POST /api/v1/ingest/sessions/{id}/data -- send XMBP batch
# d) Repeat (b)-(c) for the recording period
# e) POST /api/v1/ingest/sessions/{id}/close -- close session
# f) Enter PSM (power save mode) for 4 hours, then repeat from (a)
# === DATA RETRIEVAL (back at the office, days later) ===
# 7. Check which sessions came in from the field
curl -s "https://api.xylolabs.com/api/v1/metadata/sessions?status=closed&per_page=50" \
-H "Authorization: Bearer $FA_TOKEN" \
| jq '.items[] | {id: .id, device: .device_id, samples: .total_samples, missed: .missed_batches}'
# 8. Export a session for analysis
SESSION_ID="<pick a session id from the list above>"
curl "https://api.xylolabs.com/api/v1/metadata/sessions/$SESSION_ID/export?format=csv" \
-H "Authorization: Bearer $FA_TOKEN" \
--output dmz_corridor_session.csv
22. LTE Cat-M1 Integration Guide¶
Cat-M1 (LTE-M) is a low-power wide-area cellular technology designed for IoT devices. It works well for Xylolabs sensor deployments in locations without WiFi or Ethernet, like remote field stations, offshore buoys, and rural monitoring posts.
This section covers modem selection, bandwidth planning, AT command examples, and deployment tips specific to using Xylolabs with Cat-M1 connectivity.
Supported Modems¶
Any modem that can issue HTTP requests over Cat-M1 will work with Xylolabs. These are the modules we've tested or that community members have used successfully:
| Module | Manufacturer | Interface | Power Features | Notes |
|---|---|---|---|---|
| BG770A | Quectel | UART AT commands | PSM, eDRX | Cat-M1/NB-IoT with excellent power management. |
| SARA-R410M / R412M | u-blox | UART AT commands | PSM, eDRX | Excellent power management documentation. R412M adds NB-IoT fallback. |
| nRF9160 | Nordic Semiconductor | Integrated SoC | PSM, eDRX | System-in-package with ARM Cortex-M33 + modem. Use Nordic's SDK or AT commands via the dedicated AT client firmware. |
| HL7800 / HL7812 | Sierra Wireless | UART AT commands | PSM, eDRX | Carrier-certified on major US/EU networks. Good for deployments requiring carrier approval. |
| Type 1SC | Murata | UART AT commands | PSM, eDRX | Very compact (10.5 x 9.8 mm). Based on the ALT1250 chipset. Good for space-constrained PCB designs. |
For new projects, the nRF9160 is the easiest path if you want a single chip doing both sensing and cellular. If you already have a microcontroller and need to add connectivity, the Quectel BG770A has the most community resources and example code.
Bandwidth Budget¶
Cat-M1 provides roughly 375 kbps theoretical uplink, but in practice you should plan for about 300 kbps (~37 KB/s) after protocol overhead and real-world conditions.
How many channels of f32 at 100 Hz can you fit?
Each f32 sample in XMBP costs 12 bytes (8 bytes timestamp + 4 bytes value):
Per channel: 100 samples/sec * 12 bytes/sample = 1,200 bytes/sec
With 4 channels: 4 * 1,200 = 4,800 bytes/sec
With header overhead (~14 bytes/batch + 5 bytes/stream + HTTP headers):
Roughly 5,200 bytes/sec total
Available uplink: ~37,000 bytes/sec
Utilization: 5,200 / 37,000 = 14%
That leaves plenty of headroom. You could push up to ~25 channels of f32 at 100 Hz before saturating the link, though you'll want to keep utilization below 70% to handle retransmissions and latency spikes.
Recommended batch sizing for Cat-M1:
| Sample Rate | Samples per Batch | Send Interval | Batch Size (4 ch) | Cat-M1 TX Time |
|---|---|---|---|---|
| 10 Hz | 50 | 5.0 sec | ~2.5 KB | ~70 ms |
| 100 Hz | 250 | 2.5 sec | ~12 KB | ~330 ms |
| 500 Hz | 500 | 1.0 sec | ~24 KB | ~650 ms |
| 1000 Hz | 1000 | 1.0 sec | ~48 KB | ~1.3 sec |
For battery-powered devices, bigger batches sent less frequently are more energy-efficient because the modem spends less time in active TX mode.
AT Command Examples (Quectel BG770A)¶
These examples show the complete AT command sequence for connecting a BG770A to the cellular network and posting XMBP data to Xylolabs. Adapt as needed for other modems (the AT commands are similar across vendors).
1. Network Registration and Attachment¶
# Check if the SIM is ready
AT+CPIN?
+CPIN: READY
# Set the APN for your carrier (example: SKT Korea)
AT+CGDCONT=1,"IP","lte.sktelecom.com"
OK
# Enable Cat-M1 mode only (disable NB-IoT)
AT+QCFG="nwscanseq",02
OK
AT+QCFG="iotopmode",0
OK
# Trigger network registration
AT+COPS=0
OK
# Wait for registration (poll until +CREG: 0,1 or 0,5)
AT+CREG?
+CREG: 0,1 <-- 1 = registered (home), 5 = registered (roaming)
# Verify data attachment
AT+CGATT?
+CGATT: 1 <-- 1 = attached to packet domain
2. PDP Context Activation¶
# Activate the PDP context for data
AT+QIACT=1
OK
# Verify IP assignment
AT+QIACT?
+QIACT: 1,1,1,"10.47.128.33"
# ^^^^^^^^^^^^^ your assigned IP address
3. HTTP POST for XMBP Data Upload¶
# Configure HTTP context
AT+QHTTPCFG="contextid",1
OK
# Set the target URL for data upload
AT+QHTTPCFG="sslctxid",1
AT+QSSLCFG="seclevel",1,0
AT+QHTTPURL=82,10
CONNECT
https://api.xylolabs.com/api/v1/ingest/sessions/<SESSION_ID>/data
OK
# Set custom headers: API key and content type
AT+QHTTPCFG="requestheader",1
OK
# Prepare and send the XMBP binary payload
# The raw data must be preceded by the HTTP headers
AT+QHTTPPOST=<total_size>,30,30
CONNECT
POST /api/v1/ingest/sessions/<SESSION_ID>/data HTTP/1.1\r\n
Host: api.xylolabs.com\r\n
X-Api-Key: xk_t4r78k2m5p7r...\r\n
Content-Type: application/octet-stream\r\n
Content-Length: <xmbp_batch_size>\r\n
\r\n
<raw XMBP binary bytes here>
OK
+QHTTPPOST: 0,200 <-- 200 = success
4. HTTP POST for Audio File Upload (Chunked)¶
For large audio files over Cat-M1, you'll need to send the multipart form data in chunks. The BG770A has a limited RAM buffer (typically 4-10 KB for AT+QHTTPPOST), so you'll need to use chunked transfer or split the file.
# For files under 10 KB, you can use a single POST:
AT+QHTTPURL=55,10
CONNECT
https://api.xylolabs.com/api/v1/uploads
OK
# Build multipart body in your MCU's memory, then:
AT+QHTTPPOST=<total_size>,60,60
CONNECT
POST /api/v1/uploads HTTP/1.1\r\n
Host: api.xylolabs.com\r\n
X-Api-Key: xk_t4r78k2m5p7r...\r\n
Content-Type: multipart/form-data; boundary=XyloUpload2025\r\n
Content-Length: <body_size>\r\n
\r\n
--XyloUpload2025\r\n
Content-Disposition: form-data; name="file"; filename="hydrophone_reef_sample.flac"\r\n
Content-Type: audio/flac\r\n
\r\n
<FLAC file bytes>\r\n
--XyloUpload2025\r\n
Content-Disposition: form-data; name="metadata"\r\n
\r\n
{"source":"bg770a-hydrophone","depth_m":12}\r\n
--XyloUpload2025--\r\n
OK
+QHTTPPOST: 0,200
# For files larger than the AT buffer, use your MCU to split the file
# into chunks and use AT+QHTTPPOSTFILE with a file stored on the modem's
# filesystem, or implement HTTP chunked transfer encoding in your firmware.
5. Power Save Mode (PSM) Configuration¶
PSM lets the modem sleep for extended periods while keeping its network registration. This is critical for battery-powered field deployments.
# Request PSM with specific timers:
# T3324 (Active Timer) = 20 seconds -- how long the modem stays reachable after sending data
# T3412 (TAU Timer) = 4 hours -- how long until the next Tracking Area Update
#
# Timer encoding (3GPP TS 24.008):
# T3324: "00000100" = 4 * 2s multiples = 20 seconds (bits 7-6: 00 = 2s unit)
# Wait, let's use the right encoding:
# T3324: "00001010" = 10 * 2s = 20 seconds
# T3412: "00100100" = 4 * 1h = 4 hours (bits 7-6: 00 = 10min... )
#
# Simpler: just ask for the values and let the network negotiate
AT+CPSMS=1,,,"00100100","00001010"
OK
# Verify what the network granted
AT+CPSMS?
+CPSMS: 1,,,"00100100","00001010"
# T3412=4h T3324=20s
# Your firmware loop should look like:
# 1. Wake from PSM (modem auto-attaches)
# 2. Create session or resume
# 3. Collect and send data batches
# 4. Close session
# 5. Enter PSM: AT+QSCLK=1 (the modem handles the rest)
# 6. MCU goes to deep sleep, RTC wakes it after desired interval
Example: Field Deployment Workflow¶
This walks through a realistic deployment scenario with Cat-M1 devices.
Scenario: You're deploying three battery-powered sensors along a remote river valley. Each unit has an nRF9160 modem, a MEMS accelerometer, a temperature sensor, and a microphone. They'll wake every 4 hours, record 10 minutes of data, upload it, and go back to sleep.
Phase 1: Office preparation
Flash each unit with: - The facility's API key (from step 5 in the provisioning workflow above) - The APN for your SIM carrier - Stream definitions (accelerometer x/y/z at 100 Hz, temperature at 1 Hz) - Recording duration (10 minutes) - Sleep interval (4 hours)
Phase 2: What the firmware does each cycle
Boot
|
+-- Initialize modem (AT+CFUN=1)
|
+-- Wait for CREG: 0,1 (registered)
|
+-- Activate PDP context (AT+QIACT=1)
|
+-- POST /ingest/sessions (create session with 4 streams)
| Save the session_id in RAM
|
+-- Start sampling loop:
| Collect 250 samples (~2.5 sec at 100 Hz)
| Build XMBP batch in RAM (~12 KB)
| POST /ingest/sessions/{id}/data
| Check HTTP 200, increment batch_seq
| Repeat for 10 minutes (240 batches)
|
+-- POST /ingest/sessions/{id}/close
|
+-- (Optional) POST /uploads with a 10-min audio clip
| Only if there's enough battery and the file is small enough
|
+-- Enter PSM (AT+CPSMS=1, then deep sleep)
|
+-- [4 hours pass, RTC alarm fires, back to Boot]
Phase 3: Data retrieval from your desk
# Log in and check how many sessions came in overnight
curl -s "https://api.xylolabs.com/api/v1/metadata/sessions?status=closed&per_page=100" \
-H "Authorization: Bearer $TOKEN" \
| jq '[.items[] | {device: .device_id, started: .started_at, samples: .total_samples, missed: .missed_batches}]'
# Output:
# [
# {"device":"..north..","started":"2025-06-16T02:00:00Z","samples":96000,"missed":0},
# {"device":"..north..","started":"2025-06-16T06:00:00Z","samples":96000,"missed":1},
# {"device":"..crossing..","started":"2025-06-16T02:05:00Z","samples":96000,"missed":0},
# ...
# ]
Battery Life Estimation¶
For a typical deployment with a 3.7V 6000 mAh LiPo battery:
| Component | Current Draw | Time per Cycle | Energy per Cycle |
|---|---|---|---|
| MCU active + sensing | ~15 mA | 10 min | 2.5 mAh |
| Modem TX (Cat-M1) | ~200 mA avg | ~3 min total | 10 mAh |
| Modem idle (between POSTs) | ~10 mA | ~7 min | 1.2 mAh |
| PSM sleep (modem + MCU) | ~10 uA | 3 hr 50 min | 0.64 mAh |
| Total per cycle | 4 hours | ~14.3 mAh |
At 6 cycles per day (every 4 hours): 85.8 mAh/day. Battery life: ~70 days (6000 mAh / 85.8 mAh/day), before accounting for self-discharge and temperature effects.
In cold weather (below -10 C), expect 30-50% less battery capacity. Plan for swaps or solar charging for extended deployments.