iOS
1. Introduction
The iOS SDK is split into three main components: VitalCore
, VitalHealthKit
and VitalDevices
. VitalCore
holds common components to both VitalHealthKit
and VitalDevices
. Among other things, it has the network layer that allows us to send data from a device to a server. As their name hint, VitalHealthKit
and VitalDevices
are an abstraction over HealthKit and specific Bluetooth devices. If your app is generating data independently, you can use VitalCore
directly to push data.
We keep these frameworks split on purpose. If your app is linked to Apple HealthKit and you don’t use it, your app might be rejected during the app review process
2. Installation
We use SPM (Swift Package Manager) to manage dependencies. You can add https://github.com/tryVital/vital-ios
when adding it as a dependency to your project. Independently of using VitalDevices
, or VitalHealthKit
, make sure you always add VitalCore
. The first two depend on the latter.
3. Initial Setup
To use our SDK, start by setting up VitalClient
:
import VitalCore
VitalClient.configure(
apiKey: "xyz",
environment: .sandbox(.us)
)
There are two main topics you should be aware:
userId
.- Connected source.
1. userId
A userId
serves as a unique identifier of your user in Vital. You create one by sending us an id representing that user in your system (e.g. white_bear
, 12832001
, b5d36dbe-b745-11ec-b909-0242ac120002
, etc). We advise against the use of PII (Personal Identifiable Information). It should also be unique across the users that belong to your team. You can create a userId
like this:
let result = try await VitalClient.shared.user.create(
.init(clientUserId: "white_bear"),
setUserIdOnSuccess: false
)
A userId
is an UUID4
. Once you have a userId
, you need to set it:
VitalClient.setUserId(result.userId)
By default, when you create a userId
, we call VitalClient.setUserId
internally. If you don’t want this to happen call VitalClient.setUserId(result.userId, setUserIdOnSuccess: false)
.
2. Connected Source
A connected source is a link between a provider (e.g. Omron, Strava, Fitbit, Garmin) and your user. When you post information, it is expected that a connected source exists for that user and the source of information you are using. If it doesn’t exist, the request will fail.
Example: You want to post Ben’s Omron data to Vital. Two things need to exist: 1) Ben is a user in Vital 2) A connected source linking Ben’s user to Omron.
To achieve this you can:
/// 1)
let ben = try await VitalClient.shared.user.create(clientUserId: "white_bear")
/// Remember that by default `user.create` calls `VitalClient.setUserId` interally.
/// 2)
let result = try await VitalClient.shared.user.createConnectedSource(for: .omron)
With these two things in place, you can now post Ben’s Omron data.
4. VitalCore
For the most part, you won’t need to instantiate model objects. VitalHealthKit
and VitalDevices
will generate these models on your behalf. For VitalDevices
in particular, you are responsible for sending the data explicitly via VitalClient.shared.<domain>
. VitalClient
allows you to do exactly that. VitalHealKit
however will send the data automatically.
1. TimeSeries and Summaries
There are two main sources of data: time series and summaries. Time series data correspond to points in time (e.g. glucose, heart rate, etc). Summaries are a digest of a particular resource (e.g. workout, sleep, etc). Summaries can have time series data. For example a workout has an array of heart rate data points. For data generated by VitalDevices
, typically this will be time series.
For time series we support:
- Glucose
- Blood Pressure
For summaries:
- Workout
- Activity
- Sleep
- Profile
- Body
Posting time series data is as simple as:
let sample = QuantitySample(
value: 10,
startDate: .init(),
endDate: .init(),
type: "fingerprick",
unit: "mg/dl"
)
try await VitalClient.shared.timeSeries.post(
.glucose([samples]),
stage: .daily,
provider: .appleHealthKit
)
Likewise for summaries:
let workoutPatch: WorkoutPatch = ...
try await VitalClient.shared.summary.post(
.workout(workoutPatch),
stage: .daily,
provider: .appleHealthKit
)
It’s important to notice a few things in the above snippet:
- We set the stage of data as
.daily
. If you are sending old data, please use.historical
instead. This distinction is used for the Webhooks. If you are not sure if.historical
data makes sense, you can stick with.daily
. A.daily
will always generate a webhook with the full payload of data. For more information, please read the Webhook Flow. - The provider (
.appleHealthKit
) must match an existing connected source for that user. This means that if there’s no connected source linking the user and.appleHealthKit
, the request will fail. By default theVitalHealthKitClient
, will create a connected source on your behalf when pushing Apple HealthKit data. - Finally if you are generating your own fitness or medical data, use the
.manual
provider.
2. Creating a Connected Source
The SDK provides two ways to create a connected source. The first method is a manual approach:
let userId: UUID = ...
try await VitalClient.shared.link.createConnectedSource(userId, provider: .omron)
The second method uses web authentication:
let url = try await VitalClient.shared.link.createProviderLink(redirectURL: "vitalExample://")
The redirectURL
is the URL that’s called after the authentication is done. For an iOS app, you add the following to your Info.plist:

