Skip to content

Xylolabs API 레퍼런스

버전: v1 최종 수정: 2026-03-22


목차

  1. 개요
  2. 인증
  3. 시설 관리
  4. 사용자 관리
  5. API 키
  6. 디바이스
  7. 오디오 업로드
  8. 오디오 스트리밍
  9. 트랜스코딩 작업
  10. 태그
  11. 메타데이터 수집
  12. 메타데이터 조회
  13. 시스템 설정
  14. 대시보드 통계
  15. 헬스 체크
  16. XMBP 프로토콜 레퍼런스
  17. RBAC 레퍼런스
  18. 오류 응답
  19. 데이터 모델
  20. 페이지네이션
  21. 워크플로 예제
  22. LTE Cat-M1 통합 가이드

1. 개요

Xylolabs API란?

Xylolabs API는 IoT/임베디드 디바이스의 오디오와 센서 메타데이터를 통합 관리하는 백엔드 서비스다. Pico, ESP32, Cat-M1 모뎀 등 현장 장비가 데이터를 보내면 서버가 저장, 변환, 조회를 처리한다.

핵심 기능은 다음과 같다.

  • 오디오 업로드 및 트랜스코딩: WAV, FLAC 등 원본 파일을 업로드하면 Opus/WebM, AAC/MP4 등 웹 재생용 포맷으로 자동 변환한다.
  • 실시간 메타데이터 스트리밍: XMBP 바이너리 프로토콜로 센서 데이터를 HTTP 또는 WebSocket으로 전송한다. LTE Cat-M1처럼 대역폭이 제한된 환경에 맞춰 설계했다.
  • 멀티테넌트 시설 격리: 모든 데이터를 시설(Facility) 단위로 격리하고 역할 기반 접근 제어(RBAC)를 적용한다.
  • 관리 대시보드 API: 시설, 사용자, API 키, 디바이스, 시스템 설정의 CRUD를 지원한다.

빠른 시작

5분 안에 첫 데이터를 전송하려면 아래 순서를 따른다.

# Step 1: Log in and grab a token
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@xylolabs.com","password":"changeme"}' \
  | jq -r '.access_token')

# Step 2: Create a facility
FACILITY_ID=$(curl -s -X POST http://localhost:3000/api/v1/facilities \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"페리지에어로스페이스 옥천공장","slug":"perigee-okcheon"}' \
  | jq -r '.id')

# Step 3: Create an API key for device ingestion
API_KEY=$(curl -s -X POST http://localhost:3000/api/v1/api-keys \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"터빈홀 센서 어레이","scopes":["upload","ingest"]}' \
  | jq -r '.key')

# Step 4: Upload your first audio file
curl -X POST http://localhost:3000/api/v1/uploads \
  -H "X-Api-Key: $API_KEY" \
  -F "file=@turbine_hall_2025-06-15_14h30.wav" \
  -F 'metadata={"location":"turbine_hall","sample_rate":384000}'

echo "Done -- check the dashboard to see your upload."

아키텍처

임베디드 디바이스 (Pico, ESP32, Cat-M1 모뎀 등)
  |
  +-- 오디오 업로드 (multipart HTTP) ---> S3 (원본) ---> 자동 트랜스코딩 ---> S3 (웹 재생용)
  |
  +-- 메타데이터 스트리밍 (XMBP) -------> IngestManager ---> PostgreSQL + S3 청크
  |
  +-- LTE Cat-M1 (AT 명령 HTTP POST) ---> 위와 동일 경로 (저대역폭 최적화)

관리 대시보드 (웹)
  |
  +-- JWT 인증 ------> REST API --------> PostgreSQL / S3

기본 URL

환경 Base URL
프로덕션 API https://api.xylolabs.com/api
관리 대시보드 https://admin.api.xylolabs.com
개발 환경 http://localhost:3000/api

인증 방식

