Building a robust health data integration requires more than API calls. This guide covers permission flows, error handling, data sync strategies, privacy compliance, and production deployment best practices.
1. Permission & Onboarding Flow
⚠️ The #1 Mistake: Asking for Everything Upfront
Many apps request all health permissions immediately during onboarding. This results in:
- 78% permission denial rate (users overwhelmed by scope)
- -42% conversion from signup to active user
- High abandonment before seeing any app value
Better approach: Progressive permissions - request only what's needed, when it's needed.
✅ Good Permission Flow (Progressive)
Step 1: Core Feature First (No Permissions)
Let users explore core features before asking for health data:
- Show demo/sample data
- Explain what the app does
- Build trust and value perception
Example: Fitness app shows workout library, nutrition tips before requesting step data
Step 2: Just-In-Time Permissions
Request permissions when user triggers relevant feature:
- User taps "Track My Steps" → request step count permission
- User views "Sleep Analysis" → request sleep permission
- User enables "Heart Rate Zones" → request HR permission
Result: +68% permission grant rate
Step 3: Clear Value Communication
Explain WHY you need each permission before requesting:
// Before showing system permission dialog
showCustomDialog({
title: "Enable Sleep Tracking",
message: "We'll analyze your sleep patterns to recommend optimal bedtimes and detect sleep debt.",
benefits: ["Personalized sleep schedule", "Sleep quality trends", "Energy predictions"],
onAccept: () => requestHealthKitPermission('sleepAnalysis')
})
Step 4: Graceful Degradation
App remains useful even if permissions denied:
- Denied sleep access: Offer manual sleep logging
- Denied step count: Provide workout timer instead
- Denied all: Focus on education/content features
Key: Never lock core features behind permissions
❌ Bad Permission Flow (All-or-Nothing)
Anti-Pattern: Immediate Permission Wall
// DON'T DO THIS
onAppLaunch() {
requestHealthKitPermission([
'stepCount', 'heartRate', 'sleepAnalysis',
'activeEnergy', 'weight', 'bodyFat',
'bloodPressure', 'vo2Max', 'respiratoryRate'
])
// 10+ permissions at once = user panic
}
Result: 78% denial rate
Anti-Pattern: No Explanation
// System dialog with no context
"MyApp would like to access your Health data"
[Don't Allow] [OK]
// User thinks: "Why? What data? No thanks."
Result: -42% conversion
2. Data Sync Strategy
Real-time vs Batch Sync
⚡ Real-time Sync (Webhooks)
When to use:
- Health alerts (anomaly detection)
- Churn prediction (engagement drops)
- Live coaching (workout adjustments)
- Mental health monitoring
Platforms supporting webhooks:
- Sahha - Score updates, biomarker changes
- Terra - Device data pushes
- Spike - Medical device events
- Rook - Batch only (no webhooks)
📊 Batch Sync (Polling)
When to use:
- Analytics dashboards
- Weekly/monthly reports
- Historical trend analysis
- Research data collection
Best practices:
- Poll every 1-6 hours (not more frequent)
- Use background fetch (iOS) / WorkManager (Android)
- Respect rate limits (150 req/hour typical)
- Implement exponential backoff on errors
Webhook Implementation Best Practices
// ✅ GOOD: Webhook endpoint with signature verification
app.post('/webhooks/sahha', async (req, res) => {
// 1. Verify signature (prevent spoofing)
const signature = req.headers['x-signature']
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('hex')
if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' })
}
// 2. Acknowledge immediately (respond within 5 seconds)
res.status(200).json({ received: true })
// 3. Process asynchronously (don't block response)
processWebhookAsync(req.body)
.catch(err => logger.error('Webhook processing failed', err))
})
async function processWebhookAsync(payload) {
// 4. Idempotency check (prevent duplicate processing)
const eventId = payload.id
const processed = await db.query('SELECT 1 FROM processed_events WHERE id = ?', [eventId])
if (processed.length > 0) return
// 5. Process event
await handleSleepScoreUpdate(payload)
// 6. Mark as processed
await db.query('INSERT INTO processed_events (id, processed_at) VALUES (?, NOW())', [eventId])
}
Background Sync (Mobile Apps)
// ✅ iOS Background Fetch (every 1-6 hours)
func application(_ application: UIApplication,
performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
syncHealthData { result in
switch result {
case .newData:
completionHandler(.newData)
case .noData:
completionHandler(.noData)
case .failed:
completionHandler(.failed)
}
}
}
// ✅ Android WorkManager (periodic sync)
val syncWork = PeriodicWorkRequestBuilder(
repeatInterval = 6, repeatIntervalTimeUnit = TimeUnit.HOURS
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
).build()
WorkManager.getInstance(context).enqueue(syncWork)
3. Error Handling & Retry Logic
Common Failure Modes
- Rate limits: API throttling (150 req/hour typical)
- Token expiry: OAuth tokens expire (8 hours - 1 year depending on device)
- Network failures: User offline, API downtime
- Permissions revoked: User disables health data access
- Device disconnected: Wearable not syncing to platform
✅ Robust Error Handling
// ✅ GOOD: Exponential backoff with max retries
async function syncWithRetry(userId, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const data = await fetchHealthData(userId)
return data
} catch (error) {
if (error.status === 429) {
// Rate limit - wait longer
const waitTime = Math.pow(2, attempt) * 1000 // 2s, 4s, 8s
await sleep(waitTime)
continue
} else if (error.status === 401) {
// Token expired - refresh and retry
await refreshToken(userId)
continue
} else if (error.status === 403) {
// Permissions revoked - notify user, stop retrying
await notifyPermissionsRevoked(userId)
throw error
} else if (error.status >= 500) {
// Server error - retry with backoff
if (attempt < maxRetries) {
await sleep(Math.pow(2, attempt) * 1000)
continue
}
}
// Unrecoverable error or max retries reached
throw error
}
}
}
❌ Bad Error Handling
// ❌ DON'T DO THIS: No retry, no context
async function syncHealthData(userId) {
try {
const data = await fetchHealthData(userId)
return data
} catch (error) {
console.log('Failed to sync') // Vague error, no recovery
return null // Silent failure - user never knows
}
}
4. Privacy & Compliance
HIPAA Compliance Checklist (if applicable)
Encrypt health data at rest (AES-256)
Encrypt health data in transit (TLS 1.2+)
Implement access controls (role-based permissions)
Maintain audit logs (who accessed what, when)
Sign Business Associate Agreement (BAA) with data aggregator
Implement data retention policy (delete after 3-7 years)
Conduct annual security risk assessment
Train staff on HIPAA requirements
GDPR Compliance Checklist (EU users)
Obtain explicit consent for data collection (opt-in, not opt-out)
Provide clear privacy policy (what data, why, how long)
Implement "right to access" (users can download their data)
Implement "right to deletion" (users can delete all data)
Implement "right to portability" (export in machine-readable format)
Limit data retention (delete after purpose fulfilled)
Report breaches within 72 hours
Appoint Data Protection Officer (if processing at scale)
Data Minimization
✅ Collect Only What You Need
// Fitness app only needs activity data
const dataTypes = [
'stepCount',
'activeEnergy',
'distanceWalking'
]
// Don't request: heart rate, sleep, weight, etc.
Benefits:
- Lower permission denial rate
- Reduced storage costs
- Simpler compliance (less sensitive data)
❌ Collect Everything "Just in Case"
// DON'T DO THIS
const dataTypes = [
'stepCount', 'heartRate', 'sleepAnalysis',
'weight', 'bodyFat', 'bloodPressure',
'bloodGlucose', 'vo2Max', 'respiratoryRate',
'menstrualFlow', 'sexualActivity'
// ... 50+ more biomarkers you don't use
]
Problems:
- High permission denial
- HIPAA/GDPR violations
- Unnecessary storage costs
5. Performance Optimization
Reduce API Calls
✅ Batch Queries
// GOOD: Single query for multiple biomarkers
const data = await sahha.getBiomarkers({
types: ['sleep', 'activity', 'readiness'],
startDate: '2025-10-01',
endDate: '2025-10-16'
})
// 1 API call for 3 biomarker types
❌ Individual Queries
// BAD: Separate query per biomarker
const sleep = await sahha.getSleep(...)
const activity = await sahha.getActivity(...)
const readiness = await sahha.getReadiness(...)
// 3 API calls (3x latency, 3x rate limit usage)
Cache Intelligently
// ✅ GOOD: Cache with TTL (Time To Live)
async function getHealthData(userId, date) {
const cacheKey = `health:${userId}:${date}`
// Check cache first
const cached = await redis.get(cacheKey)
if (cached) return JSON.parse(cached)
// Fetch from API
const data = await sahha.getBiomarkers({ userId, date })
// Cache with appropriate TTL
const ttl = isToday(date) ? 3600 : 86400 // 1 hour for today, 24 hours for past
await redis.setex(cacheKey, ttl, JSON.stringify(data))
return data
}
Pagination for Large Datasets
// ✅ GOOD: Paginate when fetching historical data
async function fetchAllBiomarkers(userId, startDate, endDate) {
let allData = []
let page = 1
const pageSize = 100
while (true) {
const response = await sahha.getBiomarkers({
userId,
startDate,
endDate,
page,
pageSize
})
allData = allData.concat(response.data)
if (response.data.length < pageSize) break // Last page
page++
}
return allData
}
6. User Experience Best Practices
Handle Missing Data Gracefully
✅ Explain Why Data is Missing
- No wearable connected: "Connect a wearable to see heart rate data, or use smartphone tracking for basic metrics"
- Device not synced: "Your Apple Watch hasn't synced in 3 days. Open the Health app to sync."
- Permissions denied: "Enable sleep tracking in Settings → Privacy → Health to see sleep analysis"
Always provide actionable next step
❌ Show Empty State Without Context
- "No data available" (Why? What can I do?)
- "Sync failed" (How do I fix it?)
- Blank screen (Is the app broken?)
Users assume app is broken, churn increases
Loading States
// ✅ GOOD: Progressive loading with skeleton UI
function HealthDashboard() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchHealthData().then(data => {
setData(data)
setLoading(false)
})
}, [])
if (loading) {
return // Show layout with animated placeholders
}
return
}
7. Testing Strategy
Pre-Production Testing Checklist
Test with zero permissions (app should still function)
Test with partial permissions (e.g., steps allowed, sleep denied)
Test with all permissions granted
Test permission revocation mid-session
Test with no wearable connected
Test with multiple wearables (Apple Watch + Fitbit)
Test with stale data (device not synced in 7+ days)
Test offline mode (airplane mode)
Test API rate limits (simulate 429 errors)
Test webhook delivery failures (retry logic)
Test large date ranges (performance with 1+ year of data)
Test edge cases (user with zero health data, brand new account)
Staging Environment Best Practices
// ✅ GOOD: Separate staging credentials
const config = {
production: {
apiKey: process.env.SAHHA_PROD_API_KEY,
webhookUrl: 'https://api.myapp.com/webhooks/sahha'
},
staging: {
apiKey: process.env.SAHHA_STAGING_API_KEY,
webhookUrl: 'https://staging.myapp.com/webhooks/sahha'
}
}
// Use test/demo profiles in staging
const userId = IS_PRODUCTION ? user.id : 'demo-user-001'
8. Monitoring & Alerts
Key Metrics to Track
Metric |
Target |
Alert Threshold |
Permission grant rate |
≥60% |
Drops below 50% |
Sync success rate |
≥95% |
Drops below 90% |
API response time (p95) |
≤500ms |
Exceeds 1000ms |
Webhook delivery rate |
≥99% |
Drops below 95% |
Data freshness (median) |
≤6 hours |
Exceeds 24 hours |
Error rate |
≤1% |
Exceeds 5% |
Logging Best Practices
// ✅ GOOD: Structured logging with context
logger.info('Health data sync started', {
userId: user.id,
dataTypes: ['sleep', 'activity'],
dateRange: { start: '2025-10-01', end: '2025-10-16' },
platform: 'sahha'
})
logger.error('Sync failed', {
userId: user.id,
error: err.message,
errorCode: err.status,
retryCount: 3,
platform: 'sahha'
})
// Enables filtering: "Show all Sahha errors for userId=123"
9. Production Deployment Checklist
Pre-Launch Checklist
✅ Obtain production API keys from aggregator
✅ Configure webhook endpoints with signature verification
✅ Set up monitoring and alerts (Sentry, Datadog, etc.)
✅ Implement progressive permission flow
✅ Add retry logic with exponential backoff
✅ Implement caching with appropriate TTLs
✅ Add privacy policy and consent flows
✅ Test all failure modes (permissions denied, API errors, offline)
✅ Load test with expected user volume
✅ Document runbook for common issues
✅ Set up on-call rotation for critical alerts
✅ Prepare rollback plan if integration fails
10. Common Pitfalls to Avoid
❌ Don't Do This:
- Request all permissions upfront → Use progressive permissions
- Poll APIs every minute → Use webhooks or 1-6 hour intervals
- Ignore rate limits → Implement exponential backoff
- Store sensitive data unencrypted → Use AES-256 at rest, TLS 1.2+ in transit
- Assume data always exists → Handle missing data gracefully
- Hard-code API keys → Use environment variables
- Skip error logging → Implement structured logging
- Trust webhook payloads → Verify signatures
- Process webhooks synchronously → Use async background jobs
- Ignore HIPAA/GDPR → Consult legal counsel, implement compliance
✅ Do This Instead:
- Progressive permissions (just-in-time, with clear value communication)
- Webhook-first (real-time updates without polling)
- Retry with backoff (handle rate limits gracefully)
- Encrypt everything (at rest and in transit)
- Graceful degradation (app works even without health data)
- Environment-based config (separate staging/production)
- Structured logging (searchable, filterable logs)
- Signature verification (prevent webhook spoofing)
- Async processing (respond to webhooks within 5 seconds)
- Compliance by design (HIPAA/GDPR from day one)