Sleep Cycle SDK

iOS SDK

Advanced sleep analysis capabilities for iOS applications

Sleep Cycle SDK - iOS Documentation

Overview

The Sleep Cycle SDK for iOS enables developers to integrate advanced sleep analysis capabilities into their applications. The SDK provides real-time sleep tracking using audio and motion sensors, delivering detailed sleep insights, stage transitions, and detected events throughout the night.

System Requirements

Minimum iOS Version:

  • iOS: 16.0+
  • macOS: 13.0+

Swift:

  • Swift Version: 5.9+
  • The SDK is written in Swift and provides a Swift-native API with async/await support

Installation

The Swift package vends two products, built from the same source and both vending the SleepCycleSDK module. Depend on exactly one of them:

SleepCycleSDKSleepCycleSDKSlim
ML modelsEmbedded in the frameworkDownloaded on the first initialize(...)
Framework size embedded in your app~16 MB~1.6 MB
First initialize(...)No downloadDownloads the models your granted features require
Network requiredOnly for authorizationFor authorization and the first download

Both products are released together and expose an identical API — you write import SleepCycleSDK and call the SDK the same way either way, so switching between them is a one-line change to your package dependency. Choose SleepCycleSDKSlim to keep your app download small and accept a one-time model download on first launch; choose SleepCycleSDK when the SDK must be usable without ever downloading anything beyond authorization.

Swift Package Manager

Add the Sleep Cycle SDK to your project using Swift Package Manager:

  1. In Xcode, select FileAdd Package Dependencies
  2. Enter the package URL: https://github.com/MDLabs/sleepcycle-sdk-swift
  3. Select the version you want to use (Semantic Versioning)
  4. Add either the SleepCycleSDK or the SleepCycleSDKSlim library product to your target

Alternatively, add it to your Package.swift:

dependencies: [
    .package(url: "https://github.com/MDLabs/sleepcycle-sdk-swift", from: "1.3.0")
],
targets: [
    .target(
        name: "YourApp",
        dependencies: [
            // Models embedded in the framework
            .product(name: "SleepCycleSDK", package: "sleepcycle-sdk-swift")

            // Models downloaded at runtime (use instead of the above, not alongside it)
            // .product(name: "SleepCycleSDKSlim", package: "sleepcycle-sdk-swift")
        ]
    )
]

Both products vend the SleepCycleSDK module, so your imports stay the same:

import SleepCycleSDK

The SDK requires an API key for authorization. Contact Sleep Cycle to obtain credentials.

Using the slim product

With SleepCycleSDKSlim, initialize(...) downloads the encrypted models required by the features your API key grants and does not return until they are all available locally. Only the required models are fetched, so the download is smaller when fewer features are enabled.

The downloaded models are stored under the app's Application Support directory and reused on every later launch, so the download happens once per install (and again after an SDK upgrade that changes a model). They are removed when the app is deleted, or when you call SleepCycleSdk.clear().

Report progress to the user with the optional onDownloadProgress callback:

let features = try await SleepCycleSdk.initialize(
    logLevel: .info,
    apiKey: "your-api-key-here",
    onDownloadProgress: { progress in
        // progress is a fraction in 0...1, delivered from a background context
        Task { @MainActor in self.downloadProgress = progress }
    }
)

The callback is never invoked on the bundled product, so the same code works unchanged with both.

Provisioning failures surface as SleepCycleSdkError cases thrown from initialize(...):

.resourceUnavailable(_:) - A required model has no download location, for example with a token issued before side-loading was supported
.resourceDownloadFailed(_:) - The download failed, for example when the device is offline on first launch
.modelNotProvisioned(_:) - A model needed for analysis has not been downloaded
.modelIntegrityCheckFailed(_:) - A downloaded model is incomplete or corrupt
.modelUnavailable(_:) - The model is not part of this SDK build

Download failures are recoverable: call initialize(...) again once the device is online. Downloads resume from a partial file, so a retry does not start over.

Prerequisites

Permissions

The SDK requires microphone access for audio-based sleep analysis. Add the following to your Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>We need access to the microphone to analyze your sleep patterns and detect snoring.</string>

For motion-based analysis, you may also need:

<key>NSMotionUsageDescription</key>
<string>We use motion data to track your sleep movements.</string>

Background modes

To ensure continuous sleep analysis throughout the night, enable the appropriate background modes in your app's capabilities:

  1. Open your project in Xcode
  2. Select your app target
  3. Go to "Signing & Capabilities"
  4. Add "Background Modes" capability
  5. Enable "Audio"