방식 헤더 사용 주체 대상 엔드포인트
JWT Bearer Token Authorization: Bearer <token> 대시보드, 웹 클라이언트 모든 /api/v1/* 보호 경로
API Key X-Api-Key: xk_... 임베디드 디바이스 업로드, 수집

2. 인증

POST /api/auth/login

이메일과 비밀번호로 로그인한다. 성공하면 JWT access token과 refresh token 쌍을 돌려준다.

인증: 없음 필요 역할: 없음

요청 본문:

{
  "email": "minseok.jeon@xylolabs.com",
  "password": "your-password"
}
필드 타입 필수 설명
email string O 사용자 이메일
password string O 비밀번호

응답 200 OK:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "minseok.jeon@xylolabs.com",
    "display_name": "전민석",
    "role": "super_admin",
    "facility_id": null
  }
}

오류 케이스:

상태 코드 조건
401 Unauthorized 이메일 또는 비밀번호가 올바르지 않음
401 Unauthorized 비활성 계정

curl:

# Log in as super admin
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

리프레시 토큰으로 access token을 갱신하고 새 리프레시 토큰으로 회전한다. access token이 만료됐을 때 재로그인 없이 새 토큰을 받을 수 있다.

인증: 없음 필요 역할: 없음

요청 본문:

{
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
필드 타입 필수 설명
refresh_token string O 로그인 시 발급받은 리프레시 토큰

응답 200 OK:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

오류 케이스:

상태 코드 조건
401 Unauthorized 유효하지 않거나 만료된 리프레시 토큰
401 Unauthorized 사용자를 찾을 수 없거나 비활성 상태

curl:

# Refresh an expired access token
curl -X POST https://api.xylolabs.com/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token":"eyJhbGciOiJIUzI1NiIs..."}'

GET /api/auth/me

현재 로그인한 사용자 정보를 돌려준다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

응답 200 OK:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "email": "minseok.jeon@xylolabs.com",
  "display_name": "전민석",
  "role": "super_admin",
  "facility_id": null
}

오류 케이스:

상태 코드 조건
401 Unauthorized JWT가 없거나 유효하지 않음

curl:

# Check who you are logged in as
curl https://api.xylolabs.com/api/auth/me \
  -H "Authorization: Bearer $TOKEN"

3. 시설 관리

시설(Facility)은 Xylolabs의 최상위 조직 단위다. 사용자, 디바이스, 업로드, 수집 세션 등 모든 데이터가 특정 시설에 소속되며, 시설 간 데이터는 완전히 격리된다. 시설 CRUD는 슈퍼 관리자만 할 수 있다.

GET /api/v1/facilities

등록된 전체 시설 목록을 돌려준다.

인증: JWT Bearer Token 필요 역할: super_admin

응답 200 OK:

[
  {
    "id": "660e8400-e29b-41d4-a716-446655440000",
    "name": "페리지에어로스페이스 옥천공장",
    "slug": "perigee-okcheon",
    "description": "옥천 생산공장 음향 모니터링",
    "is_active": true,
    "created_at": "2025-01-15T09:00:00Z",
    "updated_at": "2025-01-15T09:00:00Z"
  },
  {
    "id": "660e8400-e29b-41d4-a716-446655440001",
    "name": "페리지에어로스페이스 미래기술연구소",
    "slug": "perigee-futuretech",
    "description": "미래기술연구소 센서 데이터 수집",
    "is_active": true,
    "created_at": "2025-02-20T14:00:00Z",
    "updated_at": "2025-02-20T14: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

새 시설을 만든다.

인증: JWT Bearer Token 필요 역할: super_admin

요청 본문:

{
  "name": "페리지에어로스페이스 옥천공장",
  "slug": "perigee-okcheon",
  "description": "옥천 생산공장 음향 모니터링"
}
필드 타입 필수 설명
name string O 시설 표시 이름
slug string O URL에 사용할 고유 식별자 (영문, 하이픈)
description string X 시설 설명

응답 200 OK:

{
  "id": "660e8400-e29b-41d4-a716-446655440002",
  "name": "페리지에어로스페이스 옥천공장",
  "slug": "perigee-okcheon",
  "description": "옥천 생산공장 음향 모니터링",
  "is_active": true,
  "created_at": "2025-06-15T10:00:00Z",
  "updated_at": "2025-06-15T10:00:00Z"
}

오류 케이스:

상태 코드 조건
403 Forbidden 슈퍼 관리자가 아닌 경우
409 Conflict 동일한 slug가 이미 존재

curl:

# Create a new wildlife monitoring 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}

ID로 시설 하나를 조회한다.

인증: JWT Bearer Token 필요 역할: super_admin

경로 파라미터:

파라미터 타입 설명
id UUID 시설 ID

응답 200 OK:

{
  "id": "660e8400-e29b-41d4-a716-446655440000",
  "name": "페리지에어로스페이스 옥천공장",
  "slug": "perigee-okcheon",
  "description": "옥천 생산공장 음향 모니터링",
  "is_active": true,
  "created_at": "2025-01-15T09:00:00Z",
  "updated_at": "2025-01-15T09:00:00Z"
}

오류 케이스:

상태 코드 조건
403 Forbidden 슈퍼 관리자가 아닌 경우
404 Not Found 해당 ID의 시설이 없음

curl:

# Fetch a single facility by ID
curl https://api.xylolabs.com/api/v1/facilities/660e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $TOKEN"

PATCH /api/v1/facilities/{id}

시설 정보를 부분 수정한다. 보낸 필드만 업데이트되고 나머지는 그대로 유지된다.

인증: JWT Bearer Token 필요 역할: super_admin

경로 파라미터:

파라미터 타입 설명
id UUID 시설 ID

요청 본문:

{
  "name": "페리지에어로스페이스 옥천공장 (2호동)",
  "description": "2호동 이전 완료",
  "is_active": true
}
필드 타입 필수 설명
name string X 새 시설 이름
description string X 새 설명
is_active boolean X 활성화/비활성화 토글

응답 200 OK: 수정된 시설 객체 (GET과 동일한 형식).

오류 케이스:

상태 코드 조건
403 Forbidden 슈퍼 관리자가 아닌 경우
404 Not Found 해당 ID의 시설이 없음

curl:

# Rename a facility
curl -X PATCH https://api.xylolabs.com/api/v1/facilities/660e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"페리지에어로스페이스 옥천공장 (2호동)"}'

DELETE /api/v1/facilities/{id}

시설을 삭제한다.

인증: JWT Bearer Token 필요 역할: super_admin

경로 파라미터:

파라미터 타입 설명
id UUID 시설 ID

응답 204 No Content

오류 케이스:

상태 코드 조건
403 Forbidden 슈퍼 관리자가 아닌 경우
404 Not Found 해당 ID의 시설이 없음

curl:

# Delete a facility permanently
curl -X DELETE https://api.xylolabs.com/api/v1/facilities/660e8400-e29b-41d4-a716-446655440002 \
  -H "Authorization: Bearer $TOKEN"

4. 사용자 관리

사용자는 시설에 소속되며 역할에 따라 접근 권한이 달라진다. 시설 관리자는 자기 시설 안의 사용자만 관리할 수 있고, 슈퍼 관리자는 전체 사용자를 관리한다.

GET /api/v1/users

사용자 목록을 조회한다. 시설 관리자라면 자기 시설의 사용자만, 슈퍼 관리자라면 전체 사용자가 표시된다.

인증: JWT Bearer Token 필요 역할: facility_admin 이상

응답 200 OK:

[
  {
    "id": "550e8400-e29b-41d4-a716-446655440001",
    "facility_id": "660e8400-e29b-41d4-a716-446655440000",
    "email": "minseok.jeon@xylolabs.com",
    "display_name": "전민석",
    "role": "user",
    "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": "550e8400-e29b-41d4-a716-446655440002",
    "facility_id": "660e8400-e29b-41d4-a716-446655440000",
    "email": "dongyun.shin@xylolabs.com",
    "display_name": "신동윤",
    "role": "facility_admin",
    "is_active": true,
    "last_login_at": "2025-06-14T17:45:00Z",
    "created_at": "2025-02-01T10:00:00Z",
    "updated_at": "2025-06-14T17:45:00Z"
  }
]

curl:

# List users visible to the current account
curl https://api.xylolabs.com/api/v1/users \
  -H "Authorization: Bearer $TOKEN"

POST /api/v1/users

새 사용자를 만든다.

인증: JWT Bearer Token 필요 역할: facility_admin 이상

요청 본문:

{
  "email": "minseok.jeon@xylolabs.com",
  "password": "Xylo#2025secure",
  "display_name": "전민석",
  "role": "user",
  "facility_id": "660e8400-e29b-41d4-a716-446655440000"
}
필드 타입 필수 설명
email string O 고유한 이메일 주소
password string O 비밀번호 (서버에서 Argon2로 해시 처리)
display_name string O 표시 이름
role string O super_admin, facility_admin, user 중 하나
facility_id UUID X 소속 시설 (슈퍼 관리자만 지정 가능; 시설 관리자는 자기 시설에 자동 배정)

응답 200 OK: 생성된 사용자 객체 (목록 항목과 동일한 형식).

오류 케이스:

상태 코드 조건
403 Forbidden 권한 부족
409 Conflict 이메일이 이미 등록되어 있음

curl:

# Create a read-only user in 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": "Xylo#2025secure",
    "display_name": "신동윤",
    "role": "user",
    "facility_id": "660e8400-e29b-41d4-a716-446655440000"
  }'

GET /api/v1/users/{id}

ID로 사용자 한 명을 조회한다.

인증: JWT Bearer Token 필요 역할: facility_admin 이상

경로 파라미터:

파라미터 타입 설명
id UUID 사용자 ID

응답 200 OK: 사용자 객체.

오류 케이스:

상태 코드 조건
403 Forbidden 권한 부족 또는 다른 시설의 사용자에 접근
404 Not Found 해당 사용자가 없음

curl:

# Get a specific user
curl https://api.xylolabs.com/api/v1/users/550e8400-e29b-41d4-a716-446655440001 \
  -H "Authorization: Bearer $TOKEN"

PATCH /api/v1/users/{id}

사용자 정보를 부분 수정한다. 보낸 필드만 변경된다.

인증: JWT Bearer Token 필요 역할: facility_admin 이상

경로 파라미터:

파라미터 타입 설명
id UUID 사용자 ID

요청 본문:

{
  "display_name": "전민석",
  "role": "facility_admin",
  "is_active": true
}
필드 타입 필수 설명
display_name string X 새 표시 이름
role string X 새 역할
is_active boolean X 활성화/비활성화

응답 200 OK: 수정된 사용자 객체.

오류 케이스:

상태 코드 조건
403 Forbidden 권한 부족 또는 다른 시설의 사용자에 접근
404 Not Found 해당 사용자가 없음

curl:

# Promote a user to facility_admin
curl -X PATCH https://api.xylolabs.com/api/v1/users/550e8400-e29b-41d4-a716-446655440001 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"role":"facility_admin"}'

DELETE /api/v1/users/{id}

사용자를 비활성화한다. 실제 레코드를 지우지 않고 is_activefalse로 바꾸는 소프트 삭제 방식이다.

인증: JWT Bearer Token 필요 역할: facility_admin 이상

경로 파라미터:

파라미터 타입 설명
id UUID 사용자 ID

응답 204 No Content

오류 케이스:

상태 코드 조건
403 Forbidden 권한 부족 또는 다른 시설의 사용자에 접근
404 Not Found 해당 사용자가 없음

curl:

# Soft-delete (deactivate) a user
curl -X DELETE https://api.xylolabs.com/api/v1/users/550e8400-e29b-41d4-a716-446655440001 \
  -H "Authorization: Bearer $TOKEN"

5. API 키

API 키는 임베디드 디바이스가 오디오 업로드와 메타데이터 수집에 쓰는 인증 수단이다. 각 키는 특정 시설에 묶이고, 스코프와 만료일을 세밀하게 설정할 수 있다. 평문 키는 생성 시 딱 한 번만 표시되고 다시 조회 가능한 형태로 저장되지 않으므로 즉시 안전한 곳에 저장한다.

GET /api/v1/api-keys

현재 사용자 시설의 API 키 전체를 반환한다.

인증: JWT Bearer Token 필요 역할: facility_admin 이상

응답 200 OK:

[
  {
    "id": "770e8400-e29b-41d4-a716-446655440000",
    "facility_id": "660e8400-e29b-41d4-a716-446655440000",
    "key_prefix": "xk_a1b2",
    "name": "터빈홀 센서 어레이",
    "scopes": ["upload", "ingest"],
    "is_active": true,
    "expires_at": "2026-01-01T00:00:00Z",
    "last_used_at": "2025-06-15T10:30:00Z",
    "created_by": "550e8400-e29b-41d4-a716-446655440000",
    "created_at": "2025-06-01T09:00:00Z"
  },
  {
    "id": "770e8400-e29b-41d4-a716-446655440001",
    "facility_id": "660e8400-e29b-41d4-a716-446655440000",
    "key_prefix": "xk_c3d4",
    "name": "수중 하이드로폰 #3",
    "scopes": ["ingest"],
    "is_active": true,
    "expires_at": null,
    "last_used_at": "2025-06-14T22:15:00Z",
    "created_by": "550e8400-e29b-41d4-a716-446655440002",
    "created_at": "2025-05-10T11:00:00Z"
  }
]

curl:

# List all API keys for your facility
curl https://api.xylolabs.com/api/v1/api-keys \
  -H "Authorization: Bearer $TOKEN"

POST /api/v1/api-keys

새 API 키를 발급한다. 응답의 key 필드에 평문 키가 포함되며, 이 값은 이후 다시 조회할 수 없다.

인증: JWT Bearer Token 필요 역할: facility_admin 이상

요청 본문:

{
  "name": "조류 음성 레코더",
  "scopes": ["upload", "ingest"],
  "expires_at": "2026-06-01T00:00:00Z"
}
필드 타입 필수 설명
name string O 키를 구별할 이름
scopes string[] X 권한 스코프 (생략 시 전체 권한)
expires_at ISO 8601 X 만료일 (null = 무기한)

응답 200 OK:

{
  "id": "770e8400-e29b-41d4-a716-446655440002",
  "facility_id": "660e8400-e29b-41d4-a716-446655440002",
  "key_prefix": "xk_e5f6",
  "name": "조류 음성 레코더",
  "scopes": ["upload", "ingest"],
  "expires_at": "2026-06-01T00:00:00Z",
  "created_at": "2025-06-15T10:00:00Z",
  "key": "xk_e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6"
}

주의: key 필드는 이 응답에서만 볼 수 있다. 분실하면 키를 폐기하고 새로 발급해야 한다.

curl:

# Create an API key for a field recorder with 1-year expiry
curl -X POST https://api.xylolabs.com/api/v1/api-keys \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "조류 음성 레코더",
    "scopes": ["upload","ingest"],
    "expires_at": "2026-06-01T00:00:00Z"
  }'

DELETE /api/v1/api-keys/{id}

API 키를 폐기한다. 폐기된 키로는 더 이상 인증할 수 없다.

인증: JWT Bearer Token 필요 역할: facility_admin 이상

경로 파라미터:

파라미터 타입 설명
id UUID API 키 ID

응답 204 No Content

오류 케이스:

상태 코드 조건
403 Forbidden 권한 부족 또는 다른 시설의 키에 접근

curl:

# Revoke a compromised API key
curl -X DELETE https://api.xylolabs.com/api/v1/api-keys/770e8400-e29b-41d4-a716-446655440002 \
  -H "Authorization: Bearer $TOKEN"

6. 디바이스

디바이스는 시설에 배치된 물리 장비(RPi Pico 2, ESP32, Cat-M1 모뎀 등)를 나타낸다. 시설 안에서 device_uid 정수로 구별하고, 대시보드에서 직접 등록하거나 수집 세션이 새 device_uid를 참조하면 자동으로 생성된다.

GET /api/v1/devices

현재 사용자 시설의 디바이스 전체를 반환한다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

응답 200 OK:

[
  {
    "id": "cc0e8400-e29b-41d4-a716-446655440000",
    "facility_id": "660e8400-e29b-41d4-a716-446655440000",
    "device_uid": 1,
    "name": "터빈홀 센서 어레이",
    "hardware_version": "rp2350",
    "firmware_version": "1.2.0",
    "is_active": true,
    "last_seen_at": "2025-06-15T10:30:00Z",
    "created_at": "2025-06-01T09:00:00Z"
  },
  {
    "id": "cc0e8400-e29b-41d4-a716-446655440001",
    "facility_id": "660e8400-e29b-41d4-a716-446655440000",
    "device_uid": 2,
    "name": "수중 하이드로폰 #3",
    "hardware_version": "esp32-s3",
    "firmware_version": "2.0.1",
    "is_active": true,
    "last_seen_at": "2025-06-15T09:45:00Z",
    "created_at": "2025-04-10T13:00:00Z"
  }
]

curl:

# List devices in your facility
curl https://api.xylolabs.com/api/v1/devices \
  -H "Authorization: Bearer $TOKEN"

POST /api/v1/devices

새 디바이스를 등록한다.

인증: JWT Bearer Token 필요 역할: facility_admin 이상

요청 본문:

{
  "device_uid": 3,
  "name": "조류 음성 레코더",
  "hardware_version": "nrf9160",
  "firmware_version": "0.9.0"
}
필드 타입 필수 설명
device_uid u32 O 시설 안에서 유일한 디바이스 번호
name string O 디바이스 표시 이름
hardware_version string X 하드웨어 버전
firmware_version string X 펌웨어 버전

응답 201 Created:

{
  "id": "cc0e8400-e29b-41d4-a716-446655440002",
  "facility_id": "660e8400-e29b-41d4-a716-446655440002",
  "device_uid": 3,
  "name": "조류 음성 레코더",
  "hardware_version": "nrf9160",
  "firmware_version": "0.9.0",
  "is_active": true,
  "last_seen_at": null,
  "created_at": "2025-06-15T10:00:00Z"
}

오류 케이스:

상태 코드 조건
403 Forbidden 권한 부족
409 Conflict 같은 시설에 동일한 device_uid가 이미 존재

curl:

# Register a new Cat-M1 field recorder
curl -X POST https://api.xylolabs.com/api/v1/devices \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "device_uid": 3,
    "name": "조류 음성 레코더",
    "hardware_version": "nrf9160",
    "firmware_version": "0.9.0"
  }'

GET /api/v1/devices/{id}

ID로 디바이스 하나를 조회한다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

경로 파라미터:

파라미터 타입 설명
id UUID 디바이스 ID

응답 200 OK: 디바이스 객체.

오류 케이스:

상태 코드 조건
403 Forbidden 다른 시설의 디바이스에 접근
404 Not Found 해당 디바이스가 없음

curl:

# Get device details
curl https://api.xylolabs.com/api/v1/devices/cc0e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $TOKEN"

PATCH /api/v1/devices/{id}

디바이스 정보를 부분 수정한다.

인증: JWT Bearer Token 필요 역할: facility_admin 이상

경로 파라미터:

파라미터 타입 설명
id UUID 디바이스 ID

요청 본문:

{
  "name": "터빈홀 센서 어레이 (교체)",
  "firmware_version": "1.3.0",
  "is_active": true
}
필드 타입 필수 설명
name string X 새 디바이스 이름
hardware_version string X 새 하드웨어 버전
firmware_version string X 새 펌웨어 버전
is_active boolean X 활성화/비활성화

응답 200 OK: 수정된 디바이스 객체.

오류 케이스:

상태 코드 조건
403 Forbidden 권한 부족 또는 다른 시설의 디바이스에 접근
404 Not Found 해당 디바이스가 없음

curl:

# Update firmware version after OTA
curl -X PATCH https://api.xylolabs.com/api/v1/devices/cc0e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"firmware_version":"1.3.0"}'

7. 오디오 업로드

POST /api/v1/uploads

multipart form data로 오디오 파일을 업로드한다. 서버가 원본을 S3에 저장한 뒤, 웹 재생에 맞는 포맷으로 트랜스코딩 작업을 자동 생성한다.

인증: API Key (X-Api-Key 헤더) 필요 역할: 해당 없음 (API Key로 시설 범위 지정)

Content-Type: multipart/form-data

Multipart 필드:

필드 타입 필수 설명
file file O 오디오 파일 (WAV, FLAC, MP3 등)
metadata text (JSON) X 부가 메타데이터 JSON
tags text (JSON) X 태그 UUID 배열

파일 크기 참고: 서버에서 multipart 요청의 최대 크기를 제한할 수 있다. Cat-M1 같은 저대역폭 환경에서는 파일을 잘라서 올리거나 XMBP 스트리밍으로 전환하는 것이 좋는다. 384kHz/24bit WAV 1분 분량은 약 138MB이므로 Cat-M1으로는 1시간 이상 걸릴 수 있다.

응답 200 OK:

{
  "id": "880e8400-e29b-41d4-a716-446655440000",
  "facility_id": "660e8400-e29b-41d4-a716-446655440000",
  "api_key_id": "770e8400-e29b-41d4-a716-446655440000",
  "uploaded_by": null,
  "original_filename": "turbine_hall_2025-06-15_14h30.wav",
  "original_format": "wav",
  "original_size_bytes": 115200000,
  "duration_ms": null,
  "sample_rate_hz": null,
  "channels": null,
  "bit_depth": null,
  "codec": null,
  "metadata": {"location": "turbine_hall", "sample_rate": 384000, "bit_depth": 24},
  "status": "pending",
  "tags": [],
  "created_at": "2025-06-15T14:30:00Z",
  "updated_at": "2025-06-15T14:30:00Z"
}

업로드 상태: pending(대기) -> processing(변환 중) -> completed(완료) 또는 failed(실패).

curl:

# Upload a high-res WAV recording with metadata and tags
curl -X POST https://api.xylolabs.com/api/v1/uploads \
  -H "X-Api-Key: xk_a1b2c3d4e5f6..." \
  -F "file=@turbine_hall_2025-06-15_14h30.wav" \
  -F 'metadata={"location":"turbine_hall","sample_rate":384000,"bit_depth":24}' \
  -F 'tags=["990e8400-e29b-41d4-a716-446655440000"]'

Python 업로드 예제:

import requests

API_KEY = "xk_a1b2c3d4e5f6..."
BASE_URL = "https://api.xylolabs.com/api/v1"

# Upload a FLAC file with metadata
with open("hydrophone_reef_sample.flac", "rb") as f:
    resp = requests.post(
        f"{BASE_URL}/uploads",
        headers={"X-Api-Key": API_KEY},
        files={"file": ("hydrophone_reef_sample.flac", f, "audio/flac")},
        data={
            "metadata": '{"location":"reef_zone_alpha","depth_m":12.5}',
            "tags": '["990e8400-e29b-41d4-a716-446655440000"]',
        },
    )

upload = resp.json()
print(f"Upload ID: {upload['id']}, status: {upload['status']}")

오류 케이스:

상태 코드 조건
400 Bad Request file 필드 누락 또는 잘못된 multipart
401 Unauthorized 유효하지 않거나 누락된 API 키

저장 흐름:

디바이스 --> HTTP POST --> 서버 --> S3 (원본 그대로 저장)
                                     |
                              자동 트랜스코딩
                                     |
                              S3 (웹 재생용 사본)

GET /api/v1/uploads

시설의 업로드 목록을 페이지네이션으로 조회한다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

쿼리 파라미터:

파라미터 타입 기본값 설명
page integer 1 페이지 번호 (1부터 시작)
per_page integer 20 페이지당 항목 수 (1-100)
status string -- 상태 필터: pending, processing, completed, failed

응답 200 OK:

{
  "items": [
    {
      "id": "880e8400-e29b-41d4-a716-446655440000",
      "facility_id": "660e8400-e29b-41d4-a716-446655440000",
      "original_filename": "turbine_hall_2025-06-15_14h30.wav",
      "original_format": "wav",
      "original_size_bytes": 115200000,
      "status": "completed",
      "tags": [
        {
          "id": "990e8400-e29b-41d4-a716-446655440000",
          "facility_id": "660e8400-e29b-41d4-a716-446655440000",
          "name": "터빈 진동",
          "color": "#F44336",
          "created_at": "2025-06-01T09:00:00Z"
        }
      ],
      "created_at": "2025-06-15T14:30:00Z",
      "updated_at": "2025-06-15T14:35:00Z"
    },
    {
      "id": "880e8400-e29b-41d4-a716-446655440001",
      "facility_id": "660e8400-e29b-41d4-a716-446655440000",
      "original_filename": "hydrophone_reef_sample.flac",
      "original_format": "flac",
      "original_size_bytes": 48000000,
      "status": "completed",
      "tags": [],
      "created_at": "2025-06-14T22:00:00Z",
      "updated_at": "2025-06-14T22:03: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}

업로드 하나를 상세 조회한다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

경로 파라미터:

파라미터 타입 설명
id UUID 업로드 ID

응답 200 OK: 전체 업로드 객체 (목록 항목과 동일한 스키마).

오류 케이스:

상태 코드 조건
403 Forbidden 다른 시설의 업로드에 접근
404 Not Found 해당 업로드가 없음

curl:

# Get details of a specific upload
curl https://api.xylolabs.com/api/v1/uploads/880e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $TOKEN"

PATCH /api/v1/uploads/{id}

업로드의 메타데이터나 태그를 수정한다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

경로 파라미터:

파라미터 타입 설명
id UUID 업로드 ID

요청 본문:

{
  "metadata": {
    "location": "turbine_hall_east",
    "sample_rate": 384000,
    "notes": "East side sensor replaced 2025-06-15"
  },
  "tags": ["990e8400-e29b-41d4-a716-446655440000"]
}
필드 타입 필수 설명
metadata object X 메타데이터 전체 교체 (기존 값을 덮어씀)
tags UUID[] X 태그 전체 교체 (기존 태그 제거 후 새 태그 연결)

응답 200 OK: 수정된 업로드 객체.

오류 케이스:

상태 코드 조건
403 Forbidden 다른 시설의 업로드에 접근
404 Not Found 해당 업로드가 없음

curl:

# Update upload metadata and tags
curl -X PATCH https://api.xylolabs.com/api/v1/uploads/880e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "metadata": {"location":"turbine_hall_east","notes":"East side sensor replaced"},
    "tags": ["990e8400-e29b-41d4-a716-446655440000"]
  }'

DELETE /api/v1/uploads/{id}

업로드를 완전히 삭제한다. S3에서 파일을 지우고 DB 레코드를 삭제하며, 연결된 트랜스코딩 작업과 태그 연결도 함께 삭제된다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

경로 파라미터:

파라미터 타입 설명
id UUID 업로드 ID

응답 204 No Content

오류 케이스:

상태 코드 조건
403 Forbidden 다른 시설의 업로드에 접근
404 Not Found 해당 업로드가 없음

curl:

# Permanently delete an upload and its transcoded copies
curl -X DELETE https://api.xylolabs.com/api/v1/uploads/880e8400-e29b-41d4-a716-446655440001 \
  -H "Authorization: Bearer $TOKEN"

POST /api/v1/uploads/{id}/retranscode

기존 업로드를 현재 시스템 설정 기준으로 다시 트랜스코딩한다. 기본 출력 포맷을 변경한 뒤 일괄 재변환할 때 쓸 수 있다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

경로 파라미터:

파라미터 타입 설명
id UUID 업로드 ID

응답 200 OK:

{
  "job_id": "aa0e8400-e29b-41d4-a716-446655440000"
}

오류 케이스:

상태 코드 조건
403 Forbidden 다른 시설의 업로드에 접근
404 Not Found 해당 업로드가 없음

curl:

# Trigger retranscoding with current default settings
curl -X POST https://api.xylolabs.com/api/v1/uploads/880e8400-e29b-41d4-a716-446655440000/retranscode \
  -H "Authorization: Bearer $TOKEN"

8. 오디오 스트리밍

GET /api/v1/streams/{id}

트랜스코딩이 끝난 오디오를 백엔드가 직접 프록시해서 스트리밍한다. 내부 S3 엔드포인트를 브라우저에 노출하지 않고도 바로 재생할 수 있다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

경로 파라미터:

파라미터 타입 설명
id UUID 업로드 ID

응답 200 OK:

헤더:

Content-Type: audio/webm
Cache-Control: private, max-age=3600

오류 케이스:

상태 코드 조건
403 Forbidden 다른 시설의 업로드에 접근
404 Not Found 업로드가 없거나 완료된 트랜스코딩이 없음

curl:

# Follow redirect and save the transcoded audio to a file
curl -L https://api.xylolabs.com/api/v1/streams/880e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $TOKEN" \
  --output turbine_hall_transcoded.webm

9. 트랜스코딩 작업

GET /api/v1/transcode-jobs

시설의 트랜스코딩 작업 목록을 페이지네이션으로 조회한다.

인증: JWT Bearer Token 필요 역할: user 이상

쿼리 파라미터:

파라미터 타입 기본값 설명
page integer 1 페이지 번호
per_page integer 20 페이지당 항목 수 (1-100)
status string -- 필터: queued, running, completed, failed, cancelled

응답 200 OK:

{
  "items": [
    {
      "id": "aa0e8400-e29b-41d4-a716-446655440000",
      "upload_id": "880e8400-e29b-41d4-a716-446655440000",
      "facility_id": "660e8400-e29b-41d4-a716-446655440000",
      "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:05Z",
      "started_at": "2025-06-15T14:30:10Z",
      "completed_at": "2025-06-15T14:30:25Z",
      "worker_id": "worker-01"
    }
  ],
  "total": 10,
  "page": 1,
  "per_page": 20
}

curl:

# List completed transcoding jobs
curl "https://api.xylolabs.com/api/v1/transcode-jobs?status=completed" \
  -H "Authorization: Bearer $TOKEN"

GET /api/v1/transcode-jobs/{id}

트랜스코딩 작업 하나를 상세 조회한다.

인증: JWT Bearer Token 필요 역할: user 이상

경로 파라미터:

파라미터 타입 설명
id UUID 트랜스코딩 작업 ID

응답 200 OK: 트랜스코딩 작업 객체 (목록 항목과 동일한 스키마).

오류 케이스:

상태 코드 조건
403 Forbidden 다른 시설의 작업에 접근
404 Not Found 해당 작업이 없음

curl:

# Check status of a specific transcoding job
curl https://api.xylolabs.com/api/v1/transcode-jobs/aa0e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $TOKEN"

POST /api/v1/transcode-jobs/{id}/cancel

대기(queued) 또는 실행 중(running)인 트랜스코딩 작업을 취소한다.

인증: JWT Bearer Token 필요 역할: facility_admin 이상

경로 파라미터:

파라미터 타입 설명
id UUID 트랜스코딩 작업 ID

응답 204 No Content

오류 케이스:

상태 코드 조건
403 Forbidden 권한 부족 또는 다른 시설의 작업에 접근
404 Not Found 해당 작업이 없음

curl:

# Cancel a queued transcoding job
curl -X POST https://api.xylolabs.com/api/v1/transcode-jobs/aa0e8400-e29b-41d4-a716-446655440000/cancel \
  -H "Authorization: Bearer $TOKEN"

10. 태그

태그는 업로드를 분류하고 정리하는 레이블이다. 시설별로 관리되며 색상 코드를 지정할 수 있다.

GET /api/v1/tags

시설에 등록된 태그 전체를 반환한다.

인증: JWT Bearer Token 필요 역할: user 이상

응답 200 OK:

[
  {
    "id": "990e8400-e29b-41d4-a716-446655440000",
    "facility_id": "660e8400-e29b-41d4-a716-446655440000",
    "name": "터빈 진동",
    "color": "#F44336",
    "created_at": "2025-06-01T09:00:00Z"
  },
  {
    "id": "990e8400-e29b-41d4-a716-446655440001",
    "facility_id": "660e8400-e29b-41d4-a716-446655440000",
    "name": "해양 환경음",
    "color": "#2196F3",
    "created_at": "2025-06-02T10:00:00Z"
  },
  {
    "id": "990e8400-e29b-41d4-a716-446655440002",
    "facility_id": "660e8400-e29b-41d4-a716-446655440002",
    "name": "조류 음성",
    "color": "#4CAF50",
    "created_at": "2025-06-03T11:00:00Z"
  }
]

curl:

# List all tags in the facility
curl https://api.xylolabs.com/api/v1/tags \
  -H "Authorization: Bearer $TOKEN"

POST /api/v1/tags

새 태그를 만든다.

인증: JWT Bearer Token 필요 역할: facility_admin 이상

요청 본문:

{
  "name": "야간 녹음",
  "color": "#9C27B0"
}
필드 타입 필수 설명
name string O 태그 이름
color string X 대시보드 표시용 hex 색상 (예: #9C27B0)

응답 200 OK: 생성된 태그 객체.

curl:

# Create a tag for nighttime recordings
curl -X POST https://api.xylolabs.com/api/v1/tags \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"야간 녹음","color":"#9C27B0"}'

DELETE /api/v1/tags/{id}

태그를 삭제한다. 삭제해도 이미 업로드에 연결된 태그 관계만 끊기며 업로드 자체는 영향받지 않는다.

인증: JWT Bearer Token 필요 역할: facility_admin 이상

경로 파라미터:

파라미터 타입 설명
id UUID 태그 ID

응답 204 No Content

curl:

# Delete an unused tag
curl -X DELETE https://api.xylolabs.com/api/v1/tags/990e8400-e29b-41d4-a716-446655440002 \
  -H "Authorization: Bearer $TOKEN"

11. 메타데이터 수집

메타데이터 수집(Ingest) 서브시스템은 임베디드 디바이스가 XMBP(Xylolabs Metadata Binary Protocol)로 센서 데이터를 실시간 전송하는 기능이다. 세션을 열고, 데이터를 보내고, 세션을 닫는 3단계로 동작한다.

수집 흐름

1. 세션 생성 (POST /ingest/sessions)
   --> 세션 ID + 스트림 정의 반환

2. 데이터 전송 (반복)
   |-- HTTP: POST /ingest/sessions/{id}/data (XMBP binary body)
   |-- WebSocket: /ingest/ws (XMBP binary frames)

3. 세션 종료 (POST /ingest/sessions/{id}/close)

세션 라이프사이클 참고: 세션은 active -> closing -> closed 순서로 진행한다. 세션을 명시적으로 닫지 않아도 데이터가 유실되지는 않지만, closed 상태여야 export/조회 시 데이터셋이 완전하다고 보장할 수 있다. 디바이스 펌웨어에서 정상 종료 시 반드시 /close를 호출한다.

HTTP POST vs WebSocket, 어떤 걸 쓸까? - HTTP POST: 구현이 간단하고, Cat-M1 모뎀처럼 WebSocket을 지원하지 않는 환경에 맞는다. 배치마다 요청-응답 한 쌍이 오가므로 간헐적 전송에 좋는다. - WebSocket: 한 번 연결하면 프레임 오버헤드가 거의 없다. 연속 스트리밍(수백 Hz 이상)에서 효율이 높지만, 재연결 로직을 직접 짜야 한다. - Cat-M1 환경: 대부분의 Cat-M1 AT 명령 스택은 HTTP만 지원하므로 HTTP POST를 쓴다.

POST /api/v1/ingest/sessions

스트림 정의와 함께 새 수집 세션을 생성한다. device_uid에 해당하는 디바이스가 아직 없으면 자동으로 등록된다.

인증: API Key (X-Api-Key 헤더) 필요 역할: 해당 없음 (API Key로 시설 범위 지정)

요청 본문:

{
  "device_uid": 1,
  "name": "터빈홀 연속 모니터링 2025-06-15",
  "streams": [
    {
      "stream_index": 0,
      "name": "temperature",
      "value_type": "f32",
      "unit": "celsius",
      "sample_rate_hz": 100.0,
      "description": "Turbine bearing surface temperature"
    },
    {
      "stream_index": 1,
      "name": "vibration_rms",
      "value_type": "f32",
      "unit": "g",
      "sample_rate_hz": 100.0,
      "description": "RMS vibration of turbine shaft"
    },
    {
      "stream_index": 2,
      "name": "humidity",
      "value_type": "f32",
      "unit": "percent",
      "sample_rate_hz": 10.0,
      "description": "Ambient relative humidity"
    },
    {
      "stream_index": 3,
      "name": "pressure",
      "value_type": "f32",
      "unit": "hPa",
      "sample_rate_hz": 10.0,
      "description": "Barometric pressure"
    }
  ],
  "metadata": {
    "location": "turbine_hall_east",
    "firmware": "1.2.0",
    "calibration_date": "2025-06-01"
  }
}

CreateIngestSessionRequest 필드:

필드 타입 필수 설명
device_uid u32 O 시설 내 고유 디바이스 번호
name string X 세션 이름
streams StreamDefinition[] O 스트림 정의 배열
metadata object X 세션에 첨부할 메타데이터

StreamDefinition 필드:

필드 타입 필수 설명
stream_index u16 O 스트림 인덱스 (XMBP의 stream_id와 매칭)
name string O 스트림 이름
value_type string O 값 타입: f32, f64, i32, i64, bool, string, bytes, f64_array, f32_array, i32_array, json, i16, i8
unit string X 단위 레이블 (표시용)
sample_rate_hz f32 X 샘플링 레이트 (표시용)
description string X 설명

응답 201 Created:

{
  "id": "dd0e8400-e29b-41d4-a716-446655440000",
  "facility_id": "660e8400-e29b-41d4-a716-446655440000",
  "device_id": "cc0e8400-e29b-41d4-a716-446655440000",
  "status": "active",
  "started_at": "2025-06-15T10:00:00Z",
  "total_samples": 0,
  "total_bytes": 0,
  "missed_batches": 0,
  "streams": [
    {
      "id": "ee0e8400-e29b-41d4-a716-446655440000",
      "stream_index": 0,
      "name": "temperature",
      "value_type": "f32",
      "unit": "celsius",
      "sample_rate_hz": 100.0
    },
    {
      "id": "ee0e8400-e29b-41d4-a716-446655440001",
      "stream_index": 1,
      "name": "vibration_rms",
      "value_type": "f32",
      "unit": "g",
      "sample_rate_hz": 100.0
    }
  ]
}

curl:

# Create a session with temperature + vibration streams
curl -X POST https://api.xylolabs.com/api/v1/ingest/sessions \
  -H "X-Api-Key: xk_a1b2c3d4e5f6..." \
  -H "Content-Type: application/json" \
  -d '{
    "device_uid": 1,
    "name": "터빈홀 연속 모니터링 2025-06-15",
    "streams": [
      {
        "stream_index": 0,
        "name": "temperature",
        "value_type": "f32",
        "unit": "celsius",
        "sample_rate_hz": 100
      },
      {
        "stream_index": 1,
        "name": "vibration_rms",
        "value_type": "f32",
        "unit": "g",
        "sample_rate_hz": 100
      }
    ],
    "metadata": {"location":"turbine_hall_east","firmware":"1.2.0"}
  }'

POST /api/v1/ingest/sessions/{id}/data

HTTP POST로 XMBP 바이너리 배치를 전송한다. 배치 구성 방법은 XMBP 프로토콜 레퍼런스를 참고한다.

인증: API Key (X-Api-Key 헤더) 필요 역할: 해당 없음 (API Key로 시설 범위 지정)

Content-Type: application/octet-stream

경로 파라미터:

파라미터 타입 설명
id UUID 세션 ID

요청 본문: 원시 XMBP 바이너리 배치 (XMBP 프로토콜 레퍼런스 참조).

응답 200 OK:

{
  "accepted_samples": 1000,
  "missed_batches": 0
}
필드 타입 설명
accepted_samples u32 이 세션에서 지금까지 받은 총 샘플 수
missed_batches u32 batch_seq 기준으로 감지된 누락 배치 수

오류 케이스:

상태 코드 조건
400 Bad Request XMBP 디코딩 오류 (잘못된 magic, 손상된 페이로드 등)
401 Unauthorized 유효하지 않은 API 키
404 Not Found 세션이 없거나 시설 불일치

curl:

# Send a binary XMBP batch to an active session
curl -X POST https://api.xylolabs.com/api/v1/ingest/sessions/dd0e8400-e29b-41d4-a716-446655440000/data \
  -H "X-Api-Key: xk_a1b2c3d4e5f6..." \
  -H "Content-Type: application/octet-stream" \
  --data-binary @batch_001.bin

POST /api/v1/ingest/sessions/{id}/close

수집 세션을 종료한다. 서버 내부 버퍼를 플러시하고 세션 상태를 closed로 바꾼다. 종료된 세션에는 더 이상 데이터를 보낼 수 없다.

인증: API Key (X-Api-Key 헤더) 필요 역할: 해당 없음 (API Key로 시설 범위 지정)

경로 파라미터:

파라미터 타입 설명
id UUID 세션 ID

응답 200 OK: 최종 세션 상태.

{
  "id": "dd0e8400-e29b-41d4-a716-446655440000",
  "facility_id": "660e8400-e29b-41d4-a716-446655440000",
  "device_id": "cc0e8400-e29b-41d4-a716-446655440000",
  "status": "closed",
  "started_at": "2025-06-15T10:00:00Z",
  "total_samples": 1200000,
  "total_bytes": 9600000,
  "missed_batches": 2,
  "streams": [
    {
      "id": "ee0e8400-e29b-41d4-a716-446655440000",
      "stream_index": 0,
      "name": "temperature",
      "value_type": "f32",
      "unit": "celsius",
      "sample_rate_hz": 100.0
    }
  ]
}

오류 케이스:

상태 코드 조건
403 Forbidden 세션이 다른 시설에 속함
404 Not Found 해당 세션이 없음

curl:

# Close a session and flush all buffered data
curl -X POST https://api.xylolabs.com/api/v1/ingest/sessions/dd0e8400-e29b-41d4-a716-446655440000/close \
  -H "X-Api-Key: xk_a1b2c3d4e5f6..."

GET /api/v1/ingest/ws

실시간 바이너리 데이터 스트리밍을 위한 WebSocket 업그레이드 엔드포인트이다.

인증: X-Api-Key 헤더 필수 필요 역할: 해당 없음

쿼리 파라미터:

파라미터 타입 필수 설명
session_id UUID O 대상 세션 ID

쿼리 파라미터에 API 키를 넣으면 프록시, CDN, 브라우저 로그에 남을 수 있으므로 지원하지 않는다.

연결:

GET /api/v1/ingest/ws?session_id=dd0e8400-...
Connection: Upgrade
Upgrade: websocket
X-Api-Key: xk_a1b2...

프로토콜:

방향 프레임 타입 내용
클라이언트 --> 서버 Binary XMBP 배치
서버 --> 클라이언트 Binary ACK: 4바이트 u32 big-endian (accepted_samples)
서버 --> 클라이언트 Text 오류 메시지 (이후 연결 종료)

동작 사양: - 유휴 타임아웃: 60초간 메시지가 없으면 서버가 연결을 끊는다. - Ping/Pong: 서버는 Ping 프레임에 자동으로 Pong으로 응답한다. - 오류: XMBP 디코딩 실패 시 Text 프레임으로 오류를 보내고 연결을 끊을 수 있다. - 최대 프레임 크기: 64 KB - 최대 메시지 크기: 256 KB

ACK 처리 (C pseudocode):

// Read 4-byte big-endian ACK from server
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];
printf("Server accepted %u total samples\n", accepted);

WebSocket vs HTTP 비교:

항목 HTTP POST WebSocket
연결 오버헤드 배치마다 새 연결 (또는 keep-alive) 한 번 연결
헤더 오버헤드 요청당 ~200바이트 프레임당 2-14바이트
ACK 형식 JSON 응답 4바이트 바이너리
적합한 용도 간헐적 전송, Cat-M1 연속 고속 스트리밍
재연결 불필요 직접 구현 필요

12. 메타데이터 조회

수집이 끝난 센서 데이터를 조회한다. 모든 엔드포인트에 JWT 인증이 필요하다.

GET /api/v1/metadata/sessions

시설의 수집 세션 목록을 조회한다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

쿼리 파라미터:

파라미터 타입 기본값 설명
page integer 1 페이지 번호
per_page integer 20 페이지당 항목 수 (1-100)
status string -- 필터: active, closing, closed, error
device_id UUID -- 특정 디바이스의 세션만 표시

응답 200 OK:

{
  "items": [
    {
      "id": "dd0e8400-e29b-41d4-a716-446655440000",
      "facility_id": "660e8400-e29b-41d4-a716-446655440000",
      "device_id": "cc0e8400-e29b-41d4-a716-446655440000",
      "status": "closed",
      "started_at": "2025-06-15T10:00:00Z",
      "total_samples": 1200000,
      "total_bytes": 9600000,
      "missed_batches": 2,
      "streams": [
        {
          "id": "ee0e8400-e29b-41d4-a716-446655440000",
          "stream_index": 0,
          "name": "temperature",
          "value_type": "f32",
          "unit": "celsius",
          "sample_rate_hz": 100.0
        },
        {
          "id": "ee0e8400-e29b-41d4-a716-446655440001",
          "stream_index": 1,
          "name": "vibration_rms",
          "value_type": "f32",
          "unit": "g",
          "sample_rate_hz": 100.0
        }
      ]
    }
  ],
  "total": 5,
  "page": 1,
  "per_page": 20
}

curl:

# List closed sessions for a specific device
curl "https://api.xylolabs.com/api/v1/metadata/sessions?status=closed&device_id=cc0e8400-e29b-41d4-a716-446655440000" \
  -H "Authorization: Bearer $TOKEN"

GET /api/v1/metadata/sessions/{id}

스트림 정의를 포함한 세션 상세 정보를 반환한다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

경로 파라미터:

파라미터 타입 설명
id UUID 세션 ID

응답 200 OK: 세션 객체 (목록 항목과 동일한 스키마).

오류 케이스:

상태 코드 조건
403 Forbidden 다른 시설의 세션에 접근
404 Not Found 해당 세션이 없음

curl:

# Get full session details including stream definitions
curl https://api.xylolabs.com/api/v1/metadata/sessions/dd0e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $TOKEN"

GET /api/v1/metadata/sessions/{id}/streams/{stream_id}/data

특정 스트림의 데이터 샘플을 시간 범위와 다운샘플링 옵션으로 조회한다. 많은 양의 데이터를 브라우저 차트에 표시할 때 downsample 파라미터로 서버 쪽에서 포인트 수를 줄일 수 있다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

경로 파라미터:

파라미터 타입 설명
id UUID 세션 ID
stream_id UUID 스트림 ID

쿼리 파라미터:

파라미터 타입 기본값 설명
start_us i64 0 시작 타임스탬프 (마이크로초)
end_us i64 MAX 종료 타임스탬프 (마이크로초)
downsample u32 -- 대상 포인트 수 (서버 측 다운샘플링)

응답 200 OK:

{
  "stream_id": "ee0e8400-e29b-41d4-a716-446655440000",
  "stream_name": "temperature",
  "value_type": "f32",
  "sample_count": 1000,
  "timestamps_us": [1718442000000000, 1718442010000, 1718442020000],
  "values": [72.3, 72.5, 72.1]
}

curl:

# Get temperature data downsampled to 1000 points for charting
curl "https://api.xylolabs.com/api/v1/metadata/sessions/dd0e8400-e29b-41d4-a716-446655440000/streams/ee0e8400-e29b-41d4-a716-446655440000/data?start_us=0&downsample=1000" \
  -H "Authorization: Bearer $TOKEN"

GET /api/v1/metadata/sessions/{id}/export

세션의 전체 데이터를 CSV 또는 JSON 파일로 내보낸다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

경로 파라미터:

파라미터 타입 설명
id UUID 세션 ID

쿼리 파라미터:

파라미터 타입 기본값 설명
format string csv 내보내기 형식: csv 또는 json

응답 200 OK:

CSV 형식:

Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="session_dd0e8400-....csv"

timestamp_us,temperature,vibration_rms,humidity,pressure
1718442000000000,72.3,0.42,45.2,1013.25
1718442010000,72.5,0.38,45.1,1013.24
1718442020000,72.1,0.55,45.3,1013.26

JSON 형식:

{
  "session_id": "dd0e8400-e29b-41d4-a716-446655440000",
  "streams": [
    {
      "stream_id": "ee0e8400-e29b-41d4-a716-446655440000",
      "stream_name": "temperature",
      "value_type": "f32",
      "sample_count": 1200000,
      "timestamps_us": [1718442000000000, 1718442010000],
      "values": [72.3, 72.5]
    },
    {
      "stream_id": "ee0e8400-e29b-41d4-a716-446655440001",
      "stream_name": "vibration_rms",
      "value_type": "f32",
      "sample_count": 1200000,
      "timestamps_us": [1718442000000000, 1718442010000],
      "values": [0.42, 0.38]
    }
  ]
}

오류 케이스:

상태 코드 조건
400 Bad Request 지원하지 않는 내보내기 형식
403 Forbidden 다른 시설의 세션에 접근
404 Not Found 해당 세션이 없음

curl:

# Export session data as CSV for offline analysis
curl "https://api.xylolabs.com/api/v1/metadata/sessions/dd0e8400-e29b-41d4-a716-446655440000/export?format=csv" \
  -H "Authorization: Bearer $TOKEN" \
  --output turbine_monitoring_2025-06-15.csv

GET /api/v1/metadata/sessions/{id}/live

Server-Sent Events(SSE)로 활성 수집 세션의 실시간 데이터를 구독한다. 대시보드에서 라이브 차트를 그릴 때 쓴다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

경로 파라미터:

파라미터 타입 설명
id UUID 세션 ID (active 상태여야 함)

응답: SSE 스트림

event: sample
data: {"stream_index":0,"timestamp_us":1718442000000000,"value":72.3}

event: sample
data: {"stream_index":1,"timestamp_us":1718442000000000,"value":0.42}

event: lag
data: {"skipped":10}

이벤트:

이벤트 설명
sample 디바이스에서 받은 새 데이터 포인트
lag 클라이언트 소비 속도가 느려서 일부 이벤트를 건너뜀

curl:

# Subscribe to live sensor data (Ctrl+C to stop)
curl -N "https://api.xylolabs.com/api/v1/metadata/sessions/dd0e8400-e29b-41d4-a716-446655440000/live" \
  -H "Authorization: Bearer $TOKEN"

13. 시스템 설정

슈퍼 관리자가 관리하는 런타임 설정이다. 값은 DB에 저장되고 메모리에 캐시되어 변경 즉시 반영된다.

GET /api/v1/config

카테고리별로 묶인 전체 설정 항목을 반환한다.

인증: JWT Bearer Token 필요 역할: super_admin

응답 200 OK:

{
  "categories": [
    {
      "category": "transcode",
      "entries": [
        {
          "key": "transcode.default_format",
          "value": "opus_webm",
          "category": "transcode",
          "value_type": "string",
          "description": "Default transcoding output format",
          "updated_at": "2025-06-15T10:00:00Z"
        },
        {
          "key": "transcode.default_bitrate",
          "value": "128k",
          "category": "transcode",
          "value_type": "string",
          "description": "Default transcoding bitrate",
          "updated_at": "2025-06-15T10:00:00Z"
        }
      ]
    }
  ]
}

curl:

# Get all system configuration grouped by category
curl https://api.xylolabs.com/api/v1/config \
  -H "Authorization: Bearer $TOKEN"

GET /api/v1/config/{key}

설정 항목 하나를 조회한다.

인증: JWT Bearer Token 필요 역할: super_admin

경로 파라미터:

파라미터 타입 설명
key string 설정 키 (예: transcode.default_format)

응답 200 OK:

{
  "key": "transcode.default_format",
  "value": "opus_webm",
  "category": "transcode",
  "value_type": "string",
  "description": "Default transcoding output format",
  "updated_at": "2025-06-15T10:00:00Z"
}

오류 케이스:

상태 코드 조건
403 Forbidden 슈퍼 관리자가 아닌 경우
404 Not Found 해당 키가 없음

curl:

# Read a single config entry
curl https://api.xylolabs.com/api/v1/config/transcode.default_format \
  -H "Authorization: Bearer $TOKEN"

PUT /api/v1/config/{key}

설정 값을 변경한다.

인증: JWT Bearer Token 필요 역할: super_admin

경로 파라미터:

파라미터 타입 설명
key string 설정 키

요청 본문:

{
  "value": "aac_mp4"
}
필드 타입 필수 설명
value any (JSON) O 새 값 (value_type에 맞는 타입이어야 함)

응답 200 OK: 수정된 설정 항목 객체.

curl:

# Switch default transcoding 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}

설정 값을 기본값으로 되돌립니다.

인증: JWT Bearer Token 필요 역할: super_admin

경로 파라미터:

파라미터 타입 설명
key string 설정 키

응답 200 OK: 기본값으로 초기화된 설정 항목 객체.

curl:

# Reset transcoding format back to default (opus_webm)
curl -X POST https://api.xylolabs.com/api/v1/config/reset/transcode.default_format \
  -H "Authorization: Bearer $TOKEN"

14. 대시보드 통계

GET /api/v1/stats/overview

대시보드에 표시할 통계 요약을 반환한다. 역할에 따라 응답 필드가 달라진다.

인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자

응답 200 OK:

시설 관리자 / 일반 사용자:

{
  "users": 15,
  "uploads": 342
}

슈퍼 관리자 (시설 수 포함):

{
  "users": 47,
  "uploads": 1284,
  "facilities": 5
}

curl:

# Get dashboard overview stats
curl https://api.xylolabs.com/api/v1/stats/overview \
  -H "Authorization: Bearer $TOKEN"

15. 헬스 체크

GET /api/health

서버가 살아 있는지 확인하는 기본 헬스 체크이다. 서버가 실행 중이면 항상 200을 반환한다. 로드 밸런서의 liveness probe로 쓴다.

인증: 없음 필요 역할: 없음

응답 200 OK:

{
  "status": "ok"
}

curl:

# Basic liveness check
curl https://api.xylolabs.com/api/health

GET /api/health/ready

PostgreSQL과 S3 연결 상태까지 확인하는 readiness 체크이다.

인증: 없음 필요 역할: 없음

응답 200 OK:

{
  "status": "ready",
  "postgres": "ok",
  "s3": "ok"
}

S3에 접근할 수 없는 경우:

{
  "status": "ready",
  "postgres": "ok",
  "s3": "error"
}

curl:

# Readiness check (DB + S3 connectivity)
curl https://api.xylolabs.com/api/health/ready

GET/POST /api/v1/ping

API 키 유효성 검증 및 연결 테스트. X-Api-Key 헤더가 유효한지 확인하고 키 이름과 연결된 시설 정보를 반환한다. 선택적으로 message 필드를 보내면 그대로 돌려준다.

MCU 펌웨어에서 인제스트 세션 시작 전에 API 키가 작동하는지 확인할 때 사용한다.

인증: API 키 (X-Api-Key 헤더)

요청 본문 (선택, POST만 해당):

{
  "message": "hello from pico"
}

응답 200 OK:

{
  "pong": true,
  "message": "hello from pico",
  "api_key_name": "production-sensor-01",
  "facility_id": "b666b8c3-bbc2-4675-a3ac-8b6a1e6dad9f"
}

본문을 보내지 않으면 message"pong"이 기본값이다.

오류: - 401 — API 키 누락 또는 유효하지 않음

curl:

# GET — 간단한 키 검증
curl -H "X-Api-Key: xk_your_key_here" https://api.xylolabs.com/api/v1/ping

# POST — 에코 메시지 포함
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 프로토콜 레퍼런스

XMBP(Xylolabs Metadata Binary Protocol)는 Cat-M1 같은 저대역폭 환경에서 센서 데이터를 적은 바이트로 전송하기 위한 바이너리 프로토콜이다. 모든 숫자는 빅엔디안 바이트 순서를 사용한다.

Batch Envelope

HTTP POST 본문 또는 WebSocket 바이너리 프레임 하나에 대응하는 최상위 구조이다.

+--------+------+--------------------------------------+
| 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:

비트 이름 설명
0 (0x01) HAS_DEVICE_ID 설정 시 offset 8에 device_id 필드(4바이트) 포함
1 (0x02) ZSTD_COMPRESSED 예약 (향후 zstd 압축용)
2-7 -- 예약

HAS_DEVICE_ID가 설정되면 stream_count는 offset 12에서, 그렇지 않으면 offset 8에서 시작한다.

StreamBlock

단일 스트림(채널)의 데이터 블록이다.

+--------+------+--------------------------------------+
| Offset | Size | Field                                |
+--------+------+--------------------------------------+
|      0 |    2 | stream_id     (u16)                  |
|      2 |    1 | value_type    (u8)                   |
|      3 |    2 | sample_count  (u16)                  |
|      5 |  ... | Sample[sample_count]                 |
+--------+------+--------------------------------------+

Sample 형식

고정 크기 타입 (f32, f64, i32, i64, bool):

+--------+------+--------------------------------------+
| Offset | Size | Field                                |
+--------+------+--------------------------------------+
|      0 |    8 | timestamp_us  (u64, microseconds)    |
|      8 |    N | value         (fixed size per type)  |
+--------+------+--------------------------------------+

가변 크기 타입 (string, bytes, arrays, json):

+--------+------+--------------------------------------+
| Offset | Size | Field                                |
+--------+------+--------------------------------------+
|      0 |    8 | timestamp_us  (u64, microseconds)    |
|      8 |    2 | length        (u16, bytes or count)  |
|     10 |  ... | data          (variable)             |
+--------+------+--------------------------------------+

Value Type 태그

태그 타입 크기 설명
0 f64 8바이트 64-bit IEEE 754 float
1 f32 4바이트 32-bit IEEE 754 float
2 i64 8바이트 64-bit signed integer
3 i32 4바이트 32-bit signed integer
4 bool 1바이트 0 = false, nonzero = true
5 string 가변 u16 length + UTF-8 bytes
6 bytes 가변 u16 length + raw bytes
7 f64_array 가변 u16 element count + f64[]
8 f32_array 가변 u16 element count + f32[]
9 i32_array 가변 u16 element count + i32[]
10 json 가변 u32 length + UTF-8 JSON string
11 i16 2바이트 16비트 부호 있는 정수, big-endian
12 i8 1바이트 8비트 부호 있는 정수 (u8 cast to i8)

XMBP 배치 구성 단계별 워크스루

f32 온도 데이터 3샘플을 담은 단일 스트림 배치를 직접 만들어 보겠는다.

Step 1: 헤더 작성

Offset  Hex              Field
------  ---------------  ---------------------------
0x00    58 4D 42 50      magic = "XMBP"
0x04    01               version = 1
0x05    01               flags = HAS_DEVICE_ID
0x06    00 00            batch_seq = 0
0x08    00 00 00 01      device_id = 1
0x0C    00 01            stream_count = 1

Step 2: StreamBlock 작성

Offset  Hex              Field
------  ---------------  ---------------------------
0x0E    00 00            stream_id = 0
0x10    01               value_type = 1 (f32)
0x11    00 03            sample_count = 3

Step 3: Sample 데이터 작성

Offset  Hex                          Field
------  ---------------------------  ---------------------------
0x13    00 00 06 39 8C 92 58 00      timestamp = 1718442000000000 us
0x1B    42 90 CC CD                  value = 72.4 (f32 big-endian)

0x1F    00 00 06 39 8C 92 60 70      timestamp = 1718442002160 us
0x27    42 91 33 33                  value = 72.6 (f32 big-endian)

0x2B    00 00 06 39 8C 92 68 E0      timestamp = 1718442004320 us
0x33    42 90 66 66                  value = 72.2 (f32 big-endian)

전체 배치 크기: 0x37 = 55바이트

Cat-M1 최적화 참고: Cat-M1의 실측 uplink는 약 37KB/s이다. 위 예제처럼 작은 배치는 1ms 이내에 전송할 수 있지만, 실전에서는 배치당 250샘플 정도로 묶어서 HTTP 오버헤드 대비 페이로드 비율을 높여야 한다. 아래 대역폭 예산 섹션에서 자세히 다룬다.

배치 크기 계산

고정 크기 타입이면 배치 크기를 미리 계산할 수 있다.

Header size:
  base = 4 (magic) + 1 (version) + 1 (flags) + 2 (batch_seq) + 2 (stream_count) = 10
  with device_id: +4 = 14

Stream block 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

예시: 4채널 f32, 250 samples/batch, device_id 포함

14 + 4 * (5 + 250 * (8 + 4))
= 14 + 4 * (5 + 250 * 12)
= 14 + 4 * 3005
= 14 + 12020
= 12,034 bytes (~12KB)

이 크기는 Cat-M1으로 약 325ms면 전송할 수 있어 실시간 스트리밍에 쓸 수 있다.


17. RBAC 레퍼런스

역할

역할 레벨 설명
super_admin 3 모든 시설에 대한 전체 접근 권한
facility_admin 2 소속 시설의 사용자, 키, 디바이스, 데이터 관리
user 1 소속 시설 데이터 읽기 전용

역할 확인은 >= 비교를 사용한다. super_admin은 어떤 역할 검사도 통과한다.

권한 매트릭스

엔드포인트 super_admin facility_admin user
시설 CRUD O -- --
사용자 CRUD O 자기 시설만 --
API 키 CRUD O 자기 시설만 --
디바이스 조회 O O O
디바이스 생성/수정 O 자기 시설만 --
업로드 조회 O O O
업로드 생성 (API Key) API Key 사용 API Key 사용 API Key 사용
업로드 수정/삭제 O O O
업로드 재트랜스코딩 O O O
스트리밍 조회 O O O
트랜스코딩 작업 조회 O O O
트랜스코딩 작업 취소 O 자기 시설만 --
태그 조회 O O O
태그 생성/삭제 O 자기 시설만 --
수집 (API Key) API Key 사용 API Key 사용 API Key 사용
메타데이터 조회 O O O
시스템 설정 CRUD O -- --
통계 개요 O O O

시설 격리

  • 슈퍼 관리자가 아닌 사용자는 자기가 배정된 시설의 데이터만 볼 수 있다.
  • API 키는 하나의 시설에 묶여 있다.
  • 다른 시설의 리소스에 접근하면 403 Forbidden이 반환된다.

18. 오류 응답

모든 오류는 같은 JSON 형식을 따른다.

{
  "error": "human-readable error message"
}

HTTP 상태 코드

상태 코드 의미 주요 원인
400 Bad Request 잘못된 요청 필드 누락, 잘못된 JSON, XMBP 디코딩 오류
401 Unauthorized 인증 실패 JWT/API Key가 없거나 유효하지 않음, 토큰 만료
403 Forbidden 권한 부족 역할 미달, 다른 시설 접근 시도
404 Not Found 리소스 없음 잘못된 ID, 삭제된 리소스
409 Conflict 리소스 충돌 중복 이메일, 중복 slug, 중복 device_uid
500 Internal Server Error 서버 오류 DB 오류, S3 오류, 예상치 못한 실패

오류 응답 예시

401 (인증 실패):

{"error": "invalid email or password"}

403 (권한 부족):

{"error": "requires role: super_admin"}

404 (리소스 없음):

{"error": "facility 660e8400-e29b-41d4-a716-446655440000 not found"}

409 (중복 충돌):

{"error": "facility with slug 'perigee-okcheon' already exists"}


19. 데이터 모델

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 (API 키)

{
  "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. 페이지네이션

페이지네이션을 지원하는 엔드포인트는 동일한 쿼리 파라미터와 응답 형식을 쓴다.

쿼리 파라미터

파라미터 타입 기본값 범위 설명
page integer 1 >= 1 페이지 번호 (1부터 시작)
per_page integer 20 1-100 페이지당 항목 수

응답 형식

{
  "items": [],
  "total": 142,
  "page": 1,
  "per_page": 20
}
필드 타입 설명
items array 현재 페이지의 데이터
total integer 전체 항목 수
page integer 현재 페이지 번호
per_page integer 페이지당 항목 수

페이지네이션 지원 엔드포인트

  • GET /api/v1/uploads
  • GET /api/v1/transcode-jobs
  • GET /api/v1/metadata/sessions

21. 워크플로 예제

전체 설정 흐름

시설 생성부터 오디오 업로드, 조회까지의 전체 과정이다.

#!/usr/bin/env bash
# End-to-end workflow: facility setup -> audio upload -> verification

# Step 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-here"
  }' | jq -r '.access_token')
echo "Logged in, token starts with: ${TOKEN:0:20}..."

# Step 2: Create a facility for Perigee Okcheon plant
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')
echo "Created facility: $FACILITY_ID"

# Step 3: Create a facility admin for the new site
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\": \"Perigee#Okcheon2025\",
    \"display_name\": \"전민석\",
    \"role\": \"facility_admin\",
    \"facility_id\": \"$FACILITY_ID\"
  }" | jq -r '.id')
echo "Created facility admin: $USER_ID"

# Step 4: Log in as the 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": "Perigee#Okcheon2025"
  }' | jq -r '.access_token')

# Step 5: Create an API key for the underwater hydrophone
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": "수중 하이드로폰 #3",
    "scopes": ["upload","ingest"],
    "expires_at": "2026-06-01T00:00:00Z"
  }' | jq -r '.key')
echo "API key created (save this!): ${API_KEY:0:12}..."

# Step 6: Upload an audio recording via the API key
curl -X POST https://api.xylolabs.com/api/v1/uploads \
  -H "X-Api-Key: $API_KEY" \
  -F "file=@hydrophone_reef_sample.flac" \
  -F 'metadata={"location":"reef_zone_alpha","depth_m":12.5,"sample_rate":96000}'
echo ""

# Step 7: Verify the upload appears in the listing
echo "Recent uploads:"
curl -s "https://api.xylolabs.com/api/v1/uploads?page=1&per_page=5" \
  -H "Authorization: Bearer $FA_TOKEN" | jq '.items[] | {id, original_filename, status}'

디바이스 메타데이터 스트리밍 흐름

세션 생성 -> XMBP 데이터 전송 -> 세션 종료 -> 데이터 조회 및 내보내기의 전체 과정이다.

#!/usr/bin/env bash
# Metadata streaming workflow: session lifecycle + data export

# Step 1: Create a session with multiple sensor 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": "터빈홀 연속 모니터링 2025-06-15",
    "streams": [
      {
        "stream_index": 0,
        "name": "temperature",
        "value_type": "f32",
        "unit": "celsius",
        "sample_rate_hz": 100,
        "description": "Turbine bearing surface temperature"
      },
      {
        "stream_index": 1,
        "name": "vibration_rms",
        "value_type": "f32",
        "unit": "g",
        "sample_rate_hz": 100,
        "description": "RMS vibration of turbine shaft"
      }
    ],
    "metadata": {"location":"turbine_hall_east","firmware":"1.2.0"}
  }' | jq -r '.id')
echo "Session created: $SESSION_ID"

# Step 2: Send XMBP binary batches (repeat as needed)
# In production, your firmware builds these in a loop
curl -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_001.bin
echo "Batch 1 sent"

curl -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_002.bin
echo "Batch 2 sent"

# Step 3: Close the session when monitoring is done
echo "Closing session..."
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}'

# Step 4: Review the session from the dashboard
echo "Session details:"
curl -s "https://api.xylolabs.com/api/v1/metadata/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $TOKEN" | jq '.'

# Step 5: Export data as CSV for offline analysis
curl -s "https://api.xylolabs.com/api/v1/metadata/sessions/$SESSION_ID/export?format=csv" \
  -H "Authorization: Bearer $TOKEN" \
  --output "turbine_monitoring_$(date +%Y%m%d).csv"
echo "Exported to turbine_monitoring_$(date +%Y%m%d).csv"

관리자 설정 흐름

시스템 설정을 확인하고 변경하는 과정이다.

#!/usr/bin/env bash
# Admin config workflow: inspect, modify, and reset settings

# Step 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-here"
  }' | jq -r '.access_token')

# Step 2: View all config grouped by category
echo "Current configuration:"
curl -s https://api.xylolabs.com/api/v1/config \
  -H "Authorization: Bearer $TOKEN" | jq '.categories[] | {category, entries: [.entries[] | {key, value}]}'

# Step 3: Switch default transcoding to AAC/MP4
echo "Changing default format to AAC/MP4..."
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 '{key, value}'

# Step 4: Reset back to default if needed
echo "Resetting to default..."
curl -s -X POST https://api.xylolabs.com/api/v1/config/reset/transcode.default_format \
  -H "Authorization: Bearer $TOKEN" | jq '{key, value}'

# Step 5: Check dashboard stats
echo "System overview:"
curl -s https://api.xylolabs.com/api/v1/stats/overview \
  -H "Authorization: Bearer $TOKEN" | jq '.'

Cat-M1 디바이스 현장 배포 워크플로

Cat-M1 모뎀이 장착된 현장 디바이스의 최초 배포 과정이다. 관리자가 대시보드에서 시설과 키를 준비하고, 디바이스 펌웨어가 AT 명령으로 세션을 열어 데이터를 보낸다.

#!/usr/bin/env bash
# Cat-M1 field deployment: end-to-end from provisioning to data verification

# === ADMIN SIDE (laptop/dashboard) ===

# Step 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-here"
  }' | jq -r '.access_token')

# Step 2: Create a field facility for Jeju wildlife monitoring
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')

# Step 3: Create an API key for the Cat-M1 device
API_KEY=$(curl -s -X POST https://api.xylolabs.com/api/v1/api-keys \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "조류 음성 레코더 (nRF9160)",
    "scopes": ["upload","ingest"]
  }' | jq -r '.key')
echo "Flash this API key into device firmware: ${API_KEY:0:12}..."

# Step 4: Pre-register the device
curl -s -X POST https://api.xylolabs.com/api/v1/devices \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "device_uid": 1,
    "name": "조류 음성 레코더",
    "hardware_version": "nrf9160",
    "firmware_version": "0.9.0"
  }' | jq '{id, device_uid, name}'

# === DEVICE SIDE (firmware behavior, simulated with curl) ===

# Step 5: Device creates a session after boot
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": "dawn-chorus-recording-2025-06-16",
    "streams": [
      {
        "stream_index": 0,
        "name": "audio_level_rms",
        "value_type": "f32",
        "unit": "dBFS",
        "sample_rate_hz": 10,
        "description": "RMS audio level from MEMS microphone"
      },
      {
        "stream_index": 1,
        "name": "battery_voltage",
        "value_type": "f32",
        "unit": "V",
        "sample_rate_hz": 0.1,
        "description": "LiPo battery voltage"
      }
    ],
    "metadata": {"gps_lat":33.3617,"gps_lon":126.5292,"altitude_m":850}
  }' | jq -r '.id')
echo "Device session started: $SESSION_ID"

# Step 6: Device sends XMBP batches over Cat-M1 (every 2.5s for 100Hz streams)
for i in 1 2 3; 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_00${i}.bin | jq -c '{accepted_samples, missed_batches}'
done

# Step 7: Device closes session before entering sleep
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, missed_batches}'
echo "Device entering PSM sleep mode..."

# === ADMIN SIDE (later, verifying collected data) ===

# Step 8: Check that data arrived
echo "Verifying data collection:"
curl -s "https://api.xylolabs.com/api/v1/metadata/sessions?device_id=$(
  curl -s https://api.xylolabs.com/api/v1/devices \
    -H "Authorization: Bearer $TOKEN" | jq -r '.[0].id'
)" -H "Authorization: Bearer $TOKEN" | jq '.items[] | {id, status, total_samples, started_at}'

# Step 9: Export for analysis
curl -s "https://api.xylolabs.com/api/v1/metadata/sessions/$SESSION_ID/export?format=csv" \
  -H "Authorization: Bearer $TOKEN" \
  --output "jeju_dawn_chorus_$(date +%Y%m%d).csv"
echo "Data exported for ornithology analysis"

22. LTE Cat-M1 통합 가이드

Cat-M1(LTE-M)은 저전력 광역 통신(LPWAN) 기술로, 배터리로 구동되는 현장 센서에 맞는다. 이 섹션에서는 Cat-M1 모뎀을 Xylolabs API와 연동하는 방법을 다룹니다.

지원 모뎀

아래 모뎀들은 UART AT 명령으로 HTTP POST를 지원하며 Xylolabs API와 호환된다.

모뎀 인터페이스 특징
Quectel BG770A UART AT 명령 Cat-M1/NB-IoT, 우수한 전력 관리.
u-blox SARA-R410M / R412M UART AT 명령 PSM/eDRX 지원이 좋아 배터리 수명 최적화에 유리.
Nordic nRF9160 통합 SoC (AT 또는 네이티브 SDK) MCU + 모뎀 원칩. Zephyr SDK로 네이티브 개발 가능.
Sierra Wireless HL7800 / HL7812 UART AT 명령 주요 통신사 인증 완료. 상용 배포에 맞음.
Murata Type 1SC UART AT 명령 12mm x 12mm 초소형 모듈. 공간이 제한된 설계에 맞음.

대역폭 예산

Cat-M1의 이론상 uplink 대역폭은 약 375 kbps이지만, 실측치는 약 300 kbps (~37 KB/s) 이다. XMBP 데이터를 보낼 때 이 한계를 고려해야 한다.

f32 @ 100Hz 채널 기준 계산:

Single channel data rate:
  sample_size = 8 (timestamp) + 4 (f32) = 12 bytes
  data_rate = 12 * 100 = 1,200 bytes/s = 1.2 KB/s

Number of channels that fit in Cat-M1 bandwidth:
  usable_bandwidth = 37 KB/s (practical Cat-M1 uplink)
  http_overhead_per_batch ~= 300 bytes (headers + TLS)
  effective_bandwidth ~= 35 KB/s (after overhead)
  max_channels = 35 / 1.2 ≈ 29 channels

With 4 channels:
  data_rate = 4 * 1.2 = 4.8 KB/s
  utilization = 4.8 / 37 = 13% of Cat-M1 bandwidth
  --> Plenty of headroom

Cat-M1 배치 크기 권장사항:

파라미터 권장값 근거
samples/batch 250 100Hz 기준 2.5초 치 데이터. HTTP 오버헤드 대비 효율적.
batch interval 2.5초 250 samples / 100 Hz
batch size (4ch f32) ~12 KB 14 + 4(5 + 25012) = 12,034 bytes
transfer time ~325 ms 12 KB / 37 KB/s
duty cycle ~13% 325 ms / 2500 ms

배터리 수명을 늘리려면 배치 인터벌을 5~10초로 늘리고 samples/batch를 그에 맞게 조정한다. 샘플링 레이트 자체를 낮추는 것도 좋는다.

AT 명령 예제 (Quectel BG770A)

아래는 BG770A 모뎀에서 AT 명령으로 Xylolabs API와 통신하는 시퀀스이다. 실제 펌웨어에서는 각 명령 사이에 응답을 파싱하고 에러를 처리해야 한다.

1. 네트워크 등록 및 접속

# Check SIM and registration status
AT+CPIN?                              # Expect: +CPIN: READY
AT+CEREG?                             # Expect: +CEREG: 0,1 (registered) or 0,5 (roaming)

# Configure Cat-M1 mode (disable NB-IoT)
AT+QCFG="nwscanseq",02               # Scan LTE-M first
AT+QCFG="iotopmode",0                 # LTE-M only

# Set APN for your carrier
AT+CGDCONT=1,"IP","your.carrier.apn"

# Activate PDP context
AT+QIACT=1                            # Activate context 1
AT+QIACT?                             # Verify: shows IP address

2. XMBP 데이터 업로드 (HTTP POST)

# Configure HTTP for XMBP binary data upload
AT+QHTTPCFG="contextid",1
AT+QHTTPCFG="requestheader",1         # We will send custom headers

# Set the URL for the data endpoint
AT+QHTTPURL=89,10
> https://api.xylolabs.com/api/v1/ingest/sessions/dd0e8400-.../data
# Expect: OK

# Send XMBP binary batch (12034 bytes in this example)
# First, upload the request with headers
AT+QHTTPPOST=12334,30,30
> POST /api/v1/ingest/sessions/dd0e8400-.../data HTTP/1.1\r\n
> Host: api.xylolabs.com\r\n
> X-Api-Key: xk_a1b2c3d4e5f6...\r\n
> Content-Type: application/octet-stream\r\n
> Content-Length: 12034\r\n
> \r\n
> <12034 bytes of raw XMBP binary data>
# Expect: +QHTTPPOST: 0,200
# (0 = no error, 200 = HTTP 200 OK)

# Read response body
AT+QHTTPREAD=10
# Expect: {"accepted_samples":1000,"missed_batches":0}

3. 오디오 파일 업로드 (chunked)

# For large audio files, use chunked transfer
# First upload the file to modem's UFS storage
AT+QFUPL="RAM:audio.wav",48000,30     # Upload to modem RAM (48KB file)

# Then HTTP POST with multipart
AT+QHTTPCFG="requestheader",1
AT+QHTTPURL=52,10
> https://api.xylolabs.com/api/v1/uploads

AT+QHTTPPOSTFILE="RAM:audio.wav",30
# Note: for multipart, firmware must construct the boundary and headers
# See Quectel HTTP AT Commands manual for multipart upload details

4. PSM(Power Save Mode) 설정

배터리 구동 디바이스에서 Cat-M1 모뎀의 PSM을 활용하면 대기 전류를 수 uA 수준으로 줄일 수 있다.

# Enable PSM with custom timing
# T3412 (periodic TAU timer): "00100001" = 10 hours
# T3324 (active timer): "00000101" = 10 seconds
AT+CPSMS=1,,,"00100001","00000101"

# Workflow per wake cycle:
#   1. MCU wakes up, reads sensors, builds XMBP batch
#   2. AT+CFUN=1 (wake modem from PSM)
#   3. Wait for +CEREG: 1 (re-registration)
#   4. POST XMBP batch
#   5. Modem enters PSM after T3324 (10s) idle
#   6. MCU enters deep sleep until next cycle

# Check PSM status
AT+QPSMCFG?
# Expect: shows current PSM timers and mode

PSM 타이밍 참고: T3324(active timer)는 마지막 데이터 전송 후 모뎀이 깨어 있는 시간이다. XMBP POST + 응답 수신에 2~3초면 충분하므로 T3324을 10초로 설정하면 여유가 있다. T3412(TAU timer)는 배치 전송 주기보다 길게 설정하되, 너무 길면 네트워크 재등록이 오래 걸릴 수 있으니 6~24시간 사이가 적당한다.

예제: 현장 배포 워크플로

Cat-M1 디바이스의 현장 배포 과정은 21. 워크플로 예제의 Cat-M1 디바이스 현장 배포 워크플로에서 bash 스크립트로 다루고 있다. 관리자 쪽 프로비저닝(시설, 키 생성)부터 디바이스 쪽 동작(세션 생성, XMBP 전송, 세션 종료, PSM 진입), 다시 관리자 쪽 데이터 확인까지 전체 흐름이 담겨 있다.

요약하면 다음과 같는다.

  1. 관리자: 시설 생성, API 키 발급, 디바이스 사전 등록
  2. 디바이스 펌웨어: 부팅 -> AT 명령으로 네트워크 접속 -> 세션 생성 -> XMBP 배치 전송 (2.5초 간격) -> 세션 종료 -> PSM 진입
  3. 관리자: 대시보드에서 수집 데이터 확인, CSV 내보내기로 분석