After having the url
, you can open it with SFSafariViewController
or WKWebView
. On completion, your app will be called with an URL with the following shape:
- On success:
vitalExample://?state=success&isMobile=true&provider=<provider>
. - On failure:
vitalExample://?state=error&isMobile=true&provider=<provider>&error=<error description>
.
3. Clean Up
When logging out a user, you should call:
await VitalClient.shared.cleanUp()
Calling cleanUp
does one thing:
- Prepare the SDK to be configured again (either with new API keys and / or another
userId
).
If you are using VitalHealthKit
, call this method
instead.
5. VitalDevices
1. Bluetooth
VitalDevices
connects your app to other devices via bluetooth. Currently we support glucose meters and blood pressure readers.
You start by fetching all devices supported:
import VitalDevices
/// Get the brands
let brands = DevicesManager.brands()
/// Each brand has devices
let devices = DevicesManager.devices(for: brands[0])
Based on the device
, you start scanning your surroundings to find it. This approach filters out devices we are not interested in:
let device = devices[0]
let manager = DevicesManager()
let publisher: AnyPublisher<ScannedDevice, Never> = manager.search(for: device)
You can observe the publisher (e.g. via sink
) until you find a device. Once you find a device you create a reader:
let scannedDevice: ScannedDevice = ...
if scannedDevice.kind == .bloodPressure {
let reader: BloodPressureReadable = manager.bloodPressureReader(for: scannedDevice)
}
if scannedDevice.kind == .glucoseMeter {
let reader: GlucoseMeterReadable = manager.glucoseMeter(for: scannedDevice)
}
Depending on the flow of your app, and/or the device you are working with, you can either just pair, or pair and read. The “just” pair is needed for devices that can only pair while in pairing mode. The devices we tested were able to pair and read while not in pairing mode, but your experience might be different.
For blood pressure monitors:
let reader: BloodPressureReadable = manager.bloodPressureReader(for: scannedDevice)
let justPair: AnyPublisher<Void, Error> = reader.pair(device: scannedDevice)
let pairAndRead: AnyPublisher<[BloodPressureSample], Error> = reader.read(device: scannedDevice)
And for glucose meters:
let reader: GlucoseMeterReadable = manager.glucoseMeter(for: scannedDevice)
let justPair: AnyPublisher<Void, Error> = reader.pair(device: scannedDevice)
let pairAndRead: AnyPublisher<[QuantitySample], Error> = reader.read(device: scannedDevice)
You can monitor the connection to the device via:
let monitorDevice: AnyPublisher<Bool, Never> = manager.monitorConnection(for: device)
When you finish scanning for a device, you need to terminate the scanning. If you don’t do this, you won’t be able to connect and extract data from the device. You can achieve this by holding onto a Cancellable
(via sink
) and call cancel()
. Or by using a more declarative approach (e.g. publisher.first()
).
You can check our example app, to see how we do it.
2. Freestyle Libre 1
We currently support Libre 1 sensors via NFC readings. Please make sure you add NFC capabilities in your app:

Also add the key NFCReaderUsageDescription
in your info.plist. This key should explain why your app needs to use NFC.
To use the reader:
let reader = Libre1Reader(
readingMessage: "Ready for reading",
errorMessage: "Failed reading from sensor",
completionMessage: "Completed successfully!",
queue: mainQueue
)
let reading = try await reader.read()
A reading
is a payload with two properties: 1) the sensor information 2) an array of glucose readings.
let sensor: Libre1Sensor = reading.sensor
let samples: [QuantitySample] = reading.samples
As previously mentioned, you are responsible for sending the samples to the server. You can do so via VitalClient.shared.timeSeries.post
.
Readings taken with the SDK are not guaranteed to match the official Freestyle Libre app. This mismatch happens due to the algorithm difference used by us, compared to the official Freestyle Libre.
6. VitalHealthKit
1. Quick Intro
VitalHealthKit
is an abstraction on top of HealthKit that: 1) automates the extraction of data 2) automatically pushs that data to Vital.
You start by enabling HealthKit capabilities in your app. Please follow this guide. It should looks like this:

Also add the following key in your Info.plist
:
<key>NSHealthShareUsageDescription</key>
<string>your description</string>
You then set-up the VitalClient — if you already haven’t done so — and the VitalHealthKitClient:
import VitalHealthKit
VitalClient.configure(
apiKey: "xyz",
environment: .sandbox(.us)
)
VitalClient.setUserId(result.userId)
// precondition: VitalClient must have been configured.
VitalHealthKitClient.configure(
.init(
mode: ..., // automatic by default
backgroundDeliveryEnabled: ..., // false by default
logsEnabled: ..., // false by default
numberOfDaysToBackFill: ..., // 90 by default
)
)
That’s it!
VitalHealthKit
will check if the userId
has an associted Apple HealthKit connected source. If it doesn’t, it will create one automatically. This means that you don’t need to do this:
let result = try await VitalClient.shared.user.createConnectedSource(for: .appleHealthKit)
When configuring VitalHealthKitClient
, you can enable logs, background delivery, number of days you want to backfill and mode. The latter in particular, specifies if you want to be responsible for pushing data youself, or allow the SDK to do that on your behalf. By default the SDK is set to automatic.
Be aware that you need to setup VitalClient
in order for data to be pushed. The VitalHealthKitClient
is suspended until both VitalClient.configure
and VitalClient.setUserId
are called. This means that it’s safe to configure VitalHealthKitClient
before the VitalClient
.
After this initial setup, you need to ask permission to acccess the user’s data:
let resources = [.profile, .body]
let outcome = await VitalHealthKitClient.shared.ask(readPermissions: resources, writePermissions: [])
After calling ask(readPermissions:writePermissions:)
, check the outcome
to see if the user granted permission. On success, you can start syncing data:
import VitalHealthKit
let resources = [.profile, .body]
VitalHealthKitClient.shared.syncData(for: resources)
/// You can also sync all resources
VitalHealthKitClient.shared.syncData()
Everything that holds true for HealthKit, holds true for VitalHealthKit
. For example, if someone declines to give permission to a particular type, the SDK won’t be aware. For more information please read HealthKit’s Protecting User Privacy.
Although syncData()
and syncData(for:)
make your life easier, they can sometimes feel like a blackbox. To help you understand what’s happening under the hood, we expose an status: AnyPublisher<Status, Never>
. A Status
looks like this:
public enum Status {
case failedSyncing(VitalResource, Error?)
case successSyncing(VitalResource, ProcessedResourceData)
case nothingToSync(VitalResource)
case syncing(VitalResource)
case syncingCompleted
}
You can observe and convey this information like this:
cancellable = VitalHealthKitClient.shared.status.sink { value in
print(value)
}
2. Writing Data
You can also write data for a limited set of resources. Just like for reading, you need to first update your Info.plist
file:
<key>NSHealthUpdateUsageDescription</key>
<string>your description</string>
You then ask for permission for writing:
let readResources: [VitalResource] = [.water]
let writeResources: [WritableVitalResource] = [.water]
let outcome = await VitalHealthKitClient.shared.ask(readPermissions: readResources, writePermissions: writeResources)
Finally you are able to write data:
VitalHealthKitClient.shared.write(input: .water(milliliters: 1000), startDate: startDate, endDate: endDate)
Each type will hint you about its units. For example, water is in milliliters.
3. Background Delivery
Vital iOS SDK can continuously observe new data written to Apple HealthKit through the Background Delivery mechanism.
Please refer to this guide on enabling Background Delivery that is applicable for all platforms (iOS, Flutter and React Native).
4. Clean Up
When logging out a user, you should call:
await VitalHealthKitClient.shared.cleanUp()
Internally this method will also call VitalClient.shared.cleanUp()
.
Calling cleanUp
does this two things:
- Makes sure no more data will be synced between the device’s Apple HealthKit and our servers.
- Prepare the SDK to be configured again (either with new API keys and / or another
userId
).
Do notice that the data already synced is not deleted, nor is the user. To delete the data you need call the deregister provider method:
await VitalClient.shared.user.deregisterProvider(provider: .appleHealthKit)
The above method will disconnect the user from Apple HealthKit at the server level.
If you want to both reset the SDK (e.g. logging out a user) and remove the connection to Apple HealthKit, you can do so by calling these methods in order:
await VitalHealthKitClient.shared.cleanUp()
await VitalClient.shared.user.deregisterProvider(provider: .appleHealthKit)
5. Miscellaneous Information
i) VitalHealthKit configuration
The VitalHealthKitClient
can be configured with the following options:
mode
: Whether or not to automatically sync data. If inautomatic
, data is synced on your behalf. Otherwise, you are responsible for doing so. By default, this isautomatic
.backgroundDeliveryEnabled
: Whether or not to enable background delivery. You need to enable this on the project settings besides passingtrue
, otherwise the app will crash. Iftrue
as soon as configure is called, a sync is kicked off by default. This means you don’t need bothautoSyncEnabled
andbackgroundDeliveryEnabled
set totrue
. By default, this isfalse
.logsEnabled
: Whether or not to enable logs. Useful when debugging. By default, this isfalse
.numberOfDaysToBackFill
: Number of days to fetch data for. The first you configure the SDK, we will get X days of amount of data for the permissions you requested. By default, this is90
.
ii) sync
versus background delivery
If you are using background delivery, you don’t need to call VitalHealthKit.shared.sync
. When you call VitalHealthKit.configure
with backgroundDeliveryEnabled: true
, the SDK will automatically sync data. Afterwards, it will periodically sync data when it’s available. You should call VitalHealthKit.shared.sync
if you are confident that there’s new data available (e.g. your app is writting to HealthKit) and you want to sync immediately.
iii) App with existing HealthKit permissions
As of version 0.4.6
we are syncing the following types:
/// Activity
HKSampleType.quantityType(forIdentifier: .stepCount)!
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!
HKSampleType.quantityType(forIdentifier: .flightsClimbed)!
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!
HKSampleType.quantityType(forIdentifier: .vo2Max)!
/// Body
HKSampleType.quantityType(forIdentifier: .bodyFatPercentage)!
HKSampleType.quantityType(forIdentifier: .bodyMass)!
/// Profile
HKCharacteristicType.characteristicType(forIdentifier: .biologicalSex)!
HKCharacteristicType.characteristicType(forIdentifier: .dateOfBirth)!
HKQuantityType.quantityType(forIdentifier: .height)!
/// Sleep
HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
HKSampleType.quantityType(forIdentifier: .heartRate)!
HKSampleType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!
HKSampleType.quantityType(forIdentifier: .oxygenSaturation)!
HKSampleType.quantityType(forIdentifier: .restingHeartRate)!
HKSampleType.quantityType(forIdentifier: .respiratoryRate)!
/// Workout
HKSampleType.workoutType(),
HKSampleType.quantityType(forIdentifier: .respiratoryRate)!
HKSampleType.quantityType(forIdentifier: .heartRate)!
/// Vitals(Heartrate)
HKSampleType.quantityType(forIdentifier: .heartRate)!
/// Vitals(glucose)
HKSampleType.quantityType(forIdentifier: .bloodGlucose)!
/// Vitals(blood pressure)
HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!
HKSampleType.quantityType(forIdentifier: .bloodPressureDiastolic)!
If your app already asked for one, or more, of these permissions, we will try to sync it automatically. You don’t have to do anything. Keep in mind the following caveats:
- For example, if the user gave permission for steps, we will try sync steps as part of an Activity. This means that this data is available via
/summary/activity/
endpoint. - Types part of
Vitals
(e.g.heartrate
,bloodGlucose
), are treated as timeseries data, so they are available via the timeseries API:/timeseries/
.
iv) Information flow
For most users, the SDK is configured with background delivery to true
. Data is synced on your behalf and you don’t need to think about it. Eventually, to display the information back to an end-user, you make an HTTP request to a server. The data flow usually looks like this:
Apple HealthKit -> Vital SDK -> Vital Server -> Your Server <- User's app
You can bypass the above flow, by using VitalHealthKit.read(resource:startDate:endDate:)
. This is a static method that doesn’t require the SDK to be configured. This is a great option if you are only interested in Apple HealthKit data. You can still keep background delivery enabled, but read data from Apple HealthKit directly. The previous flow will then look like this:
/// Background, handled automatically by the SDK
Apple HealthKit -> Vital SDK <-> Vital Server -> Your Server
/// In the app
Apple HealthKit -> Vital SDK <-> User's app
If your user is sharing Apple HealthKit data and some other provider, we highly recommend reading the data from the server. Finally even if it’s only Apple HealthKit, we do extra calculations on the server side (e.g. sleep effiency). If you use VitalHealthKit.read(resource:startDate:endDate:)
you will miss these values.
v) Sleep Data
For Apple HealthKit we restrict the apps (or sources) from which we read sleep data. We do this because many third party apps will write their own version of sleep data to the device. For example, if you sleep with your Apple Watch and you have the Pillow app installed on your phone, there will be two instances of a sleep stored in Apple HealthKit. For the time being, the only apps we are accepting data from can be found here.