Alternatively, add this to your Info.plist:

<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>

The SDK uses the audio background mode to maintain continuous audio processing during sleep analysis. Your app should also implement proper session management to prevent iOS from suspending the analysis process.

General

The SDK is thread-safe and uses Swift concurrency (async/await) for all asynchronous operations.

Initialize the SDK

The SDK requires authentication before use. The initialization process validates your credentials and determines available features.

import SleepCycleSDK

Task {
    do {
        let features = try await SleepCycleSdk.initialize(
            logLevel: .info,
            apiKey: "your-api-key-here"
        )
        print("Authorized with features: \(features)")
    } catch {
        print("Initialization error: \(error)")
    }
}

Parameters:

logLevel LogLevel - SDK log verbosity (.debug, .info, .warning, .error, .noLog)
logger Logger? - Optional custom logger (defaults to the system logger)
apiKey String - Your Sleep Cycle API key
forceTokenRefresh Bool - Force a fresh token fetch even if a cached token exists (defaults to false)
onDownloadProgress ((Double) -> Void)? - Optional model download progress in 0...1, delivered from a background context

With SleepCycleSDKSlim, initialize(...) also downloads the models your granted features require and does not return until they are available locally. See Using the slim product.

The returned SleepAnalysisFeatures indicates which capabilities are available for your API key:

sleepStaging Bool - Sleep staging analysis
smartAlarm Bool - Smart alarm functionality
audioEvents Bool - Audio event detection
snoringDetection Bool - Snoring detection
realTimeSleepStaging Bool - Real-time sleep staging
multiChannelAnalysis Bool - Multi-channel analysis (stereo, two channels)
extendedAudioEvents Bool - Extended audio event types beyond the standard set

Access feature flags at runtime:

if SleepCycleSdk.isFeatureEnabled(\.audioEvents) {
    // Present snore/talk event UI
}

Deinitialize and clear the SDK

deinitialize() releases the SDK's resources and returns it to the uninitialized state:

await SleepCycleSdk.deinitialize()

It stops the data source but does not finalize or end active sessions, so a session interrupted by an app shutdown can be picked up again with resumeAnalysis(...). Call stopAnalysis(at:) or stopDataSource() first to end them, or clearActiveSessions() afterwards.

clear() goes further: it deinitializes the SDK and wipes persisted state — the cached authorization token and any models downloaded by the slim product:

await SleepCycleSdk.clear()

The next initialize(...) re-authorizes and, with the slim product, downloads the required models again. Models embedded in SleepCycleSDK are unaffected.

Get the SDK state

Monitor SDK state changes using the AsyncStream:

import SleepCycleSDK

Task {
    for await state in await SleepCycleSdk.stateStream {
        switch state {
        case .uninitialized:
            print("SDK not initialized")
        case .initialized:
            print("SDK ready")
        case .running:
            print("Analysis in progress")
        }
    }
}

Get the current state:

let currentState = await SleepCycleSdk.currentState

Start a sleep analysis session

Once initialized, you can start a sleep analysis session. The method returns a UUID that identifies the session.

import SleepCycleSDK

Task {
    do {
        let sessionId: UUID = try await SleepCycleSdk.startAnalysis(
            config: SleepAnalysisConfig(
                useAudio: true,
                useAccelerometer: true
            )
        )
        print("Analysis started with session ID: \(sessionId)")
    } catch {
        print("Failed to start analysis: \(error)")
    }
}

Parameters:

config SleepAnalysisConfig - Configuration object specifying which sensors to use
at Date - The start time for the analysis (defaults to current time)
using DataSource? - Optional data source (e.g., live or file replay)
eventListeners [AudioEventListener] - Optional array of listeners that receive callbacks during audio analysis. Use this to capture audio samples and events in real-time

Resume a session

The SDK supports resuming a previously started analysis session. This is useful when your app restarts or the background task is terminated by the system.

if await SleepCycleSdk.isResumePossible() {
    let resumedSessions: [ResumedSession] = try await SleepCycleSdk.resumeAnalysis()
    for session in resumedSessions {
        print("Resumed \(session.sessionId) on channel \(session.channel)")
    }
}

resumeAnalysis() returns an array of ResumedSession, each containing the sessionId, channel, and any persisted smartAlarmConfig for the resumed session.

