Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:58:23 +08:00
commit 9faa5d88f3
22 changed files with 9600 additions and 0 deletions

View File

@@ -0,0 +1,519 @@
---
name: sleeptrack-be
description: This skill provides comprehensive backend REST API integration for Asleep sleep tracking platform. Use this skill when building server-side applications, API proxies for mobile apps, webhook event handlers, cross-platform backends (React Native, Flutter), analytics dashboards, or multi-tenant sleep tracking systems. Covers authentication, user management, session retrieval, statistics, webhook integration, and production-ready patterns with code examples in Python, Node.js, and curl.
---
# Sleeptrack Backend API Integration
## Overview
This skill provides comprehensive guidance for integrating the Asleep REST API into backend applications. It covers server-side user management, session data retrieval, statistics aggregation, webhook event handling, and production-ready patterns for building robust sleep tracking backends.
**Use this skill when:**
- Building backend/server-side sleep tracking integrations
- Creating API proxies for mobile applications
- Implementing webhook handlers for real-time sleep data
- Developing cross-platform backends (React Native, Flutter)
- Building analytics dashboards and reporting systems
- Creating multi-tenant sleep tracking applications
- Integrating sleep data with other health platforms
## Quick Start
### 1. Get Your API Key
1. Sign up at https://dashboard.asleep.ai
2. Generate an API key for your application
3. Store securely in environment variables (never commit to version control)
### 2. Basic Authentication
All API requests require the `x-api-key` header:
**curl:**
```bash
curl -X GET "https://api.asleep.ai/ai/v1/users/USER_ID" \
-H "x-api-key: YOUR_API_KEY"
```
**Python:**
```python
import requests
headers = {"x-api-key": "YOUR_API_KEY"}
response = requests.get(
"https://api.asleep.ai/ai/v1/users/USER_ID",
headers=headers
)
```
**Node.js:**
```javascript
const axios = require('axios');
const response = await axios.get(
'https://api.asleep.ai/ai/v1/users/USER_ID',
{
headers: { 'x-api-key': 'YOUR_API_KEY' }
}
);
```
## API Client Structure
Build a reusable API client to handle authentication, error handling, and common operations.
**Key Components:**
- Base URL configuration (`https://api.asleep.ai`)
- API key authentication in headers
- Error handling for common HTTP status codes (401, 403, 404)
- Request methods for all API endpoints
- Session management with persistent connections
**For complete implementations:**
- Python: See `references/python_client_implementation.md`
- Node.js: See `references/nodejs_client_implementation.md`
- REST API details: See `references/rest_api_reference.md`
**Basic Client Structure:**
```python
class AsleepClient:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.asleep.ai"
self.session = requests.Session()
self.session.headers.update({"x-api-key": api_key})
def _request(self, method: str, path: str, **kwargs):
# Handle authentication and errors
# See python_client_implementation.md for full code
pass
```
## User Management
### Creating Users
Create users before tracking sleep. User IDs are managed by your application.
```python
# Create user with metadata
user_id = client.create_user(metadata={
"birth_year": 1990,
"gender": "male",
"height": 175.5, # cm
"weight": 70.0 # kg
})
```
**Available Metadata Fields:**
- `birth_year` (Integer): Birth year
- `birth_month` (Integer): 1-12
- `birth_day` (Integer): 1-31
- `gender` (String): `male`, `female`, `non_binary`, `other`, `prefer_not_to_say`
- `height` (Float): Height in cm (0-300)
- `weight` (Float): Weight in kg (0-1000)
### Retrieving User Information
```python
user_data = client.get_user(user_id)
# Returns: user_id, to_be_deleted status, last_session_info, metadata
```
**Response includes:**
- User ID and deletion status
- Last session information (if available)
- User metadata (demographic information)
### Deleting Users
Permanently removes user and all associated data (sessions, reports).
```python
client.delete_user(user_id)
```
**For detailed examples and response structures:**
- See `references/rest_api_reference.md` (User Management section)
- See `references/python_client_implementation.md` or `references/nodejs_client_implementation.md`
## Session Management
### Retrieving Session Details
Get comprehensive sleep analysis for a specific session.
```python
session = client.get_session(
session_id="session123",
user_id="user123",
timezone="America/New_York"
)
# Access key metrics
print(f"Sleep efficiency: {session['stat']['sleep_efficiency']:.1f}%")
print(f"Total sleep time: {session['stat']['sleep_time']}")
print(f"Sleep stages: {session['session']['sleep_stages']}")
```
**Sleep Stage Values:**
- `-1`: Unknown/No data
- `0`: Wake
- `1`: Light sleep
- `2`: Deep sleep
- `3`: REM sleep
**Key Metrics:**
- `sleep_efficiency`: (Total sleep time / Time in bed) × 100%
- `sleep_latency`: Time to fall asleep (seconds)
- `waso_count`: Wake after sleep onset episodes
- `time_in_bed`: Total time in bed (seconds)
- `time_in_sleep`: Actual sleep time (seconds)
- `time_in_light/deep/rem`: Stage durations (seconds)
### Listing Sessions
Retrieve multiple sessions with date filtering and pagination.
```python
sessions = client.list_sessions(
user_id="user123",
date_gte="2024-01-01",
date_lte="2024-01-31",
limit=50,
order_by="DESC"
)
```
**Pagination Example:**
```python
all_sessions = []
offset = 0
limit = 100
while True:
result = client.list_sessions(
user_id="user123",
offset=offset,
limit=limit
)
sessions = result['sleep_session_list']
all_sessions.extend(sessions)
if len(sessions) < limit:
break
offset += limit
```
### Deleting Sessions
```python
client.delete_session(session_id="session123", user_id="user123")
```
**For detailed session data structures and examples:**
- See `references/rest_api_reference.md` (Session Management section)
- See client implementation references for language-specific examples
## Statistics and Analytics
### Average Statistics
Get aggregated sleep metrics over a time period (max 100 days).
```python
stats = client.get_average_stats(
user_id="user123",
start_date="2024-01-01",
end_date="2024-01-31",
timezone="UTC"
)
avg = stats['average_stats']
print(f"Average sleep time: {avg['sleep_time']}")
print(f"Average efficiency: {avg['sleep_efficiency']:.1f}%")
print(f"Light sleep ratio: {avg['light_ratio']:.1%}")
print(f"Number of sessions: {len(stats['slept_sessions'])}")
```
**Returned Metrics:**
- Average sleep time, bedtime, wake time
- Average sleep efficiency
- Sleep stage ratios (light, deep, REM)
- List of sessions included in calculation
**For trend analysis and advanced analytics:**
- See `references/python_client_implementation.md` (Analytics section)
- See `references/rest_api_reference.md` (Statistics section)
## Webhook Integration
Webhooks enable real-time notifications for sleep session events.
### Webhook Event Types
1. **INFERENCE_COMPLETE**: Incremental sleep data during tracking (every 5-40 minutes)
2. **SESSION_COMPLETE**: Final comprehensive sleep analysis when session ends
### Setting Up Webhook Endpoint
**Basic Structure:**
```python
@app.route('/asleep-webhook', methods=['POST'])
def asleep_webhook():
# 1. Verify authentication (x-api-key header)
# 2. Parse event data
# 3. Handle event type (INFERENCE_COMPLETE or SESSION_COMPLETE)
# 4. Process and store data
# 5. Return 200 response
pass
```
**Key Implementation Points:**
- Verify `x-api-key` header matches your API key
- Validate `x-user-id` header
- Handle both INFERENCE_COMPLETE and SESSION_COMPLETE events
- Implement idempotency (check if event already processed)
- Process asynchronously for better performance
- Return 200 status immediately
**For complete webhook implementations:**
- Python (Flask): See `references/webhook_implementation_guide.md`
- Node.js (Express): See `references/webhook_implementation_guide.md`
- Webhook payloads: See `references/webhook_reference.md`
### Webhook Best Practices
**Idempotency:**
Check if webhook event was already processed to avoid duplicates.
**Asynchronous Processing:**
Queue webhook events for background processing and respond immediately.
**Error Handling:**
Return 200 even if processing fails internally to prevent retries.
**For detailed patterns:**
- See `references/webhook_implementation_guide.md`
- See `references/production_patterns.md` (Background Jobs section)
## Common Backend Patterns
### 1. API Proxy for Mobile Apps
Create a backend proxy to:
- Hide API keys from mobile clients
- Add custom authentication
- Implement business logic
- Track usage and analytics
**Key endpoints:**
- POST `/api/users` - Create Asleep user for authenticated app user
- GET `/api/sessions/{id}` - Proxy session retrieval with auth
- GET `/api/sessions` - List sessions with filtering
- GET `/api/statistics` - Get aggregated statistics
**For complete implementation:**
- See `references/python_client_implementation.md` (API Proxy section)
### 2. Analytics Dashboard Backend
Aggregate and analyze sleep data across multiple users:
- Calculate comprehensive sleep scores
- Generate weekly/monthly reports
- Analyze cohort sleep patterns
- Provide personalized insights
**Key features:**
- Sleep score calculation (efficiency + consistency + duration)
- Trend analysis over time
- Multi-user aggregation
- Report generation
**For complete implementation:**
- See `references/python_client_implementation.md` (Analytics section)
### 3. Multi-Tenant Application
Manage sleep tracking for multiple organizations or teams:
- Organization-level user management
- Aggregated organization statistics
- Role-based access control
- Per-organization settings
**For complete implementation:**
- See `references/python_client_implementation.md` (Multi-Tenant section)
## Error Handling
### Common Error Codes
- **401 Unauthorized**: Invalid API key
- **403 Forbidden**: Rate limit exceeded or insufficient permissions
- **404 Not Found**: Resource does not exist
- **422 Unprocessable Entity**: Invalid request parameters
### Retry Logic with Exponential Backoff
```python
def retry_with_exponential_backoff(func, max_retries=3, base_delay=1.0):
for attempt in range(max_retries):
try:
return func()
except requests.exceptions.HTTPError as e:
if e.response.status_code == 403 and "rate limit" in str(e):
if attempt < max_retries - 1:
delay = min(base_delay * (2 ** attempt), 60.0)
time.sleep(delay)
continue
raise
```
### Custom Exception Classes
```python
class AsleepAPIError(Exception):
"""Base exception for Asleep API errors"""
pass
class RateLimitError(AsleepAPIError):
"""Rate limit exceeded"""
pass
class ResourceNotFoundError(AsleepAPIError):
"""Resource not found"""
pass
```
**For comprehensive error handling:**
- See `references/python_client_implementation.md` or `references/nodejs_client_implementation.md`
- See `references/production_patterns.md` (Error Recovery section)
## Testing
### Local Webhook Testing
Use ngrok to expose local server for webhook testing:
```bash
# Start local server
python app.py # or npm start
# Expose with ngrok
ngrok http 5000
# Use ngrok URL in webhook configuration
# Example: https://abc123.ngrok.io/asleep-webhook
```
### Mock API Responses
```python
@patch('requests.Session.request')
def test_create_user(mock_request):
mock_response = Mock()
mock_response.json.return_value = {
"result": {"user_id": "test_user_123"}
}
mock_request.return_value = mock_response
user_id = client.create_user()
assert user_id == "test_user_123"
```
**For complete testing examples:**
- See `references/python_client_implementation.md` (Testing section)
## Production Best Practices
### Security
1. **API Key Management:**
- Store in environment variables or secret management system
- Never commit to version control
- Rotate keys periodically
- Use different keys for dev/staging/production
2. **Webhook Security:**
- Verify `x-api-key` header
- Use HTTPS endpoints only
- Implement rate limiting
- Log all webhook attempts
3. **User Data Privacy:**
- Encrypt sensitive data at rest
- Implement proper access controls
- Handle data deletion requests
- Comply with GDPR/CCPA
### Performance
1. **Caching:** Cache immutable session data
2. **Rate Limiting:** Protect your backend from overload
3. **Connection Pooling:** Reuse HTTP connections
4. **Batch Processing:** Process multiple requests in parallel
### Monitoring
1. **Logging:** Structured logging for all API requests
2. **Metrics:** Track request duration, error rates, throughput
3. **Health Checks:** Implement `/health`, `/ready`, `/live` endpoints
4. **Alerting:** Alert on error rate spikes or API failures
### Deployment
1. **Configuration:** Environment-based settings with validation
2. **Health Checks:** Support Kubernetes liveness/readiness probes
3. **Graceful Shutdown:** Handle termination signals properly
4. **Error Recovery:** Circuit breaker pattern for API failures
**For comprehensive production patterns:**
- Caching strategies: See `references/production_patterns.md`
- Rate limiting: See `references/production_patterns.md`
- Monitoring: See `references/production_patterns.md`
- Deployment: See `references/production_patterns.md`
## Resources
### Reference Documentation
This skill includes comprehensive reference files:
- `references/python_client_implementation.md`: Complete Python client with all methods, analytics classes, and examples
- `references/nodejs_client_implementation.md`: Complete Node.js client with Express integration
- `references/webhook_implementation_guide.md`: Full webhook handlers in Python and Node.js with best practices
- `references/rest_api_reference.md`: Complete REST API endpoint documentation with request/response examples
- `references/webhook_reference.md`: Webhook integration guide with payload structures
- `references/production_patterns.md`: Caching, rate limiting, monitoring, deployment, and performance optimization
To access detailed information:
```
Read references/python_client_implementation.md
Read references/nodejs_client_implementation.md
Read references/webhook_implementation_guide.md
Read references/rest_api_reference.md
Read references/webhook_reference.md
Read references/production_patterns.md
```
### Official Documentation
- **Main Documentation**: https://docs-en.asleep.ai
- **API Basics**: https://docs-en.asleep.ai/docs/api-basics.md
- **Webhook Guide**: https://docs-en.asleep.ai/docs/webhook.md
- **Dashboard**: https://dashboard.asleep.ai
- **LLM-Optimized Reference**: https://docs-en.asleep.ai/llms.txt
### Related Skills
- **sleeptrack-foundation**: Core concepts, authentication, data structures, and platform-agnostic patterns
- **sleeptrack-ios**: iOS SDK integration for native iOS applications
- **sleeptrack-android**: Android SDK integration for native Android applications
### Support
For technical support and API issues:
- Check Dashboard for API usage and status
- Review error logs and response codes
- Contact support through Asleep Dashboard