When a resumable session is not going to be resumed, end it and discard its results with clearActiveSessions(), which makes isResumePossible() return false again:

try await SleepCycleSdk.clearActiveSessions()

It throws SleepCycleSdkError.analysisAlreadyRunning if the data source is running.

Stop a session

To stop an active analysis session and retrieve the results:

Task {
    do {
        let result = try await SleepCycleSdk.stopAnalysis()

        print("Session ID: \(result.sessionId)")

        if let statistics = result.statistics {
            print("Total sleep duration: \(statistics.totalSleepDuration ?? 0)")
            print("Sleep efficiency: \(statistics.sleepEfficiency ?? 0)")

            if let snoreSessions = statistics.snoreSessions {
                for session in snoreSessions {
                    print("Snoring session: \(session.interval)")
                }
            }
        }

        for event in result.events {
            print("\(event.type) detected: \(event.interval), p=\(event.probability)")
        }

        for breathingRate in result.breathingRates {
            print("Breathing rate: \(breathingRate.bpm) bpm at \(breathingRate.timestamp)")
        }

        for stageInterval in result.sleepStageIntervals {
            print("\(stageInterval.stage): \(stageInterval.interval)")
        }

        if let audioStats = result.audioStatistics {
            for interval in audioStats.healthIntervals {
                print("Audio \(interval.status): \(interval.interval)")
            }
        }
    } catch {
        print("Failed to stop analysis: \(error)")
    }
}

Analysis result

The AnalysisResult contains the complete output of a sleep analysis session:

sessionId UUID - Unique session identifier
startTime Date - Session start time
endTime Date - Session end time
timeZone TimeZone - Time zone captured at session start
events [Event] - Detected sleep events
breathingRates [BreathingRate] - Breathing rate measurements
sleepStageIntervals [SleepStageInterval] - Sleep stage data
realTimeSleepStageIntervals [SleepStageInterval] - Sleep stages emitted live during the session
statistics SleepStatistics? - Aggregated sleep statistics (optional)
audioStatistics AudioStatistics? - Audio health statistics (optional)

SleepStatistics

When available, statistics contains aggregated metrics about the sleep session:

totalSleepDuration Double? - Total time spent sleeping
sleepOnsetLatency Double? - Time to fall asleep
sleepEfficiency Double? - Ratio of sleep to time in bed (0.0 to 1.0)
finalWakeTime Date? - Time of final awakening
numberOfAwakenings Int? - Number of awakenings during the night
snoreTime Double? - Total time spent snoring
snoreSessions [SnoreSession]? - Individual snoring sessions
sleepStageDurations [SleepStage: Double]? - Duration per sleep stage

AudioStatistics

When available, audioStatistics contains information about audio input health throughout the session.

Sleep score

After a session completes, you can compute a sleep score for the night with SleepScoring.compute(result:history:dateOfBirth:chronoType:). The score combines the night's result with a short history of previous nights.

import SleepCycleSDK

// Build a history entry from each completed result and persist it for future nights
let historyEntry = result.toSleepScoreHistoryEntry()

let score = SleepScoring.compute(
    result: result,
    history: recentHistoryEntries,
    dateOfBirth: userDateOfBirth,   // optional, age-adjusts the quality subscore
    chronoType: userChronoType      // optional, selects the timing subscore window
)

print("Sleep score: \(score.total) (duration=\(score.duration), quality=\(score.quality), routine=\(score.routine))")

Each SleepScore field is in the range 0.0–1.0, where higher is better:

total Float - Overall sleep score for the night
duration Float - How much the user slept
quality Float - How well the user slept
routine Float - The user's sleep schedule

SleepScoring.identifyChronoType(history:) derives the user's ChronoType (.extremeMorning, .morning, .intermediate, .evening, .extremeEvening) from recent history, or returns nil when there isn't enough data. The result can be passed back into compute(...) as the chronoType argument.

Real-time events

The SDK provides real-time event updates during analysis through an AsyncStream API:

Task {
    for await events in await SleepCycleSdk.eventStream {
        for event in events {
            switch event.type {
            case .movement:
                handleMovement(event)
            case .snoring:
                handleSnoring(event)
            case .talking:
                handleTalking(event)
            case .coughing:
                handleCoughing(event)
            default:
                handleOtherEvent(event)
            }
        }
    }
}

When the extendedAudioEvents feature is enabled for your API key, additional EventType values are reported alongside the standard ones: .bird, .cat, .digestive, .dog, .fart, .music, .sneeze, .throatClearing, .traffic, .water, and .wind. Use EventType.standardTypes and EventType.extendedTypes to distinguish the two sets.

Each Event contains:

type EventType - The type of event
interval DateInterval - Time interval of the event
probability Double - Confidence score (0.0 to 1.0)
source EventSource - Source of detection
sessionId UUID - The session this event belongs to
signature [Float]? - Optional feature vector (for snoring events)

Real-time breathing rate

The SDK provides real-time breathing rate measurements during analysis:

Task {
    for await breathingRate in await SleepCycleSdk.breathingRateStream {
        print("Breathing rate: \(breathingRate.bpm) bpm (confidence: \(breathingRate.confidence))")
    }
}

Each BreathingRate contains:

timestamp Date - The time when the measurement was recorded
bpm Double - Breathing rate in breaths per minute
confidence Double - Confidence score of the measurement (0.0 to 1.0)
sessionId UUID - The session this measurement belongs to

Real-time sleep staging (Experimental)

This feature is experimental and may change in future releases. The API and behavior are subject to modification without notice.

The SDK can provide real-time sleep stage predictions during analysis. This feature requires the realTimeSleepStaging capability to be enabled for your API key.

Task {
    for await stageInterval in await SleepCycleSdk.sleepStageStream {
        switch stageInterval.stage {
        case .awake:
            print("Awake: \(stageInterval.interval)")
        case .light:
            print("Light sleep: \(stageInterval.interval)")
        case .deep:
            print("Deep sleep: \(stageInterval.interval)")
        case .rem:
            print("REM sleep: \(stageInterval.interval)")
        }
    }
}

The stream emits SleepStageInterval objects approximately every 30 seconds during analysis, providing near real-time feedback on sleep state transitions.

Real-time audio health

The SDK monitors the health of the audio input during analysis and emits status updates when the audio state changes:

Task {
    for await update in await SleepCycleSdk.audioHealthStream {
        switch update.status {
        case .healthy:
            print("Audio input healthy")
        case .flatline:
            print("Audio flatline detected")
        case .missingInput:
            print("Audio input missing")
        }
    }
}

AudioHealthStatus values:

.healthy - Audio input contains a varying signal
.flatline - Constant value detected (non-functional microphone or muted input)
.missingInput - No audio input received for an extended period

Smart alarm

The smart alarm monitors movement during a wake-up window and emits events when the user is in a light sleep phase, allowing you to wake them at an optimal moment. This requires the smartAlarm feature to be enabled for your API key and is only supported on the primary channel.

Configure a wake-up window with SmartAlarmConfig and pass it when starting analysis:

import SleepCycleSDK

let wakeupWindow = DateInterval(start: windowStart, end: windowEnd)

try await SleepCycleSdk.startAnalysis(
    config: SleepAnalysisConfig(useAudio: true, useAccelerometer: true),
    smartAlarmConfig: SmartAlarmConfig(wakeupWindow: wakeupWindow)
)

Observe alarm lifecycle events through smartAlarmStream:

Task {
    for await event in await SleepCycleSdk.smartAlarmStream {
        switch event.type {
        case .armed:
            print("Smart alarm armed")
        case .triggered(let reason):
            switch reason {
            case .optimalWakeUp, .userInteraction, .windowEnd:
                ringAlarm()
            }
        }
    }
}

Change the alarm while the session is running with updateSmartAlarm(config:). It replaces the alarm the session started with, adds one to a session started without an alarm, or cancels it when passed nil:

// Move the wake-up window
try await SleepCycleSdk.updateSmartAlarm(
    config: SmartAlarmConfig(wakeupWindow: DateInterval(start: newStart, end: newEnd))
)

// Cancel the alarm
try await SleepCycleSdk.updateSmartAlarm(config: nil)

The new configuration is persisted, so a session resumed after an app kill continues with it, and an alarm that has already fired is re-armed by a new configuration so it can fire again. A wake-up window that is already past fires .windowEnd on the next analysis tick. The call targets the .primary session and throws SleepCycleSdkError.analysisNotActive if no session is active on that channel.

The smartAlarmConfig parameter is also available on startMultiChannelAnalysis(...), which rejects it for any channel other than .primary.

Event signatures

For snoring events, the Event.signature property contains a 16-dimensional feature vector that represents unique characteristics of the detected snore. Snore events from the same person are grouped close to each other in the signature space, allowing clustering of events by person.