View File

@@ -0,0 +1,467 @@
# Node.js Client Implementation Guide
This reference provides complete Node.js client implementations for the Asleep API, including webhook servers and production patterns.
## Complete Node.js API Client
```javascript
const axios = require('axios');
class AsleepClient {
constructor(apiKey, baseURL = 'https://api.asleep.ai') {
this.apiKey = apiKey;
this.baseURL = baseURL;
this.client = axios.create({
baseURL: baseURL,
headers: { 'x-api-key': apiKey }
});
}
async _request(method, path, options = {}) {
try {
const response = await this.client.request({
method,
url: path,
...options
});
return response.data;
} catch (error) {
if (error.response) {
const status = error.response.status;
const detail = error.response.data?.detail || 'Unknown error';
if (status === 401) {
throw new Error('Invalid API key');
} else if (status === 403) {
throw new Error(`API access error: ${detail}`);
} else if (status === 404) {
throw new Error('Resource not found');
}
}
throw error;
}
}
// User management
async createUser(metadata = null) {
const data = metadata ? { metadata } : {};
const result = await this._request('POST', '/ai/v1/users', { data });
return result.result.user_id;
}
async getUser(userId) {
const result = await this._request('GET', `/ai/v1/users/${userId}`);
return result.result;
}
async deleteUser(userId) {
await this._request('DELETE', `/ai/v1/users/${userId}`);
}
// Session management
async getSession(sessionId, userId, timezone = 'UTC') {
const result = await this._request('GET', `/data/v3/sessions/${sessionId}`, {
headers: { 'x-user-id': userId, 'timezone': timezone }
});
return result.result;
}
async listSessions(userId, options = {}) {
const { dateGte, dateLte, offset = 0, limit = 20, orderBy = 'DESC' } = options;
const params = { offset, limit, order_by: orderBy };
if (dateGte) params.date_gte = dateGte;
if (dateLte) params.date_lte = dateLte;
const result = await this._request('GET', '/data/v1/sessions', {
headers: { 'x-user-id': userId },
params
});
return result.result;
}
async deleteSession(sessionId, userId) {
await this._request('DELETE', `/ai/v1/sessions/${sessionId}`, {
headers: { 'x-user-id': userId }
});
}
// Statistics
async getAverageStats(userId, startDate, endDate, timezone = 'UTC') {
const result = await this._request('GET', `/data/v1/users/${userId}/average-stats`, {
headers: { 'timezone': timezone },
params: { start_date: startDate, end_date: endDate }
});
return result.result;
}
}
// Usage
const client = new AsleepClient(process.env.ASLEEP_API_KEY);
```
## Express Webhook Server
```javascript
const express = require('express');
const app = express();
app.use(express.json());
const EXPECTED_API_KEY = process.env.ASLEEP_API_KEY;
app.post('/asleep-webhook', async (req, res) => {
// Verify authentication
const apiKey = req.headers['x-api-key'];
const userId = req.headers['x-user-id'];
if (apiKey !== EXPECTED_API_KEY) {
console.warn('Unauthorized webhook attempt');
return res.status(401).json({ error: 'Unauthorized' });
}
const { event, session_id, stat } = req.body;
console.log(`Received ${event} event for user ${userId}`);
try {
switch (event) {
case 'INFERENCE_COMPLETE':
await handleInferenceComplete(req.body);
break;
case 'SESSION_COMPLETE':
await handleSessionComplete(req.body);
break;
default:
console.warn(`Unknown event type: ${event}`);
}
res.status(200).json({ status: 'success' });
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Processing failed' });
}
});
async function handleInferenceComplete(event) {
const { session_id, user_id, sleep_stages } = event;
// Update real-time dashboard
await updateLiveDashboard(session_id, sleep_stages);
// Store incremental data
await db.collection('incremental_data').insertOne(event);
console.log(`Processed INFERENCE_COMPLETE for session ${session_id}`);
}
async function handleSessionComplete(event) {
const { session_id, user_id, stat, session } = event;
// Store complete report
await db.collection('sleep_reports').insertOne({
user_id,
session_id,
date: session.start_time,
statistics: stat,
session_data: session,
created_at: new Date()
});
// Send user notification
await sendPushNotification(user_id, {
title: 'Sleep Report Ready',
body: `Sleep time: ${stat.sleep_time}, Efficiency: ${stat.sleep_efficiency.toFixed(1)}%`
});
// Update user statistics
await updateUserAggregatedStats(user_id);
console.log(`Processed SESSION_COMPLETE for session ${session_id}`);
}
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Webhook server listening on port ${PORT}`);
});
```
## Retry with Exponential Backoff
```javascript
async function retryWithExponentialBackoff(
func,
maxRetries = 3,
baseDelay = 1000,
maxDelay = 60000
) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await func();
} catch (error) {
if (error.response?.status === 403) {
const detail = error.response.data?.detail || '';
if (detail.toLowerCase().includes('rate limit')) {
if (attempt < maxRetries - 1) {
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
console.log(`Rate limited, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
}
}
throw error;
}
}
}
// Usage
const result = await retryWithExponentialBackoff(
() => client.getSession('session123', 'user123')
);
```
## Basic Usage Examples
### Creating Users
```javascript
// Create user with metadata
const userId = await client.createUser({
birth_year: 1990,
gender: 'male',
height: 175.5,
weight: 70.0
});
console.log(`Created user: ${userId}`);
// Create user without metadata
const userId = await client.createUser();
```
### Getting Sessions
```javascript
// Get sessions for date range
const sessions = await client.listSessions('user123', {
dateGte: '2024-01-01',
dateLte: '2024-01-31',
limit: 50,
orderBy: 'DESC'
});
console.log(`Found ${sessions.sleep_session_list.length} sessions`);
sessions.sleep_session_list.forEach(session => {
console.log(`Session ${session.session_id}: ${session.session_start_time}`);
console.log(` State: ${session.state}, Time in bed: ${session.time_in_bed}s`);
});
```
### Getting Session Details
```javascript
const session = await client.getSession(
'session123',
'user123',
'America/New_York'
);
console.log(`Sleep efficiency: ${session.stat.sleep_efficiency.toFixed(1)}%`);
console.log(`Total sleep time: ${session.stat.sleep_time}`);
console.log(`Sleep stages: ${session.session.sleep_stages}`);
console.log(`Sleep cycles: ${session.stat.sleep_cycle.length}`);
```
### Getting Statistics
```javascript
const stats = await client.getAverageStats(
'user123',
'2024-01-01',
'2024-01-31',
'UTC'
);
const avg = stats.average_stats;
console.log(`Average sleep time: ${avg.sleep_time}`);
console.log(`Average efficiency: ${avg.sleep_efficiency.toFixed(1)}%`);
console.log(`Average bedtime: ${avg.start_time}`);
console.log(`Average wake time: ${avg.end_time}`);
console.log(`Light sleep ratio: ${(avg.light_ratio * 100).toFixed(1)}%`);
console.log(`Deep sleep ratio: ${(avg.deep_ratio * 100).toFixed(1)}%`);
console.log(`REM sleep ratio: ${(avg.rem_ratio * 100).toFixed(1)}%`);
console.log(`Number of sessions: ${stats.slept_sessions.length}`);
```
## Asynchronous Webhook Processing
```javascript
const Queue = require('bull');
const webhookQueue = new Queue('asleep-webhooks', {
redis: {
host: 'localhost',
port: 6379
}
});
app.post('/webhook', async (req, res) => {
const event = req.body;
// Queue for async processing
await webhookQueue.add(event);
// Respond immediately
res.status(200).json({ status: 'queued' });
});
// Process queued webhooks
webhookQueue.process(async (job) => {
const event = job.data;
if (event.event === 'SESSION_COMPLETE') {
await handleSessionComplete(event);
} else if (event.event === 'INFERENCE_COMPLETE') {
await handleInferenceComplete(event);
}
});
```
## Idempotency Pattern
```javascript
async function handleSessionComplete(event) {
const sessionId = event.session_id;
// Check if already processed
const existing = await db.collection('processed_webhooks').findOne({
session_id: sessionId,
event: 'SESSION_COMPLETE'
});
if (existing) {
console.log(`Session ${sessionId} already processed, skipping`);
return;
}
// Process event
await saveSleepReport(event);
// Mark as processed
await db.collection('processed_webhooks').insertOne({
session_id: sessionId,
event: 'SESSION_COMPLETE',
processed_at: new Date()
});
}
```
## Comprehensive Error Handling
```javascript
class AsleepAPIError extends Error {
constructor(message, statusCode, detail) {
super(message);
this.name = 'AsleepAPIError';
this.statusCode = statusCode;
this.detail = detail;
}
}
class RateLimitError extends AsleepAPIError {
constructor(detail) {
super('Rate limit exceeded', 403, detail);
this.name = 'RateLimitError';
}
}
class ResourceNotFoundError extends AsleepAPIError {
constructor(detail) {
super('Resource not found', 404, detail);
this.name = 'ResourceNotFoundError';
}
}
async function safeApiRequest(requestFunc) {
try {
return await requestFunc();
} catch (error) {
if (error.response) {
const status = error.response.status;
const detail = error.response.data?.detail || 'Unknown error';
if (status === 401) {
throw new AsleepAPIError('Authentication failed', 401, detail);
} else if (status === 403) {
if (detail.toLowerCase().includes('rate limit')) {
throw new RateLimitError(detail);
} else {
throw new AsleepAPIError('Access forbidden', 403, detail);
}
} else if (status === 404) {
throw new ResourceNotFoundError(detail);
} else {
throw new AsleepAPIError(`API error (${status})`, status, detail);
}
}
throw error;
}
}
// Usage
try {
const user = await safeApiRequest(() => client.getUser('user123'));
} catch (error) {
if (error instanceof ResourceNotFoundError) {
console.log('User not found, creating new user...');
const userId = await client.createUser();
} else if (error instanceof RateLimitError) {
console.log('Rate limited, try again later');
} else if (error instanceof AsleepAPIError) {
console.error(`API error: ${error.message}`);
}
}
```
## Production Configuration
```javascript
// config.js
require('dotenv').config();
class Config {
static get ASLEEP_API_KEY() {
return process.env.ASLEEP_API_KEY;
}
static get ASLEEP_BASE_URL() {
return process.env.ASLEEP_BASE_URL || 'https://api.asleep.ai';
}
static get DATABASE_URL() {
return process.env.DATABASE_URL;
}
static get REDIS_URL() {
return process.env.REDIS_URL;
}
static get WEBHOOK_SECRET() {
return process.env.WEBHOOK_SECRET;
}
static get ENABLE_CACHING() {
return process.env.ENABLE_CACHING !== 'false';
}
static validate() {
if (!this.ASLEEP_API_KEY) {
throw new Error('ASLEEP_API_KEY environment variable required');
}
if (!this.DATABASE_URL) {
throw new Error('DATABASE_URL environment variable required');
}
}
}
module.exports = Config;
```

View File

@@ -0,0 +1,696 @@
# Production Patterns and Best Practices
This reference provides comprehensive production-ready patterns for deploying and maintaining Asleep API integrations in production environments.
## Caching Strategies
### Session Caching
Sessions are immutable once complete, making them ideal for caching:
```python
from functools import lru_cache
from datetime import datetime, timedelta
import json
import redis
redis_client = redis.Redis(host='localhost', port=6379, db=0)
class CachedAsleepClient(AsleepClient):
"""Client with response caching"""
@lru_cache(maxsize=128)
def get_session_cached(self, session_id: str, user_id: str) -> Dict:
"""Get session with caching (sessions are immutable once complete)"""
return self.get_session(session_id, user_id)
def get_recent_sessions(self, user_id: str, days: int = 7) -> List[Dict]:
"""Get recent sessions with Redis caching"""
cache_key = f"sessions:{user_id}:{days}"
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
end_date = datetime.now()
start_date = end_date - timedelta(days=days)
result = self.list_sessions(
user_id=user_id,
date_gte=start_date.strftime("%Y-%m-%d"),
date_lte=end_date.strftime("%Y-%m-%d")
)
# Cache for 5 minutes
redis_client.setex(cache_key, 300, json.dumps(result))
return result
def invalidate_user_cache(self, user_id: str):
"""Invalidate all caches for a user"""
pattern = f"sessions:{user_id}:*"
for key in redis_client.scan_iter(match=pattern):
redis_client.delete(key)
```
## Rate Limiting
### Application-Level Rate Limiting
Protect your backend from being overwhelmed:
```python
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["100 per hour"]
)
@app.route('/api/sessions/<session_id>')
@limiter.limit("10 per minute")
def get_session(session_id):
"""Rate-limited session endpoint"""
# Implementation
pass
@app.route('/api/statistics')
@limiter.limit("5 per minute")
def get_statistics():
"""Statistics endpoint with stricter rate limiting"""
# Implementation
pass
```
### API Request Rate Limiting
Respect Asleep API rate limits with request throttling:
```python
import time
from collections import deque
from threading import Lock
class RateLimitedClient(AsleepClient):
"""Client with built-in rate limiting"""
def __init__(self, api_key: str, requests_per_second: int = 10):
super().__init__(api_key)
self.requests_per_second = requests_per_second
self.request_times = deque()
self.lock = Lock()
def _wait_for_rate_limit(self):
"""Wait if necessary to stay within rate limits"""
with self.lock:
now = time.time()
# Remove requests older than 1 second
while self.request_times and self.request_times[0] < now - 1:
self.request_times.popleft()
# If at limit, wait
if len(self.request_times) >= self.requests_per_second:
sleep_time = 1 - (now - self.request_times[0])
if sleep_time > 0:
time.sleep(sleep_time)
self.request_times.popleft()
self.request_times.append(time.time())
def _request(self, method: str, path: str, **kwargs):
"""Rate-limited request"""
self._wait_for_rate_limit()
return super()._request(method, path, **kwargs)
```
## Connection Pooling
### HTTP Session with Connection Pool
Reuse connections for better performance:
```python
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def create_session_with_retries():
"""Create session with connection pooling and retries"""
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
method_whitelist=["HEAD", "GET", "OPTIONS", "POST", "PUT", "DELETE"]
)
adapter = HTTPAdapter(
max_retries=retry_strategy,
pool_connections=10,
pool_maxsize=20
)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
class PooledAsleepClient(AsleepClient):
"""Client with connection pooling"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.asleep.ai"
self.session = create_session_with_retries()
self.session.headers.update({"x-api-key": api_key})
```
## Monitoring and Logging
### Structured Logging
```python
import logging
import json
from datetime import datetime
class StructuredLogger:
"""Structured logging for API requests"""
def __init__(self, name: str):
self.logger = logging.getLogger(name)
def log_request(self, method: str, path: str, user_id: str = None):
"""Log API request"""
self.logger.info(json.dumps({
'event': 'api_request',
'timestamp': datetime.now().isoformat(),
'method': method,
'path': path,
'user_id': user_id
}))
def log_response(self, method: str, path: str, status_code: int, duration: float):
"""Log API response"""
self.logger.info(json.dumps({
'event': 'api_response',
'timestamp': datetime.now().isoformat(),
'method': method,
'path': path,
'status_code': status_code,
'duration_ms': duration * 1000
}))
def log_error(self, method: str, path: str, error: Exception, duration: float):
"""Log API error"""
self.logger.error(json.dumps({
'event': 'api_error',
'timestamp': datetime.now().isoformat(),
'method': method,
'path': path,
'error_type': type(error).__name__,
'error_message': str(error),
'duration_ms': duration * 1000
}))
class MonitoredAsleepClient(AsleepClient):
"""Client with comprehensive logging"""
def __init__(self, api_key: str):
super().__init__(api_key)
self.logger = StructuredLogger(__name__)
def _request(self, method: str, path: str, **kwargs):
"""Monitored API request"""
start_time = datetime.now()
user_id = kwargs.get('headers', {}).get('x-user-id')
self.logger.log_request(method, path, user_id)
try:
result = super()._request(method, path, **kwargs)
duration = (datetime.now() - start_time).total_seconds()
self.logger.log_response(method, path, 200, duration)
return result
except Exception as e:
duration = (datetime.now() - start_time).total_seconds()
self.logger.log_error(method, path, e, duration)
raise
```
### Metrics Collection
```python
from datadog import statsd
class MetricsClient(AsleepClient):
"""Client with metrics collection"""
def _request(self, method: str, path: str, **kwargs):
"""Request with metrics"""
start_time = datetime.now()
try:
result = super()._request(method, path, **kwargs)
duration = (datetime.now() - start_time).total_seconds()
# Record success metrics
statsd.increment('asleep_api.request.success')
statsd.timing('asleep_api.request.duration', duration)
statsd.histogram('asleep_api.response_time', duration)
return result
except Exception as e:
duration = (datetime.now() - start_time).total_seconds()
# Record error metrics
statsd.increment('asleep_api.request.error')
statsd.increment(f'asleep_api.error.{type(e).__name__}')
statsd.timing('asleep_api.request.duration', duration)
raise
```
## Security Best Practices
### API Key Management
```python
import os
from dotenv import load_dotenv
class SecureConfig:
"""Secure configuration management"""
def __init__(self):
load_dotenv()
self._validate_config()
def _validate_config(self):
"""Validate required environment variables"""
required = ['ASLEEP_API_KEY', 'DATABASE_URL']
missing = [var for var in required if not os.getenv(var)]
if missing:
raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
@property
def asleep_api_key(self) -> str:
"""Get API key from environment"""
return os.getenv('ASLEEP_API_KEY')
@property
def database_url(self) -> str:
"""Get database URL from environment"""
return os.getenv('DATABASE_URL')
@property
def redis_url(self) -> str:
"""Get Redis URL from environment"""
return os.getenv('REDIS_URL', 'redis://localhost:6379')
```
### Webhook Security
```python
import hmac
import hashlib
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
"""Verify webhook payload signature"""
expected_signature = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
@app.route('/asleep-webhook', methods=['POST'])
def secure_webhook():
"""Webhook endpoint with signature verification"""
# Verify API key
api_key = request.headers.get('x-api-key')
if api_key != EXPECTED_API_KEY:
return jsonify({"error": "Unauthorized"}), 401
# Verify signature (if implemented)
signature = request.headers.get('x-signature')
if signature:
if not verify_webhook_signature(request.data, signature, WEBHOOK_SECRET):
return jsonify({"error": "Invalid signature"}), 401
# Process webhook
event = request.json
process_webhook(event)
return jsonify({"status": "success"}), 200
```
## Deployment Configuration
### Environment-Based Configuration
```python
import os
from enum import Enum
class Environment(Enum):
DEVELOPMENT = "development"
STAGING = "staging"
PRODUCTION = "production"
class Config:
"""Environment-based configuration"""
def __init__(self):
self.env = Environment(os.getenv('ENVIRONMENT', 'development'))
self.asleep_api_key = os.getenv('ASLEEP_API_KEY')
self.asleep_base_url = os.getenv('ASLEEP_BASE_URL', 'https://api.asleep.ai')
self.database_url = os.getenv('DATABASE_URL')
self.redis_url = os.getenv('REDIS_URL')
# Feature flags
self.enable_caching = self._parse_bool('ENABLE_CACHING', True)
self.enable_webhooks = self._parse_bool('ENABLE_WEBHOOKS', True)
self.enable_metrics = self._parse_bool('ENABLE_METRICS', True)
# Performance settings
self.max_connections = int(os.getenv('MAX_CONNECTIONS', '100'))
self.request_timeout = int(os.getenv('REQUEST_TIMEOUT', '30'))
self._validate()
def _parse_bool(self, key: str, default: bool) -> bool:
"""Parse boolean environment variable"""
value = os.getenv(key, str(default)).lower()
return value in ('true', '1', 'yes')
def _validate(self):
"""Validate configuration"""
if not self.asleep_api_key:
raise ValueError("ASLEEP_API_KEY is required")
if self.env == Environment.PRODUCTION:
if not self.database_url:
raise ValueError("DATABASE_URL is required in production")
@property
def is_production(self) -> bool:
return self.env == Environment.PRODUCTION
@property
def is_development(self) -> bool:
return self.env == Environment.DEVELOPMENT
```
### Health Check Endpoint
```python
from flask import Flask, jsonify
import requests
@app.route('/health')
def health_check():
"""Health check endpoint for load balancers"""
checks = {
'status': 'healthy',
'timestamp': datetime.now().isoformat(),
'environment': config.env.value,
'checks': {}
}
# Check database connection
try:
db.command('ping')
checks['checks']['database'] = 'ok'
except Exception as e:
checks['status'] = 'unhealthy'
checks['checks']['database'] = f'error: {str(e)}'
# Check Redis connection
try:
redis_client.ping()
checks['checks']['redis'] = 'ok'
except Exception as e:
checks['status'] = 'unhealthy'
checks['checks']['redis'] = f'error: {str(e)}'
# Check Asleep API connectivity
try:
response = requests.get(
f"{config.asleep_base_url}/health",
headers={"x-api-key": config.asleep_api_key},
timeout=5
)
if response.status_code == 200:
checks['checks']['asleep_api'] = 'ok'
else:
checks['checks']['asleep_api'] = f'status: {response.status_code}'
except Exception as e:
checks['status'] = 'unhealthy'
checks['checks']['asleep_api'] = f'error: {str(e)}'
status_code = 200 if checks['status'] == 'healthy' else 503
return jsonify(checks), status_code
@app.route('/ready')
def readiness_check():
"""Readiness check for Kubernetes"""
# Check if app is ready to serve traffic
if not app.initialized:
return jsonify({'status': 'not ready'}), 503
return jsonify({'status': 'ready'}), 200
@app.route('/live')
def liveness_check():
"""Liveness check for Kubernetes"""
# Simple check that app is running
return jsonify({'status': 'alive'}), 200
```
## Error Recovery
### Circuit Breaker Pattern
```python
from datetime import datetime, timedelta
class CircuitBreaker:
"""Circuit breaker for API calls"""
def __init__(self, failure_threshold: int = 5, timeout: int = 60):
self.failure_threshold = failure_threshold
self.timeout = timeout
self.failure_count = 0
self.last_failure_time = None
self.state = 'closed' # closed, open, half_open
def call(self, func, *args, **kwargs):
"""Execute function with circuit breaker"""
if self.state == 'open':
if datetime.now() - self.last_failure_time > timedelta(seconds=self.timeout):
self.state = 'half_open'
else:
raise Exception("Circuit breaker is open")
try:
result = func(*args, **kwargs)
if self.state == 'half_open':
self.state = 'closed'
self.failure_count = 0
return result
except Exception as e:
self.failure_count += 1
self.last_failure_time = datetime.now()
if self.failure_count >= self.failure_threshold:
self.state = 'open'
raise
class ResilientAsleepClient(AsleepClient):
"""Client with circuit breaker"""
def __init__(self, api_key: str):
super().__init__(api_key)
self.circuit_breaker = CircuitBreaker()
def _request(self, method: str, path: str, **kwargs):
"""Request with circuit breaker"""
return self.circuit_breaker.call(
super()._request,
method,
path,
**kwargs
)
```
## Database Patterns
### Session Storage
```python
from pymongo import MongoClient
from datetime import datetime
class SessionStore:
"""Store and retrieve sleep sessions"""
def __init__(self, db):
self.collection = db.sleep_sessions
self._create_indexes()
def _create_indexes(self):
"""Create database indexes for performance"""
self.collection.create_index([('user_id', 1), ('session_start_time', -1)])
self.collection.create_index([('session_id', 1)], unique=True)
self.collection.create_index([('created_at', 1)])
def store_session(self, session_data: Dict):
"""Store session in database"""
doc = {
'session_id': session_data['session']['id'],
'user_id': session_data['user_id'],
'session_start_time': session_data['session']['start_time'],
'session_end_time': session_data['session']['end_time'],
'statistics': session_data['stat'],
'sleep_stages': session_data['session']['sleep_stages'],
'created_at': datetime.now(),
'updated_at': datetime.now()
}
self.collection.update_one(
{'session_id': doc['session_id']},
{'$set': doc},
upsert=True
)
def get_user_sessions(self, user_id: str, limit: int = 10) -> List[Dict]:
"""Get recent sessions for user"""
return list(
self.collection
.find({'user_id': user_id})
.sort('session_start_time', -1)
.limit(limit)
)
def get_sessions_by_date_range(
self,
user_id: str,
start_date: str,
end_date: str
) -> List[Dict]:
"""Get sessions within date range"""
return list(
self.collection.find({
'user_id': user_id,
'session_start_time': {
'$gte': start_date,
'$lte': end_date
}
})
.sort('session_start_time', -1)
)
```
## Background Job Processing
### Celery Task Queue
```python
from celery import Celery
celery = Celery('tasks', broker='redis://localhost:6379')
@celery.task(bind=True, max_retries=3)
def process_webhook_task(self, webhook_data: Dict):
"""Process webhook asynchronously"""
try:
if webhook_data['event'] == 'SESSION_COMPLETE':
# Store in database
store_session(webhook_data)
# Send notification
send_notification(webhook_data['user_id'], webhook_data)
# Update analytics
update_user_stats(webhook_data['user_id'])
except Exception as e:
# Retry with exponential backoff
raise self.retry(exc=e, countdown=2 ** self.request.retries)
@app.route('/webhook', methods=['POST'])
def webhook():
"""Webhook endpoint with async processing"""
event = request.json
# Queue for background processing
process_webhook_task.delay(event)
# Respond immediately
return jsonify({"status": "queued"}), 200
```
## Performance Optimization
### Batch Processing
```python
from concurrent.futures import ThreadPoolExecutor, as_completed
def fetch_sessions_batch(client: AsleepClient, user_ids: List[str]) -> Dict[str, List]:
"""Fetch sessions for multiple users in parallel"""
results = {}
with ThreadPoolExecutor(max_workers=10) as executor:
future_to_user = {
executor.submit(client.list_sessions, user_id): user_id
for user_id in user_ids
}
for future in as_completed(future_to_user):
user_id = future_to_user[future]
try:
results[user_id] = future.result()
except Exception as e:
print(f"Error fetching sessions for {user_id}: {e}")
results[user_id] = []
return results
```
### Query Optimization
```python
def get_user_summary_optimized(client: AsleepClient, user_id: str) -> Dict:
"""Get user summary with optimized queries"""
# Fetch only what's needed
user_data = client.get_user(user_id)
# Use average stats instead of fetching all sessions
stats = client.get_average_stats(
user_id=user_id,
start_date=(datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d"),
end_date=datetime.now().strftime("%Y-%m-%d")
)
return {
'user_id': user_id,
'last_session': user_data.get('last_session_info'),
'monthly_average': stats['average_stats'],
'session_count': len(stats['slept_sessions'])
}
```

View File

@@ -0,0 +1,561 @@
# Python Client Implementation Guide
This reference provides complete Python client implementations for the Asleep API, including advanced patterns for analytics, production usage, and multi-tenant applications.
## Complete Python API Client
```python
import os
import requests
from typing import Dict, Any, Optional
class AsleepClient:
"""Asleep API client for backend integration"""
def __init__(self, api_key: str, base_url: str = "https://api.asleep.ai"):
self.api_key = api_key
self.base_url = base_url
self.session = requests.Session()
self.session.headers.update({"x-api-key": api_key})
def _request(
self,
method: str,
path: str,
headers: Optional[Dict[str, str]] = None,
**kwargs
) -> Dict[str, Any]:
"""Make authenticated API request with error handling"""
url = f"{self.base_url}{path}"
req_headers = self.session.headers.copy()
if headers:
req_headers.update(headers)
try:
response = self.session.request(method, url, headers=req_headers, **kwargs)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
# Handle API errors
if e.response.status_code == 401:
raise ValueError("Invalid API key")
elif e.response.status_code == 403:
error_detail = e.response.json().get("detail", "Access forbidden")
raise ValueError(f"API access error: {error_detail}")
elif e.response.status_code == 404:
raise ValueError("Resource not found")
else:
raise
# User management methods
def create_user(self, metadata: Optional[Dict[str, Any]] = None) -> str:
"""Create new user and return user_id"""
data = {"metadata": metadata} if metadata else {}
result = self._request("POST", "/ai/v1/users", json=data)
return result["result"]["user_id"]
def get_user(self, user_id: str) -> Dict[str, Any]:
"""Get user information"""
result = self._request("GET", f"/ai/v1/users/{user_id}")
return result["result"]
def delete_user(self, user_id: str) -> None:
"""Delete user and all associated data"""
self._request("DELETE", f"/ai/v1/users/{user_id}")
# Session management methods
def get_session(self, session_id: str, user_id: str, timezone: str = "UTC") -> Dict[str, Any]:
"""Get detailed session data"""
headers = {"x-user-id": user_id, "timezone": timezone}
result = self._request("GET", f"/data/v3/sessions/{session_id}", headers=headers)
return result["result"]
def list_sessions(
self,
user_id: str,
date_gte: Optional[str] = None,
date_lte: Optional[str] = None,
offset: int = 0,
limit: int = 20,
order_by: str = "DESC"
) -> Dict[str, Any]:
"""List user sessions with filtering"""
headers = {"x-user-id": user_id}
params = {"offset": offset, "limit": limit, "order_by": order_by}
if date_gte:
params["date_gte"] = date_gte
if date_lte:
params["date_lte"] = date_lte
result = self._request("GET", "/data/v1/sessions", headers=headers, params=params)
return result["result"]
def delete_session(self, session_id: str, user_id: str) -> None:
"""Delete session and all associated data"""
headers = {"x-user-id": user_id}
self._request("DELETE", f"/ai/v1/sessions/{session_id}", headers=headers)
# Statistics methods
def get_average_stats(
self,
user_id: str,
start_date: str,
end_date: str,
timezone: str = "UTC"
) -> Dict[str, Any]:
"""Get average statistics for date range (max 100 days)"""
headers = {"timezone": timezone}
params = {"start_date": start_date, "end_date": end_date}
result = self._request(
"GET",
f"/data/v1/users/{user_id}/average-stats",
headers=headers,
params=params
)
return result["result"]
# Usage
client = AsleepClient(api_key=os.getenv("ASLEEP_API_KEY"))
```
## Advanced Analytics Implementation
```python
from typing import List, Dict
from datetime import datetime, timedelta
class SleepAnalytics:
"""Backend analytics for sleep tracking platform"""
def __init__(self, client: AsleepClient, db):
self.client = client
self.db = db
def get_user_sleep_score(self, user_id: str, days: int = 30) -> Dict:
"""Calculate comprehensive sleep score for user"""
end_date = datetime.now()
start_date = end_date - timedelta(days=days)
stats = self.client.get_average_stats(
user_id=user_id,
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d")
)
avg = stats['average_stats']
# Calculate weighted sleep score (0-100)
efficiency_score = avg['sleep_efficiency'] # Already 0-100
consistency_score = self._calculate_consistency_score(stats)
duration_score = self._calculate_duration_score(avg)
overall_score = (
efficiency_score * 0.4 +
consistency_score * 0.3 +
duration_score * 0.3
)
return {
'overall_score': round(overall_score, 1),
'efficiency_score': round(efficiency_score, 1),
'consistency_score': round(consistency_score, 1),
'duration_score': round(duration_score, 1),
'period_days': days,
'session_count': len(stats['slept_sessions'])
}
def _calculate_consistency_score(self, stats: Dict) -> float:
"""Score based on sleep schedule consistency"""
# Implement consistency scoring based on variance in sleep times
# Placeholder implementation
return 80.0
def _calculate_duration_score(self, avg: Dict) -> float:
"""Score based on sleep duration (7-9 hours optimal)"""
sleep_hours = avg['time_in_sleep'] / 3600
if 7 <= sleep_hours <= 9:
return 100.0
elif 6 <= sleep_hours < 7 or 9 < sleep_hours <= 10:
return 80.0
elif 5 <= sleep_hours < 6 or 10 < sleep_hours <= 11:
return 60.0
else:
return 40.0
def get_cohort_analysis(self, user_ids: List[str], days: int = 30) -> Dict:
"""Analyze sleep patterns across user cohort"""
cohort_data = []
for user_id in user_ids:
try:
score = self.get_user_sleep_score(user_id, days)
cohort_data.append({
'user_id': user_id,
'score': score['overall_score'],
'efficiency': score['efficiency_score'],
'sessions': score['session_count']
})
except Exception as e:
print(f"Error fetching data for user {user_id}: {e}")
if not cohort_data:
return {}
return {
'cohort_size': len(cohort_data),
'avg_score': sum(u['score'] for u in cohort_data) / len(cohort_data),
'avg_efficiency': sum(u['efficiency'] for u in cohort_data) / len(cohort_data),
'total_sessions': sum(u['sessions'] for u in cohort_data),
'users': cohort_data
}
def generate_weekly_report(self, user_id: str) -> Dict:
"""Generate comprehensive weekly sleep report"""
stats = self.client.get_average_stats(
user_id=user_id,
start_date=(datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d"),
end_date=datetime.now().strftime("%Y-%m-%d")
)
avg = stats['average_stats']
return {
'period': 'Last 7 days',
'summary': {
'avg_sleep_time': avg['sleep_time'],
'avg_bedtime': avg['start_time'],
'avg_wake_time': avg['end_time'],
'avg_efficiency': avg['sleep_efficiency']
},
'sleep_stages': {
'light_hours': avg['time_in_light'] / 3600,
'deep_hours': avg['time_in_deep'] / 3600,
'rem_hours': avg['time_in_rem'] / 3600
},
'insights': self._generate_insights(avg),
'session_count': len(stats['slept_sessions'])
}
def _generate_insights(self, avg: Dict) -> List[str]:
"""Generate personalized sleep insights"""
insights = []
if avg['sleep_efficiency'] < 75:
insights.append("Your sleep efficiency is below average. Try establishing a consistent bedtime routine.")
if avg['deep_ratio'] < 0.15:
insights.append("You're getting less deep sleep than optimal. Avoid caffeine after 2 PM.")
if avg['waso_count'] > 3:
insights.append("You're waking up frequently during the night. Consider reducing screen time before bed.")
return insights
# Usage
analytics = SleepAnalytics(client, db)
score = analytics.get_user_sleep_score("user123", days=30)
print(f"Sleep score: {score['overall_score']}/100")
report = analytics.generate_weekly_report("user123")
print(f"Weekly report: {report}")
```
## Multi-Tenant Application Implementation
```python
class MultiTenantSleepTracker:
"""Multi-tenant sleep tracking backend"""
def __init__(self, client: AsleepClient, db):
self.client = client
self.db = db
def create_organization(self, org_id: str, name: str, settings: Dict) -> Dict:
"""Create new organization"""
org = {
'org_id': org_id,
'name': name,
'settings': settings,
'created_at': datetime.now(),
'user_count': 0
}
self.db.organizations.insert_one(org)
return org
def add_user_to_organization(self, org_id: str, user_email: str, metadata: Dict = None) -> str:
"""Add user to organization and create Asleep user"""
# Verify organization exists
org = self.db.organizations.find_one({'org_id': org_id})
if not org:
raise ValueError(f"Organization {org_id} not found")
# Create Asleep user
asleep_user_id = self.client.create_user(metadata=metadata)
# Store user mapping
self.db.users.insert_one({
'org_id': org_id,
'user_email': user_email,
'asleep_user_id': asleep_user_id,
'metadata': metadata,
'created_at': datetime.now()
})
# Update organization user count
self.db.organizations.update_one(
{'org_id': org_id},
{'$inc': {'user_count': 1}}
)
return asleep_user_id
def get_organization_statistics(self, org_id: str, days: int = 30) -> Dict:
"""Get aggregated statistics for entire organization"""
# Get all users in organization
users = list(self.db.users.find({'org_id': org_id}))
end_date = datetime.now()
start_date = end_date - timedelta(days=days)
org_stats = {
'org_id': org_id,
'user_count': len(users),
'period_days': days,
'users_data': []
}
total_efficiency = 0
total_sleep_time = 0
total_sessions = 0
for user in users:
try:
stats = self.client.get_average_stats(
user_id=user['asleep_user_id'],
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d")
)
avg = stats['average_stats']
session_count = len(stats['slept_sessions'])
org_stats['users_data'].append({
'user_email': user['user_email'],
'efficiency': avg['sleep_efficiency'],
'sleep_time': avg['sleep_time'],
'session_count': session_count
})
total_efficiency += avg['sleep_efficiency']
total_sleep_time += avg['time_in_sleep']
total_sessions += session_count
except Exception as e:
print(f"Error fetching stats for user {user['user_email']}: {e}")
if users:
org_stats['avg_efficiency'] = total_efficiency / len(users)
org_stats['avg_sleep_hours'] = (total_sleep_time / len(users)) / 3600
org_stats['total_sessions'] = total_sessions
return org_stats
# Usage
tracker = MultiTenantSleepTracker(client, db)
# Create organization
tracker.create_organization(
org_id="acme-corp",
name="Acme Corporation",
settings={'timezone': 'America/New_York'}
)
# Add users
tracker.add_user_to_organization("acme-corp", "john@acme.com")
tracker.add_user_to_organization("acme-corp", "jane@acme.com")
# Get organization stats
org_stats = tracker.get_organization_statistics("acme-corp", days=30)
print(f"Organization average efficiency: {org_stats['avg_efficiency']:.1f}%")
```
## FastAPI Backend Example
```python
from fastapi import FastAPI, HTTPException, Depends, Header
from typing import Optional
import os
app = FastAPI()
asleep_client = AsleepClient(api_key=os.getenv("ASLEEP_API_KEY"))
# Authentication dependency
async def verify_app_token(authorization: str = Header(...)):
"""Verify mobile app authentication"""
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid authorization header")
token = authorization[7:]
# Verify token with your auth system
user = verify_jwt_token(token)
if not user:
raise HTTPException(status_code=401, detail="Invalid token")
return user
# Proxy endpoints
@app.post("/api/users")
async def create_user(
metadata: Optional[dict] = None,
user: dict = Depends(verify_app_token)
):
"""Create Asleep user for authenticated app user"""
try:
# Create user in Asleep
asleep_user_id = asleep_client.create_user(metadata=metadata)
# Store mapping in your database
db.user_mappings.insert_one({
'app_user_id': user['id'],
'asleep_user_id': asleep_user_id,
'created_at': datetime.now()
})
return {"user_id": asleep_user_id}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/sessions/{session_id}")
async def get_session(
session_id: str,
user: dict = Depends(verify_app_token)
):
"""Get session data for authenticated user"""
# Get Asleep user ID from mapping
mapping = db.user_mappings.find_one({'app_user_id': user['id']})
if not mapping:
raise HTTPException(status_code=404, detail="User not found")
asleep_user_id = mapping['asleep_user_id']
# Fetch session from Asleep
try:
session = asleep_client.get_session(session_id, asleep_user_id)
return session
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@app.get("/api/sessions")
async def list_sessions(
date_gte: Optional[str] = None,
date_lte: Optional[str] = None,
user: dict = Depends(verify_app_token)
):
"""List sessions for authenticated user"""
mapping = db.user_mappings.find_one({'app_user_id': user['id']})
if not mapping:
raise HTTPException(status_code=404, detail="User not found")
asleep_user_id = mapping['asleep_user_id']
sessions = asleep_client.list_sessions(
user_id=asleep_user_id,
date_gte=date_gte,
date_lte=date_lte
)
return sessions
@app.get("/api/statistics")
async def get_statistics(
start_date: str,
end_date: str,
user: dict = Depends(verify_app_token)
):
"""Get average statistics for authenticated user"""
mapping = db.user_mappings.find_one({'app_user_id': user['id']})
if not mapping:
raise HTTPException(status_code=404, detail="User not found")
asleep_user_id = mapping['asleep_user_id']
stats = asleep_client.get_average_stats(
user_id=asleep_user_id,
start_date=start_date,
end_date=end_date
)
return stats
```
## Monthly Trends Analysis
```python
from datetime import datetime, timedelta
from typing import List, Dict
def get_monthly_trends(client, user_id: str, months: int = 6) -> List[Dict]:
"""Get monthly sleep trends for the past N months"""
trends = []
today = datetime.now()
for i in range(months):
# Calculate month boundaries
end_date = today.replace(day=1) - timedelta(days=i * 30)
start_date = end_date - timedelta(days=30)
try:
stats = client.get_average_stats(
user_id=user_id,
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d")
)
trends.append({
'month': end_date.strftime("%Y-%m"),
'avg_sleep_time': stats['average_stats']['sleep_time'],
'avg_efficiency': stats['average_stats']['sleep_efficiency'],
'session_count': len(stats['slept_sessions'])
})
except Exception as e:
print(f"Error fetching stats for {end_date.strftime('%Y-%m')}: {e}")
return trends
# Usage
trends = get_monthly_trends(client, "user123", months=6)
for trend in trends:
print(f"{trend['month']}: {trend['avg_sleep_time']} sleep, "
f"{trend['avg_efficiency']:.1f}% efficiency, "
f"{trend['session_count']} sessions")
```
## Pagination Pattern
```python
# Fetch all sessions with pagination
all_sessions = []
offset = 0
limit = 100
while True:
result = client.list_sessions(
user_id="user123",
date_gte="2024-01-01",
date_lte="2024-12-31",
offset=offset,
limit=limit
)
sessions = result['sleep_session_list']
all_sessions.extend(sessions)
if len(sessions) < limit:
break
offset += limit
print(f"Total sessions: {len(all_sessions)}")
```

View File

@@ -0,0 +1,725 @@
# Asleep REST API Reference
This reference provides comprehensive documentation for the Asleep REST API endpoints.
## Base URL
```
https://api.asleep.ai
```
## Authentication
All API requests require authentication via the `x-api-key` header:
```http
x-api-key: YOUR_API_KEY
```
Obtain your API key from the [Asleep Dashboard](https://dashboard.asleep.ai).
## Common Headers
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| x-api-key | String | Yes | API authentication key |
| x-user-id | String | Conditional | Required for session operations |
| timezone | String | No | Response timezone (default: UTC) |
## Response Format
All responses follow this structure:
```json
{
"detail": "message about the result",
"result": { /* response data */ }
}
```
## Common Error Codes
| Status | Error | Description |
|--------|-------|-------------|
| 401 | Unauthorized | API Key missing or invalid |
| 403 | Plan expired | Subscription period ended |
| 403 | Rate limit exceeded | Request quota temporarily exceeded |
| 403 | Quota exceeded | Total usage limit surpassed |
| 404 | Not Found | Resource doesn't exist |
---
## User Management APIs
### [POST] Create User
Creates a new user for sleep tracking.
**Endpoint:**
```
POST https://api.asleep.ai/ai/v1/users
```
**Headers:**
```
x-api-key: YOUR_API_KEY
```
**Request Body (Optional):**
```json
{
"metadata": {
"birth_year": 1990,
"birth_month": 5,
"birth_day": 15,
"gender": "male",
"height": 175.5,
"weight": 70.0
}
}
```
**Metadata Fields:**
- `birth_year` (Integer): User's birth year
- `birth_month` (Integer): User's birth month (1-12)
- `birth_day` (Integer): User's birth day (1-31)
- `gender` (String): One of: `male`, `female`, `non_binary`, `other`, `prefer_not_to_say`
- `height` (Float): Height in cm (0-300)
- `weight` (Float): Weight in kg (0-1000)
**Response (201 Created):**
```json
{
"detail": "success",
"result": {
"user_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
```
**Example (curl):**
```bash
curl -X POST "https://api.asleep.ai/ai/v1/users" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"metadata": {
"birth_year": 1990,
"gender": "male",
"height": 175.5,
"weight": 70.0
}
}'
```
**Example (Python):**
```python
import requests
response = requests.post(
"https://api.asleep.ai/ai/v1/users",
headers={"x-api-key": "YOUR_API_KEY"},
json={
"metadata": {
"birth_year": 1990,
"gender": "male",
"height": 175.5,
"weight": 70.0
}
}
)
user_id = response.json()["result"]["user_id"]
```
---
### [GET] Get User
Retrieves user information and last session data.
**Endpoint:**
```
GET https://api.asleep.ai/ai/v1/users/{user_id}
```
**Headers:**
```
x-api-key: YOUR_API_KEY
```
**Path Parameters:**
- `user_id` (String): User identifier
**Response (200 OK):**
```json
{
"detail": "success",
"result": {
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"to_be_deleted": false,
"last_session_info": {
"session_id": "abc123",
"state": "COMPLETE",
"session_start_time": "2024-01-20T22:00:00+00:00",
"session_end_time": "2024-01-21T06:30:00+00:00"
},
"metadata": {
"birth_year": 1990,
"birth_month": 5,
"birth_day": 15,
"gender": "male",
"height": 175.5,
"weight": 70.0
}
}
}
```
**Session States:**
- `OPEN`: Session in progress, audio uploads available
- `CLOSED`: Session terminated, analysis in progress
- `COMPLETE`: All analysis completed
**Error Response (404):**
```json
{
"detail": "user does not exist"
}
```
**Example (curl):**
```bash
curl -X GET "https://api.asleep.ai/ai/v1/users/USER_ID" \
-H "x-api-key: YOUR_API_KEY"
```
**Example (Python):**
```python
import requests
response = requests.get(
f"https://api.asleep.ai/ai/v1/users/{user_id}",
headers={"x-api-key": "YOUR_API_KEY"}
)
user_data = response.json()["result"]
```
---
### [DELETE] Delete User
Permanently removes a user and all associated data.
**Endpoint:**
```
DELETE https://api.asleep.ai/ai/v1/users/{user_id}
```
**Headers:**
```
x-api-key: YOUR_API_KEY
```
**Path Parameters:**
- `user_id` (String): User identifier
**Response (204 No Content):**
User information successfully deleted.
**Error Responses:**
401 Unauthorized:
```json
{
"detail": "user_id is invalid"
}
```
404 Not Found:
```json
{
"detail": "user does not exist"
}
```
**Example (curl):**
```bash
curl -X DELETE "https://api.asleep.ai/ai/v1/users/USER_ID" \
-H "x-api-key: YOUR_API_KEY"
```
**Example (Python):**
```python
import requests
response = requests.delete(
f"https://api.asleep.ai/ai/v1/users/{user_id}",
headers={"x-api-key": "YOUR_API_KEY"}
)
# 204 No Content on success
```
---
## Session Management APIs
### [GET] Get Session
Retrieves comprehensive sleep analysis data for a specific session.
**Endpoint:**
```
GET https://api.asleep.ai/data/v3/sessions/{session_id}
```
**Headers:**
```
x-api-key: YOUR_API_KEY
x-user-id: USER_ID
timezone: Asia/Seoul # Optional, defaults to UTC
```
**Path Parameters:**
- `session_id` (String): Session identifier
**Query Parameters:**
None
**Response (200 OK):**
```json
{
"detail": "success",
"result": {
"timezone": "UTC",
"peculiarities": [],
"missing_data_ratio": 0.0,
"session": {
"id": "session123",
"state": "COMPLETE",
"start_time": "2024-01-20T22:00:00+00:00",
"end_time": "2024-01-21T06:30:00+00:00",
"sleep_stages": [0, 0, 1, 1, 2, 3, 2, 1, 0],
"snoring_stages": [0, 0, 0, 1, 1, 0, 0, 0, 0]
},
"stat": {
"sleep_time": "06:30:00",
"sleep_index": 85.5,
"sleep_latency": 900,
"time_in_bed": 30600,
"time_in_sleep": 27000,
"time_in_light": 13500,
"time_in_deep": 6750,
"time_in_rem": 6750,
"sleep_efficiency": 88.24,
"waso_count": 2,
"longest_waso": 300,
"sleep_cycle": [
{
"order": 1,
"start_time": "2024-01-20T22:15:00+00:00",
"end_time": "2024-01-21T01:30:00+00:00"
}
]
}
}
}
```
**Sleep Stages Values:**
- `-1`: Unknown/No data
- `0`: Wake
- `1`: Light sleep
- `2`: Deep sleep
- `3`: REM sleep
**Snoring Stages Values:**
- `0`: No snoring
- `1`: Snoring detected
**Peculiarities:**
- `IN_PROGRESS`: Session still being analyzed
- `NEVER_SLEPT`: No sleep detected in session
- `TOO_SHORT_FOR_ANALYSIS`: Session duration < 5 minutes
**Error Responses:**
400 Bad Request:
```json
{
"detail": "Invalid timezone format"
}
```
404 Not Found:
```json
{
"detail": "Session not found"
}
```
**Example (curl):**
```bash
curl -X GET "https://api.asleep.ai/data/v3/sessions/SESSION_ID" \
-H "x-api-key: YOUR_API_KEY" \
-H "x-user-id: USER_ID" \
-H "timezone: UTC"
```
**Example (Python):**
```python
import requests
response = requests.get(
f"https://api.asleep.ai/data/v3/sessions/{session_id}",
headers={
"x-api-key": "YOUR_API_KEY",
"x-user-id": user_id,
"timezone": "UTC"
}
)
session_data = response.json()["result"]
```
---
### [GET] List Sessions
Retrieves multiple sessions with filtering and pagination.
**Endpoint:**
```
GET https://api.asleep.ai/data/v1/sessions
```
**Headers:**
```
x-api-key: YOUR_API_KEY
x-user-id: USER_ID
timezone: UTC # Optional
```
**Query Parameters:**
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| date_gte | String (YYYY-MM-DD) | No | - | Sessions on or after this date |
| date_lte | String (YYYY-MM-DD) | No | - | Sessions on or before this date |
| order_by | String (ASC/DESC) | No | DESC | Sort direction by start time |
| offset | Integer | No | 0 | Number of records to skip |
| limit | Integer (0-100) | No | 20 | Maximum records per request |
**Response (200 OK):**
```json
{
"detail": "success",
"result": {
"timezone": "UTC",
"sleep_session_list": [
{
"session_id": "session123",
"state": "COMPLETE",
"session_start_time": "2024-01-20T22:00:00+00:00",
"session_end_time": "2024-01-21T06:30:00+00:00",
"created_timezone": "UTC",
"unexpected_end_time": null,
"last_received_seq_num": 156,
"time_in_bed": 30600
}
]
}
}
```
**Error Response (400):**
```json
{
"detail": "Invalid timezone"
}
```
**Example (curl):**
```bash
curl -X GET "https://api.asleep.ai/data/v1/sessions?date_gte=2024-01-01&limit=10" \
-H "x-api-key: YOUR_API_KEY" \
-H "x-user-id: USER_ID"
```
**Example (Python):**
```python
import requests
response = requests.get(
"https://api.asleep.ai/data/v1/sessions",
headers={
"x-api-key": "YOUR_API_KEY",
"x-user-id": user_id
},
params={
"date_gte": "2024-01-01",
"date_lte": "2024-01-31",
"limit": 50,
"order_by": "DESC"
}
)
sessions = response.json()["result"]["sleep_session_list"]
```
---
### [DELETE] Delete Session
Permanently removes a session and all associated data.
**Endpoint:**
```
DELETE https://api.asleep.ai/ai/v1/sessions/{session_id}
```
**Headers:**
```
x-api-key: YOUR_API_KEY
x-user-id: USER_ID
```
**Path Parameters:**
- `session_id` (String): Session identifier
**Response (204 No Content):**
Session, uploaded audio, and analysis data successfully deleted.
**Error Responses:**
401 Unauthorized:
```json
{
"detail": "x-user-id is invalid"
}
```
404 Not Found (User):
```json
{
"detail": "user does not exist"
}
```
404 Not Found (Session):
```json
{
"detail": "session does not exist"
}
```
**Example (curl):**
```bash
curl -X DELETE "https://api.asleep.ai/ai/v1/sessions/SESSION_ID" \
-H "x-api-key: YOUR_API_KEY" \
-H "x-user-id: USER_ID"
```
**Example (Python):**
```python
import requests
response = requests.delete(
f"https://api.asleep.ai/ai/v1/sessions/{session_id}",
headers={
"x-api-key": "YOUR_API_KEY",
"x-user-id": user_id
}
)
# 204 No Content on success
```
---
## Statistics APIs
### [GET] Get Average Stats
Retrieves average sleep metrics over a specified time period (up to 100 days).
**Endpoint:**
```
GET https://api.asleep.ai/data/v1/users/{user_id}/average-stats
```
**Headers:**
```
x-api-key: YOUR_API_KEY
timezone: UTC # Optional
```
**Path Parameters:**
- `user_id` (String): User identifier
**Query Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| start_date | String (YYYY-MM-DD) | Yes | Period start date |
| end_date | String (YYYY-MM-DD) | Yes | Period end date (max 100 days from start) |
**Response (200 OK):**
```json
{
"detail": "success",
"result": {
"period": {
"start_date": "2024-01-01",
"end_date": "2024-01-31",
"days": 31
},
"peculiarities": [],
"average_stats": {
"start_time": "22:30:00",
"end_time": "06:45:00",
"sleep_time": "07:15:00",
"wake_time": "06:45:00",
"sleep_latency": 900,
"wakeup_latency": 300,
"time_in_bed": 30600,
"time_in_sleep_period": 29700,
"time_in_sleep": 26100,
"time_in_wake": 3600,
"time_in_light": 13050,
"time_in_deep": 6525,
"time_in_rem": 6525,
"time_in_snoring": 1800,
"time_in_no_snoring": 24300,
"sleep_efficiency": 85.29,
"wake_ratio": 0.12,
"sleep_ratio": 0.88,
"light_ratio": 0.50,
"deep_ratio": 0.25,
"rem_ratio": 0.25,
"snoring_ratio": 0.07,
"no_snoring_ratio": 0.93,
"waso_count": 2.5,
"longest_waso": 420,
"sleep_cycle_count": 4.2,
"snoring_count": 15.3
},
"never_slept_sessions": [],
"slept_sessions": [
{
"session_id": "session123",
"session_start_time": "2024-01-20T22:00:00+00:00"
}
]
}
}
```
**Metrics Explanation:**
**Time Metrics** (HH:MM:SS format or seconds):
- `start_time`: Average bedtime
- `end_time`: Average wake time
- `sleep_time`: Average time of falling asleep
- `wake_time`: Average time of waking up
- `sleep_latency`: Average time to fall asleep (seconds)
- `wakeup_latency`: Average time from wake to getting up (seconds)
- `time_in_bed`: Average total time in bed (seconds)
- `time_in_sleep_period`: Average time from sleep onset to wake (seconds)
- `time_in_sleep`: Average actual sleep time (seconds)
- `time_in_wake`: Average wake time during sleep period (seconds)
**Sleep Stage Durations** (seconds):
- `time_in_light`: Average light sleep duration
- `time_in_deep`: Average deep sleep duration
- `time_in_rem`: Average REM sleep duration
**Snoring Metrics** (seconds):
- `time_in_snoring`: Average snoring duration
- `time_in_no_snoring`: Average non-snoring duration
**Ratio Metrics** (0-1 decimal):
- `sleep_efficiency`: Sleep time / Time in bed
- `wake_ratio`, `sleep_ratio`: Wake/sleep proportions
- `light_ratio`, `deep_ratio`, `rem_ratio`: Sleep stage proportions
- `snoring_ratio`, `no_snoring_ratio`: Snoring proportions
**Event Counts**:
- `waso_count`: Average wake after sleep onset episodes
- `longest_waso`: Average longest wake episode (seconds)
- `sleep_cycle_count`: Average number of sleep cycles
- `snoring_count`: Average snoring episodes
**Peculiarities:**
- `NO_BREATHING_STABILITY`: Inconsistent breathing data
**Error Responses:**
400 Bad Request:
```json
{
"detail": "The period should be less than or equal to 100 days"
}
```
404 Not Found:
```json
{
"detail": "Unable to find the user of id {user_id}"
}
```
**Example (curl):**
```bash
curl -X GET "https://api.asleep.ai/data/v1/users/USER_ID/average-stats?start_date=2024-01-01&end_date=2024-01-31" \
-H "x-api-key: YOUR_API_KEY" \
-H "timezone: UTC"
```
**Example (Python):**
```python
import requests
response = requests.get(
f"https://api.asleep.ai/data/v1/users/{user_id}/average-stats",
headers={
"x-api-key": "YOUR_API_KEY",
"timezone": "UTC"
},
params={
"start_date": "2024-01-01",
"end_date": "2024-01-31"
}
)
stats = response.json()["result"]
```
---
## Rate Limiting
The Asleep API implements rate limiting to ensure fair usage:
- **Rate Limit Exceeded (403)**: Temporary quota exceeded
- **Quota Exceeded (403)**: Total usage limit reached
- **Plan Expired (403)**: Subscription period ended
Monitor your usage in the [Asleep Dashboard](https://dashboard.asleep.ai).
**Best Practices:**
- Implement exponential backoff for retries
- Cache responses when appropriate
- Batch requests when possible
- Monitor usage proactively
---
## API Versioning
The Asleep API uses versioned endpoints (e.g., `/v1/`, `/v3/`). Version upgrades occur when:
- Renaming response object fields
- Modifying data types or enum values
- Restructuring response objects
- Introducing breaking changes
Non-breaking changes (like adding new fields) don't trigger version upgrades.
**Current Versions:**
- User Management: `/ai/v1/`
- Session Data: `/data/v3/` (Get Session), `/data/v1/` (List Sessions)
- Statistics: `/data/v1/`

View File

@@ -0,0 +1,594 @@
# Webhook Implementation Guide
This reference provides complete webhook handler implementations for both Python and Node.js, including best practices and payload examples.
## Flask Webhook Handler (Python)
```python
from flask import Flask, request, jsonify
import os
import logging
app = Flask(__name__)
logger = logging.getLogger(__name__)
EXPECTED_API_KEY = os.getenv("ASLEEP_API_KEY")
@app.route('/asleep-webhook', methods=['POST'])
def asleep_webhook():
"""Handle Asleep webhook events"""
# Verify authentication
api_key = request.headers.get('x-api-key')
user_id = request.headers.get('x-user-id')
if api_key != EXPECTED_API_KEY:
logger.warning(f"Unauthorized webhook attempt from {request.remote_addr}")
return jsonify({"error": "Unauthorized"}), 401
# Parse event
event = request.json
event_type = event.get('event')
logger.info(f"Received {event_type} event for user {user_id}")
try:
if event_type == 'INFERENCE_COMPLETE':
handle_inference_complete(event)
elif event_type == 'SESSION_COMPLETE':
handle_session_complete(event)
else:
logger.warning(f"Unknown event type: {event_type}")
return jsonify({"status": "success"}), 200
except Exception as e:
logger.error(f"Webhook processing error: {e}", exc_info=True)
return jsonify({"status": "error", "message": str(e)}), 500
def handle_inference_complete(event):
"""Process incremental sleep data"""
session_id = event['session_id']
user_id = event['user_id']
sleep_stages = event['sleep_stages']
# Update real-time dashboard
update_live_dashboard(session_id, sleep_stages)
# Store incremental data
db.incremental_data.insert_one(event)
logger.info(f"Processed INFERENCE_COMPLETE for session {session_id}")
def handle_session_complete(event):
"""Process complete sleep report"""
session_id = event['session_id']
user_id = event['user_id']
stat = event['stat']
# Store complete report
db.sleep_reports.insert_one({
'user_id': user_id,
'session_id': session_id,
'date': event['session']['start_time'],
'statistics': stat,
'session_data': event['session'],
'created_at': datetime.now()
})
# Send user notification
send_push_notification(user_id, {
'title': 'Sleep Report Ready',
'body': f"Sleep time: {stat['sleep_time']}, Efficiency: {stat['sleep_efficiency']:.1f}%"
})
# Update user statistics
update_user_aggregated_stats(user_id)
logger.info(f"Processed SESSION_COMPLETE for session {session_id}")
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
```
## Express Webhook Handler (Node.js)
```javascript
const express = require('express');
const app = express();
app.use(express.json());
const EXPECTED_API_KEY = process.env.ASLEEP_API_KEY;
app.post('/asleep-webhook', async (req, res) => {
// Verify authentication
const apiKey = req.headers['x-api-key'];
const userId = req.headers['x-user-id'];
if (apiKey !== EXPECTED_API_KEY) {
console.warn('Unauthorized webhook attempt');
return res.status(401).json({ error: 'Unauthorized' });
}
const { event, session_id, stat } = req.body;
console.log(`Received ${event} event for user ${userId}`);
try {
switch (event) {
case 'INFERENCE_COMPLETE':
await handleInferenceComplete(req.body);
break;
case 'SESSION_COMPLETE':
await handleSessionComplete(req.body);
break;
default:
console.warn(`Unknown event type: ${event}`);
}
res.status(200).json({ status: 'success' });
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Processing failed' });
}
});
async function handleInferenceComplete(event) {
const { session_id, user_id, sleep_stages } = event;
// Update real-time dashboard
await updateLiveDashboard(session_id, sleep_stages);
// Store incremental data
await db.collection('incremental_data').insertOne(event);
console.log(`Processed INFERENCE_COMPLETE for session ${session_id}`);
}
async function handleSessionComplete(event) {
const { session_id, user_id, stat, session } = event;
// Store complete report
await db.collection('sleep_reports').insertOne({
user_id,
session_id,
date: session.start_time,
statistics: stat,
session_data: session,
created_at: new Date()
});
// Send user notification
await sendPushNotification(user_id, {
title: 'Sleep Report Ready',
body: `Sleep time: ${stat.sleep_time}, Efficiency: ${stat.sleep_efficiency.toFixed(1)}%`
});
// Update user statistics
await updateUserAggregatedStats(user_id);
console.log(`Processed SESSION_COMPLETE for session ${session_id}`);
}
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Webhook server listening on port ${PORT}`);
});
```
## Webhook Event Payloads
### INFERENCE_COMPLETE Event
Sent every 5-40 minutes during sleep tracking with incremental data.
```json
{
"event": "INFERENCE_COMPLETE",
"version": "V3",
"timestamp": "2024-01-21T06:15:00Z",
"user_id": "user123",
"session_id": "session123",
"seq_num": 60,
"inference_seq_num": 12,
"sleep_stages": [1, 1, 2, 2, 2],
"snoring_stages": [0, 0, 1, 1, 0]
}
```
**Fields:**
- `event`: Event type identifier
- `version`: API version (V3)
- `timestamp`: Event timestamp in ISO 8601 format
- `user_id`: User identifier
- `session_id`: Sleep session identifier
- `seq_num`: Sequence number for raw data
- `inference_seq_num`: Sequence number for inference results
- `sleep_stages`: Array of sleep stage values (see sleep stages reference)
- `snoring_stages`: Array of snoring detection values (0 = no snoring, 1 = snoring)
### SESSION_COMPLETE Event
Sent when sleep session ends with complete analysis.
```json
{
"event": "SESSION_COMPLETE",
"version": "V3",
"timestamp": "2024-01-21T06:30:00Z",
"user_id": "user123",
"session_id": "session123",
"session": {
"id": "session123",
"state": "COMPLETE",
"start_time": "2024-01-20T22:00:00+00:00",
"end_time": "2024-01-21T06:30:00+00:00",
"sleep_stages": [0, 0, 1, 1, 2, 3, 2, 1, 0],
"snoring_stages": [0, 0, 0, 1, 1, 0, 0, 0, 0]
},
"stat": {
"sleep_time": "06:30:00",
"sleep_efficiency": 88.24,
"time_in_bed": 30600,
"time_in_sleep": 27000,
"time_in_wake": 3600,
"time_in_light": 14400,
"time_in_deep": 7200,
"time_in_rem": 5400,
"waso_count": 2,
"sleep_latency": 900,
"sleep_cycle": [
{
"index": 0,
"start_time": "2024-01-20T22:15:00+00:00",
"end_time": "2024-01-21T01:45:00+00:00"
},
{
"index": 1,
"start_time": "2024-01-21T01:45:00+00:00",
"end_time": "2024-01-21T05:15:00+00:00"
}
]
}
}
```
**Sleep Stage Values:**
- `-1`: Unknown/No data
- `0`: Wake
- `1`: Light sleep
- `2`: Deep sleep
- `3`: REM sleep
## Webhook Best Practices
### 1. Idempotency
Handle duplicate webhook deliveries gracefully:
**Python:**
```python
def handle_session_complete(event):
session_id = event['session_id']
# Check if already processed
if db.processed_webhooks.find_one({'session_id': session_id, 'event': 'SESSION_COMPLETE'}):
logger.info(f"Session {session_id} already processed, skipping")
return
# Process event
save_sleep_report(event)
# Mark as processed
db.processed_webhooks.insert_one({
'session_id': session_id,
'event': 'SESSION_COMPLETE',
'processed_at': datetime.now()
})
```
**Node.js:**
```javascript
async function handleSessionComplete(event) {
const sessionId = event.session_id;
// Check if already processed
const existing = await db.collection('processed_webhooks').findOne({
session_id: sessionId,
event: 'SESSION_COMPLETE'
});
if (existing) {
console.log(`Session ${sessionId} already processed, skipping`);
return;
}
// Process event
await saveSleepReport(event);
// Mark as processed
await db.collection('processed_webhooks').insertOne({
session_id: sessionId,
event: 'SESSION_COMPLETE',
processed_at: new Date()
});
}
```
### 2. Asynchronous Processing
Process webhooks asynchronously to respond quickly:
**Python (Celery):**
```python
from celery import Celery
celery = Celery('tasks', broker='redis://localhost:6379')
@app.route('/webhook', methods=['POST'])
def webhook():
event = request.json
# Queue for async processing
process_webhook_async.delay(event)
# Respond immediately
return jsonify({"status": "queued"}), 200
@celery.task
def process_webhook_async(event):
"""Process webhook asynchronously"""
if event['event'] == 'SESSION_COMPLETE':
handle_session_complete(event)
```
**Node.js (Bull):**
```javascript
const Queue = require('bull');
const webhookQueue = new Queue('asleep-webhooks', {
redis: { host: 'localhost', port: 6379 }
});
app.post('/webhook', async (req, res) => {
const event = req.body;
// Queue for async processing
await webhookQueue.add(event);
// Respond immediately
res.status(200).json({ status: 'queued' });
});
// Process queued webhooks
webhookQueue.process(async (job) => {
const event = job.data;
if (event.event === 'SESSION_COMPLETE') {
await handleSessionComplete(event);
}
});
```
### 3. Security
Always verify webhook authenticity:
```python
@app.route('/webhook', methods=['POST'])
def webhook():
# Verify API key
api_key = request.headers.get('x-api-key')
if api_key != EXPECTED_API_KEY:
logger.warning(f"Unauthorized webhook from {request.remote_addr}")
return jsonify({"error": "Unauthorized"}), 401
# Verify user ID presence
user_id = request.headers.get('x-user-id')
if not user_id:
logger.warning("Missing x-user-id header")
return jsonify({"error": "Missing user ID"}), 400
# Process webhook
# ...
```
### 4. Error Handling
Implement robust error handling:
```python
@app.route('/webhook', methods=['POST'])
def webhook():
try:
event = request.json
event_type = event.get('event')
if event_type == 'SESSION_COMPLETE':
handle_session_complete(event)
elif event_type == 'INFERENCE_COMPLETE':
handle_inference_complete(event)
else:
logger.warning(f"Unknown event type: {event_type}")
return jsonify({"error": "Unknown event type"}), 400
return jsonify({"status": "success"}), 200
except ValueError as e:
logger.error(f"Validation error: {e}")
return jsonify({"error": str(e)}), 400
except Exception as e:
logger.error(f"Processing error: {e}", exc_info=True)
return jsonify({"error": "Internal server error"}), 500
```
### 5. Logging
Log all webhook events for debugging:
```python
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('webhooks.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
user_id = request.headers.get('x-user-id')
event = request.json
event_type = event.get('event')
logger.info(f"Webhook received - Type: {event_type}, User: {user_id}, Session: {event.get('session_id')}")
try:
# Process webhook
# ...
logger.info(f"Webhook processed successfully - Session: {event.get('session_id')}")
except Exception as e:
logger.error(f"Webhook processing failed - Session: {event.get('session_id')}, Error: {e}", exc_info=True)
```
## Testing Webhooks Locally
### Using ngrok
```bash
# Start your local server
python app.py # or npm start
# In another terminal, expose with ngrok
ngrok http 5000
# Use the ngrok URL as webhook URL in Asleep Dashboard
# Example: https://abc123.ngrok.io/asleep-webhook
```
### Mock Webhook for Testing
**Python:**
```python
import requests
import json
def send_test_webhook(url, event_type='SESSION_COMPLETE'):
"""Send test webhook to local server"""
if event_type == 'SESSION_COMPLETE':
payload = {
"event": "SESSION_COMPLETE",
"version": "V3",
"timestamp": "2024-01-21T06:30:00Z",
"user_id": "test_user",
"session_id": "test_session",
"session": {
"id": "test_session",
"state": "COMPLETE",
"start_time": "2024-01-20T22:00:00+00:00",
"end_time": "2024-01-21T06:30:00+00:00",
"sleep_stages": [0, 1, 2, 3, 2, 1, 0]
},
"stat": {
"sleep_time": "06:30:00",
"sleep_efficiency": 88.24,
"time_in_bed": 30600,
"time_in_sleep": 27000
}
}
else:
payload = {
"event": "INFERENCE_COMPLETE",
"version": "V3",
"timestamp": "2024-01-21T06:15:00Z",
"user_id": "test_user",
"session_id": "test_session",
"seq_num": 60,
"inference_seq_num": 12,
"sleep_stages": [1, 1, 2, 2, 2]
}
headers = {
'x-api-key': 'your_api_key',
'x-user-id': 'test_user',
'Content-Type': 'application/json'
}
response = requests.post(url, headers=headers, json=payload)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
# Test locally
send_test_webhook('http://localhost:5000/asleep-webhook')
```
## Common Webhook Patterns
### Real-time Dashboard Updates
```python
def handle_inference_complete(event):
"""Update real-time dashboard with incremental data"""
session_id = event['session_id']
user_id = event['user_id']
sleep_stages = event['sleep_stages']
# Broadcast to connected clients via WebSocket
socketio.emit('sleep_update', {
'session_id': session_id,
'sleep_stages': sleep_stages,
'timestamp': event['timestamp']
}, room=user_id)
```
### Sleep Report Notifications
```python
def handle_session_complete(event):
"""Send notification when sleep report is ready"""
user_id = event['user_id']
stat = event['stat']
# Send push notification
send_push_notification(user_id, {
'title': 'Your Sleep Report is Ready',
'body': f"You slept for {stat['sleep_time']} with {stat['sleep_efficiency']:.1f}% efficiency",
'data': {
'session_id': event['session_id'],
'action': 'view_report'
}
})
```
### Data Aggregation
```python
def handle_session_complete(event):
"""Update aggregated user statistics"""
user_id = event['user_id']
stat = event['stat']
# Update rolling averages
db.user_stats.update_one(
{'user_id': user_id},
{
'$inc': {
'total_sessions': 1,
'total_sleep_time': stat['time_in_sleep']
},
'$push': {
'recent_efficiency': {
'$each': [stat['sleep_efficiency']],
'$slice': -30 # Keep last 30 sessions
}
}
},
upsert=True
)
```

View File

@@ -0,0 +1,821 @@
# Asleep Webhook Reference
This reference provides comprehensive documentation for implementing Asleep webhooks in backend applications.
## Overview
Asleep webhooks enable real-time notifications about sleep session events. The system sends HTTP POST requests to your configured callback URL when specific events occur.
## Webhook Configuration
Webhooks are configured by providing a callback URL during session operations (via SDK) or through the Asleep Dashboard.
**Callback URL Requirements:**
- Must be publicly accessible HTTPS endpoint
- Should respond with 2xx status code
- Should handle requests within 30 seconds
## Authentication
Webhook requests include authentication headers:
```http
x-api-key: YOUR_API_KEY
x-user-id: USER_ID
```
**Security Best Practices:**
- Verify the `x-api-key` matches your expected API key
- Validate the `x-user-id` belongs to your system
- Use HTTPS for your webhook endpoint
- Implement request signing if needed
- Log all webhook attempts for audit
## Supported Events
Asleep webhooks support two primary event types:
### 1. INFERENCE_COMPLETE
Triggered during sleep session analysis at regular intervals (every 5 or 40 minutes).
**Use Cases:**
- Real-time sleep stage monitoring
- Live dashboard updates
- Progressive data analysis
- User notifications during tracking
**Timing:**
- Fires every 5 minutes during active tracking
- May also fire at 40-minute intervals
- Multiple events per session
### 2. SESSION_COMPLETE
Triggered when complete sleep session analysis finishes.
**Use Cases:**
- Final report generation
- User notifications
- Data storage
- Statistics calculation
- Integration with other systems
**Timing:**
- Fires once per session
- Occurs after session end
- Contains complete analysis
## Webhook Payload Schemas
### INFERENCE_COMPLETE Payload
Provides incremental sleep analysis data.
**Structure:**
```json
{
"event": "INFERENCE_COMPLETE",
"version": "V3",
"timestamp": "2024-01-21T06:15:00Z",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"session_id": "session123",
"seq_num": 60,
"inference_seq_num": 12,
"sleep_stages": [1, 1, 2, 2, 2],
"breath_stages": [0, 0, 0, 0, 0],
"snoring_stages": [0, 0, 1, 1, 0],
"time_window": {
"start": "2024-01-21T06:10:00Z",
"end": "2024-01-21T06:15:00Z"
}
}
```
**Field Descriptions:**
| Field | Type | Description |
|-------|------|-------------|
| event | String | Always "INFERENCE_COMPLETE" |
| version | String | API version (V1, V2, V3) |
| timestamp | String (ISO 8601) | Event generation time |
| user_id | String | User identifier |
| session_id | String | Session identifier |
| seq_num | Integer | Audio data upload sequence number |
| inference_seq_num | Integer | Analysis sequence (5-minute increments) |
| sleep_stages | Array[Integer] | Sleep stage values for time window |
| breath_stages | Array[Integer] | Breathing stability indicators |
| snoring_stages | Array[Integer] | Snoring detection values |
| time_window | Object | Time range for this analysis chunk |
**Sleep Stage Values:**
- `-1`: Unknown/No data
- `0`: Wake
- `1`: Light sleep
- `2`: Deep sleep
- `3`: REM sleep
**Snoring Stage Values:**
- `0`: No snoring
- `1`: Snoring detected
**Example Handler (Python):**
```python
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def handle_inference():
# Verify authentication
api_key = request.headers.get('x-api-key')
user_id = request.headers.get('x-user-id')
if api_key != EXPECTED_API_KEY:
return jsonify({"error": "Unauthorized"}), 401
# Parse payload
data = request.json
if data['event'] == 'INFERENCE_COMPLETE':
session_id = data['session_id']
sleep_stages = data['sleep_stages']
# Process incremental data
update_live_dashboard(session_id, sleep_stages)
# Store for real-time analysis
store_incremental_data(data)
return jsonify({"status": "received"}), 200
```
**Example Handler (Node.js):**
```javascript
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook', async (req, res) => {
// Verify authentication
const apiKey = req.headers['x-api-key'];
const userId = req.headers['x-user-id'];
if (apiKey !== process.env.ASLEEP_API_KEY) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { event, session_id, sleep_stages } = req.body;
if (event === 'INFERENCE_COMPLETE') {
// Update real-time dashboard
await updateLiveDashboard(session_id, sleep_stages);
// Store incremental data
await storeIncrementalData(req.body);
}
res.status(200).json({ status: 'received' });
});
```
---
### SESSION_COMPLETE Payload
Provides comprehensive final sleep analysis.
**Structure:**
```json
{
"event": "SESSION_COMPLETE",
"version": "V3",
"timestamp": "2024-01-21T06:30:00Z",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"session_id": "session123",
"session": {
"id": "session123",
"state": "COMPLETE",
"start_time": "2024-01-20T22:00:00+00:00",
"end_time": "2024-01-21T06:30:00+00:00",
"timezone": "UTC",
"sleep_stages": [0, 0, 1, 1, 2, 3, 2, 1, 0],
"snoring_stages": [0, 0, 0, 1, 1, 0, 0, 0, 0]
},
"stat": {
"sleep_time": "06:30:00",
"sleep_index": 85.5,
"sleep_latency": 900,
"time_in_bed": 30600,
"time_in_sleep": 27000,
"time_in_light": 13500,
"time_in_deep": 6750,
"time_in_rem": 6750,
"sleep_efficiency": 88.24,
"waso_count": 2,
"longest_waso": 300,
"sleep_cycle": [
{
"order": 1,
"start_time": "2024-01-20T22:15:00+00:00",
"end_time": "2024-01-21T01:30:00+00:00"
}
]
},
"peculiarities": []
}
```
**Field Descriptions:**
| Field | Type | Description |
|-------|------|-------------|
| event | String | Always "SESSION_COMPLETE" |
| version | String | API version (V1, V2, V3) |
| timestamp | String (ISO 8601) | Event generation time |
| user_id | String | User identifier |
| session_id | String | Session identifier |
| session | Object | Complete session data |
| stat | Object | Comprehensive sleep statistics |
| peculiarities | Array[String] | Special session conditions |
**Session Object Fields:**
- `id`: Session identifier
- `state`: Always "COMPLETE" for this event
- `start_time`, `end_time`: Session timestamps (ISO 8601)
- `timezone`: Timezone of the session
- `sleep_stages`: Complete sleep stage timeline
- `snoring_stages`: Complete snoring timeline
**Stat Object Fields:**
- `sleep_time`: Total sleep duration (HH:MM:SS)
- `sleep_index`: Overall sleep quality score (0-100)
- `sleep_latency`: Time to fall asleep (seconds)
- `time_in_bed`: Total time in bed (seconds)
- `time_in_sleep`: Total actual sleep time (seconds)
- `time_in_light/deep/rem`: Stage durations (seconds)
- `sleep_efficiency`: Percentage of time spent sleeping
- `waso_count`: Wake after sleep onset episodes
- `longest_waso`: Longest wake episode (seconds)
- `sleep_cycle`: Array of sleep cycle objects
**Peculiarities:**
- `IN_PROGRESS`: Analysis still ongoing (shouldn't occur for COMPLETE)
- `NEVER_SLEPT`: No sleep detected
- `TOO_SHORT_FOR_ANALYSIS`: Session < 5 minutes
- `NO_BREATHING_STABILITY`: Inconsistent breathing data
**Example Handler (Python):**
```python
from flask import Flask, request, jsonify
import logging
app = Flask(__name__)
logger = logging.getLogger(__name__)
@app.route('/webhook', methods=['POST'])
def handle_session_complete():
# Verify authentication
api_key = request.headers.get('x-api-key')
user_id = request.headers.get('x-user-id')
if api_key != EXPECTED_API_KEY:
logger.warning(f"Unauthorized webhook attempt from {request.remote_addr}")
return jsonify({"error": "Unauthorized"}), 401
# Parse payload
data = request.json
if data['event'] == 'SESSION_COMPLETE':
session_id = data['session_id']
stat = data['stat']
# Store complete report
save_sleep_report(user_id, session_id, data)
# Send user notification
notify_user(user_id, {
'session_id': session_id,
'sleep_time': stat['sleep_time'],
'sleep_efficiency': stat['sleep_efficiency'],
'sleep_index': stat['sleep_index']
})
# Update user statistics
update_user_statistics(user_id)
# Trigger integrations
sync_to_health_platform(user_id, data)
logger.info(f"Processed SESSION_COMPLETE for {session_id}")
return jsonify({"status": "processed"}), 200
```
**Example Handler (Node.js):**
```javascript
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook', async (req, res) => {
// Verify authentication
const apiKey = req.headers['x-api-key'];
const userId = req.headers['x-user-id'];
if (apiKey !== process.env.ASLEEP_API_KEY) {
console.warn('Unauthorized webhook attempt');
return res.status(401).json({ error: 'Unauthorized' });
}
const { event, session_id, stat } = req.body;
if (event === 'SESSION_COMPLETE') {
try {
// Store complete report
await saveSleepReport(userId, session_id, req.body);
// Send user notification
await notifyUser(userId, {
sessionId: session_id,
sleepTime: stat.sleep_time,
sleepEfficiency: stat.sleep_efficiency,
sleepIndex: stat.sleep_index
});
// Update statistics
await updateUserStatistics(userId);
// Sync to integrations
await syncToHealthPlatform(userId, req.body);
console.log(`Processed SESSION_COMPLETE for ${session_id}`);
res.status(200).json({ status: 'processed' });
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Processing failed' });
}
} else {
res.status(200).json({ status: 'received' });
}
});
```
---
## Webhook Versioning
Webhooks support three format versions for backward compatibility:
### V1 (Legacy)
Original webhook format. Use V3 for new implementations.
### V2 (Legacy)
Updated format with additional fields. Use V3 for new implementations.
### V3 (Current)
Latest format with comprehensive data structures. Recommended for all new integrations.
**Version Selection:**
Configure webhook version through SDK initialization or Dashboard settings.
---
## Implementation Guide
### 1. Set Up Webhook Endpoint
Create a public HTTPS endpoint to receive webhook events:
**Python (Flask):**
```python
from flask import Flask, request, jsonify
import hmac
import hashlib
app = Flask(__name__)
@app.route('/asleep-webhook', methods=['POST'])
def asleep_webhook():
# Verify authentication
if not verify_webhook(request):
return jsonify({"error": "Unauthorized"}), 401
# Parse event
event = request.json
event_type = event.get('event')
# Route to appropriate handler
if event_type == 'INFERENCE_COMPLETE':
handle_inference_complete(event)
elif event_type == 'SESSION_COMPLETE':
handle_session_complete(event)
return jsonify({"status": "success"}), 200
def verify_webhook(request):
api_key = request.headers.get('x-api-key')
return api_key == EXPECTED_API_KEY
if __name__ == '__main__':
app.run(host='0.0.0.0', port=443, ssl_context='adhoc')
```
**Node.js (Express):**
```javascript
const express = require('express');
const https = require('https');
const fs = require('fs');
const app = express();
app.use(express.json());
app.post('/asleep-webhook', async (req, res) => {
// Verify authentication
if (!verifyWebhook(req)) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { event } = req.body;
try {
switch (event) {
case 'INFERENCE_COMPLETE':
await handleInferenceComplete(req.body);
break;
case 'SESSION_COMPLETE':
await handleSessionComplete(req.body);
break;
default:
console.warn(`Unknown event type: ${event}`);
}
res.status(200).json({ status: 'success' });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Processing failed' });
}
});
function verifyWebhook(req) {
const apiKey = req.headers['x-api-key'];
return apiKey === process.env.ASLEEP_API_KEY;
}
// HTTPS server
const options = {
key: fs.readFileSync('private-key.pem'),
cert: fs.readFileSync('certificate.pem')
};
https.createServer(options, app).listen(443);
```
### 2. Configure Webhook URL
Configure your webhook URL through:
- SDK initialization (for mobile apps)
- Asleep Dashboard (for backend integrations)
**SDK Example (Android):**
```kotlin
AsleepConfig.init(
apiKey = "YOUR_API_KEY",
userId = "user123",
callbackUrl = "https://your-domain.com/asleep-webhook"
)
```
### 3. Handle Webhook Events
Implement handlers for each event type:
**Python Example:**
```python
def handle_inference_complete(event):
"""Process incremental sleep data"""
session_id = event['session_id']
sleep_stages = event['sleep_stages']
# Update real-time dashboard
redis_client.set(f"session:{session_id}:latest", json.dumps(sleep_stages))
# Notify connected clients via WebSocket
websocket_broadcast(session_id, sleep_stages)
# Store for analysis
db.incremental_data.insert_one(event)
def handle_session_complete(event):
"""Process complete sleep report"""
user_id = event['user_id']
session_id = event['session_id']
stat = event['stat']
# Store complete report
db.sleep_reports.insert_one({
'user_id': user_id,
'session_id': session_id,
'date': event['session']['start_time'],
'statistics': stat,
'created_at': datetime.now()
})
# Update user's latest statistics
update_user_stats(user_id)
# Send push notification
send_notification(user_id, {
'title': 'Sleep Report Ready',
'body': f"Sleep time: {stat['sleep_time']}, Efficiency: {stat['sleep_efficiency']:.1f}%"
})
# Trigger downstream processes
calculate_weekly_trends(user_id)
check_sleep_goals(user_id, stat)
```
### 4. Error Handling
Implement robust error handling:
**Retry Logic:**
```python
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def process_webhook(event):
"""Process webhook with automatic retry"""
# Your processing logic here
pass
@app.route('/webhook', methods=['POST'])
def webhook_endpoint():
try:
event = request.json
process_webhook(event)
return jsonify({"status": "success"}), 200
except Exception as e:
logger.error(f"Webhook processing failed: {e}")
return jsonify({"status": "error", "message": str(e)}), 500
```
**Idempotency:**
```python
def handle_session_complete(event):
session_id = event['session_id']
# Check if already processed
if db.processed_webhooks.find_one({'session_id': session_id}):
logger.info(f"Session {session_id} already processed")
return
# Process event
save_sleep_report(event)
# Mark as processed
db.processed_webhooks.insert_one({
'session_id': session_id,
'processed_at': datetime.now()
})
```
### 5. Testing
Test webhook handling locally:
**ngrok for Local Testing:**
```bash
# Start your local server
python app.py
# In another terminal, expose with ngrok
ngrok http 5000
# Use the ngrok URL as your webhook URL
# Example: https://abc123.ngrok.io/webhook
```
**Mock Webhook Requests:**
```bash
# Test INFERENCE_COMPLETE
curl -X POST http://localhost:5000/webhook \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-H "x-user-id: test_user" \
-d '{
"event": "INFERENCE_COMPLETE",
"version": "V3",
"session_id": "test123",
"sleep_stages": [1, 1, 2]
}'
# Test SESSION_COMPLETE
curl -X POST http://localhost:5000/webhook \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-H "x-user-id: test_user" \
-d '{
"event": "SESSION_COMPLETE",
"version": "V3",
"session_id": "test123",
"stat": {
"sleep_time": "07:30:00",
"sleep_efficiency": 88.5
}
}'
```
---
## Best Practices
### Security
- Always verify `x-api-key` header
- Use HTTPS for webhook endpoints
- Implement request signing if handling sensitive data
- Rate limit webhook endpoint
- Log all webhook attempts
### Reliability
- Respond quickly (< 5 seconds ideal)
- Process asynchronously if needed
- Implement idempotency checks
- Handle duplicate events gracefully
- Return 2xx status even if processing fails (retry logic)
### Performance
- Use message queues for heavy processing
- Implement caching where appropriate
- Batch database operations
- Monitor webhook response times
- Scale horizontally if needed
### Monitoring
- Log all webhook events
- Track processing success/failure rates
- Monitor response times
- Set up alerts for failures
- Dashboard for webhook metrics
### Error Handling
- Catch and log all exceptions
- Return appropriate HTTP status codes
- Implement exponential backoff
- Dead letter queue for failed events
- Manual review process for failures
---
## Common Use Cases
### Real-Time Dashboard Updates
```python
@app.route('/webhook', methods=['POST'])
def webhook():
event = request.json
if event['event'] == 'INFERENCE_COMPLETE':
# Broadcast to connected WebSocket clients
socketio.emit('sleep_update', {
'session_id': event['session_id'],
'sleep_stages': event['sleep_stages'],
'timestamp': event['timestamp']
}, room=event['user_id'])
return jsonify({"status": "success"}), 200
```
### User Notifications
```python
def handle_session_complete(event):
user_id = event['user_id']
stat = event['stat']
# Generate insights
insights = generate_sleep_insights(stat)
# Send push notification
send_push_notification(user_id, {
'title': 'Your Sleep Report is Ready!',
'body': f"You slept for {stat['sleep_time']} with {stat['sleep_efficiency']:.0f}% efficiency",
'data': {
'session_id': event['session_id'],
'insights': insights
}
})
```
### Data Analytics Pipeline
```python
def handle_session_complete(event):
# Store in data warehouse
bigquery_client.insert_rows_json('sleep_data.sessions', [{
'user_id': event['user_id'],
'session_id': event['session_id'],
'date': event['session']['start_time'],
'statistics': json.dumps(event['stat']),
'ingested_at': datetime.now().isoformat()
}])
# Trigger analytics jobs
trigger_weekly_report_job(event['user_id'])
update_cohort_analysis()
```
### Integration with Other Systems
```python
def handle_session_complete(event):
user_id = event['user_id']
stat = event['stat']
# Sync to Apple Health
sync_to_apple_health(user_id, {
'sleep_analysis': stat,
'date': event['session']['start_time']
})
# Update CRM
update_crm_profile(user_id, {
'last_sleep_date': event['session']['start_time'],
'avg_sleep_efficiency': calculate_avg_efficiency(user_id)
})
```
---
## Troubleshooting
### Webhook Not Received
**Check:**
- Endpoint is publicly accessible
- HTTPS is properly configured
- Firewall allows incoming requests
- Webhook URL is correctly configured
- Server is running and healthy
### Authentication Failures
**Check:**
- `x-api-key` validation logic
- API key matches dashboard
- Headers are correctly parsed
- Case sensitivity of header names
### Duplicate Events
**Solution:**
```python
def handle_webhook(event):
event_id = f"{event['session_id']}:{event['event']}:{event['timestamp']}"
# Check if already processed
if redis_client.exists(f"processed:{event_id}"):
return
# Process event
process_event(event)
# Mark as processed (expire after 24 hours)
redis_client.setex(f"processed:{event_id}", 86400, "1")
```
### Processing Delays
**Solution:**
```python
from celery import Celery
celery = Celery('tasks', broker='redis://localhost:6379')
@app.route('/webhook', methods=['POST'])
def webhook():
event = request.json
# Queue for async processing
process_webhook_async.delay(event)
# Respond immediately
return jsonify({"status": "queued"}), 200
@celery.task
def process_webhook_async(event):
# Heavy processing here
pass
```
---
## Resources
- **Official Documentation**: https://docs-en.asleep.ai/docs/webhook.md
- **API Basics**: https://docs-en.asleep.ai/docs/api-basics.md
- **Dashboard**: https://dashboard.asleep.ai