Audio event listener

The AudioEventListener protocol allows you to receive real-time audio analysis updates during a session. Implement this protocol to access raw audio samples, event detection, and volume information as analysis progresses.

class MyAudioListener: AudioEventListener {
    func onAudioAnalysisBatchCompleted(
        sessionId: UUID,
        audioSamples: [Float],
        audioSampleRate: Int,
        audioStartTime: Date,
        audioEndTime: Date,
        audioProbability: [EventType: Float],
        eventsStarted: [EventStartedInfo],
        eventsEnded: [EventEndedInfo],
        dbSpl: [Float]
    ) {
        // Process audio samples and events
    }
}

let listener = MyAudioListener()
try await SleepCycleSdk.startAnalysis(
    config: SleepAnalysisConfig(useAudio: true),
    eventListeners: [listener]
)

The audioSamples parameter contains all processed audio data in sequence, without any gaps or overlap between batches. Each batch continues exactly where the previous batch ended, ensuring complete coverage of all analyzed audio.

The audioProbability parameter provides the per-batch probability for each EventType in the analyzed time window. The dbSpl parameter provides the A-weighted sound volume in dB SPL for each time frame in the batch.

Audio clips

The SDK can capture short audio recordings when specific sleep events are detected, such as snoring, sleep talking, or coughing.

Create an AudioEventListener using AudioClipsConfig and AudioClipsReceiver:

import SleepCycleSDK

let listener = SleepCycleSdk.createAudioClipsProducer(
    audioClipsConfig: AudioClipsConfig(
        activeTypes: [
            .snoring: EventTypeConfig(minDuration: 0.5),
            .talking: EventTypeConfig(minDuration: 0.5)
        ],
        clipLength: 5.0
    ),
    receiver: MyAudioHandler()
)

try await SleepCycleSdk.startAnalysis(
    config: SleepAnalysisConfig(useAudio: true),
    eventListeners: [listener]
)

Each AudioClip contains:

startTime Date - Start timestamp
type EventType - The event type that triggered the capture
samples [Float] - Raw audio samples
sampleRate Int - Sample rate in Hz
sessionId UUID - The session this clip belongs to

Multi-channel analysis

The SDK supports analyzing two channels simultaneously using a stereo audio source. Multi-channel analysis separates the data source lifecycle from individual session lifecycles, allowing you to start and stop sessions on each channel independently.

The stereo stream is expected to come from two separate mono microphones combined into a single stereo stream, with one microphone per channel.

This requires the multiChannelAnalysis feature to be enabled for your API key.

Channel separation

When using stereo input, ChannelSeparationConfig controls how audio events are assigned to channels. Built-in presets:

  • .bedSideMics — bedside microphones placed apart (default)
  • .centeredMicArray — closely spaced microphone array
  • .detectionStrengthOnly() — no spatial filtering, uses only detection confidence

All parameters (mic distance, ambiguous zone, confidence threshold, per-event-type settings) can be tuned individually to fit your specific hardware setup and use case.

Data source lifecycle

Start the data source with a stereo audio configuration before starting individual sessions:

import SleepCycleSDK

let dataSource = SleepCycleSdk.createLiveDataSource()

try await SleepCycleSdk.startDataSource(
    using: dataSource,
    channelSeparationConfig: .bedSideMics
)

Starting sessions on each channel

Once the data source is running, start a session on each channel:

let primarySessionId: UUID = try await SleepCycleSdk.startMultiChannelAnalysis(
    channel: .primary,
    config: SleepAnalysisConfig(useAudio: true, useAccelerometer: true)
)

let secondarySessionId: UUID = try await SleepCycleSdk.startMultiChannelAnalysis(
    channel: .secondary,
    config: SleepAnalysisConfig(useAudio: true, useAccelerometer: false)
)

AnalysisChannel values:

.primary - First audio channel (or mono)
.secondary - Second audio channel in stereo

Stopping sessions independently

Each session can be stopped independently to retrieve its result:

let primaryResult = try await SleepCycleSdk.stopAnalysis(sessionId: primarySessionId)
let secondaryResult = try await SleepCycleSdk.stopAnalysis(sessionId: secondarySessionId)

Stopping the data source

After all sessions have been stopped, stop the data source:

await SleepCycleSdk.stopDataSource()

Calling stopDataSource() while sessions are still active will force-stop them and discard their results. To retrieve results, stop each session first.

On this page