# Authentication Source: https://docs.tryvital.io/api-details/authentication ## Server to Server Server to server traffic uses API Key authentication. Vital API Keys can be created through the [Vital Team Dashboard](https://app.tryvital.io). Requests from your backend services should present the API Key in the `X-Vital-API-Key` header. For example: ```bash cURL curl --request GET --url '{{BASE_URL}}/v2/providers' --header 'X-Vital-API-Key: ' ``` ### API Key format | Prefix | Environment | Region | | ----------- | ----------- | ------ | | `sk_us_...` | Sandbox | US | | `sk_eu_...` | Sandbox | Europe | | `pk_us_...` | Production | US | | `pk_eu_...` | Production | Europe | ## Vital Mobile SDKs [Vital Mobile SDKs](/wearables/sdks/) supports two authentication methods: 1. [Vital Sign-In Token](/wearables/sdks/authentication#vital-sign-in-token) — Recommended for production usage 2. [Vital API Keys](/wearables/sdks/authentication#vital-api-keys) — Recommended only for evaluation in Sandbox Refer to the [Mobile SDK Authentication](/wearables/sdks/authentication) documentation for more information. # Data Flow Source: https://docs.tryvital.io/api-details/data_flow When you *connect* your user to Vital several processes are started - which very much depend on which *provider* your user connects to. Firstly, Vital will try to fetch all the *historical* (data that exists on a user's device prior to connecting to Vital) data for that user. This process can take several minutes so we send a Webhook event to you when that data is collected. For all subsequent data, that being new data from a user's device, the data flow will follow the *daily* or *webhook* process. For some provider's, data is pushed to Vital via a webhook. Upon receipt of the webhook, Vital will process the data and send a Webhook event to you. This event will signify that the data is ready to be retrieved from our database. For other providers, Vital will pull data from the provider's API on a schedule, multiple times a day. If new data is available, Vital will process it and send a Webhook event to you. To get more details of which provider's see the [Provider's Guide](/wearables/providers/introduction). For some user's they want near real-time data updates. We provider a [`refresh`]() API endpoint that will allow you to request a refresh of the data for a user at any point in time. Vital will only refresh data for users that have connected to Vital via a provider. If you have not connected to Vital via a provider, you will not be able to use the refresh API. **The refresh endpoint is a paid service.** # Rate Limiting Source: https://docs.tryvital.io/api-details/rate_limiting Junction does not impose a per-customer rate limit on server-to-server API calls at this time. However, you may occasionally encounter **429 Too Many Requests** or **503 Service Unavailable** from our infrastructure. You should be prepared to retry your API requests with a reasonable exponential backoff strategy. ## Targeted rate limits You may encounter **429 Too Many Requests** in the these specific scenarios: | Endpoint | Authentication Scheme | Rate Limit | | ------------------------------ | --------------------- | ------------------- | | `POST /user/refresh/{user_id}` | Vital API Key | 8 per hour per user | Junction may adjust rate limiting based on real-world usage patterns. We will give you sufficient notices if a new enforcement would impact your existing usage patterns. # Regions Source: https://docs.tryvital.io/api-details/regions Vital stores data depending on the region specified when creating a team. There are currently two regions supported within Vital. | Regions | | | ------- | -------------------------------------------- | | US | Data stored in US-Central (Subject to HIPAA) | | EU | Data stored in the EU (Subject to GPDR) | # Create Code Source: https://docs.tryvital.io/api-reference/link/create-code POST /v2/link/code/create Generate a token to invite a user of Vital mobile app to your team Used for Vital iOS Apps to share Apple HealthKit with you. Please refer [here](/wearables/vital-app/introduction) for more information. ```bash cURL curl --request POST \ --url {{BASE_URL}}/v2/link/code/create\?user_id\=1875c190-0cd6-46c1-8670-5b56a7794b78 \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --header 'x-vital-api-key: ' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; import { LinkCodeCreateRequest } from '@tryvital/vital-node/api'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const request: LinkCodeCreateRequest = { userId: '', } const data = await client.link.codeCreate(request); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.link.code_create(user_id='') ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.link.requests.LinkCodeCreateRequest; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); LinkCodeCreateRequest request = LinkCodeCreateRequest .builder() .userId("") .build(); var data = vital.link().codeCreate(request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) request := &vital.LinkCodeCreateRequest{ UserId: "", } response, err := client.Link.CodeCreate(context.TODO(), request) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` # Generate a link token Source: https://docs.tryvital.io/api-reference/link/generate-link-token POST /v2/link/token Endpoint to generate a user link token, to be used throughout the vital link process. The vital link token is a one time use token, that expires after 10 minutes. If you would like vital-link widget to launch with a specific provider, pass in a provider in the body. If you would like to redirect to a custom url after successful or error connection, pass in your own custom redirect_url parameter. ```bash cURL curl --request POST \ --url {{BASE_URL}}/v2/link/token \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --header 'x-vital-api-key: ' \ --data ' { "user_id": "1875c190-0cd6-46c1-8670-5b56a7794b78" } ' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; import { LinkTokenExchange } from '@tryvital/vital-node/api'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const request: LinkTokenExchange = { userId: "", provider: "oura", } const data = await client.link.token(request) ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment from vital.types.providers import Providers client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.link.token(user_id="", provider=Providers.OURA) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.link.requests.LinkTokenExchange; import com.vital.api.types.Providers; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); LinkTokenExchange request = LinkTokenExchange .builder() .userId("") .provider(Providers.OURA) .build(); var data = vital.link().token(request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) provider := vital.ProvidersOura request := &vital.LinkTokenExchange{ UserId: "", Provider: &provider, } response, err := client.Link.Token(context.TODO(), request) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` # Link OAuth provider Source: https://docs.tryvital.io/api-reference/link/link-oauth-provider GET /v2/link/provider/oauth/{oauth_provider} This endpoint generates an OAuth link for oauth provider ```bash cURL curl --request GET \ --url {{BASE_URL}}/v2/link/provider/oauth/{oauth_provider} \ --header 'Accept: application/json' --header 'Content-Type: application/json' \ --header 'x-vital-link-token: ' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; import { LinkTokenExchange, LinkGenerateOauthLinkRequest, OAuthProviders } from '@tryvital/vital-node/api'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const request: LinkTokenExchange = { userId: "", } const tokenResponse = await client.link.token(request) const linkRequest: LinkGenerateOauthLinkRequest = { vitalLinkToken: tokenResponse.linkToken, } const auth = await client.link.generateOauthLink( OAuthProviders., linkRequest ) ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment from vital.types.o_auth_providers import OAuthProviders client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) token_response = client.link.token(user_id="") oauth = client.link.generate_oauth_link( oauth_provider=OAuthProviders., vital_link_token=token_response.link_token, ) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.link.requests.LinkGenerateOauthLinkRequest; import com.vital.api.resources.link.requests.LinkTokenExchange; import com.vital.api.types.OAuthProviders; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); LinkTokenExchange request = LinkTokenExchange .builder() .userId("") .build(); var tokenResponse = vital.link().token(request); LinkGenerateOauthLinkRequest link_request = LinkGenerateOauthLinkRequest .builder() .vitalLinkToken(tokenResponse.getLinkToken()) .build(); var oauth = vital.link().generateOauthLink(OAuthProviders.OURA, link_request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) provider := vital.ProvidersOura request := &vital.LinkTokenExchange{ UserId: "", Provider: &provider, } response, err := client.Link.Token(context.TODO(), request) if err != nil { return err } generateLinkRequest := &vital.LinkGenerateOauthLinkRequest{ VitalLinkToken: response.LinkToken, } oauth, err := client.Link.GenerateOauthLink(context.TODO(), vital.OAuthProvidersOura, generateLinkRequest) if err != nil { return err } fmt.Printf("Received data %s\n", oauth) ``` # List of Providers Source: https://docs.tryvital.io/api-reference/providers GET /v2/providers Get Provider list ```bash cURL curl --request GET \ --url https://api.tryvital.io/v2/providers/ \ --header 'Accept: application/json' --header 'x-vital-api-key: ' \ ' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.providers.getAll(); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.providers.get_all() ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var data = vital.providers().getAll(); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.Providers.GetAll(context.TODO()) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` # Create Insurance Source: https://docs.tryvital.io/api-reference/user/create-insurance POST /v2/user/{user_id}/insurance ```bash cURL curl --request POST \ --url {{BASE_URL}}/v2/user/{user_id}/insurance \ --header 'Accept: application/json' \ --header 'x-vital-api-key: ' \ --header 'Content-Type: application/json' \ --data ' { "payor_code": "UNITE", "member_id": "test", "group_id": "123", "relationship": "Self", "insured": { "first_name": "John", "last_name": "Doe", "email": "john@email.com", "phone_number":"+1123123123", "gender": "Male", "dob": "1999-01-01", "address": { "first_line": "Some Street", "second_line": null, "zip_code": "85004", "state": "AZ", "city": "Phoenix", } } } ' ``` # Create Sign-In Token Source: https://docs.tryvital.io/api-reference/user/create-sign-in-token POST /v2/user/{user_id}/sign_in_token Create a [Vital Sign-In Token](/wearables/sdks/authentication#vital-sign-in-token) for your mobile apps to sign in with [Vital Mobile SDKs](/wearables/sdks/authentication). Avoid requesting Vital Sign-In Token every time your mobile app relaunches. Your mobile app only needs to sign-in once with the Vital Mobile SDK. A signed-in session is persistent on device. ```cURL curl --request POST \ --url {{BASE_URL}}/v2/user/{{USER_ID}}/sign_in_token \ --header 'Accept: application/json' \ --header 'x-vital-api-key: ' \ --header 'Content-Type: application/json' \ ``` ```python Python import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.user.get_user_sign_in_token( '' ); ``` ```javascript Node from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.user.getUserSignInToken( user_id="" ); ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var data = vital.user().getUserSignInToken(""); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.User.GetUserSignInToken(context.TODO(), "") if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` # Create User Source: https://docs.tryvital.io/api-reference/user/create-user POST /v2/user POST Create a Vital user given a client_user_id and returns the user_id. When the supplied `client_user_id` conflicts with an existing user, the 400 Bad Request error response includes the Vital User ID (`user_id`) and the creation date (`created_on`) of the conflicting user. ```bash cURL curl --request POST \ --url {{BASE_URL}}/v2/user/ \ --header 'Accept: application/json' \ --header 'x-vital-api-key: ' \ --header 'Content-Type: application/json' \ --data ' { "client_user_id": "your_unique_id" } ' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.user.create( '' ); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.user.create( client_user_id="" ) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var data = vital.user().create(""); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.User.Create(context.TODO(), "") if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` # Delete User Source: https://docs.tryvital.io/api-reference/user/delete-user DELETE /v2/user/{user_id} Deleting a user would also [deregister their all provider connections](/api-reference/user/deregister-a-provider) immediately. Vital erases the user data after a 7-day grace period. If you wish to undo a user deletion for any reason, you can undo the deletion through the [Undo User Deletion](/api-reference/user/undo-delete-user) API. Note that undoing would not restore any provider connections. ```bash cURL curl --request DELETE \ --url {{BASE_URL}}/v2/user/{user_id} \ --header 'x-vital-api-key: ' \ --header 'Accept: application/json' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.user.delete( '' ); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.user.delete( user_id="" ) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var data = vital.user().delete(""); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.User.Delete(context.TODO(), "") if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` # Deregister Connection Source: https://docs.tryvital.io/api-reference/user/deregister-a-provider DELETE /v2/user/{user_id}/{provider} Deregistration has well-defined behaviour only on [cloud-based providers](/wearables/providers/introduction#cloud-based-providers). While you can call this endpoint against these [SDK-based providers](/wearables/providers/introduction#sdk-based-providers), Vital Mobile SDK would automatically "reconnect" on the next data sync attempt. We do not have a "disconnect" mechanism for SDK-based providers at this time. You can pause [Health SDK data sync](/wearables/sdks/vital-health#pausing-data-synchronization) client-side, or [sign-out the user](/wearables/sdks/vital-core#reset-the-sdk-sign-out) from the SDK. ```bash cURL curl --request DELETE \ --url {{BASE_URL}}/v2/user/{user_id}/{provider} \ --header 'Accept: application/json' \ --header 'x-vital-api-key: ' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.user.deregisterProvider( '', '' ); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.user.deregister_provider( user_id="", provider="" ); ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var data = vital.user().deregisterProvider( "", "" ); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.User.DeregisterProvider( context.TODO(), "", "", ) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` # Get User Demographcis Source: https://docs.tryvital.io/api-reference/user/get-info-latest GET /v2/user/{user_id}/info/latest ```bash cURL curl --request GET \ --url {{BASE_URL}}/v2/user/{user_id}/info/latest \ --header 'Accept: application/json' \ --header 'x-vital-api-key: ' \ --header 'Content-Type: application/json' \ ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.user.get_latest_user_info( '' ); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.user.get_latest_user_info( user_id="" ) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var data = vital.user().getLatestUserInfo(""); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.User.GetLatestUserInfo(context.TODO(), "") if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` ```json Response { "first_name": "John", "last_name": "Doe", "email": "john@email.com", "phone_number":"+1123123123", "gender": "Male", "dob": "1999-01-01", "address": { "first_line": "Some Street", "second_line": null, "zip_code": "85004", "state": "AZ", "city": "Phoenix", } } ``` # Get Latest Insurance Source: https://docs.tryvital.io/api-reference/user/get-latest-insurance GET /v2/user/{user_id}/insurance/latest ```bash cURL curl --request GET \ --url {{BASE_URL}}/v2/user/{user_id}/insurance/latest \ --header 'Accept: application/json' \ --header 'x-vital-api-key: ' \ --header 'Content-Type: application/json' \ ``` ```json Response { "payor_code": "UNITE", "member_id": "test", "group_id": "123", "relationship": "Self", "insured": { "first_name": "John", "last_name": "Doe", "email": "john@email.com", "phone_number":"+1123123123", "gender": "Male", "dob": "1999-01-01", "address": { "first_line": "Some Street", "second_line": null, "zip_code": "85004", "state": "AZ", "city": "Phoenix", } } } ``` # Get User Source: https://docs.tryvital.io/api-reference/user/get-user GET /v2/user/{user_id} GET User details given the user_id. ```bash cURL curl --request GET \ --url {{BASE_URL}}/v2/user/{user_id} \ --header 'x-vital-api-key: ' \ --header 'Accept: application/json' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.user.get( '' ); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.user.get( user_id="" ) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var data = vital.user().get(""); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.User.Get(context.TODO(), "") if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` # Get Teams Users Source: https://docs.tryvital.io/api-reference/user/get-users GET /v2/user GET All users for team. ```bash cURL curl --request GET \ --url {{BASE_URL}}/v2/user/ \ --header 'Accept: application/json' \ --header 'x-vital-api-key: ' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.user.getAll(); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.user.get_all() ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var data = vital.user().getAll(); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.User.GetAll(context.TODO(), nil) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` # Get User Connections Source: https://docs.tryvital.io/api-reference/user/get-users-connected-providers GET /v2/user/providers/{user_id} GET Users connected providers ```bash cURL curl --request GET \ --url {{BASE_URL}}/v2/user/providers/{user_id} \ --header 'Accept: application/json' \ --header 'x-vital-api-key: API_KEY' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.user.getConnectedProviders(""); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.user.get_connected_providers("") ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var data = vital.user().getConnectedProviders(""); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.User.GetConnectedProviders(context.TODO(), "") if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` ```json Response { "providers": [ { "name": "Fitbit", "slug": "fitbit", "logo": "https://example.com/fitbit.png", "status": "connected", "created_on": "2010-01-23T12:34:56+00:00", "resource_availability": { "body": { "status": "available", "scope_requirements": { "user_granted": { "required": [ "weight" ], "optional": [] }, "user_denied": { "required": [], "optional": [] } } }, "sleep": { "status": "unavailable", "scope_requirements": { "user_granted": { "required": [], "optional": [] }, "user_denied": { "required": [ "sleep" ], "optional": [ "heartrate", "oxygen_saturation", "respiratory_rate" ] } } } }, "error_details": null } ] } ``` # Patch User Source: https://docs.tryvital.io/api-reference/user/patch-user PATCH /v2/user/{user_id} ```bash cURL curl --request POST \ --url {{BASE_URL}}/v2/user/ \ --header 'Accept: application/json' \ --header 'x-vital-api-key: ' \ --header 'Content-Type: application/json' \ --data ' { "fallback_time_zone": "Europe/London", "fallback_birth_date" "1980-03-13" } ' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.user.patch( '' ); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.user.patch( user_id="" ) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var data = vital.user().patch(""); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.User.Patch(context.TODO(), "") if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` # Refresh User Data Source: https://docs.tryvital.io/api-reference/user/refresh-user-data POST /v2/user/refresh/{user_id} Trigger a manual refresh for a specific user ```bash cURL curl --request POST \ --url {{BASE_URL}}/v2/user/refresh/{user_id} \ --header 'Accept: application/json' \ --header 'x-vital-api-key: ' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.user.refresh( '' ); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.user.refresh( user_id="" ) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var data = vital.user().refresh(""); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.User.Refresh(context.TODO(), "") if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` # Get User by Client User ID Source: https://docs.tryvital.io/api-reference/user/resolve-user GET /v2/user/resolve/{client_user_id} Get User by their `client_user_id`. ```bash cURL curl --request GET \ --url {{BASE_URL}}/v2/user/resolve/{client_user_id} \ --header 'x-vital-api-key: ' \ --header 'Accept: application/json' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.user.getByClientUserId( '' ); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.user.get_by_client_user_id( client_user_id="" ) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var data = vital.user().getByClientUserId(""); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.User.GetByClientUserId(context.TODO(), "") if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` # Undo User Deletion Source: https://docs.tryvital.io/api-reference/user/undo-delete-user POST /v2/user/undo_delete You can undo any [user deletion](/api-reference/user/delete-user) that is still in their 7-day grace period. Undoing a deletion does not restore any provider connections. You cannot undo a deletion if you have already [created a new user](/api-reference/user/create-user) using the same `client_user_id`. ```bash cURL curl --request DELETE \ --url {{BASE_URL}}/v2/user/undo_delete \ --header 'x-vital-api-key: ' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data ' { "client_user_id": "8DS6YRVBCSQQ4S0", } ' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.user.undoDelete({ clientUserId: '8DS6YRVBCSQQ4S0', }); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.user.undo_delete( client_user_id="8DS6YRVBCSQQ4S0" ) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.user.requests.UserUndoDeleteRequest; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); UserUndoDeleteRequest request = UserUndoDeleteRequest.builder() .clientUserId("8DS6YRVBCSQQ4S0") .build(); var data = vital.user().undoDelete(request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) request = &vital.UserUndoDeleteRequest{ ClientUserId: "8DS6YRVBCSQQ4S0", } response, err := client.User.UndoDelete(context.TODO(), request) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` # Create User Demographics Source: https://docs.tryvital.io/api-reference/user/upsert-info PATCH /v2/user/{user_id}/info ```bash cURL curl --request PATCH \ --url {{BASE_URL}}/v2/user/{user_id}/info \ --header 'Accept: application/json' \ --header 'x-vital-api-key: ' \ --header 'Content-Type: application/json' \ --data ' { "first_name": "John", "last_name": "Doe", "email": "john@email.com", "phone_number":"+1123123123", "gender": "Male", "dob": "1999-01-01", "address": { "first_line": "Some Street", "second_line": null, "zip_code": "85004", "state": "AZ", "city": "Phoenix", } } ' ``` ```json Response { "first_name": "John", "last_name": "Doe", "email": "john@email.com", "phone_number":"+1123123123", "gender": "Male", "dob": "1999-01-01", "address": { "first_line": "Some Street", "second_line": null, "zip_code": "85004", "state": "AZ", "city": "Phoenix", } } ``` # Environments Source: https://docs.tryvital.io/home/environments When you sign up to Vital you get access to two environments, Sandbox and Production. | Environment URLs | | | ---------------- | -------------------------- | | `production` | api.tryvital.io | | `production-eu` | api.eu.tryvital.io | | `sandbox` | api.sandbox.tryvital.io | | `sandbox-eu` | api.sandbox.eu.tryvital.io |

Sandbox

  • For app integration
  • For formal integration testing
  • Up to 50 live devices
  • Stateful, data is persisted
  • Requires authentication
  • Production

  • Uses production data
  • Should not be used for testing
  • Unlimited devices
  • # Getting Support Source: https://docs.tryvital.io/home/getting-support ## Support channels When you encounter an issue which you cannot figure out, or an unexpected API error, you are welcome to get in touch with Vital support through these channels: * Email Support ([help@tryvital.io](mailto:help@tryvital.io)) * Vital Slack community support channel (#vital-support) * Your dedicated Slack support channel (for Scale plan customers). We appreciate as much context as possible when you raise an issue. This helps us understand your issue, enabling a quicker investigation turnaround. ## I have an issue with... ### Anything in general | Item | Remarks | | --------------------- | ------------------------------------------ | | Your Vital Team ID | - | | Your Vital region | US or EU | | The Vital User ID | ...if the issue occurs on a specific user. | | The Vital environment | Sandbox, Production or both | ### Vital Mobile SDK | Item | Remarks | | ------------------ | ------------------------------------ | | Vital SDK Platform | Native, React Native, Flutter | | Vital SDK Version | - | | Device OS | Android or iOS | | Device OS Version | - | | Auth Scheme | Vital Sign-In Token, or Team API Key | ### A Vital API endpoint When you encounter an unexpected response from the Vital API, please provide the `traceparent` header value from the HTTP response you received if possible. This helps us narrow down the context surrounding your issue. A `traceparent` response header looks like this: ``` traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 ``` It contains a trace ID that links back to the original API request: * If you make the API request without a `traceparent` header, Vital's infrastructure assigns a unique trace ID to it on entry. * If your API request is instrumented using OpenTelemetry, W3C Trace Context or other interoperable tools, these tools would inject a `traceparent` header automatically. Vital's infrastructure would adopt the trace ID from your API request. ### A wearables provider connection Before reporting an issue, we encourage you to try out the following steps: The [Get User Connections](https://docs.tryvital.io/api-reference/user/get-users-connected-providers) endpoint reports the resource availability of all the connections. More specifically, it tells you: 1. What resources are available for a given user connection 2. Why each individual resource is available or unavailable, in terms of provider API access scopes. If you are having issues with historical data specifically, the [Historical Pull Introspection](https://docs.tryvital.io/api-reference/introspection/historical-pulls) endpoint provides a track record of historical pulls of all your user connections. If you have troubles with data availability in general, the [User Resource Introspection](https://docs.tryvital.io/api-reference/introspection/user-resources) endpoint is a live data ingestion record of all your user connections. This is inclusive of all historical data covered by the Historical Pull Introspection endpoint. If you are not able to diagnose your issue using these tools, feel free to contact Vital support for further assistance. # Libraries Source: https://docs.tryvital.io/home/libraries We have created a few libraries to help you integrate with the Vital API. We'll continue to add support for more libraries as we add more devices. Email [support@tryvital.io](mailto:support@tryvital.io) for specific library support. ### Typed Bindings of Vital API These typed bindings track the Vital API OpenAPI Schema. | | | | ---------------------------------------------------------------- | ------------------------ | | [vital-java](https://github.com/tryVital/vital-java) | Vital API Java client | | [vital-go](https://github.com/tryVital/vital-go) | Vital API Go client | | [vital-node](https://www.npmjs.com/package/@tryvital/vital-node) | Vital API Node.js client | | [vital-python](https://pypi.org/project/vital) | Vital API Python client | ### Mobile SDK | | | | -------------------------------------------------------------------- | -------------------------------- | | [vital-ios](https://github.com/tryVital/vital-ios) | Vital iOS Client | | [vital-android](https://github.com/tryVital/vital-android) | Vital Android Client | | [vital-flutter](https://github.com/tryVital/vital-flutter) | Vital Flutter Client | | [vital-react-native](https://github.com/tryVital/vital-react-native) | Vital React Native Client | | [vital-connect](https://github.com/tryVital/vital-connect-rn) | Whitelabel App using Vital's API | ### Web SDK | | | | ---------------------------------------------------------------- | ----------------------------------- | | [vital-link](https://www.npmjs.com/package/@tryvital/vital-link) | React Library for initializing link | # Quickstart Source: https://docs.tryvital.io/home/quickstart A quick introduction to building with Vital ## 1. API keys Let's test out running Vital locally by cloning the [Quickstart app](https://github.com/tryVital/quickstart). You can get API keys by signing up in the [Dashboard](https://app.tryvital.io). Once registered, you can create a team. A team is associated with a region (either `EU` or `US`), the region dictates where data is stored. You can learn more at [regions](/api-details/regions). To create a team and get your API keys, you first need to sign up for a Vital account in the [Dashboard](https://app.tryvital.io). Once registered, you can create a team by hovering over your username at the bottom of the Dashboard sidebar. A team is associated with a region (either `EU` or `US`), the region dictates where data is stored. You can learn more at [regions](/api-details/regions). To create your API keys, go to the configuration section of the Dashboard. For each region, you'll have access to two environments Sandbox and Production. We'll start in the Sandbox environment, so create a new Sandbox API key. If you get stuck at any point in the Quickstart, help is just a click away! Join our Slack channel or send us a message to [support@tryvital.io](mailto:support@tryvital.io) | Environment | | | | ------------ | ------------------------------------------- | ----------------------- | | `sandbox` | Testing, Connect up to 10 live users | api.sandbox.tryvital.io | | `production` | Live environment to use with real customers | api.tryvital.io | ## 2. Running Quickstart locally Once you have your API keys, it's time to run the Vital Quickstart locally! The instructions below will guide you through the process of cloning the [Quickstart repository](https://github.com/tryVital/quickstart), customizing the `.env` file with your own Vital `API_KEY` and finally, building and running the app. ```bash 1. Clone quickstart and run locally # Note: If on Windows, run # git clone -c core.symlinks=true https://github.com/tryVital/quickstart # instead to ensure correct symlink behavior git clone https://github.com/tryVital/quickstart.git # Create .env, then fill # out VITAL_API_KEY, VITAL_REGION (eu, us) and VITAL_ENV in .env touch .env # Note: must use python 3 # For virtualenv users: # virtualenv venv # source venv/bin/activate poetry install # Start the backend app cd backend/python source ./start.sh ``` Open a new shell and start the frontend app. Your app will be running at `http://localhost:3000`. ```bash 2. Run quickstart frontend # Install dependencies cd quickstart/frontend npm install # Open .env.local, then fill # out NEXT_PUBLIC_VITAL_API_KEY, NEXT_PUBLIC_VITAL_ENV and NEXT_PUBLIC_VITAL_REGION # Start the frontend app npm run dev # Go to http://localhost:3000 ``` ## 3. Creating your first User When retrieving data or connecting devices, Vital will require a `user_id` as input. A `user_id` is a unique representation that we hold for a user. It allows you to fetch data for that user. To create a user, you need to pass an unique id (`client_user_id`). This represents the user in your system. Our recommendation is to store the Vital `user_id` in your db against the user row. Personally identifiable information (PII), such as an email address or phone number, should not be used as input for the `user_id` parameter. Enter a new `client_user_id` and tap Create: quickstart {" "} This can also be achieved via the API as follows. ```bash Creating a Vital user (bash) curl --request POST \ --url {{BASE_URL}}/v2/user/ \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --header 'x-vital-api-key: ' \ --data '{"client_user_id":""}' ``` ```js Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; import { UserCreateBody } from '@tryvital/vital-node/api'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const request: UserCreateBody = { clientUserId: "" } const data = await client.user.create(request) ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.user.create(client_user_id="") ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.user.requests.UserCreateBody; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); UserCreateBody request = UserCreateBody .builder() .clientUserId("") .build(); var data = vital.user().create(request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) request := &vital.UserCreateBody{ ClientUserId: "", } response, err := client.User.Create(context.TODO(), request) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` ```swift Swift let user = try await VitalClient.shared.user.create(clientUserId) ``` ## 4. Connecting a source A source, at Vital, is a medical device, wearable, or lab. It is a source of information for health data. To connect a source tap the connect button, this will launch the Vital Link Widget for that user. Once you have entered your credentials and moved to the next screen, you have connected your first source! You can now make API calls to retrieve data for that Source. ### How it works As you might have noticed, you use both a server and a client-side component to access the Vital APIs. A more detailed explanation on how linking works can be found in [link flow](/wearables/connecting-providers/link_flow) The first step is to create a new `link_token` by making a `/link/token` request and passing in the required configuration. This `link_token` is a short lived, one-time use token that authenticates your app with Vital Link, our frontend module. ```python Generating a Link Token # TEST BACKEND IMPLEMENTATION from vital.client import Vital from vital.environment import VitalEnvironment from vital.client import Vital from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware client = Vital( api_key=, environment=VitalEnvironment.SANDBOX, timeout=30 ) app = FastAPI() app.add_middleware( # type: ignore CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/token/{user_key}") def get_token(user_key: str): return client.link.token(user_id=user_key) ``` Once you have a `link_token`, you can use it to initialize `Link`. `Link` is a drop-in client-side module available for web, iOS, and Android that handles the authentication process. The Quickstart uses `Link` on the web, which is a pure JavaScript integration that you trigger via your own client-side code. ```javascript Generating a Link Token import { Button } from "@chakra-ui/react"; import { useState, useCallback } from "react"; import { useVitalLink } from "@tryvital/vital-link"; export const LinkButton: React.FC<{ userID: string | null }> = ({ userID }) => { const [isLoading, setLoading] = useState(false); const onSuccess = useCallback((metadata) => { // Device is now connected. console.log("onSuccess", metadata); }, []); const onExit = useCallback((metadata) => { // User has quit the link flow. console.log("onExit", metadata); }, []); const onError = useCallback((metadata) => { // Error encountered in connecting device. console.log("onError", metadata); }, []); const config = { onSuccess, onExit, onError, env: "sandbox", region: "us", }; const { open, ready, error } = useVitalLink(config); const handleVitalOpen = async () => { setLoading(true); const token = await getTokenFromBackend(userID); open(token.link_token); setLoading(false); }; return ( ); }; ``` This is what your users see to connect their medical device or wearables: Link specific ## 5. Making your first API request We can now explore what happens when you press the analyze button in the Quickstart to make an API call. As an example, we'll look at the Quickstart's call to `/summary/sleep`, which retrieves sleep summary data for a user. The request is simple and requires the Vital `user_id`, `start_date` and `end_date`. **Getting user sleep data** ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.sleep.get( user_id="", start_date="2021-01-01", end_date="2021-01-02" ) ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; import { SleepGetRequest } from '@tryvital/vital-node/api'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const request: SleepGetRequest = { startDate: '2021-01-01', endDate: '2021-01-02', } const data = await client.sleep.get('user_id', request) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.sleep.requests.SleepGetRequest; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); SleepGetRequest request = SleepGetRequest .builder() .startDate("2021-01-01") .endDate("2021-01-02") .build(); var data = vital.sleep().get("", request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) EndDate := "2021-01-02" request := &vital.SleepGetRequest{ StartDate: "2021-01-01", EndDate: &EndDate, } response, err := client.Sleep.Get( context.TODO(), "", request ) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` ```swift Swift let sleepData = try await VitalClient.shared.summary.sleep(userId, startDate, endDate) ``` ## 6. SDK's and Libraries We offer different SDKs so you can start building your app right away: | | | | ---------------------------------------------------------------- | ---------------------------------------------------- | | [vital-python](https://pypi.org/project/vital) | Python library for calling Vital API on your backend | | [vital-link](https://www.npmjs.com/package/@tryvital/vital-link) | React Library for initializing link | | [vital-node](https://www.npmjs.com/package/@tryvital/vital-node) | Vital Node Client | | [vital-ios](https://github.com/tryVital/vital-ios) | Vital iOS Client | | Java | Soon to be added | | Go | Soon to be added | You can also download our [API collection](https://www.postman.com/collections/35339909-b0d080b7-3870-4aa8-a68b-a675f93a0533) or by installing postman first and clicking on the below button. [![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/35339909-b0d080b7-3870-4aa8-a68b-a675f93a0533?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D35339909-b0d080b7-3870-4aa8-a68b-a675f93a0533%26entityType%3Dcollection%26workspaceId%3Ddd82502b-0c9d-4fe4-9760-73a54bc2b8bf) ## Next Steps Congratulations, you have completed the Vital Quickstart! There are a few directions you can go in now: A client-side component your users will interact with in order to link their accounts with Vital. It allows you to access their accounts via the Vital API. Native toolkits to integrate Vital into iOS, Android, and Flutter Webhooks are a way to receive data from Vital. We frequently poll to receive data from the various providers. * [Vital Link](/wearables/vital-link/introduction) * [Vital SDKs](/wearables/sdks/ios) * [Vital Webhooks](/webhooks/Introduction) # Welcome to our Docs Source: https://docs.tryvital.io/home/welcome Here you'll find guides, resources, and references to build using Junction's APIs and SDKs. We've just **rebranded from Vital to Junction**. You may see some references to Vital among the docs, please bear with us as we get this updated. The Quickstart walk-through gives you an overview of how to integrate with Junction. By the end of this guide, you'll have a working app with both backend and frontend. Here you can find all the available providers, alongside our roadmap. You can see how each provider behave (pull vs webhook) and the default max amount of days we pull from each. For custom integrations, please contact us. This gives you a quick glance about our environments and regions. It also tells you the difference between production and sandbox Junction’s lab test API allows digital health companies to carry out at-home lab testing. Companies can use the API to order a variety of tests, using various sampling methods. ## Popular Topics Learn how to integrate with Apple HealthKit through Junction Mobile SDKs, available in Native iOS, React Native and Flutter. Learn how to integrate with Android Health Connect through Junction Mobile SDKs, available in Native Android, React Native and Flutter. Junction Link enables your users to connect their account with with their wearable data providers. Learn how to integrate the Link Widget for a no-fuzz starter integration. Learn how to use the Junction Link API to build your own Link Widget. Learn about timestamp and time zone handling of wearable device data. Junction can push wearable device data as webhook events to you as soon as they are discovered. Learn about the delivery stages, event structure as well as the advanced ETL Pipeline options for high volume needs. Learn how to use Demo Connections — which emit synthetic data mimicking selected providers — as an alternative way to test your Junction integration. # Communications Source: https://docs.tryvital.io/lab/at-home-phlebotomy/communications Communication for patients is done via email or SMS, for At-Home Phlebotomy orders. Customers have the following options when setting this up: * `Default` - Email and SMS communications are enabled. * `SMS Only` - Only SMS communication is enabled. * `Disable` - All communications from Vital are disabled. Each option has a different set of content and status changes, depending on what triggered them. You can enable or disable SMS and Email communications individually through the [**Vital Dashboard**](http://app.tryvital.io/), under the Team Settings section. ## Default Communications ### SMS Messages A table of the **Order Status** and **default SMS** is provided bellow: | Order Status | Default Message | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `requisition_created` | *"Hey **patient\_first\_name**, it's time to book your at-home phlebotomy draw. You can book a slot using the following link **booking\_link**."* | | `appointment_scheduled` | *"Your appointment with the phlebotomist has been booked at **date**! You can reschedule or cancel using the following link **booking\_link**."* | | `appointment_cancelled` | *"Hey, your at-home-phlebotomy appointment at **date** for the **team\_name** has been cancelled. You can rebook using the following link **booking\_link**."* | | `draw_completed` | *"Your at-home phlebotomy draw is complete! We're delivering your sample to the lab for processing."* | | `completed` | *"The lab has finished processing your blood sample, your results should be ready soon :)"* | | `cancelled` | *"Hey, your order for the **team\_name** at-home-phlebotomy appointment has been cancelled. If this is by accident please contact support."* | SMS Texts are customizable, and can be enabled or disabled individually. ### Emails For emails, the following table describes what information each email contains for each Order Statuses: | Order Status | Email Content Description | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `requisition_created` | An email with the booking link for **Vital Booking Widget** will be sent to the patient. | | `appointment_scheduled` | An email confirming the appointment date and time will be sent to the patient, with the possibility of rescheduling or cancelling the appointment. | | `appointment_cancelled` | An email confirming the cancellation of the appointment is sent, providing the possibility of scheduling a new appointment. | Emails can be white labelled and sent from your own domain. ## Disable Communications In this case, no communications will be sent from Vital. You have the ability to produce completely customized communications using the [`Webhook events`](/lab/at-home-phlebotomy/webhooks) described previously. # Order and Appointment Lifecycle Source: https://docs.tryvital.io/lab/at-home-phlebotomy/order-appointment-lifecycle The **At-Home Phlebotomy** orders are composed of two different lifecycles, the **Order** lifecycle and the **appointment** lifecycle, detailed in the following sections. ## Order Lifecycle As discussed in [`Lab Test Lifecycle - Statuses`](/lab/workflow/lab-test-lifecycle#statuses), each lab testing modality has the following format `[HIGH-LEVEL STATUS].[TEST MODALITY].[LOW-LEVEL STATUS]` For each modality, there can be multiple **low-level status**, for **At Home Phlebotomy** the possible low-level status are: * `ordered`: Vital received the order, stored it into our system, and started processing it asynchronously. * `requisition_created`: An order requisition form was validated and created with the partner laboratory, making the order available to be carried out. * `appointment_pending`: An appointment was placed in Vital's system for the order, but doesn't have a scheduled date. * `appointment_scheduled`: An appointment was scheduled or rescheduled for the order. * `draw_completed`: The phlebotomy appointment was completed and the blood was drawn successfuly. * `appointment_cancelled`: The appointment was cancelled, by either the patient, Vital or you. * `partial_results`: The laboratory has started making partial results available. * `completed`: The laboratory processed the blood sample and final results are available. * `cancelled`: The order was cancelled by either the patient, Vital or you. The Finite State Machine that defines the possible transitions for the low-level statuses described above is illustrated in the following diagram.
    In **sandbox**, there is no async transition from the `ordered` state to the `requisition_created` state, this must be **manually triggered** via the **Vital Dashboard**. ## Appointment Lifecycle The appointment lifecycle is separate from the order lifecyle, and it corresponds to a single appointment. The possible status are defined as follows: * `pending`: An appointment was placed in the system, and is pending updates from the phlebotomy service. * `scheduled`: An appointment was scheduled or rescheduled. * `in-progress`: The phlebotomist is in their way to the patient address. * `cancelled`: The appointment was cancelled by either the patient, Vital or you. * `completed`: The phlebotomy appointment was completed and the blood was drawn successfuly The Finite State Machine that defines the possible transitions for the appointment statuses described above is illustrated in the following diagram. The events are related to a single appointment. An order can have multiple existing appointments in Vital system, although only one appointment will be considered active and returned when using the [GET Appointment endpoint](/api-reference/lab-testing-at-home-phlebotomy/get-appointment). # Overview Source: https://docs.tryvital.io/lab/at-home-phlebotomy/overview At-home phlebotomy tests are one of our offered modalities. This modality is focused on patients that don't want to go to a Laboratory, but instead want a phlebotomist to come to their home or office to carry out the phlebotomy draw. The phlebotomist is responsible for bringing the required equipment and tubes and then delivering those to our partner Laboratories. ## Ordering Flow overview To achieve this, a high-level overview of the process is defined as: * An order is placed in Vital's system through our Dashboard or API. * The order is added to a background queue and additional checks are made before a test requisition is created with the chosen Laboratory. * After the requisition is created, some form of communication is carried out with the patient so he can book an appointment with our phlebotomy partners. * At the day of the appointment, our partner phlebotomist will contact the patient directly to assert that everything is correct for carrying out the appointment. * The phlebotomist goes to the appointment, draws the patient's blood and then delivers it to the Laboratory. * The Laboratory processes the patient's blood sample and generates the required results. * Vital exposes the results via API as soon as they are available, both on PDF and structured data via API. ## Constraints * Phlebotomy appointments are not supported for patients under 18 years of age. * The same patient can't have more than one active appointment with a specific **Phlebotomy provider**. * The addresses provided when fetching the appointment availability slots must be reachable by the phlebotomist, containing street number and unit (if exists). To find out more details, see [`Order and Appointment Lifecycle`](/lab/at-home-phlebotomy/order-appointment-lifecycle), [`Communications`](/lab/at-home-phlebotomy/communications) and [`Webhooks`](/lab/at-home-phlebotomy/webhooks). # Phlebotomy Service Tiers Source: https://docs.tryvital.io/lab/at-home-phlebotomy/service-tiers Vital offers multiple Tiers of phlebotomy services, with different coverage capabilities: * **Appointment Ready:** An appointment is booked in Vital using the patient’s address and the appointment’s date time. The scheduling is completely synchronous and fully controlled by Vital's customers through our API. * **Appointment Request:** An appointment is requested through Vital system using the patient’s address. A phlebotomist will eventually assign themselves to the appointment and define the appointment’s date time with the patient. The following Providers are available for each Tier: | | Getlabs | Phlebfinders (Beta) | | :------------------ | :-----: | :-----------------: | | Appointment Ready | X | | | Appointment Request | | X | `appointment-request` appointments will start in the `pending` status, and won't have any time or date information. ## High Level flow for Appointment Scheduling
    The recommend high level flow for selecting an appointment at Vital is: * Place an At-Home Phlebotomy order with the [`POST /v3/order`](/api-reference/lab-testing/create-order) endpoint. * Wait for the `Requisition Ready` status updates through our Webhooks. * Fetch Provider data via the [`GET /v3/order/area/info`](/api-reference/lab-testing/area-info) endpoint, the response payload should look like this: ```json { "zip_code": "85004", "phlebotomy": { "is_served": true, "providers": [ { "name": "getlabs", "tier": ["appointment-ready"] }, { "name": "phlebfinders", "tier": ["appointment-request"] } ] } } ``` * Select the Provider that best fit your needs. * If you use the [`POST /v3/order/{order_id}/phlebotomy/appointment/book`](/api-reference/lab-testing-at-home-phlebotomy/appointment-booking) endpoint, an `appointment-ready` provider is chosen on your behalf. * if you use the [`POST /v3/order/{order_id}/phlebotomy/appointment/request`](/api-reference/lab-testing-at-home-phlebotomy/appointment-request) endpoint, you must select a provider that offers an `appointment-request` tier. * Wait for the `Appointment Webhooks` and `Order Webhooks` described in the [Webhooks](/lab/at-home-phlebotomy/webhooks) section. # Webhooks Source: https://docs.tryvital.io/lab/at-home-phlebotomy/webhooks The following webhook events are of interest, when placing an At-Home Phlebotomy order. Those are described in detail in the following sections. ## Order webhook events Based on the status present in [`Order and Appointment Lifecycle - Order Lifecycle`](/lab/at-home-phlebotomy/order-appointment-lifecycle#order-lifecycle), Vital will trigger two kinds of webhook events, [`labtest.order.created`](/event-catalog/labtest.order.created) and [`labtest.order.updated`](/event-catalog/labtest.order.updated). The `labtest.order.created` event is triggered when an order is created in the system, having the `ordered` status, and all subsequent status changes will trigger a `labtest.order.updated` event in the system. The `partial_results` status does not trigger a Webhook unless specifically requested from Vital. The webhook payload body will have the following information if the Order is in the `appointment_scheduled` status: ```json Phlebotomy Order Updated { "id": "84d96c03-6b1c-4226-ad8f-ef44a6bc08af", "team_id": "6353bcab-3526-4838-8c92-063fa760fb6b", "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "patient_details": { "dob": "2020-01-01", "gender": "male" }, "patient_address": { "receiver_name": "John Doe", "first_line": "123 Main St.", "second_line": "Apt. 208", "city": "San Francisco", "state": "CA", "zip": "91189", "country": "United States", "phone_number": "+1123456789" }, "details": { "type": "at_home_phlebotomy", "data": { "id": "a655f0e4-6405-4a1d-80b7-66f06c2108a7", "appointment_id": "d55210cc-3d9f-4115-8262-5013f700c7be", "created_at": "2020-01-01T00:00:00Z", "updated_at": "2020-01-01T00:00:00Z" } }, "sample_id": "123456789", "notes": "This is a note", "created_at": "2020-01-01T00:00:00Z", "updated_at": "2020-01-01T00:00:00Z", "status": "collecting_sample", "events": [ { "id": 1, "created_at": "2022-01-01T00:00:00Z", "status": "received.at_home_phlebotomy.ordered" }, { "id": 2, "created_at": "2022-01-02T00:00:00Z", "status": "received.at_home_phlebotomy.requisition_created" }, { "id": 3, "created_at": "2022-01-03T00:00:00Z", "status": "collecting_sample.at_home_phlebotomy.appointment_scheduled" } ] } ``` ## Appointment webhook events Based on the status present in [`Order and Appointment Lifecycle - Appointment Lifecycle`](/lab/at-home-phlebotomy/order-appointment-lifecycle#appointment-lifecycle), Vital will trigger the [`labtest.appointment.updated`](/event-catalog/labtest.appointment.updated) webhook event. The `labtest.appointment.updated` event is triggered for all possible appointment statuses, and is the **recommended** way of integrating to fetch all **At-Home Phlebotomy** status updates, together with the [Order Webhooks](#order-webhook-events). The webhook payload body may have the following information if the appointment is in the `scheduled` status, after a **reschedule** has happened: ```json Phlebotomy Appointment Updated { "event_type": "labtest.appointment.updated", "data": { "id": "06c2c65b-74a0-4f25-a4a9-44f796296355", "user_id": "acf79a82-0c2c-4ca0-998b-378931793905", "order_id": "1ed9c8d7-e1b4-4d61-8123-0f99de5ae99a", "address": { "first_line": "123 Main St.", "second_line": "Apt. 208", "city": "San Francisco", "state": "CA", "zip_code": "91189", "country": "United States" }, "location": { "lng": -122.4194155, "lat": 37.7749295 }, "start_at": "2022-01-01T00:00:00", "end_at": "2022-01-01T00:00:00", "iana_timezone": "America/New_York", "type": "phlebotomy", "provider": "getlabs", "status": "pending", "event_status": "scheduled", "provider_id": "123", "can_reschedule": true, "event_data": { "origin": "patient", "is_reschedule": true }, "events": [ { "created_at": "2022-01-01T00:00:00Z", "data": null, "status": "scheduled" }, { "created_at": "2022-01-01T00:00:00Z", "data": { "origin": "patient", "is_reschedule": true }, "status": "scheduled" }, ] } } ```
    The `event_data` field contains relevant information regarding the current appointment status, and may be specific for each `provider`. # Insurance Ordering Source: https://docs.tryvital.io/lab/overview/insurance Vital supports ordering with commercial insurance billing. To set this up, there are a set of steps required. This document explains the complete flow for ordering via commercial insurance. Only commercial payors are supported and public insurance payors (medicare, medicaid, etc...) are not supported. ## Setting Insurance Data To place an order for a `user` in Vital with commercial insurance, you must first provide Vital with insurance data. This can be done via the [`POST /{user_id}/insurance`](/api-reference/user/create-insurance) endpoint, with the following payload: Note that you can only set insurance data for users that already have patient information set, either via an order or via the [`PATCH /v2/user/{user_id}/info`](/api-reference/user/upsert-info). It is expected that insurance data will become outdated or change with time. It is the customer's responsability to update this data with Vital in order to minimise the risk of a denied claim. ```json "insurance": { "payor_code": "string", "member_id": "string", "group_id": "string", // optional "relationship": "Self, Spouse, Other", "insured": { "first_name": "string", "last_name": "string", "dob": "YYYY-MM-DD", "gender": "string", "address": { "first_line": "string", "second_line": "string", // optional "zip_code": "string", "state": "string", "city": "string", }, "phone_number": "string", "email": "string" }, "guarantor": { "first_name": "string", "last_name": "string", "dob": "YYYY-MM-DD", "gender": "string", "address": { "first_line": "string", "second_line": "string", // optional "zip_code": "string", "state": "string", "city": "string", }, "phone_number": "string", "email": "string", } // optional if relationship is Self } ``` ### Insured, Guarantor and Relationship First, let's understand the `relationship`, `insured` and `guarantor` fields. `insured` refers to the insured person, and must always be provided. The `relationship` field, is the relationship between the patient and the holder of the insurance. For example, if the insured person and the insurance holder are the same, then the relationship is `Self`. The `guarantor` is an optional field, that must be provided when the relationship is **NOT** `Self`. The guarantor is the financially responsible party. If provided when the relationship is `Self`, we will store it, but it may not be propagated to all lab partners, as not all partners accept a guarantor in this situation. ### Payor Code The `payor code` is a lab specific identifier for an insurance company. Vital has abstracted this away into our own Vital payor codes, allowing customers to supply one lab-agnostic payor code for all labs. To obtain this code, use the [Search Payor Code](/api-reference/lab-testing-insurance/search-payor-get) endpoint, using the insurance company name, and select the `code` from your insurance company. It is also possible to use this endpoint to search using external provider's payor codes (e.g Change HealthCare or Availity). ```json [ { "code": "AARPA", "name": "AARP", "aliases": [ "AARP", "AARP" ], "org_address": { "first_line": "PO BOX 740819", "second_line": null, "country": "US", "zip": "30374", "city": "ATLANTA", "state": "GA" } }, ] ``` ## Ordering Once the above steps are complete, you can place an order as usual, with two differences. In order to trigger the commercial insurance flow, you must supply the following extra fields in the [POST /order](/api-reference/lab-testing/create-order) payload: ```json { "icd_codes": ["ICD.10"], "billing_type": "commercial_insurance" } ``` ### Billing type By default, Vital orders are Client Bill. In order to trigger the insurance flow, you must supply the `billing_type` field, with `commercial_insurance`. ### Diagnosis Codes Insurance orders require diagnosis codes to be supplied. You can search for diagnosis codes in our [Search ICD Code](/api-reference/lab-testing-insurance/search-diagnosis) endpoint. ## Insurance Availability Not all labs and not all states are cleared for insurance ordering. You can verify if a zip code is served for insurance via the [`GET /v3/order/area/info`](/api-reference/lab-testing/area-info) endpoint. If a particular lab supports insurance, then you should see `commercial_insurance` in the supported bill types. ```json { "zip_code": "85007", "central_labs": { "labcorp": { "patient_service_centers": { "within_radius": 15, "radius": "25" }, "supported_bill_types": [ "commercial_insurance" ] }, ... } } ``` ## Error Cases 1. Order with `commercial_insurance` billing for a lab that does not support it. 2. Order with `commercial_insurance` billing for a state that does not support it. 3. Order with `commercial_insurance` billing and provide no ICD codes or invalid ICD codes. 4. Order with `commercial_insurance` billing and the user has no insurance data. # Introduction Source: https://docs.tryvital.io/lab/overview/introduction Vital's lab test API allows digital health companies to carry out at-home lab testing. Companies can use the API to order a variety of tests, using multiple sampling methods. We partner with CLIA and CAP certified labs across the United States that cover a range of different diagnostic tests. For example lipids panels, metabolic profiles, hormone tests and more. We collect these test samples via at-home kits, or phlebotomy. ### Features Our lab test API gives you an end-to-end experience, from ordering a test all the way to receiving results, all with one API call! What we offer: * Multiple [test modalities](/lab/overview/testing-modalities) through one unified API. * 50 State Physician network - you can use your own physician if you have one, or we will automatically assign one from our network. * Updates throughout the test lifecycle - we automatically send you webhook updates and SMS updates to your patients so that you both never miss an update. * Test results: we will update you once the test results are ready. In case of abnormal results, we also provide follow-ups through our Physician network. * 1:1 support: after production launch, you will have direct access to Slack and our engineers. ### Start integrating You can sign-up today to our sandbox environment. The [quickstart](/lab/overview/quickstart-api) guide will help you getting from zero to ordering tests in less than 30 minutes! Once the integration is ready, switching to production will just be a matter of switching API keys. ### Production launch As a first step, we need to define what type of testing you want to offer to your users. We will have an introduction call to go over the test, projected volumes, customizations for kits and other questions you might have. Once we have all this information, we will begin setting up the test kits for distribution. Once the integration is complete, you will have access to the dashboard to start ordering tests via our API. In [this page](https://tryvital.io/labs), you can check out some examples of the test panels we offer, and book an introductory call with us. ### Support We will assist you throughout the whole integration with Vital. After launch, you will have a dedicated account manager with access to Slack, and 1:1 support with our engineers. ### Coverage Our lab test API is available throughout the US. This is our current coverage by test modality: * [At-home test kits](/lab/overview/testing-modalities#at-home-test-kits): 49 states (all excluding NY). * [Walk-in tests](/lab/overview/testing-modalities#walk-in-tests): 49 states (all excluding NY). * [At-home phlebotomy](/lab/overview/testing-modalities#at-home-phlebotomy): 35 states at the moment. Full coverage coming soon. To find out more about our coverage, you can use the [area info endpoint](/api-reference/lab-testing/area-info) - this endpoint takes a zip code and tells whether that area is covered by our service or not. Or you can contact us at [support@tryvital.io](mailto:support@tryvital.io) for more info. # Order and Follow-up Physician Source: https://docs.tryvital.io/lab/overview/physicians For an order to be valid/complete, both the request and the results need to be validated by a physician. As such, Vital provides three flows: ## 1. Order and Results through Vital Physician Network The validation of both the order and the results is done by Vital’s physician network. For each order, Vital's physician network validates the requisition and, when the results are available, they are uploaded to Vital’s physician network to be evaluated for abnormal/critical results. On the physician's judgement, the patient may receive a phone call regarding their results. ## 2. Order and Results with Customer Physician Network In this flow, the Customer must specify a `physician` when making an order request. Both the order and the results are the responsability of the Customer's chosen physician. ## 3. Order with Vital Physician Network and Results with Customer Physician Network In this flow, the Customer does not specify a `physician` when making an order request - they use Vital’s physician network. However, the results aren’t uploaded to Vital’s physician network and it's the Customer's physicians responsability to validate the results and do the follow ups. When there are critical results, Vital’s physician network will always be notified. # Quickstart Source: https://docs.tryvital.io/lab/overview/quickstart We have carefully designed Vital’s API to ensure a seamless and hassle-free experience for developers. You can get up and running with just a few lines of code. In this guide, we will walk you through the essential steps to order your first lab test. ### What you need * If you haven't done so yet, you can get API keys by signing up for a Vital account in the [Dashboard](https://app.tryvital.io). Detailed instructions are available [here](/home/quickstart#1-api-keys). * One of our [client libraries](/home/libraries). The lab testing API is available for the [python](https://pypi.org/project/vital/) and [node](https://www.npmjs.com/package/@tryvital/vital-node) SDKs. If none of those fit your use-case, you can always directly call the API. Email [support@tryvital.io](mailto:support@tryvital.io) for specific library support. In the following, replace `{{BASE_URL}}` with your team's [environment URL](/home/environments). ### (1) Creating a user In the context of our API, a user refers to the patient who will be undergoing the lab test. All you need to provide is a string identifying the user on your side (`client_user_id`). The response to this call returns the userId on our side, which you'll be able to use for future requests. You can find more details in our [API quickstart guide](/home/quickstart#3-creating-your-first-user). ```bash Creating a Vital user (bash) curl --request POST \ --url {{BASE_URL}}/v2/user/ \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --header 'x-vital-api-key: ' \ --data '{"client_user_id":""}' ``` ```js Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; import { UserCreateBody } from '@tryvital/vital-node/api'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const request: UserCreateBody = { clientUserId: "" } const data = await client.user.create(request) ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.user.create(client_user_id="") ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.user.requests.UserCreateBody; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); UserCreateBody request = UserCreateBody .builder() .clientUserId("") .build(); var data = vital.user().create(request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) request := &vital.UserCreateBody{ ClientUserId: "", } response, err := client.User.Create(context.TODO(), request) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` ### (2) Listing Available Tests To retrieve the set of lab tests you have access to, use the `/v3/lab_tests` API endpoint. In Sandbox, you will already have access to a default set of tests. Once you're ready to head to production, we can [setup a call](/lab/overview/introduction#production-launch) and get you ready for launch! ```bash Listing available tests (bash) curl --request GET \ --url {{BASE_URL}}/v3/lab_tests/ \ --header 'Accept: application/json' \ --header 'x-vital-api-key: ' \ --header 'Content-Type: application/json' ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.lab_tests.get(); ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.labTests.get(); ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var data = vital.labTests().get(); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.LabTests.Get(context.TODO()) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` This will return a list of all the available tests to you. They include a description, turnaround time and price: ```json Response { "lab_tests": [ { "id": "e2eaa385-a311-4f17-b33f-2165e3d24dd9" "name": "Lipids Panel", "description": "Cholesterol tests", "sample_type": "dried blood spot", "method": "testkit", "price": 10.0, "is_active": True, "lab": { "slug": "USSL", "name": "US Specialty Lab", "first_line_address": "123 Main St", "city": "New York", "zipcode": "10001", }, "markers": [ { "name": "Thyroid Stimulating Hormone", "slug": "tsh", "description": "", "min_value": 100, "max_value": 200, "unit": "fg/L" } ], } ] } ``` You can then use the test `id` when creating an order. ### (3) Placing an order Ordering a test for your user is as simple as making an API call: ```bash Ordering a test (bash) curl --request POST \ --url {{BASE_URL}}/v3/order/ \ --header 'Accept: application/json' \ --header 'x-vital-api-key: ' \ --header 'Content-Type: application/json' \ --data ' { "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "patient_details": { "dob": "2022-07-06T22:20:26.796Z", "gender": "male | female", "email": "test@test.com" }, "patient_address": { "receiver_name": "John Doe", "street": "Hazel Road", "street_number": "102", "city": "San Francisco", "state": "CA", "zip": "91789", "country": "U.S.", "phone_number": "+14158180852" }, "lab_test_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "physician": { "first_name": "John", "last_name": "Doe", "email": "john@doe.com", "npi": "123456789", "licensed_states": ["CA", "NY"], "created_at": "2022-07-06T22:20:26.796Z", "updated_at": "2022-07-06T22:20:26.796Z" } } ' ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment from vital.types.gender import Gender from vital.types.patient_address_compatible import PatientAddressCompatible from vital.types.patient_details import PatientDetails from datetime import datetime client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.lab_tests.create_order( user_id="63661a2b-2bb3-4125-bb1a-b590f64f057f", lab_test_id="5b41f610-ebc5-4803-8f0c-a61c3bdc7faf", patient_details=PatientDetails( first_name="John", last_name="Doe", dob=datetime.fromisoformat("2020-01-01"), gender=Gender.MALE, phone_number="+1123456789", email="email@email.com" ), patient_address=PatientAddressCompatible( receiver_name="john Doe", first_line="123 Main St.", second_line="Apt. 208", city="San Francisco", state="CA", zip="91189", country="US", phone_number="+1123456789" ), ) ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; import { CreateOrderRequestCompatible} from '@tryvital/vital-node/api/resources/labTests'; import { Gender } from '@tryvital/vital-node/api'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const request: CreateOrderRequestCompatible = { userId: "63661a2b-2bb3-4125-bb1a-b590f64f057f", labTestId: "5b41f610-ebc5-4803-8f0c-a61c3bdc7faf", patientDetails: { firstName: "John", lastName: "Doe", dob: new Date("2020-01-01"), gender: Gender.Male, phoneNumber: "+1123456789", email: "email@email.com" }, patientAddress: { receiverName: "john Doe", firstLine: "123 Main St.", secondLine: "Apt. 208", city: "San Francisco", state: "CA", zip: "91189", country: "US", phoneNumber: "+1123456789" }, } const data = await client.labTests.createOrder(request) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.labtests.requests.LabTestsGetAreaInfoRequest; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); PatientDetails details = PatientDetails.builder() .firstName("John") .lastName("Doe") .dob(OffsetDateTime.parse("2020-01-01")) .gender(Gender.MALE) .phoneNumber("+1123456789") .email("email@email.com") .build(); PatientAddressCompatible address = PatientAddressCompatible .builder() .firstLine("123 Main St.") .city("San Francisco") .state("CA") .zip("91189") .country("US") .phoneNumber("+1123456789") .build(); CreateOrderRequestCompatible request = CreateOrderRequestCompatible .builder() .userId("63661a2b-2bb3-4125-bb1a-b590f64f057f") .labTestId("5b41f610-ebc5-4803-8f0c-a61c3bdc7faf") .patientDetails(details) .patientAddress(address) .build(); var data = vital.labTests().createOrder(request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) details := &vital.PatientDetails{ FirstName: "John", LastName: "Doe", Dob: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), Gender: vital.GenderMale, PhoneNumber: "+1123456789", Email: "email@email.com", } address := &vital.PatientAddressCompatible{ ReceiverName: vital.String("john Doe"), FirstLine: "123 Main St.", SecondLine: vital.String("Apt. 208"), City: "San Francisco", State: "CA", Zip: "91189", Country: "US", PhoneNumber: vital.String("+1123456789"), } request := &vital.CreateOrderRequestCompatible{ UserId: "63661a2b-2bb3-4125-bb1a-b590f64f057f", LabTestId: "5b41f610-ebc5-4803-8f0c-a61c3bdc7faf", PatientDetails: details, PatientAddress: address, } response, err := client.LabTests.CreateOrder(context.TODO(), request) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` ```json Response { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"), "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "patient_details": {"dob": "2020-01-01", "gender": "male"}, "patient_address": { "receiver_name": "John Doe", "first_line": "123 Main St.", "second_line": "Apt. 208", "city": "San Francisco", "state": "CA", "zip": "91189", "country": "United States", "phone_number": "+1123456789", }, "details": { "type": "testkit", "data": { "id": "a655f0e4-6405-4a1d-80b7-66f06c2108a7", "shipment": { "id": "d55210cc-3d9f-4115-8262-5013f700c7be", "outbound_tracking_number": "", "outbound_tracking_url": "", "inbound_tracking_number": "", "inbound_tracking_url": "", "outbound_courier": "usps", "inbound_courier": "usps", "notes": "", "created_at": "2020-01-01T00:00:00.000Z", "updated_at": "2020-01-01T00:00:00.000Z", }, "created_at": "2020-01-01T00:00:00Z", "updated_at": "2020-01-01T00:00:00Z", }, }, "diagnostic_lab_test": { "name": "Lipids Panel", "description": "Cholesterol test", "method": "testkit", }, "sample_id": "123456789", "notes": "This is a note", "created_at": "2020-01-01T00:00:00Z", "updated_at": "2020-01-01T00:00:00Z", "status": "collecting_sample", "events": [ { "id": 1, "created_at": "2022-01-01T00:00:00Z", "status": "received.testkit.ordered", } ], } ``` As demonstrated in this Quickstart Guide, integrating with our API is straightforward, allowing you to access a wide range of lab tests with minimal effort. Placing an order for a lab test is as simple as making a request to the appropriate endpoint. With our user-friendly design and clear documentation, you can harness the power of our clinical test API to enhance your application and deliver a seamless experience to your users. # Testing in Sandbox Source: https://docs.tryvital.io/lab/overview/sandbox In order for Customers to be able to simulate the ordering process in the sandbox environment, Vital provides both simulated results and a way to transition an order through it's [lifecycle](/lab/workflow/lab-test-lifecycle). ## Creating Lab Tests Every created team comes with 3 standard `Lab Tests` to allow the Customer to test all 3 modalities. However, since most customers choose to have their own custom lab tests, you should create your own tests using the [API](/lab/workflow/create-test). This is encouraged, as Vital generates results based on the associated markers. More on this on the [results section](/lab/overview/sandbox#results). ## Lifecycle When an order is placed in the sandbox environment, it is "stuck" in the `ordered` state and will not progress unless triggered to do so. This can be done through the dashboard, or through the [API](/api-reference/lab-testing/simulate-order). Initiating the simulation process will progress the order through the expected success states defined in the [lifecycle section](/lab/workflow/lab-test-lifecycle), triggering all expected webhooks, emails and SMS's. When testing the At-Home Phlebotomy, there is only one specific address which is allowed, this address is `West Lincoln Street, Phoenix, AZ 85004, USA`. ## Results In order to allow Customers to test, Vital generate's fake results for each lab test, based on the [expected markers](/lab/results/result-formats#expected-results). The generated results will cycle through all possible [result types](/lab/results/result-formats#resulttype). Note that generated values might not match the expected unit. As for the PDF results, these are not generated, and Vital returns an example PDF that does not reflect the ordered test. # Testing Modalities Source: https://docs.tryvital.io/lab/overview/testing-modalities By integrating with our API, you will have access to the following testing modalities: * At-home test kit. * At-home phlebotomy visit. * Walk-in test visit. ## At-home test kits This involves sending a kit to a patient's home. They will then be able to collect their sample specimen and send it back to our lab networks via mail. At home testing kits are comprised of a number of components such as:
    * Mailer Box * Blade Lancets * Plasters * Gauze * ADX Card * Blood collection card * Saliva Tubes * Collection Supplies * Instruction Booklet Additional components can be included in the test kit box. To fully customize the kit, just contact us [support@tryvital.io](mailto:support@tryvital.io).
    ## At-home phlebotomy At-home phlebotomy requires a phlebotomist to visit a patient's home and carrying out the phlebotomy draw. The sample is then delivered to the lab by the phlebotomist. The flow for at-home phlebotomy orders is as follows: * [Place an order](/lab/overview/quickstart) via the API. * The order is processed in the background. For at-home phlebotomy, this means generating the requisition form for the lab test. * Once the requisition form is ready, a webhook update and an email to the patient are sent. The email will them know that they can now book an appointment. * The patient goes through Vital's appointment booking dashboard, where they can select a location and time from the available slots. * Once the appointment takes place and the sample is drawn, it will be taken to the lab. * Once the lab analyzes the results, the order is complete and the results can be [fetched](/lab/results/result-formats). ## Walk-in tests In-lab draws involve the patient going to a lab location, either Quest or Labcorp, to carry out the test. To carry out this test you need a requisition form. Vital's API can generate this for you, if the patient meets certain conditions. Results are then retrieved via Vital's API. The flow of an walk-in test is very similar to the at-home phlebotomy one. The only difference is that the patient does not book an appointment beforehand, but goes directly to the lab instead. A patient might book an appointment with Labcorp or Quest for walk-in phlebotomy tests, instead of directly going to the lab. We don't support tracking such appointments through our API yet. But we might add this capability in the future! # Critical Results Source: https://docs.tryvital.io/lab/results/critical-results When our Lab partners return results to Vital, they may return specific interpretations for each Marker: * `normal`, the marker value is inside the lab reference range. * `abnormal`, the marker value is outside the lab reference range. * `critical`, the marker value falls in the critical range for that marker, which can be life-threatening. These values are returned in an `interpretation` field for results, both in each individual marker and the overall results object: ```json Resuls Object { "metadata": { "age": 19, "dob": "18/08/1993", "clia_number": "12331231", "patient": "Bob Smith", "provider": "Dr. Jack Smith", "laboratory": "LabCorp", "date_reported": "2020-01-01", "date_collected": "2022-02-02", "specimen_number": "123131", "date_received": "2022-01-01", "status": "final", "interpretation": "critical" }, "results": [ { "name": "Hemoglobin", "slug": "hemoglobin", "unit": "g/dL", "notes": null, "value": 100.2, "timestamp": null, "max_range_value": 17.7, "min_range_value": 13.0, "is_above_max_range": true, "is_below_min_range": false, "interpretation": "critical" }, { "name": "Sodium", "slug": "unknown", "unit": "mmol/L", "notes": null, "value": 136, "timestamp": null, "max_range_value": 144, "min_range_value": 134, "is_above_max_range": false, "is_below_min_range": false, "interpretation": "normal" } ] } ``` Besides the `interpretation` field, there is also the `status` field, which can assume the values of `final` or `partial`, as the critical results are notified as soon as they're available, not all tests are guaranteed to have been processed by the lab. For critical results, the laboratory will contact a physician, so they can provide further information to the patient. The physician called is the Vital assigned physician. They act as the ordering physician when placing the order. After the call is made, and Vital has received the results from the laboratory, a `labtest.result.critical` event is sent. The webhook payload contains the following information: ```json Critical Results Webhook Payload { "data": { "created_at": "2023-09-04T19:02:33.042533+00:00", "updated_at": "2023-09-04T19:02:47.106778+00:00", "order_id": "93ae03a7-626f-4cf4-802b-1fb6b3c339c7", "sample_id": "W4123LKB3VTL", "status": "final", "interpretation": "critical", "team_id": "482gh249-cd9d-4049-bb42-db5682cdf1d2", "user_id": "db5d34d5-bf7b-4e25-b119-9asd53694f45" }, "event_type": "labtest.result.critical" } ``` After you receive the webhook, you can use the [`GET /order/{order_id}/result`](/api-reference/results/get-results) endpoint to fetch the raw results. You can test the webhook event by using the [Vital Dashboard](https://app.tryvital.io) Webhooks section, where you can register an endpoint and subscribe to the `labtest.result.critical` event. The following images show what the screen should look like after you have an endpoint registered and have selected the `Testing` section inside the endpoint configurations. You can then click the `Send Example` button and should receive a HTTP call in the registered url. # null Source: https://docs.tryvital.io/lab/results/follow-up When the test results are ready, you will receive a `labtest.order.updated` webhook where the order has a `complete` status. That means the results are ready and can be [fetched](/lab/results/result-formats). In case of abnormal results, our physician will determine if a call with the patient is required, and if so, they will follow up to figure out the next steps. Please keep in mind that the follow up call with the patient only happens for orders processed by our physician network. If you provided your own physician when you placed the order, you will be responsible for following up with your patients. # Result Formats Source: https://docs.tryvital.io/lab/results/result-formats Vital's API returns results in two different formats. * `PDF` * `JSON` ## PDF Results We return the raw results in PDF form, that we receive directly from our partner labs. This can be retrieved as follows: ```bash Get order results PDF curl --request GET \ --url {{BASE_URL}}/v3/order/result/pdf \ --header 'Accept: application/json' \ --header 'Content-Type: application/pdf' \ --header 'x-vital-api-key: ' \ ``` An example result: ## JSON Results We also return the parsed results in JSON format, so you can use them to generate your own forms. These results are returned in a structured format, which you can find [here](/api-reference/results/get-results). The `results` field, according to the spec, can return either a `list[BiomarkerResult]` or an untyped `dict`. This is due to backwards compatibility, and you can disregard the untyped `dict` ### Result Status The `status` field can be one of the following: 1. `ResultStatus.PARTIAL` - The results are partial. Labs can return results before all biomarkers are available. Vital makes these results available to you as soon as we receive them, but does not send a webhook notification for `partial` results. This means that if you probe the API for results, you might get a `partial` result, even if there was no webhook for a `labtest.update` event. This is done due to the possibility of [critical values](/lab/workflow/lab-test-lifecycle#critical-results) in the results. 2. `ResultStatus.FINAL` - The results are complete. This means that all biomarkers are available, and the results are final. You will receive a `labtest.update` webhook notification for this event. ### BiomarkerResult A `BiomarkerResult` has the following definition: ```python name: str slug: str value: float # deprecated result: str type: ResultType unit: str | None timestamp: datetime | None notes: str | None min_range_value: float | None max_range_value: float | None is_above_max_range: bool | None is_below_min_range: bool | None interpretation: str = Interpretation.NORMAL loinc: str | None loinc_slug: str | None provider_id: str | None source_markers: List[ParentBiomarkerData] | None ``` #### ResultType Results can fall into one of the following categories: 1. `ResultType.NUMERIC` - A numeric result, e.g. `1.2` In this case, the `result` field will be a string representation of the number, and the `value` field will be a float representation of the number. 2. `ResultType.RANGE` - A range result, e.g. `<1.2` In this case, the `result` field will be a string representation of the range value, and the `value` field will be `-1`. Note that you will also find the `<1.2` value in the `notes` field. A range result will always be a value following the pattern `^([<>]=?\d*(\.\d+)?|(\d*(\.\d+)?-\d*(\.\d+)?))$`. 3. `ResultType.COMMENT` - A text result, e.g. `Positive` In this case, the `result` field will be a string representation of the text, and the `value` field will be `-1`. Note that you will also find the `Positive` value in the `notes` field. The `value` field in deprecated and will eventually be removed. #### Interpretation Interpretation is a string value that can be one of the following: 1. `Interpretation.NORMAL` - The result is within normal parameters. 2. `Interpretation.ABNORMAL` - The result is outside of normal parameters. 3. `Interpretation.CRITICAL` - The result is outside of critical parameters. In this case, refer to the [critical values](/lab/workflow/lab-test-lifecycle#critical-results) section. #### Standardisation - LOINC It's possible to test the same biomarkers across different laboratories. For these to match, we use the [LOINC](https://loinc.org/) standard. In the `BiomarkerResult` object, you can see two fields `loinc_slug` and `loinc`. These fields refer to the LOINC standard. Customers should use this standard, so it's possible to match results across different laboratories. You can expect that the `slug` field is what the laboratory returns to us - and the `loinc_slug` is the standardised version. An example: | Lab | Slug | LOINC | LOINC Slug | | ------- | --------------- | ------ | --------------------------- | | Labcorp | hdl-cholesterol | 2085-9 | cholesterol-in-hdl-mass-vol | | USSL | hdl | 2085-9 | cholesterol-in-hdl-mass-vol | As you can see, the same biomarker `HDL Cholesterol` can have different slugs across different laboratories. However it's represented by the same LOINC value. #### Expected Results When ordering a `lab_test`, you can see which `markers` each test orders. These can either be `panels` composed of multiple `biomarkers` or just individual `biomarkers`. This means that a `lab_test` with only one associated `marker`, such as `Lipid Panel`, can return multiple `result markers`. We call these expected results. Each `marker` can thus be composed of multiple `expected results` which match to a `loinc`. As an example, here's the expected results for the `Lipid Panel` marker: ```json "expected_results":[ { "id":1108, "name":"VLDL Cholesterol Cal", "slug":"vldl-cholesterol-cal", "lab_id":6, "provider_id":"011919", "loinc":{ "id":5062, "name":"Cholesterol in VLDL Calc [Mass/Vol]", "slug":"cholesterol-in-vldl-calc-mass-vol", "code":"13458-5", "unit":"mg/dL" } }, { "id":1109, "name":"Cholesterol, Total", "slug":"cholesterol-total", "lab_id":6, "provider_id":"001065", "loinc":{ "id":11940, "name":"Cholesterol [Mass/Vol]", "slug":"cholesterol-mass-vol", "code":"2093-3", "unit":"mg/dL" } }, { "id":1110, "name":"HDL Cholesterol", "slug":"hdl-cholesterol", "lab_id":6, "provider_id":"011817", "loinc":{ "id":11858, "name":"Cholesterol in HDL [Mass/Vol]", "slug":"cholesterol-in-hdl-mass-vol", "code":"2085-9", "unit":"mg/dL" } }, { "id":1112, "name":"Triglycerides", "slug":"triglycerides", "lab_id":6, "provider_id":"001172", "loinc":{ "id":16384, "name":"Triglyceride [Mass/Vol]", "slug":"triglyceride-mass-vol", "code":"2571-8", "unit":"mg/dL" } }, { "id":1113, "name":"LDL Chol Calc (NIH)", "slug":"ldl-chol-calc-nih", "lab_id":6, "provider_id":"012059", "loinc":{ "id":5060, "name":"Cholesterol in LDL Calc [Mass/Vol]", "slug":"cholesterol-in-ldl-calc-mass-vol", "code":"13457-7", "unit":"mg/dL" } } ] ``` You can use this information to verify if the final results are composed of all expected results. In order to obtain this data, you can use the following endpoints: 1. [GET /v3/lab\_tests/markers](/api-reference/lab-testing/biomarkers) This allows you to search markers based on laboratory or name. 2. [GET /v3/lab\_tests/\{id}/markers](/api-reference/lab-testing/lab-test-markers) This allows you to see all markers associated with a lab test and it's expected results. #### Source Markers As mentioned above, a marker can be composed of one or more results. This means that if you order a `Lipid Panel`, there will be no `Lipid Panel` result returned, but instead a series of markers that originate from the `Lipid Panel`. Vital identifies the source marker via the `source_markers` field. ```json { "name": "Sex Horm Binding Glob, Serum", "slug": "sex-horm-binding-glob-serum", "value": 30.4, "result": "30.4", "type": "numeric", "unit": "nmol/L", "timestamp": "2024-10-31T09:08:00+00:00", "notes": "Final", "min_range_value": 24.6, "max_range_value": 122, "is_above_max_range": false, "is_below_min_range": false, "interpretation": "normal", "loinc": "13967-5", "loinc_slug": "sex-hormone-binding-globulin-moles-vol", "provider_id": "082016", "source_markers": [ { "marker_id": 229, "name": "Testosterone Free, Profile I", "slug": "testosterone-free-profile-i", "provider_id": "140226" } ] }, ``` When Vital cannot identify the source, then this field will be `null`, indicating that this is an unsolicited result. There may also be more than one `source` marker, if there are two or more ordered markers that contain the same underlying result, using the same testing method. #### Missing Results At times labs will commit mistakes, and expected results will be missing. Vital identifies these and parses them into a separate structure, named `missing_results`. This data has the following format: ```python name: str slug: str inferred_failure_type: FailureType note: str | None = None loinc: str | None = None loinc_slug: str | None = None provider_id: str | None = None source_markers: List[ParentBiomarkerData] | None = None ``` `inferred_failure_type` is the Vital assigned error type. The error type is inferred from the comments received from the lab. They are to help assess possibly root causes of missing results, and aid the customer in identifying issues in aggregate. The way that we infer these error types is subject to change as we continue to refine and achieve more granular understanding of failure modes. 1. `quantity_not_sufficient_failure` The lab could not process this result due to an insufficient quantity of collected sample. This could be due to the patient refusing to collect more, the phlebotomist being to unable to collect the proper volume of blood from the sample, or the phlebotomist not collecting all of the sample they were meant to collect. 2. `collection_process_failure` This is indicative of potential failures to follow the entirety phlebotomy process, often immediately following the collection. For example, improper centrifugation of the collected sample, improper refrigeration. While these are the most likely causes, there are other reasons for why sample quality may have been degraded. 3. `drop_off_failure` This speaks to a specific form of collection process failures. Specifically that the sample was not received by the lab in proper condition. Possible issues correspond to improper freezing, or refrigeration, or was exposed to excessive transport delay. 4. `internal_lab_failure` This speaks to failures that are most likely to have happened internal to the lab. This includes issues such as misplacing a collected sample, or errors that could not be best attributed to any external cause. 5. `order_entry_failure` The test was not performed because it was not properly ordered at the lab. 6. `non_failure` This speaks to a failure that should not impact the patient. For example, it may indicate that a duplicate test was ordered. 7. `unknown_failure` This is a failure mode that could not be properly attributed to any specific failure mode. Potentially the results were simply left out and Vital was not provided any additional information. 8. `patient_condition_failure` This speaks to a failure to result due to specifics of the patient's physical condition. For example, the patient may have had some food very high in fats that immediately prior to a collection. It is possible that improper storage and handling can cause samples to fail in ways that appear to be a patient condition failure. 9. `missing_result_calc_failure` This failure indicates that a calculated field is missing because the underlying tests required to perform the calculation were either unable to be processed, or yielded a result outside of the allowable parameters to perform the calculation 10. `missing_demo_aoe_calc_failure` Some results may require additional information reported in AOE (ask on order entry) in order to yield results, such as age, to be properly calculated. # Communications Source: https://docs.tryvital.io/lab/testkits/communications Communication for patients is done via SMS, for At-home Teskit orders. Customers have the following options when setting this up: * `Default` - SMS communications are enabled. * `Disable` - All communications from Vital are disabled. Each option has a different set of content and status changes, depending on what triggered them. You can enable or disable SMS individually through the [**Vital Dashboard**](http://app.tryvital.io/), under the Team Settings section. ## Default Communications ### SMS Messages A table of the **Order Status** and **default SMS** is provided bellow: | Order Status | Default Message | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `ordered` | Hey **receiver\_name**, your order for the **team\_name** health kit has been placed! We'll update you with where it is in the process via messages :) | | `transit_customer` | Your order for the **team\_name** test kit is in transit, and should be arriving by **eta**. | | `out_for_delivery` | Your order for the **team\_name** test kit is out for delivery, and should be arriving by today | | `delivered_to_lab` | Your test kit has arrived to our labs we're processing your sample now! | | `completed` | The lab has finished processing your test kit, your results should be ready soon :) | | `cancelled` | Hey, your order for the **team\_name** test kit has been cancelled, If this is by accident please contact support. | SMS Texts are customizable, and can be enabled or disabled individually. ### Emails We do not send emails for At-home Testkits. ## Disable Communications In this case, no communications will be sent from Vital. You have the ability to produce completely customized communications using the [`Webhook events`](/lab/testkits/webhooks) described previously. # Order Lifecycle Source: https://docs.tryvital.io/lab/testkits/order-lifecycle The **At-Home Testkit** can be composed of two distinct **Order** lifecycles, which we detail bellow. ## Order Lifecycle As discussed in [`Lab Test Lifecycle - Statuses`](/lab/workflow/lab-test-lifecycle#statuses), each lab testing modality has the following format `[HIGH-LEVEL STATUS].[TEST MODALITY].[LOW-LEVEL STATUS]` For each modality, there can be multiple **low-level status**, for **At-Home Testkit** the possible low-level status are: ### Registered Teskits * `ordered`: Vital received the order, stored it into our system, and started processing it asynchronously. * `requisition_created`: An order requisition form was validated and created with the partner laboratory, making the order available to be carried out. * `transit_customer`: The teskit is shipped and in transit to the customer. * `out_for_delivery`: The teskit is out for delivery. * `with_customer`: The teskit is delivered to the customer. * `transit_lab`: The customer has sent the testkit back to the lab. * `delivered_to_lab`: The lab has received the testkit. * `failure_to_deliver_to_customer`: The shipping company was unable to deliver the testkit to the customer. * `failure_to_deliver_to_lab`: The shipping company was unable to deliver the testkit to the lab. * `problem_in_transit_customer`: The shipping company encountered a problem while delivering the testkit to the customer. * `problem_in_transit_lab`: The shipping company encountered a problem while delivering the testkit to the lab. * `sample_error`: The collected sample was unprocessable by the lab. * `completed`: The laboratory processed the blood sample and final results are available. * `cancelled`: The order was cancelled by either the patient, Vital or you. The Finite State Machine that defines the possible transitions for the low-level statuses described above is illustrated in the following diagram. ### Registrable Testkits * `ordered`: Vital received the order, stored it into our system, and started processing it asynchronously. * `awaiting_registration`: Order is created but no user has been registered yet * `transit_customer`: The teskit is shipped and in transit to the customer. * `out_for_delivery`: The teskit is out for delivery. * `with_customer`: The teskit is delivered to the customer. * `registered`: Order has been registered, and a requisition will be created. * `requisition_created`: An order requisition form was validated and created with the partner laboratory, making the order available to be carried out. * `transit_lab`: The customer has sent the testkit back to the lab. * `delivered_to_lab`: The lab has received the testkit. * `failure_to_deliver_to_customer`: The shipping company was unable to deliver the testkit to the customer. * `failure_to_deliver_to_lab`: The shipping company was unable to deliver the testkit to the lab. * `problem_in_transit_customer`: The shipping company encountered a problem while delivering the testkit to the customer. * `problem_in_transit_lab`: The shipping company encountered a problem while delivering the testkit to the lab. * `sample_error`: The collected sample was unprocessable by the lab. * `completed`: The laboratory processed the blood sample and final results are available. * `cancelled`: The order was cancelled by either the patient, Vital or you. The Finite State Machine that defines the possible transitions for the low-level statuses described above is illustrated in the following diagram.
    In **sandbox**, there is no async transition from the `ordered` state to the `requisition_created` state, this must be **manually triggered** via the **Vital Dashboard**. # Overview Source: https://docs.tryvital.io/lab/testkits/overview At-home Testkits are one of our offered modalities. This modality involves sending a kit to a patient’s home, where the patient will collect their own sample and send the kit to our Lab network through mail. Within this modality, we provide two types of testkits, those which are pre-registered to a user, and those which need to to be (registered by the patient upon receiving the kit)\[lab/workflow/order-registrable-testkit]. ## Ordering Flow overview To achieve this, a high-level overview of the process is defined as: * An order is placed in Vital's system through our Dashboard or API. * The order is added to a background queue and additional checks are made before the kit is shipped to the provided address. * Once the order shipment is created, a requisition is generated. * After the requisition is created, some form of communication is carried out with the patient. * If the kit is not registered, then the user must register the kit before results can be provided. * The patient receives the kit, collects their sample, and sends the kit back through mail. * The Laboratory processes the patient's blood sample and generates the required results. * Vital exposes the results via API as soon as they are available, both on PDF and structured data via API. ## Constraints * If the teskit is un-registered, then the user must register it in order for the results to be processed. To find out more details, see [`Order and Appointment Lifecycle`](/lab/testkits/order-lifecycle), [`Communications`](/lab/testkits/communications) and [`Webhooks`](/lab/testkits/webhooks). # Webhooks Source: https://docs.tryvital.io/lab/testkits/webhooks The following webhook events are of interest, when placing an At-Home Testkit order. Those are described in detail in the following sections. ## Order webhook events Based on the status present in [`Order Lifecycle`](/lab/testkit/order-lifecycle), Vital will trigger two kinds of webhook events, [`labtest.order.created`](/event-catalog/labtest.order.created) and [`labtest.order.updated`](/event-catalog/labtest.order.updated). The `labtest.order.created` event is triggered when an order is created in the system, having the `ordered` status, and all subsequent status changes will trigger a `labtest.order.updated` event in the system. The webhook payload body will have the following information if the Order is in the `requisition_created` status: ```json Testkit Order Updated { "data":{ "created_at":"2023-09-01T18:02:41.210495+00:00", "details":{ "data":{ "created_at":"2023-09-01T18:02:41.247443+00:00", "id":"3c046d74-347e-4e28-9e0d-c5b720a8e219", "shipment":{ "id":"ae0323d4-2c18-4aea-a0e7-d73b377315b1", "inbound_courier":null, "inbound_tracking_number":null, "inbound_tracking_url":null, "notes":null, "outbound_courier":null, "outbound_tracking_number":null, "outbound_tracking_url":null }, "updated_at":"2023-09-01T18:02:42.186312+00:00" }, "type":"testkit" }, "events":[ { "created_at":"2023-09-01T18:02:41.271401+00:00", "id":4056, "status":"received.testkit.ordered" }, { "created_at":"2023-09-01T18:05:12.618156+00:00", "id":4057, "status":"received.testkit.requisition_created" } ], "health_insurance_id":null, "id":"e1c380c1-7df4-487f-869e-f1be0193ca25", "lab_test":{ "fasting":false, "id":"0cb9f34f-c3df-4a13-8ca1-19429a82611b", "is_active":true, "is_delegated":false, "lab":null, "markers":null, "method":"testkit", "name":"Female General Wellness", "price":45, "sample_type":"dried_blood_spot", "slug":"general_wellness_female_002" }, "notes":null, "patient_address":{ ... }, "patient_details":{ ... }, "physician":{ ... }, "priority":false, "requisition_form_url":null, "sample_id": "some_id, "shipping_details":null, "status":"received", "team_id":"f07f59d2-2903-4bcd-a2ac-3e87fa47c4bc", "updated_at":"2023-09-01T18:02:41.210495+00:00", "user_id":"f6cdf185-6815-4b2b-9482-798b75168689" }, "event_type":"labtest.order.updated" } ``` # Communications Source: https://docs.tryvital.io/lab/walk-in/communications Communication for patients is done via email or SMS, for Walk-in orders. Customers have the following options when setting this up: * `Default` - Email and SMS communications are enabled. * `SMS Only or Email Only` - Only SMS or Email communication is enabled. * `Disable` - All communications from Vital are disabled. Each option has a different set of content and status changes, depending on what triggered them. You can enable or disable SMS and Email communications individually through the [**Vital Dashboard**](http://app.tryvital.io/), under the Team Settings section. ## Default Communications ### SMS Messages A table of the **Order Status** and **default SMS** is provided bellow: | Order Status | Default Message | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ordered` | *"Hi, `{patient_first_name}`, your order for the `{team_name}` walk-in-test has been placed! We'll provide you with updates on the status of your order via text messages :)."* | | `requisition_created` | *"Hey `{patient_first_name}`, it's time to visit your local `{lab_name}` center. Please check the email you just received for instructions."* | | `appointment_scheduled` | *"Your appointment with the lab has been booked at {date} over at {address}!"* This message is sent only for Quest orders and Quest appointments placed through Vital API. | | `appointment_cancelled` | *"Hey, your lab appointment at {date} for {team_name} has been cancelled."* This message is sent only for Quest orders and Quest appointments placed through Vital API. | | `completed` | *"Your results have finished processing, your results should be ready soon."* | | `cancelled` | *"Hey, your order for the `{team_name}` walk-in-test has been cancelled. If this is by accident please contact support."* | SMS Texts are customizable, and can be enabled or disabled individually. ### Emails For emails, the following table describes what information each email contains for each Order Statuses: | Order Status | Email Content Description | | --------------------- | ------------------------------------------------------------------------------------------------------ | | `requisition_created` | An email with confirmation of the partner Lab and additional instructions will be sent to the patient. | Emails can be customised and sent from your own domain. ## Disable Communications In this case, no communications will be sent from Vital. You have the ability to produce completely customized communications using the [`Webhook events`](/lab/at-home-phlebotomy/webhooks) described previously. # Patient Service Centers and Appointments Source: https://docs.tryvital.io/lab/walk-in/locations # Finding nearest serviceable Patient Service Centers Verifying if a particular zip code is serviceable is an important step, as not all labs have patient service centers within a state or within a reasonable distance. The recommend high level flow for verifying PSC availability at Vital is: * Fetch PSC location data via the [`GET /v3/order/area/info`](/api-reference/lab-testing/area-info) endpoint, the response payload should look like this: ```json { "zip_code": "85004", "central_labs": { "labcorp": { "within_radius": 5, # number of PSC's within radius of provided zip code "radius": "25" # miles } } ... } ``` If specific data regarding the PSC's is required, then you can also query the [`GET /v3/order/psc/info`](/api-reference/lab-testing/psc-info) for specific information on the PSC's within the radius of the provided zip code. ```json { "lab_id": 27, "slug": "labcorp", "patient_service_centers": [ { "metadata": { "name": "LABCORP", "state": "AZ", "city": "Phoenix", "zip_code": "85006", "first_line": "1300 N 12th St", "second_line": "Ste 300", "phone_number": "480-878-3988", "fax_number": "844-346-5903", "hours": null }, "distance": "25", "site_code": "ABC", }, ] } ``` # Booking appointments at Patient Service Centers Currently only available for Quest Vital allows booking of appointments directly with the Patient Service Centers for some labs. In order to book these appointments, you can use the [PSC Appointment API](/api-reference/lab-testing/psc-schedulling/appointment-psc-availability). ## Appointment Availability You can use the [availability API](/api-reference/lab-testing/psc-schedulling/appointment-psc-availability) to obtain the available slots. You can feed in the `site_code` you obtain from the [`GET /v3/order/psc/info`](/api-reference/lab-testing/psc-info) endpoint, or supply a zip code directly. If you provider a zip code, a max of 3 PSC locations will be displayed. Note that since this endpoint can return availability for multiple PSC's, there may be multiple timezones. The individual start and end times are in UTC, and each location has it's own `iana_timezone` key which should be used to convert these start and end dates to the correct timezone. The overarching `timezone` key will always be `null` in this endpoint\` ## Booking You can [book](/api-reference/lab-testing/psc-schedulling/appointment-psc-booking), [reschedule](/api-reference/lab-testing/psc-schedulling/appointment-psc-rescheduling) or [cancel](/api-reference/lab-testing/psc-schedulling/appointment-psc-cancelling) with Vital, as many times as you want. # Order and Appointment Lifecycle Source: https://docs.tryvital.io/lab/walk-in/order-lifecycle The **Walk-In Phlebotomy** orders are composed of two different lifecycles, the **Order** lifecycle and the **appointment** lifecycle, detailed in the following sections. ## Order Lifecycle As discussed in [`Lab Test Lifecycle - Statuses`](/lab/workflow/lab-test-lifecycle#statuses), each lab testing modality has the following format `[HIGH-LEVEL STATUS].[TEST MODALITY].[LOW-LEVEL STATUS]` For each modality, there can be multiple **low-level status**, for **Walk-In Phlebotomy** the possible low-level status are: * `ordered`: Vital received the order, stored it into our system, and started processing it asynchronously. * `requisition_created`: An order requisition form was validated and created with the partner laboratory, making the order available to be carried out. * `appointment_pending`: An appointment was placed in Vital's system for the order, but doesn't have a scheduled date. * `appointment_scheduled`: An appointment was scheduled or rescheduled for the order. * `appointment_cancelled`: The appointment was cancelled, by either the patient, Vital or you. * `partial_results`: The laboratory has started making partial results available. * `completed`: The laboratory processed the blood sample and final results are available. * `cancelled`: The order was cancelled by either the patient, Vital or you. The Finite State Machine that defines the possible transitions for the low-level statuses described above is illustrated in the following diagram.
    In **sandbox**, there is no async transition from the `ordered` state to the `requisition_created` state, this must be **manually triggered** via the **Vital Dashboard**. ## Appointment Lifecycle The appointment lifecycle is separate from the order lifecyle, and it corresponds to a single appointment. The possible status are defined as follows: * `pending`: An appointment was placed in the system, and is pending updates from the phlebotomy service. * `scheduled`: An appointment was scheduled or rescheduled. The Finite State Machine that defines the possible transitions for the appointment statuses described above is illustrated in the following diagram. The events are related to a single appointment. An order can have multiple existing appointments in Vital system, although only one appointment will be considered active and returned when using the [GET Appointment endpoint](/api-reference/lab-testing/psc-schedulling/get-psc-appointment). # Overview Source: https://docs.tryvital.io/lab/walk-in/overview Walk-in tests are one of our offered modalities. This modality is focused on patients that are able to go to a Laboratory to have their blood drawn. ## Ordering Flow overview To achieve this, a high-level overview of the process is defined as: * An order is placed in Vital's system through our Dashboard or API. * The order is added to a background queue and additional checks are made before a test requisition is created with the chosen Laboratory. * After the requisition is created, some form of communication is carried out with the patient so he can go to the assigned partner Laboratory. * After that, the patient can go at any time they want to the laboratory to have their blood drawn. * The Laboratory processes the patient's blood sample and generates the required results. * Vital exposes the results via API as soon as they are available, both on PDF and structured data via API. To find out more details, see [`Order Lifecycle`](/lab/walk-in/order-lifecycle), [`Communications`](/lab/walk-in/communications) and [`Webhooks`](/lab/walk-in/webhooks). # Webhooks Source: https://docs.tryvital.io/lab/walk-in/webhooks The following webhook events are of interest, when placing a Walk-in order. Those are described in detail in the following sections. ## Order webhook events Based on the status present in [`Order Lifecycle`](/lab/walk-in/order-lifecycle), Vital will trigger two kinds of webhook events, [`labtest.order.created`](/event-catalog/labtest.order.created) and [`labtest.order.updated`](/event-catalog/labtest.order.updated). The `labtest.order.created` event is triggered when an order is created in the system, having the `ordered` status, and all subsequent status changes will trigger a `labtest.order.updated` event in the system. The `partial_results` status does not trigger a Webhook unless specifically requested from Vital. The webhook payload body will have the following information if the Order is in the `completed` status: ```json Walk-in Order Updated { "id": "84d96c03-6b1c-4226-ad8f-ef44a6bc08af", "team_id": "6353bcab-3526-4838-8c92-063fa760fb6b", "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "patient_details": { "dob": "2020-01-01", "gender": "male" }, "patient_address": { "receiver_name": "John Doe", "first_line": "123 Main St.", "second_line": "Apt. 208", "city": "San Francisco", "state": "CA", "zip": "91189", "country": "United States", "phone_number": "+1123456789" }, "details": { "type": "walk_in_test", "data": { "id": "a655f0e4-6405-4a1d-80b7-66f06c2108a7", "created_at": "2020-01-01T00:00:00Z", "updated_at": "2020-01-01T00:00:00Z" } }, "sample_id": "123456789", "notes": "This is a note", "created_at": "2020-01-01T00:00:00Z", "updated_at": "2020-01-01T00:00:00Z", "status": "completed", "events": [ { "id": 1, "created_at": "2022-01-01T00:00:00Z", "status": "received.walk_in_test.ordered" }, { "id": 2, "created_at": "2022-01-02T00:00:00Z", "status": "received.walk_in_test.requisition_created" }, { "id": 3, "created_at": "2022-01-03T00:00:00Z", "status": "collecting_sample.walk_in_test.appointment_pending" }, { "id": 4, "created_at": "2022-01-03T00:00:00Z", "status": "collecting_sample.walk_in_test.appointment_scheduled" }, { "id": 5, "created_at": "2022-01-04T00:00:00Z", "status": "sample_with_lab.walk_in_test.partial_results" }, { "id": 6, "created_at": "2022-01-04T00:00:00Z", "status": "completed.walk_in_test.completed" } ] } ``` # Ask on Order Entry (AOE) Source: https://docs.tryvital.io/lab/workflow/aoe Some biomarkers require answers to clinical questions, this is referred to as AOE. These can be as simple as the volume of a particular marker, or the ethnicity of the patient. Questions that are related to fasting (e.g code `FSTING`) do not need to be answered. Vital uses the default value you provided for fasting at the [lab test](/api-reference/lab-testing/post-test) creation time. However, if you do answer them, then the answered value overrides the preset test value. There are 4 types of AOE: 1. `choice` 2. `multiple_choice` 3. `numeric` 4. `text` `choice` and `multiple_choice` have a set list of answers to pick from, while `numeric` and `text` don't and are free form responses. AOE questions can also be required or optional. ## Where to find the questions As these AOE are directly related to biomarkers, you can find them in the [`GET /v3/lab_tests/markers`](/api-reference/lab-testing/biomarkers) endpoint or in the [`GET /v3/lab_tests/{lab_test_id}/markers`](/api-reference/lab-testing/lab-test-markers) endpoint, in the `aoe` object. As an example, let's look at the `Lead, Blood (Adult)` marker. We have omitted some extra questions for the sake of this doc. ```json { "id": 173, "name": "Lead, Blood (Adult)", "provider_id": "007625", ... "questions": [ { "id": 1234567890251, "code": "BLPURP", "type": "choice", "value": "BLOOD LEAD PURPOSE", "constraint": null, "answers": [ { "id": 1234567890252, "code": "LCANS4", "value": "I - INITIAL" }, { "id": 1234567890253, "code": "LCANS5", "value": "R - REPEAT" }, { "id": 1234567890254, "code": "LCANS6", "value": "F - FOLLOW-UP" } ], "required": true, "sequence": 1 }, ] ` ``` Each question has it's own `id`, which will be required to answer it. The types we refer to above are an enum that is represented by `type`. The `answers` list contains a list of possible answers in the case that `type` is one of `choice` or `multiple_choice`, otherwise it is empty. `required` deems whether the questions MUST be answered in order for the order to be valid, otherwise a error will be shown `Missing required questions - "question"`. ## How to answer Answering is done at order time, via the [`POST /v3/order`](/api-reference/lab-testing/create-order) endpoint, in the field `aoe_answers`. This field has the following format: ```json { "aoe_answers": [ { "marker_id": , "question_id": , "answer": , } ] } ``` You will populate this list with the answers to the questions defined in the `marker` object, as explained [above](/lab/workflow/aoe#where-to-find-the-questions). The `marker_id` refers to the Vital [marker id](/api-reference/lab-testing/biomarkers), and the `question_id` to the id of the question being answered. For example, marker `Leader, Blood (Adult)`, has the following data: ```json { "id": 173, "name": "Lead, Blood (Adult)", "provider_id": "007625", ... "questions": [ { "id": 1234567890251, "code": "BLPURP", "type": "choice", "value": "BLOOD LEAD PURPOSE", "constraint": null, "answers": [ { "id": 1234567890252, "code": "LCANS4", "value": "I - INITIAL" }, { "id": 1234567890253, "code": "LCANS5", "value": "R - REPEAT" }, { "id": 1234567890254, "code": "LCANS6", "value": "F - FOLLOW-UP" } ], "required": true, "sequence": 1 }, ] ``` In order to answer the required question, you will need the `marker_id` which is the `id` field, the `question_id` in `questions[0].id` and one of the `code` values in the `question[0].answers` field. ### Answer types The `answer` field contains the actual answer to the question and it's value will depend on the type and question itself. 1. `choice` and `multiple_choice` In this case, the `answer` should contain the `code` field in the list of `answers` provided, as shown [here](/lab/workflow/aoe#where-to-find-the-questions). e.g The question is as follows for marker of id `173`: ```json { "id": 173, "questions": [ { "id": 1234567890251, "code": "BLPURP", "type": "choice", "value": "BLOOD LEAD PURPOSE", "constraint": null, "answers": [ { "id": 1234567890252, "code": "LCANS4", "value": "I - INITIAL" }, { "id": 1234567890253, "code": "LCANS5", "value": "R - REPEAT" }, { "id": 1234567890254, "code": "LCANS6", "value": "F - FOLLOW-UP" } ], "required": true, "sequence": 1 }, ] } ``` The answer is: ```json { "aoe_answers": [ { "marker_id": 173, "question_id": 1234567890251, "answer": "LCANS4", } ] } ``` 2. `text` This case can encompass many responses, and depends on the question itself. In some instances, the `text` case contains a `constraint` field. This is a string tooltip indicator of the constraints applied by the lab on this particular question. e.g The question is as follows for marker of id `173`: ```json { "id": 173, "questions": [ { "id": 1234567890304, "code": "EDDATE", "type": "text", "constraint": null, "value": "EDD/EDC DATE", "answers": [], "required": true, "sequence": 2 }, ] } ``` The answer is: ```json { "aoe_answers": [ { "marker_id": 173, "question_id": 1234567890304, "answer": "19900101", } ] } ``` 3. `numeric` In this case, the `answer` should be a numeric string representation of the actual value. e.g The question is as follows for marker of id `173`: ```json { "id": 173, "questions": [ { "id": 1234567890240, "code": "COLVOL", "type": "numeric", "constraint": null, "value": "URINE VOLUME (MILLILITERS)", "answers": [], "required": true, "sequence": 1 } ] } ``` The answer is: ```json { "aoe_answers": [ { "marker_id": 173, "question_id": 1234567890240, "answer": "1000", } ] } ``` # Cancelling an Order Source: https://docs.tryvital.io/lab/workflow/cancelling-an-order Cancelling an order depends how far along in the process the order is in. If an order is early enough in its lifecycle, i.e.: 1. Requisition form is not generated. 2. Order wasn't send. 3. The phlebotomist is not on their way. It's possible to cancel it by making a request to the [cancel endpoint](/lab/workflow/cancelling-an-order#cancel-endpoint). For at-home phlebotomy, cancel the appointment first if it has been scheduled. You can cancel a phlebotomy appointment by making a request to the [phlebotomy appointment cancellation endpoint](/lab/workflow/cancelling-an-order#cancel-appointment-endpoint). When it comes to cancelling an order, to avoid the financial costs associated with late cancelling, we advise the following: * For test-kits, we recommend cancelling the order before it's shipped. Kits are typically shipped within 24 hours after they are ordered. * For at-home phlebotomy, appointments can be cancelled without consequence with a notice of at least 24 hours. Cancelling an order with less than 24h notice may incur a financial cost. ### Cancel appointment endpoint ```bash cURL curl --request PATCH \ --url '{{BASE_URL}}/v3/order//phlebotomy/appointment/cancel' \ --header 'accept: application/json' \ --header 'x-vital-api-key: {YOUR_KEY}' \ --data '{"cancellation_reason_id": "7dfd7da5-ed6e-40bb-a7e4-c8003f0c10a9"}' ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) client.lab_tests.cancel_phlebotomy_appointment( order_id="", cancellation_reason_id="", ) ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; import { AppointmentCancelRequest } from '@tryvital/vital-node/api'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const request: AppointmentCancelRequest = { cancellationReasonId: "", } const data = await client.labTests.cancelPhlabotomyAppointment("", request); ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.labtests.requests.AppointmentCancelRequest; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); AppointmentCancelRequest request = AppointmentCancelRequest .builder() .cancellationReasonId("", request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) request := &vital.AppointmentCancelRequest{ CancellationReasonId: "", } response, err := client.LabTests.CancelPhlabotomyAppointment(context.TODO(), "", request) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` You will receive a response specifying awhether the order was successfully cancelled or not. ```json Appointent Cancellation response { "id": "413d7205-f8a9-42ed-aa4a-edb99e481ca0", "user_id": "202b2c2f-fb4c-44dc-a4f8-621186fde227", "address": { "first_line": "West Lincoln Street", "second_line": "", "city": "Phoenix", "state": "AZ", "zip_code": "85004", "unit": "14" }, "location": { "lng": -112.0772235, "lat": 33.4421912 }, "start_at": "2023-05-17T20:00:00+00:00", "end_at": "2023-05-17T22:00:00+00:00", "iana_timezone": "America/Phoenix", "type": "phlebotomy", "provider": "getlabs", "status": "cancelled", "provider_id": "e89eb489-7382-4966-bb14-7ab4763eba6c", "can_reschedule": true } ``` ### Cancel endpoint ```bash cURL curl --request DELETE \ --url {{BASE_URL}}/v3/lab_test//cancel \ --header 'Accept: application/json' \ --header 'x-vital-api-key: ' \ --header 'Content-Type: application/json' \ ``` ```python Python from vital import Client client = Client( api_key=, environment="sandbox", region="us" ) data = client.LabTests.cancel_order(order_id=''); ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const data = await client.labTests.cancelOrder(""); ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var data = vital.labTests().cancelOrder(""); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.LabTests.CancelOrder(context.TODO(), "") if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` You will receive a response specifying whether the order was successfully cancelled or not. ```json Cancellation response { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"), "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "patient_details": {"dob": "2020-01-01", "gender": "male"}, "patient_address": { "receiver_name": "John Doe", "first_line": "123 Main St.", "second_line": "Apt. 208", "city": "San Francisco", "state": "CA", "zip": "91189", "country": "United States", "phone_number": "+1123456789", }, "details": { "type": "testkit", "data": { "id": "a655f0e4-6405-4a1d-80b7-66f06c2108a7", "shipment": { "id": "d55210cc-3d9f-4115-8262-5013f700c7be", "outbound_tracking_number": "", "outbound_tracking_url": "", "inbound_tracking_number": "", "inbound_tracking_url": "", "outbound_courier": "usps", "inbound_courier": "usps", "notes": "", "created_at": "2020-01-01T00:00:00.000Z", "updated_at": "2020-01-01T00:00:00.000Z", }, "created_at": "2020-01-01T00:00:00Z", "updated_at": "2020-01-01T00:00:00Z", }, }, "diagnostic_lab_test": { "name": "Lipids Panel", "description": "Cholesterol test", "method": "testkit", }, "sample_id": "123456789", "notes": "This is a note", "created_at": "2020-01-01T00:00:00Z", "updated_at": "2020-01-01T00:00:00Z", "status": "cancelled", "events": [ { "id": 1, "created_at": "2022-01-01T00:00:00Z", "status": "received.testkit.ordered", }, { "id": 2, "created_at": "2022-01-01T00:00:00Z", "status": "cancelled.testkit.cancelled", } ], } ``` # Creating a Lab Test Source: https://docs.tryvital.io/lab/workflow/create-test Vital provides a way to create your own lab tests. Lab tests are a collection or pre-set of orderable markers. These are made up of: 1. One or more marker IDs or Provider IDs. 2. The collection method: `testkit`, `walk_in_test`, or `at_home_phlebotomy`. Once a Lab Test is created and approved, you can use it when making a order. When you create a Lab Test, it is not immediately available. We need to validate and approve it on our side, before you can use it when making orders. ### Example To begin, make a request to our [`GET /v3/lab_tests/markers`](/api-reference/lab-testing/biomarkers) endpoint, in order to choose the markers you want to test for. ```json Get all markers { "markers": [ { "id": 1, "name": "17-OH Progesterone LCMS", "slug": "17-oh-progesterone-lcms", "description": "17-OH Progesterone LCMS", "lab_id": 1, "provider_id": "070085", "type": null, "unit": null, "price": "N/A" }, { "id": 2, "name": "ABO Grouping", "slug": "abo-grouping", "description": "ABO Grouping", "lab_id": 1, "provider_id": "006056", "type": null, "unit": null, "price": "N/A" } ], "total": 2, "page": 1, "size": 2 } ``` With this information, you can create a lab test using [`POST /v3/lab_tests`](/api-reference/lab-testing/post-test). As an example, let's say you wanted a test from `Labcorp`, with markers with provider ids `006056` and `070085`: ```python Create a new test from vital import Client client = Client(api_key, "sandbox") data = client.LabTests.create_test( name="Example test", description="Example test", method="at_home_phlebomoty", provider_ids=["006056", "070085"] # or use the marker ids e.g marker_ids=[1, 2], ) ``` It is possible to create a test with either the marker id or provider id. Provider ids are the recommended way, since these are shared accross environments. Not all labs provide these, so in their absence, use the marker id. Once your lab test creation is successful you will receive the following response: ```json { "lab_test": { "name": "Example test", "description": "Example test", "sample_type": "serum", "method": "at_home_phlebomoty", "price": 10, "is_active": false, "lab": { "slug": "labcorp", "name": "LabCorp", "first_line_address": "123 Main St", "city": "San Francisco", "zipcode": "91789" }, "markers": [ { "name": "17-OH Progesterone LCMS", "slug": "17-oh-progesterone-lcms", "description": "17-OH Progesterone LCMS" }, { "name": "ABO Grouping", "slug": "abo-grouping", "description": "ABO Grouping" } ] } } ``` Do notice that `is_active` is set to `false`. You can verify its status by calling the [lab tests endpoint](/api-reference/lab-testing/tests). # Lab Test Lifecycle Source: https://docs.tryvital.io/lab/workflow/lab-test-lifecycle ## Introduction We encapsulate our lab test modalities in the `order` object. This is the main object you will interact with, as it contains all the information related to that lab test. An order has a high-level status which represents where the order is throughout its lifecycle, e.g. received/delivered/completed/cancelled etc. The high-level status is the same across test modalities. On top of that, each modality has its own sub-statuses, to represents lifecycle stages specific to that modality. For example, test-kits involve shipping of the box, so we have a whole set of statuses to track the shipment. **Please note**: To ensure future compatibility, we ask that you avoid exhaustive matching on enum values such as an order’s status. We may introduce new statuses (and other enum values) over time, and code that assumes all current values are exhaustive could break or fail to compile with SDK upgrades. To stay compatible and benefit from future enhancements, treat unknown values gracefully—for example, by using default cases or limiting checks to only the values your integration depends on. ## Statuses Statuses are namespaced with the following format: `[HIGH-LEVEL STATUS].[TEST MODALITY].[LOW-LEVEL STATUS]` As an example, the status `collecting_sample.at_home_phlebotomy.appointment_scheduled` means the patient has scheduled an at-home phlebotomy appointment. ### High-level statuses The high-level statuses of an order, are represented by `order.status`: * `received`: we received the order, stored it into our system, and started processing it. * `collecting_sample`: these track collecting the sample from the patient. In the case of test-kits, these track the shipment of the kit. For at-home phlebotomy, these track the patient scheduling an appointment. Walk-in tests don't have this status because the patient can turn up at the lab anytime. * `sample_with_lab`: the lab received the sample and is currently analyzing it. * `completed`: the order is complete and the results are ready. * `cancelled`: the order has been cancelled, by you or the patient. * `failed`: we failed to process the order. Modality specific sub-statuses are encapsulated in the `order.events` list. These include a list of all the events for that test: ```json Test kit order status { ..., "status": "cancelled", "events": [ { "id": 1, "created_at": "2022-01-01T00:00:00Z", "status": "received.testkit.ordered", }, { "id": 2, "created_at": "2022-01-01T00:00:00Z", "status": "received.testkit.requisition_created", }, { "id": 3, "created_at": "2022-01-01T00:00:00Z", "status": "collecting_sample.testkit.transit_customer" }, ], } ``` Below is a full list of all the available statuses by test modality. The names should be self-descriptive. Please do not hesitate to contact us if you need any additional clarification! ### Test-kit statuses * `received.testkit.ordered` * `received.testkit.awaiting_registration` * `received.testkit.testkit_registered` * `received.testkit.requisition_created` * `collecting_sample.testkit.transit_customer` * `collecting_sample.testkit.out_for_delivery` * `collecting_sample.testkit.with_customer` * `collecting_sample.testkit.transit_lab` * `collecting_sample.testkit.problem_in_transit_customer` * `collecting_sample.testkit.problem_in_transit_lab` * `sample_with_lab.testkit.delivered_to_lab` * `completed.testkit.completed` * `failed.testkit.failure_to_deliver_to_customer` * `failed.testkit.failure_to_deliver_to_lab` * `failed.testkit.sample_error` * `failed.testkit.lost` * `cancelled.testkit.cancelled` * `cancelled.testkit.do_not_process` ### Walk-in visit statuses * `received.walk_in_test.ordered` * `received.walk_in_test.requisition_created` * `collecting_sample.walk_in_test.appointment_pending` * `collecting_sample.walk_in_test.appointment_scheduled` * `collecting_sample.walk_in_test.appointment_cancelled` * `sample_with_lab.walk_in_test.partial_results` * `completed.walk_in_test.completed` * `failed.walk_in_test.sample_error` * `cancelled.walk_in_test.cancelled` ### At-home phlebotomy statuses * `received.at_home_phlebotomy.ordered` * `received.at_home_phlebotomy.requisition_created` * `collecting_sample.at_home_phlebotomy.appointment_pending` * `collecting_sample.at_home_phlebotomy.appointment_scheduled` * `collecting_sample.at_home_phlebotomy.draw_completed` * `collecting_sample.at_home_phlebotomy.appointment_cancelled` * `sample_with_lab.at_home_phlebotomy.partial_results` * `completed.at_home_phlebotomy.completed` * `cancelled.at_home_phlebotomy.cancelled` # Ordering a Registrable Testkit Source: https://docs.tryvital.io/lab/workflow/order-registrable-testkit Under the [standard Testkit ordering flow](/api-reference/lab-testing/create-order), you place a Testkit order for a specific patient. This resulting Testkit is bound to that patient, and the Testkit cannot be passed on to someone else. With Registrable Testkits, Vital offers a different option — Testkits not bound to any specific patient can be ordered to a specific household address. The patient registration process is deferred until an actual patient intends to use the Testkit. Note that this only applies to Testkits, not Walk-in Phlebotomy or At-home Phlebotomy. This involves two steps: 1. Ordering the Testkit. 2. Registering a patient. After step 1, the order is sent to the requested address and generates all the same webhooks as a regular order would. However it is stuck on the `received.testkit.awaiting_registration` state, meaning that no requisition form is generated for this order, and no results can be obtained until step 2 is done. After step 2, the order flow resumes as normal, the patient sends the `testkit` to the lab, the order is progressed to the `received.testkit.testkit_registered` state and, again, all the same webhooks as in the regular order flow are dispatched. ### Example To make an order, make a request to our [`POST /v3/order/testkit`](/api-reference/lab-testing/create-unregistered-order), to fulfil step 1. ```python Python from vital import Client client = Client(api_key, "sandbox") data = client.LabTests.create_unregistered_testkit_order( user_id="63661a2b-2bb3-4125-bb1a-b590f64f057f", lab_test_id="5b41f610-ebc5-4803-8f0c-a61c3bdc7faf", shipping_details={ "receiver_name": "john Doe", "street": "123 Main St.", "street_number": "Apt. 208", "city": "San Francisco", "state": "CA", "zip": "91189", "country": "US", "phone_number": "+11234567890" } ) ``` The `testkit` is now ordered, and will be sent to the specified address. Once delivered, it can be kept until it's expiration. Once the `testkit` is with it's intended patient, it should be registered (step 2). In order to register, Vital requires a `sample_id`, which is found within the `testkit` itself. This is an unique identifier. Vital also requires the patient details and address. Besides this, if using Vital's physician network, then supplying the consents field is required. If providing your own physician, then the physician information is required. For this example, we will assume the use of your own physician. To register a `testkit` order, make a request to our [`POST /v3/order/testkit/register/`](/api-reference/lab-testing/register-order), to fulfil step 2. ```python Python client = Client(api_key, "sandbox") data = client.LabTests.register_testkit_order( user_id="63661a2b-2bb3-4125-bb1a-b590f64f057f", sample_id="123123123", patient_details={ "first_name": "John", "last_name": "Doe", "dob": "2020-01-01", "gender": "male", "phone_number": "+1123456789", "email": "email@email.com" }, patient_address={ "receiver_name": "john Doe", "street": "123 Main St.", "street_number": "Apt. 208", "city": "San Francisco", "state": "CA", "zip": "91189", "country": "US" }, physician={ "first_name": "Doctor", "last_name": "Doc", "npi": "123123123" } ) ``` Your `testkit` order is now registered, and the order flow should resume as normal. For more information regarding the lifecycle of a test, refer to the [lab test lifecyle](/lab/workflow/lab-test-lifecycle) page. # Order Requirements Source: https://docs.tryvital.io/lab/workflow/order-requirements ### Consents When you order a lab test through our API, you may have to collect consents from the patient and forward it to us. The following consents may include: * `hipaa-authorization`. * `terms-of-use`. * `telehealth-informed-consent`. ### Physician As mentioned [in the introduction](/lab/overview/introduction#features), you don't need to have your own Physician to place orders with Vital. When you [order a test](/lab/overview/quickstart#3-placing-an-order), you can pass an optional `physician` argument. If provided, the order will use your physician. If not, your order will go through with Vital's physician network. For more information, please check [our lab testing page](https://tryvital.io/labs) and [book an introductory call with us](https://cal.com/team/vital/discovery-call). # Ordering Source: https://docs.tryvital.io/lab/workflow/ordering # Concepts Vital has a series of concepts to grasp regarding ordering: * [Lab Tests](/lab/workflow/create-test) * [Lifecyle of an Order](/lab/workflow/lab-test-lifecycle) * [AoE](/lab/workflow/aoe) * [Registrable Kits](/lab/workflow/order-registrable-testkit) * [Scheduled Orders](/lab/workflow/scheduled-orders) In this document, we will focus on orderable panels/biomarkers. ### Markers [*Markers*](/api-reference/lab-testing/biomarkers) are individual, orderable tests, at the lab level. So, for example, at *Labcorp* you can order a `Lipid Panel` test and a `Vitamin D` test, a `panel` and a `biomarker` respectively. At Vital, these are both refered to as **markers**. ### Lab Tests [*Lab Tests*](/api-reference/lab-testing/post-test) are a collection of markers, or a preset combination of markers, that you can order. Using the example above, you can create a `Labcorp Lipid Panel and Vitamin D` lab test. You can then place orders at Vital using this preset. This is useful for situations where you repeatedly want to order the same markers. # Ordering When placing an [Order](/api-reference/lab-testing/create-order), you will see an `order_set` field. This is what defines what markers will be ordered. There are multiple combinations allowed with this field, so let's explore all of them. ### Ordering a *Lab Test* When ordering from **one** previously created *Lab Test*, the `order_set` field should be populated as follows: ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) client.lab_tests.create_order( order_set=OrderSet( lab_test_ids=["lab_test_id"] ) ... ) ``` ### Ordering from multiple *Lab Tests* You may want to combine two or more existing Lab Tests without creating a new one. This is possible by doing: ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) client.lab_tests.create_order( order_set=OrderSet( lab_test_ids=["lab_test_id1", "lab_test_id_2"] ) ... ) ``` ### Ordering without a *Lab Test* This is what Vital calls `à la carte` ordering. You may want to order from the marker compendium without creating a preset. This is a team level configuration that must be requested from Vital. Not all markers can be ordered in this manner. It is possible to order `a-la-carte` using Vital `marker_ids` or the lab's `provider_id`. ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) client.lab_tests.create_order( order_set=OrderSet( add_on=AddOn( provider_ids=["322022"] # marker_ids=[1] ) ), collection_method="walk_in_test", ... ) ``` ### Order *Lab Tests* with extra *Markers* You may also add extra **markers** to an order. For example, you want to order the `Labcorp Lipid Panel and Vitamin D` but for this particular patient, you also want to order a `CBC Panel`. ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) client.lab_tests.create_order( order_set=OrderSet( lab_test_ids=["lab_test_id"], add_on=AddOn( provider_ids=["322022"] # marker_ids=[1] ) ), collection_method="walk_in_test", ... ) ``` When supplying the `add_on` field, it is always required to provide the `collection_method` field. ## Collection Method Further explored in the documentation, Vital also has the concept of *Collection Method*. Vital currently supports three methods, [`At Home Phlebotomy`](lab/at-home-phlebotomy/overview), [`Walk In Phlebotomy`](lab/walk-in/overview) and [`Testkits`](lab/testkits/overview). When ordering, you must select one of these, either at lab test creation, or at ordering time. ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) client.lab_tests.create_order( order_set=OrderSet( lab_test_ids=["lab_test_id"], ), collection_method="walk_in_test", ... ) ``` ## À La Carte Markers As mentioned above, not all `markers` are `a la carte` orderable. You can find which one's are orderable via the [GET /v3/lab\_tests/markers](/api-reference/lab-testing/biomarkers). ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) markers = client.lab_tests.get_markers( name="322022", a_la_carte_enabled=True ) if not all([m.a_la_carte_enabled for m in markers.markers]): raise Exception("Markers not a_la_carte_enabled") ``` ## Error Cases With the existance of various allowed combinations with the `order_set` field, there's many validations that are done server side. Here are some of the errors you can expect to encounter: ### 400 Bad Request 1. `collection_method must be set if add_on is set`: When the `add_on` field is supplied, you must provide the `collection_method`. 2. `marker_ids or provider_ids must be set in add_on`: One of `marker_ids` or `provider_ids` must be set, if the `add_on` field is provided. 3. `cannot set both marker_ids and provider_ids in add_on`: Similarly, only one of the former fields can be provided. 4. `cannot order lab_tests from multiple labs`: You can only order multiple lab tests from the same lab. 5. `cannot order with lab tests with multiple collection methods`: You must supply a `collection_method` if ordering multiple lab tests with multiple collection methods. # Partial Result Notifications Source: https://docs.tryvital.io/lab/workflow/partials Orders may have partial results, meaning that the lab has made available part of the result, while the full result is not complete. In general, these are short lived and as such, Vital does not expose them via webhooks. Some orders however have long lived partial results, specifically when one ordered marker takes significantly longer to result than the others. In these situations, clients may want to be notified of the existance of partial results. As such, Vital provides a team level configuration that enables the delivery of partial result webhooks. Similarly to other events, these are triggered via a `labtest.order.updated` event in the system. Orders can also experience multiple partials in their lifecycle. In these cases, Vital will send a webhook for each partial update. So if your order experiences two partial results before a final, complete result, you should expect to receive two `labtest.order.updated` webhooks with `partial` status. For example, in the second partial result, you would receive a webhook with the following body: Note that the `events` block contains two `sample_with_lab.walk_in_test.partial_results` events. ```json Walk-in Order Updated { "id": "84d96c03-6b1c-4226-ad8f-ef44a6bc08af", "team_id": "6353bcab-3526-4838-8c92-063fa760fb6b", "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "patient_details": { "dob": "2020-01-01", "gender": "male" }, "patient_address": { "receiver_name": "John Doe", "first_line": "123 Main St.", "second_line": "Apt. 208", "city": "San Francisco", "state": "CA", "zip": "91189", "country": "United States", "phone_number": "+1123456789" }, "details": { "type": "walk_in_test", "data": { "id": "a655f0e4-6405-4a1d-80b7-66f06c2108a7", "created_at": "2020-01-01T00:00:00Z", "updated_at": "2020-01-01T00:00:00Z" } }, "sample_id": "123456789", "notes": "This is a note", "created_at": "2020-01-01T00:00:00Z", "updated_at": "2020-01-01T00:00:00Z", "status": "sample_with_lab", "events": [ { "id": 1, "created_at": "2022-01-01T00:00:00Z", "status": "received.walk_in_test.ordered" }, { "id": 2, "created_at": "2022-01-02T00:00:00Z", "status": "received.walk_in_test.requisition_created" }, { "id": 3, "created_at": "2022-01-03T00:00:00Z", "status": "sample_with_lab.walk_in_test.partial_results" }, { "id": 4, "created_at": "2022-01-04T00:00:00Z", "status": "sample_with_lab.walk_in_test.partial_results" } ] } ``` # Scheduled Orders Source: https://docs.tryvital.io/lab/workflow/scheduled-orders Vital provides a way to schedule your orders for a future date, both via the API and via the Vital Dashboard. A Scheduled order is one that will only be fulfilled in the future, so it won't be created in the Partner Laboratories until the defined date. This is useful for defining follow-up orders after placing an initial Lab Order for a patient. ## Ordering through the API To place a scheduled order through the API, you should use the [Create Order endpoint](/api-reference/lab-testing/create-order), passing the `activate_by` date parameter. The `activate_by` parameter defines when that order is scheduled for, and it will move from the `ordered` to the `requisition_created` status when the date arrives. If you want to query all placed orders, you may use [Get Orders endpoint](/api-reference/lab-testing/get-orders), passing an `order_activation_types` query parameter, which accept the following values `["current", "scheduled"]`. * If the parameter is not provided, every order is returned. * If `current` is provided, orders with a `null` `activate_by` date or with `activate_by` set before the current date are returned. * If `scheduled` is provided, orders with an `activate_by` date greater than the current date are returned. Placing a scheduled order for 6 of January of 2025 would look like this: ```json Placing a Scheduled Order request { "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "patient_details": { "dob": "2022-07-06T22:20:26.796Z", "gender": "male | female", "email": "test@test.com" }, "patient_address": { "receiver_name": "John Doe", "street": "Hazel Road", "street_number": "102", "city": "San Francisco", "state": "CA", "zip": "91789", "country": "U.S.", "phone_number": "+14158180852" }, "lab_test_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "physician": { "first_name": "John", "last_name": "Doe", "email": "john@doe.com", "npi": "123456789", "licensed_states": ["CA", "NY"], "created_at": "2022-07-06T22:20:26.796Z", "updated_at": "2022-07-06T22:20:26.796Z" }, "activate_by": "2025-01-06" } ``` ## Ordering through the Vital Dashboard To place a Scheduled Order through the dashboard, just follow the normal procedure until you reach the Lab Testing selection screen. Notice that there is a checkbox for defining this order as scheduled for the future, after the checkbox is selected you can pass in the desired scheduled date. After that, you can filter out the `Scheduled` and `Current` orders in the dashboard using the filter in the orders listing. # Provider Types Source: https://docs.tryvital.io/wearables/connecting-providers/auth_types Providers have different authentication flows. Some require a username and password, others require an email address and password. Vital supports the following provider types: * `OAuth` * `Email` * `Email + Password` The majority of providers are `OAuth` providers. These providers require a user to be redirected to a third party website to authenticate. Once authenticated, the user is redirected back to the link widget. The `Email` and `Email + Password` providers require a user to enter their email address and password into the link widget. This is then sent to Vital and used to connect to the provider. The list of providers and their oauth types are as follows: ### 'OAuth' Current OAuth providers are: | Provider | Description | | ------------ | ------------------------------------------------------------- | | `Fitbit` | Activity Trackers (all devices) | | `Garmin` | Fitness watches (all devices) | | `Oura` | Smart Sleep tracking ring | | `Strava` | Running & Cycling Social Network | | `Wahoo` | Cycling Equipment | | `iHealth` | Blood pressure, Fitness scales, Glucometers & Fitness watches | | `Withings` | Fitness scales, watches and health monitors | | `Google Fit` | Activity Trackers (all devices) | | `Polar` | Finnish sports tech pioneer | | `Cronometer` | Nutrition data | | `Omron` | Blood Pressure data | | `WHOOP` | Smart Activity Watches | | `Dexcom` | Glucose monitors | To connect an oauth provider: ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; import { LinkTokenExchange, LinkGenerateOauthLinkRequest, OAuthProviders } from '@tryvital/vital-node/api'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const request: LinkTokenExchange = { userId: "", } const tokenResponse = await client.link.token(request) const linkRequest: LinkGenerateOauthLinkRequest = { vitalLinkToken: tokenResponse.linkToken, } const auth = await client.link.generateOauthLink( OAuthProviders., linkRequest ) ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment from vital.types.o_auth_providers import OAuthProviders client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) token_response = client.link.token(user_id="") oauth = client.link.generate_oauth_link( oauth_provider=OAuthProviders., vital_link_token=token_response.link_token, ) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.link.requests.LinkGenerateOauthLinkRequest; import com.vital.api.resources.link.requests.LinkTokenExchange; import com.vital.api.types.OAuthProviders; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); LinkTokenExchange request = LinkTokenExchange .builder() .userId("") .build(); var token_response = vital.link().token(request); LinkGenerateOauthLinkRequest link_request = LinkGenerateOauthLinkRequest .builder() .vitalLinkToken(token_response.getLinkToken()) .build(); var oauth = vital.link().generateOauthLink(OAuthProviders.OURA, link_request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) provider := vital.ProvidersOura request := &vital.LinkTokenExchange{ UserId: "", Provider: &provider, } response, err := client.Link.Token(context.TODO(), request) if err != nil { return err } generateLinkRequest := &vital.LinkGenerateOauthLinkRequest{ VitalLinkToken: response.LinkToken, } oauth, err := client.Link.GenerateOauthLink(context.TODO(), vital.OAuthProvidersOura, generateLinkRequest) if err != nil { return err } fmt.Printf("Received data %s\n", oauth) ``` For `OAuth` Providers we return an `oauth_url` that can be used to redirect users to. In a web view, on redirection, you should check the user has connected to the provider successfully. In the case of mobile, the user should receive a message to return to the app. The possible error codes that are returned are as follows: * `401 INVALID_REQUEST` Link Token is Invalid * `400 MISSING_LINK_TOKEN` Missing link token * `400 INVALID_PROVIDER` Provider is not supported * `400 INVALID_USER_ID` User id is incorrect * `400 INVALID_CREDENTIALS` Credentials for provider are incorrect ### 'Email' Current email providers are: | Provider | Description | | ----------- | -------------------------- | | `Freestyle` | Abbott CGM Glucose monitor | ### 'Email + Password' Current email and password providers are: | Provider | Description | | ------------------- | ---------------------------------- | | `Renpho` | Fitness Scales | | `Zwift` | Virtual cycling and running | | `Peloton` | Popular Indoor Exercise bike | | `Eight Sleep` | Smart Mattress | | `Beurer` | Blood pressure and glucose devices | | `Hammerhead` | Cycling Computer | | `Dexcom G6 & Older` | CGM Glucose Monitor | | `MyFitnessPal` | Meal Tracking Application | | `Kardia` | EKG Application | To connect a email and password provider: ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; import { IndividualProviderData, PasswordProviders } from '@tryvital/vital-node/api'; const request: IndividualProviderData = { username: '', password: '', } const data = await client.link.connectPasswordProvider(PasswordProviders., request) ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment from vital.types.password_providers import PasswordProviders client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.link.connect_password_provider( PasswordProviders., username="", password="" ) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.link.requests.IndividualProviderData; import com.vital.api.types.PasswordProviders; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); IndividualProviderData request = IndividualProviderData .builder() .username("") .password("") .build(); var data = vital.link().connectPasswordProvider(PasswordProviders., request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) request := &vital.IndividualProviderData{ Username: "", Password: "", } response, err := client.Link.ConnectPasswordProvider( context.TODO(), vital.PasswordProviders, request ) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` # Bring Your Own OAuth Source: https://docs.tryvital.io/wearables/connecting-providers/bring-your-own-oauth/overview Bring Your Own OAuth (BYOO) is available for [the Grow and Scale plans](https://tryvital.io/pricing). WHOOP BYOO is available for [the Launch, Grow and Scale plans](https://tryvital.io/pricing). See [our WHOOP Guide](/wearables/guides/whoop) for more information. Vital supports bringing your own OAuth application credentials, so that your user would see the OAuth consent flow in your own brand when they connect to cloud-based OAuth providers. This provides a more cohesive experience for your users as it gives a truly whitelabelled experience for your application/mobile application. Typically when you don't use custom credentials your oauth screen will look like this and used our shared credentials: ![Default Credentials](https://mintlify.s3.us-west-1.amazonaws.com/vital/img/default_credentials.png) With custom credentials you can use your own team name and setup your own logo for the provider. ## OAuth Providers supporting BYOO Some BYOO providers do not have a developer program with open sign-up. | Provider | Slug | Remarks | | --------------------------------------------------- | ------------------- | --------------------------------------- | | [Fitbit](https://www.fitbit.com/global/uk/home) | `fitbit` | - | | [Garmin](https://www.garmin.com) | `garmin` | - | | [Google Fit](https://www.google.com/fit/) | `google_fit` | - | | [Oura](https://ouraring.com) | `oura` | - | | [Strava](https://www.strava.com) | `strava` | - | | [Wahoo](https://wahoofitness.com) | `wahoo` | - | | [Withings](https://www.withings.com) | `withings` | - | | [Dexcom](https://www.dexcom.com) | `dexcom_v3` | [↗️ Guide](/wearables/guides/dexcom_v3) | | [WHOOP](https://www.whoop.com) | `whoop_v2` | [↗️ Guide](/wearables/guides/whoop) | | [Polar](https://www.polar.com/accesslink-api/) | `polar` | - | | [Cronometer](https://www.cronometer.com) | `cronometer` | - | | [Omron](https://www.omron-healthcare.com) | `omron` | - | | [MyFitnessPal API](https://www.myfitnesspalapi.com) | `my_fitness_pal_v2` | - | | [MapMyFitness](https://developer.mapmyfitness.com) | `map_my_fitness` | - | | [Ultrahuman](https://www.ultrahuman.com) | `ultrahuman` | - | ### Provider Rate Limits This section documents the *starting* rate limit of a brand new OAuth application, should you choose to BYOO. | Provider | Starting Rate Limit | | ----------------------------------------------- | ----------------------------------------------------------------------------------- | | [Fitbit](https://www.fitbit.com/global/uk/home) | 150 requests per hour per user | | [Garmin](https://www.garmin.com) | 10,000 days of data per minute | | [Google Fit](https://www.google.com/fit/) | 600 requests per minute | | [Oura](https://ouraring.com) | 5,000 requests per 5 minutes | | [Strava](https://www.strava.com) | 100 requests per 15 minutes
    1,000 requests per day | | [Wahoo](https://wahoofitness.com) | 200 requests per 5 minutes
    1,000 requests per hour
    5,000 requests per day | | [Withings](https://www.withings.com) | 120 requests per minute | | [Dexcom](https://www.dexcom.com) | 60,000 requests per hour | | [WHOOP](https://www.whoop.com) | 100 requests per minute
    10,000 requests per day | | [Polar](https://www.polar.com/accesslink-api/) | 20 requests per 15 minutes per user
    100 requests per day per user | | [Cronometer](https://www.cronometer.com) | N/A | | [Omron](https://www.omron-healthcare.com) | N/A | ## Setting up your OAuth credentials ### Through the Org Management API Use the [Prepare Team Custom Credentials](/api-reference/org-management/team-custom-credentials/prepare-team-custom-credentials) endpoint to obtain the instructions to setup your OAuth application. Use the [Set Team Custom Credentials](/api-reference/org-management/team-custom-credentials/upsert-team-custom-credentials) endpoint to apply your OAuth application credentials to your Vital Team. BYOO credentials for the following providers are immediately active, once set through the [Set Team Custom Credentials](/api-reference/org-management/team-custom-credentials/upsert-team-custom-credentials): | Provider | Slug | | --------------------------------------------------- | ------------------- | | [Garmin](https://www.garmin.com) | `garmin` | | [Google Fit](https://www.google.com/fit/) | `google_fit` | | [Oura](https://ouraring.com) | `oura` | | [Strava](https://www.strava.com) | `strava` | | [Wahoo](https://wahoofitness.com) | `wahoo` | | [Withings](https://www.withings.com) | `withings` | | [Dexcom](https://www.dexcom.com) | `dexcom_v3` | | [WHOOP](https://www.whoop.com) | `whoop_v2` | | [Polar](https://www.polar.com/accesslink-api/) | `polar` | | [Cronometer](https://www.cronometer.com) | `cronometer` | | [Omron](https://www.omron-healthcare.com) | `omron` | | [Ultrahuman](https://www.ultrahuman.com) | `ultrahuman` | | [MyFitnessPal API](https://www.myfitnesspalapi.com) | `my_fitness_pal_v2` | | [MapMyFitness](https://developer.mapmyfitness.com) | `map_my_fitness` | The following providers are not immediately active. You need to take a couple of extra steps to activate them: [Fitbit](https://www.fitbit.com/global/uk/home) (`fitbit`) 1. You must press the "Verify" button on all the Webhook Subscribers in the Fitbit Developer Portal. 2. Once the buttons are gone and replaced by (usually zeroed) delivery statistics, it implies that Fitbit has completed the verification challenge with Vital. ### Through Vital Support To setup custom OAuth application credentials, you need to do the following: 1. Create a developer account with the cloud-based OAuth providers. 2. Generate a OAuth application credentials from your developer account. You can do this by going to the providers developer accounts page and creating a new application. 3. Email [Support@tryvital.io](mailto:Support@tryvital.io) or message us on Slack with the following information: * The provider you want to customize * The client ID and client secret for the provider * Use tools like [Doppler Secret Share](https://share.doppler.com) or [1Password](https://1password.com) to protect your credentials. * The redirect URI for the provider * The name of your team * The logo for your team 4. We will then setup the custom credentials for you and let you know when it's ready to go. Some Oauth providers are unavailable unless you can Bring Your Own OAuth (BYOO) — you will have to apply directly to the provider for an OAuth application, and provide Vital your assigned credentials once the application is accepted. # Sharing Credentials Source: https://docs.tryvital.io/wearables/connecting-providers/bring-your-own-oauth/sharing-credentials ## Audience This guide is dedicated to Vital customers looking to share the usage of their OAuth Client Credentials between Vital [Bring Your Own OAuth](/wearables/connecting-providers/bring-your-own-oauth/overview) and their existing production systems. This guide is not applicable if you: * do not have existing OAuth Client Credentials; or * can configure your OAuth Client Credentials to point exclusively at Vital. ## Background Many providers use OAuth 2.0, in which each partner entity is registered as a confidential application. Each application is issued one set of OAuth client credentials (client ID + secret). As a Cross-Site Request Forgery (CSRF) mitigation, they also typically require every application to pre-register a Redirect URI. The `/authorize` flow would then validate inbound requests against the Redirect URI on record. ## Challenge Most providers allow **only one** Redirect URI registration per OAuth application. This means your OAuth application cannot simutaneously support both your own existing OAuth callback endpoint, as well as [Vital Link](/wearables/connecting-providers/introduction) OAuth callback endpoint. ## Recommended Apporach To support this credential sharing use case, Vital recommends you to: 1. Continue to point your OAuth application at your existing OAuth callback endpoint; 2. Extend your endpoint to [detect and redirect](#details) callbacks from Vital Link originiated OAuth requests to the [Vital Link OAuth callback endpoint](#vital-oauth-callback-endpoint-url). ```mermaid sequenceDiagram participant Vital Link participant Your API participant BYOO Provider actor User Agent autonumber Vital Link-->>User Agent: OAuth 2.0 Authorization Request URL User Agent->>BYOO Provider: Navigate to URL (1) BYOO Provider-->>User Agent: Prompt User Agent-->>BYOO Provider: Approve or Deny BYOO Provider->>Your API: Redirect Your API->>Vital Link: Redirect ``` ### Details The Redirect URI in your OAuth application settings should continue to point at your existing OAuth callback endpoint. Your OAuth callback endpoints can rely on the `state` query parameter to differentiate the request origin, i.e., whether it is from Vital or from your own production systems. OAuth requests originated from Vital would have the **Vital Link Token** as the `state` query parameter. Vital Link Token is a JSON Web Token (JWT), so you can use the *unverified claims* of the JWT as a discriminator. When you detect a valid Vital Link Token, perform a `307 Temporary Redirect` to the [Vital OAuth callback endpoint](#vital-oauth-callback-endpoint-url) and passing on *all* the URL query parameters. The exact JWT structure of the Vital Link Token is as follows: A JSON Web Token (JWT). You need not verify the signature of this JWT. To prevent Cross-Site Request Forgery attacks, you must still check that the `aud` claim matches the expected [Vital Link API Base URL](#vital-link-api-base-url). | Key | Value | | --------- | ----------------------- | | `aud` | Vital Link API Base URL | | `sub` | Vital Link Session ID | | `team_id` | Vital Team ID | | `user_id` | Vital User ID | | Environment | Base URL | | ------------- | -------------------------------------------- | | Production US | `https://api.tryvital.io/v2/link` | | Production EU | `https://api.eu.tryvital.io/v2/link` | | Sandbox US | `https://api.sandbox.tryvital.io/v2/link` | | Sandbox EU | `https://api.sandbox.eu.tryvital.io/v2/link` | | Environment | Base URL | | ------------- | --------------------------------------------------------------- | | Production US | `https://api.tryvital.io/v2/link/connect/{PROVIDER}` | | Production EU | `https://api.eu.tryvital.io/v2/link/connect/{PROVIDER}` | | Sandbox US | `https://api.sandbox.tryvital.io/v2/link/connect/{PROVIDER}` | | Sandbox EU | `https://api.sandbox.eu.tryvital.io/v2/link/connect/{PROVIDER}` | ```python Python (FastAPI) import fastapi from urllib.parse import urlencode VITAL_LINK_BASE_URL = "https://api.tryvital.io/v2/link" @router.get("/oauth_callback/fitbit") def handle_oauth_callback( request: fastapi.Request, state: Annotated[str, fastapi.Query()], ..., ) -> fastapi.Response: state_string = base64.b64decode(state) # Test if this is a JWT. # If so, try to extract the unverified claim payload, and see if this is # a Vital Link JWT. if ( state_string.startswith('{"') and (state_parts := state_string.split(".", maxsplit=3)) and len(state_parts) == 3 and (unverified_claims := json.loads(state_parts[1])) and unverified_claims.get("aud") == VITAL_LINK_BASE_URL ): query = urlencode(request.query_params) return fastapi.RedirectResponse(f"{VITAL_LINK_BASE_URL}/connect/fitbit?{query}") # Not a Vital Link JWT. Fallback to existing logic return process_oauth_callback(...) ``` When you set your OAuth Client Credential through the [Set Team Custom Credentials](api-reference/org-management/team-custom-credentials/upsert-team-custom-credentials) endpoint, you must specify a Redirect URI override that points at your your OAuth callback endpoint. When a Redirect URI override is present, Vital uses the override value you provided to initiate the OAuth authorization flow. # Building a Custom Widget Source: https://docs.tryvital.io/wearables/connecting-providers/custom_widget Vital Link API covers **only** [cloud-based providers](/wearables/providers/introduction#cloud-based-providers). Your mobile app has to manage connections with SDK-based providers — such as Apple HealthKit and Android Health Connect — through the [Vital Mobile Health SDK](/wearables/sdks/vital-health), separately from Vital Link. You may want to have your own custom Link widget to match your app's UI. This is useful for customers who don't want to use Vital link but still want to use the Vital API. It's worthwhile ensuring that you have read the [Auth Types](/docs/auth-types) documentation before proceeding. A full code example in React Native can be seen [here](https://github.com/tryVital/vital-connect-rn). An overview of the process is as follows: 1. Get a list of all available providers ["/v2/providers"](/docs/api-reference/providers) 2. Build your UI using this list of providers 3. When selecting a provider, generate a link token for that provider ["/v2/link/token"](/docs/api-reference/link/generate-link-token) 4. Use the link token to generate an oauth link, or authorize an email, or email and password. ## 1. List of all available providers The first step to creating a custom widget is to make a call to the [providers endpoint](/docs/api-reference/providers). This will return a list of all available providers. You can then use this list to build your UI. ```javascript Node import { VitalClient, VitalEnvironment } from "@tryvital/vital-node"; import { LinkTokenExchange, LinkGenerateOauthLinkRequest, OAuthProviders, } from "@tryvital/vital-node/api"; const client = new VitalClient({ apiKey: "", environment: VitalEnvironment.Sandbox, }); const allProviders = await client.providers.getAll(); // allProviders // [ // { // "name": "Oura", // "slug": "oura", // "logo": "https://logo_url.com", // "auth_type": "oauth", // "supported_resources": ["workouts", "sleep"] // } // ] ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment from vital.types.o_auth_providers import OAuthProviders client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) all_providers = client.providers.get_all() # allProviders # [ # { # "name": "Oura", # "slug": "oura", # "logo": "https://logo_url.com", # "auth_type": "oauth", # "supported_resources": ["workouts", "sleep"] # } # ] ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.link.requests.LinkGenerateOauthLinkRequest; import com.vital.api.resources.link.requests.LinkTokenExchange; import com.vital.api.types.OAuthProviders; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); var all_providers = vital.providers().getAll() ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) response, err := client.Providers.getAll(context.TODO()) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` ## 2. Build your UI in your app Once you have a list of all available providers, you can build your UI. You can use the `logo` field to display the provider's logo. You can use the `name` field to display the provider's name. You can use the `supported_resources` field to display the resources that the provider supports. Custom UI ## 3. Generate a link token Once a user has selected a provider, you can generate a link token. This link token is used to generate an oauth link, or authorize an email, or email and password. You can read more about the link token [here](/docs/api-reference/link#link-token-exchange). ```javascript Node import { VitalClient, VitalEnvironment } from "@tryvital/vital-node"; import { LinkTokenExchange, LinkGenerateOauthLinkRequest, OAuthProviders, } from "@tryvital/vital-node/api"; const client = new VitalClient({ apiKey: "", environment: VitalEnvironment.Sandbox, }); const request: LinkTokenExchange = { userId: "", }; const tokenResponse = await client.link.token(request); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment from vital.types.o_auth_providers import OAuthProviders client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) token_response = client.link.token(user_id="") ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.link.requests.LinkGenerateOauthLinkRequest; import com.vital.api.resources.link.requests.LinkTokenExchange; import com.vital.api.types.OAuthProviders; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); LinkTokenExchange request = LinkTokenExchange .builder() .userId("") .build(); var token_response = vital.link().token(request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) provider := vital.ProvidersOura request := &vital.LinkTokenExchange{ UserId: "", Provider: &provider, } response, err := client.Link.Token(context.TODO(), request) fmt.Printf("Received data %s\n", response) ``` ## 4. Link the user to the provider Once you have a link token, you can use it to generate an oauth link, or authorize an email, or email and password. You can read more about the link token [here](/docs/api-reference/link#link-token-exchange). ### Connect an oauth provider ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; import { LinkTokenExchange, LinkGenerateOauthLinkRequest, OAuthProviders } from '@tryvital/vital-node/api'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const request: LinkTokenExchange = { userId: "", } const tokenResponse = await client.link.token(request) const linkRequest: LinkGenerateOauthLinkRequest = { vitalLinkToken: tokenResponse.linkToken, } const auth = await client.link.generateOauthLink( OAuthProviders., linkRequest ) ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment from vital.types.o_auth_providers import OAuthProviders client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) token_response = client.link.token(user_id="") oauth = client.link.generate_oauth_link( oauth_provider=OAuthProviders., vital_link_token=token_response.link_token, ) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.link.requests.LinkGenerateOauthLinkRequest; import com.vital.api.resources.link.requests.LinkTokenExchange; import com.vital.api.types.OAuthProviders; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); LinkTokenExchange request = LinkTokenExchange .builder() .userId("") .build(); var token_response = vital.link().token(request); LinkGenerateOauthLinkRequest link_request = LinkGenerateOauthLinkRequest .builder() .vitalLinkToken(token_response.getLinkToken()) .build(); var oauth = vital.link().generateOauthLink(OAuthProviders.OURA, link_request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) provider := vital.ProvidersOura request := &vital.LinkTokenExchange{ UserId: "", Provider: &provider, } response, err := client.Link.Token(context.TODO(), request) if err != nil { return err } generateLinkRequest := &vital.LinkGenerateOauthLinkRequest{ VitalLinkToken: response.LinkToken, } oauth, err := client.Link.GenerateOauthLink(context.TODO(), vital.OAuthProvidersOura, generateLinkRequest) if err != nil { return err } fmt.Printf("Received data %s\n", oauth) ``` ### Connact an Email/Password provider ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; import { IndividualProviderData, PasswordProviders } from '@tryvital/vital-node/api'; const request: IndividualProviderData = { username: '', password: '', } const data = await client.link.connectPasswordProvider(PasswordProviders., request) ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment from vital.types.password_providers import PasswordProviders client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.link.connect_password_provider( PasswordProviders., username="", password="" ) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.link.requests.IndividualProviderData; import com.vital.api.types.PasswordProviders; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); IndividualProviderData request = IndividualProviderData .builder() .username("") .password("") .build(); var data = vital.link().connectPasswordProvider(PasswordProviders., request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) request := &vital.IndividualProviderData{ Username: "", Password: "", } response, err := client.Link.ConnectPasswordProvider( context.TODO(), vital.PasswordProviders, request ) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` ### Bringing this all together Below is an example of how you can bring all of this together to build your own custom widget. This example is in React Native, but the same principles apply to any language. ```javascript React Native import React, {useEffect, useState} from 'react'; import { SafeAreaView, StatusBar, Linking, View, useColorScheme, Platform, } from 'react-native'; import {VitalHealth, VitalResource} from '@tryvital/vital-health-react-native'; const handleOAuth = async (userId: string, item: Provider) => { const linkToken = await Client.Link.getLinkToken( userId, `${AppConfig.slug}://link`, ); const link = await Client.Providers.getOauthUrl(item.slug, linkToken.link_token); Linking.openURL(link.oauth_url); }; const ListItem = ({ userId, item, navigation, }: { userId: string; item: Provider; navigation: any; }) => { const {colors} = useTheme(); const isDarkMode = useColorScheme() === 'dark'; const [isLoading, setLoading] = useState(false); const handleNativeHealthKit = async () => { const providerSlug = Platform.OS == 'ios' ? ManualProviderSlug.AppleHealthKit : ManualProviderSlug.HealthConnect; setLoading(true); const isHealthSDKAvailable = await VitalHealth.isAvailable(); if (!isHealthSDKAvailable) { console.warn('Health Connect is not available on this device.'); navigation.navigate('ConnectionCallback', { state: 'failed', provider: providerSlug, }); return; } try { await VitalHealth.configure({ logsEnabled: true, numberOfDaysToBackFill: 30, androidConfig: {syncOnAppStart: true}, iOSConfig: { dataPushMode: 'automatic', backgroundDeliveryEnabled: true, }, }); } catch (e) { setLoading(false); console.warn(`Failed to configure ${providerSlug}`, e); navigation.navigate('ConnectionCallback', { state: 'failed', provider: providerSlug, }); } await VitalHealth.setUserId(userId); try { await VitalHealth.askForResources([ VitalResource.Steps, VitalResource.Activity, VitalResource.HeartRate, VitalResource.Sleep, VitalResource.Workout, VitalResource.BloodPressure, VitalResource.Glucose, VitalResource.Body, VitalResource.Profile, VitalResource.ActiveEnergyBurned, VitalResource.BasalEnergyBurned, ]); await VitalCore.createConnectedSourceIfNotExist(providerSlug); setLoading(false); navigation.navigate('ConnectionCallback', { state: 'success', provider: providerSlug, }); } catch (e) { setLoading(false); navigation.navigate('ConnectionCallback', { state: 'failed', provider: providerSlug, }); } }; const onPress = async () => { if (item.auth_type === 'oauth') { await handleOAuth(userId, item); } else if ( item.slug === 'apple_health_kit' || item.slug === 'health_connect' ) { await handleNativeHealthKit(); } }; return ( onPress()}> {({isHovered, isFocused, isPressed}) => { return ( {item.name} {item.description} ); }} ); }; export const LinkDeviceScreen = ({navigation}) => { const isDarkMode = useColorScheme() === 'dark'; const {colors} = useTheme(); const styles = makeStyles(colors); const [providers, setProviders] = React.useState([]); const [devices, setDevices] = React.useState([]); const [loading, setLoading] = React.useState(false); const [searchText, setSearchText] = React.useState(''); const [error, setError] = React.useState(null); const [userId, setUserId] = React.useState(''); const handleSearch = (text: string) => { setSearchText(text); if (text === '' && text.length <= 2) { setDevices(providers); } else { const filtered = providers.filter(item => item.name.toLowerCase().includes(text.toLowerCase()), ); setDevices(filtered); } }; useEffect(() => { const getDevices = async () => { setLoading(true); setError(null); const user_id = await getData('user_id'); if (user_id) { const providers = await Client.Providers.getProviders(); setLoading(false); setUserId(user_id); setProviders(providers); } else { setUserId(''); setLoading(false); console.warn('Failed to get all supported providers'); setError('Failed to get devices'); } }; }, [navigation]); return (

    Connect a Device

    {!loading && error ? ( Failed to get supported Providers ) : ( ( )} keyExtractor={item => item.slug} /> )}
    ); }; ``` # Link Errors Source: https://docs.tryvital.io/wearables/connecting-providers/errors ## Error delivery ### URL Query Vital performs a 307 Temporary Redirect to the `redirect_url` specified in your Link Token in the following scenarios: * All connections via [the Vital Link widget](/wearables/connecting-providers/introduction) * [Connect OAuth Provider](/api-reference/link/link-oauth-provider) endpoint used by your [Custom Widget](/wearables/connecting-providers/custom_widget) Error parameters would be appended to the `redirect_url` before the redirection. For example, given a redirect\_url of `https://example.com/vital/callback`: ``` https://example.com/vital/callback ?state=error &error_type=provider_credential_error &error=Invalid%20username%20or%20password ``` ### JSON Response Vital responds 200 OK with a JSON response in the following scenarios: * [Connect Password Provider](/api-reference/link/link-password-provider) endpoint used by your [Custom Widget](/wearables/connecting-providers/custom_widget) * [Connect Email Provider](/api-reference/link/link-email-provider) endpoint used by your [Custom Widget](/wearables/connecting-providers/custom_widget) Error parameters would be embedded into the JSON response. For example: ```json { "state": "error", "error_type": "provider_credential_error", "error": "Invalid username or password" } ``` ## Error Parameters Constant: `error` One of the [Link Error Types](#error-types). A human-readable error explanation for troubleshooting purposes. Your application logic should not rely on this message. ## Error Types This list is **non-exhaustive**. Your application logic must gracefully handle any unrecognized error type. | Error Type | Description | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `invalid_token` | The Link Token is invalid or malformed. | | `token_expired` | The Link Token has expired. | | `token_consumed` | A successful connection establishment had consumed this Link Token. | | `provider_credential_error` | OAuth: An error occurred during the provider authentication flow. Password & Email: The supplied user credential is invalid. | | `provider_api_error` | The credential is valid, but an error occurred on an API call required by Vital to fully establish the connection. | | `unsupported_region` | The region you specified is not supported. Freestyle Libre only. | | `duplicate_connection` | The provider credential has an existing connection to a different user in your team. Applicable only if you have opted in to the `reject_duplicate_connection` team setting. | | `required_scopes_not_granted` | User did not grant **all** required scopes, so Vital cannot establish the connection. | | `incorrect_mfa_code` | The MFA code entered is incorrect. | # Introduction to Link Source: https://docs.tryvital.io/wearables/connecting-providers/introduction Vital Link enables your users to connect their accounts with [supported cloud-based providers](https://docs.tryvital.io/wearables/providers/introduction) to Vital. Vital Link is offered in two forms: * [Vital Link Widget](/wearables/connecting-providers/link_flow) is a web flow hosted by Vital, providing a complete provider connection wizard UX that is ready to integrate. * [Vital Link API](/wearables/connecting-providers/custom_widget) allows you to build your own Link Widget like user experience. Vital Link API covers **only** [cloud-based providers](/wearables/providers/introduction#cloud-based-providers). Your mobile app has to manage connections with SDK-based providers — such as Apple HealthKit and Android Health Connect — through the [Vital Mobile Health SDK](/wearables/sdks/vital-health), separately from Vital Link. ![link-gif](https://storage.googleapis.com/vital-assets/vitalLink.gif) # Launching Link Source: https://docs.tryvital.io/wearables/connecting-providers/launching_link Vital Link API covers **only** [cloud-based providers](/wearables/providers/introduction#cloud-based-providers). Your mobile app has to manage connections with SDK-based providers — such as Apple HealthKit and Android Health Connect — through the [Vital Mobile Health SDK](/wearables/sdks/vital-health), separately from Vital Link. Create a Vital Link Token for a user using the [Create Link Token](/api-reference/link/generate-link-token) endpoint. Vital Link Token expires 60 minutes after its creation.
    ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; import { LinkTokenExchange } from '@tryvital/vital-node/api'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const request: LinkTokenExchange = { userId: "", provider: "oura", } const data = await client.link.token(request) ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment from vital.types.providers import Providers client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.link.token(user_id="", provider=Providers.OURA) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.link.requests.LinkTokenExchange; import com.vital.api.types.Providers; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); LinkTokenExchange request = LinkTokenExchange .builder() .userId("") .provider(Providers.OURA) .build(); var data = vital.link().token(request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) provider := vital.ProvidersOura request := &vital.LinkTokenExchange{ UserId: "", Provider: &provider, } response, err := client.Link.Token(context.TODO(), request) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` ```bash cURL curl --request POST \ --url {{BASE_URL}}/v2/link/token \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' --header 'x-vital-api-key: ' ```
    Use the Link Token to [launch the Link Widget](/wearables/vital-link/link_flow) on your frontend. {" "} You can launch the Vital Link Widget using the Link Web URL (`link_web_url`) as provided by the [Generate a Link Token endpoint](/api-reference/link/generate-link-token): * If you have created the Link Token for a specific OAuth provider, Vital Link will dispatch the user automatically to the provider's sign-in page. * Otherwise, your user will be presented with a list of providers to connect to. You can customize the list via the `filter_on_providers` parameter when creating the Link Token. When the Link flow completes, the flow will redirect to the `redirect_url` you specified earlier when creating the Link Token. We will also append query parameters to the redirection target based on the Link flow outcome: | Flow state | Query parameters | | ---------- | ---------------------------------------------------------------------------------- | | Success | `state=success` | | Error | See [Link Errors](https://docs.tryvital.io/wearables/connecting-providers/errors). | ```js Launching Vital-Link import 'react-app-polyfill/ie11'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { useCallback } from 'react'; import { useVitalLink } from '@tryvital/vital-link'; import { useState } from 'react'; const API_URL = process.env.API_URL ? process.env.API_URL : "http://localhost:3001" const getTokenFromBackend = async (userKey: string, env: string) => { const resp = await fetch(); return await resp.json(); }; const App = props => { const userKey = '560596b9-924b-4af9-a670-cf21870fdac5'; const [isLoading, setLoading] = useState(false); const onSuccess = useCallback(metadata => { // Device is now connected. }, []); const onExit = useCallback(metadata => { // User has quit the link flow. }, []); const onError = useCallback(metadata => { // Error encountered in connecting device. }, []); const config = { onSuccess, onExit, onError, env: "sandbox" }; const { open, ready, error } = useVitalLink(config); const handleVitalOpen = async () => { setLoading(true); const token = await getTokenFromBackend(userKey, "sandbox"); open(token); setLoading(false); }; return ( ); }; ReactDOM.render(, document.getElementById('root')); ```
    # Link Flow Source: https://docs.tryvital.io/wearables/connecting-providers/link_flow Vital Link API covers **only** [cloud-based providers](/wearables/providers/introduction#cloud-based-providers). Your mobile app has to manage connections with SDK-based providers — such as Apple HealthKit and Android Health Connect — through the [Vital Mobile Health SDK](/wearables/sdks/vital-health), separately from Vital Link. The Vital Link flow starts when a user wants to connect a wearable device with Vital. An overview of the process can be found below. Make sure that: 1. You have your Vital API Key [dashboard](https://app.tryvital.io) ready to use. 2. You have created a Vital User that corresponds to a user in your domain. [See](/home/quickstart#3-creating-your-first-user) for an example. Call the [Generate a Link Token endpoint](/api-reference/link/generate-link-token) with the target User ID using your Vital API Key. The API response provides a short-lived Link Token (`link_token`), as well as a Web URL (`link_web_url`). You can use the Web URL to launch the Vital Link Widget directly. Alternatively, you can use the Link Token in concert with the Vital Link API to [build your custom Link Widget experience](/wearables/connecting-providers/custom_widget). # Abbott LibreView Source: https://docs.tryvital.io/wearables/guides/abbott-libreview For Abbott CGMs, e.g. Freestyle Libre. ## Overview Vital supports two ways to connect to LibreView for Abbott CGMs: | Provider Slug | Remarks | | ------------------ | ------------------------------------------------------------------------------ | | `abbott_libreview` | Connect via LibreView patient account in any region. | | `freestyle_libre` | Connect via regional LibreView practices (Vital-managed, or custom practices). | ### `freestyle_libre` for practice-based connections With the `freestyle_libre` integration, users have to share/connect their LibreView patient account with either Vital's LibreView practice, or a custom practice of yours. Vital accesses the patient CGM data as a member of the LibreView practice. ### `abbott_libreview` for patient-based connections With the `abbott_libreview` integration, users can connect their Abbott CGM devices simply by signing in with their LibreView patient account. Vital accesses the patient CGM data as their personal account. * You would not need to maintain any LibreView practices. * You would not need to instruct your users to connect their account to a LibreView practice, before they can attempt to connect via Vital Link. #### ⚠️ Prepare for regular disconnection with `abbott_libreview` Patient-based connections through `abbott_libreview` may **disconnect** at least once a year due to password rotation enforcement. You can observe the error state through: 1. the [Provider Connection Error](/event-catalog/provider.connection.error) event; and 2. the [Get User Connections endpoint](/api-reference/user/get-users-connected-providers). When you detect a disconnection, guide your user to: 1. Sign into the LibreLink app or the LibreView web portal. 2. Update their account passwords. 3. Use the Vital Link Widget or your custom widget to re-establish the `abbott_libreview` connection. ## LibreView data use floating time basis All LibreView (Freestyle Libre) data are in **floating time basis**. Please go through the [Timestamps and Time Zones](/wearables/providers/timestamps-and-time-zones) guide to understand the implications, as well as how to properly handle floating time data. ## Libre Notes Timeseries LibreView, in addition to standard CGM data, provides insights manually added by the user. This data is available in the following types: * `note`: A general note added by the user, often providing context for other entries. * `carbohydrates`: Grams of carbohydrates logged by the user. * `insulin_injection`: Units of insulin injected by the user, categorized as either `rapid_acting` or `long_acting`. Timestamps for these events have hourly granularity and are marked with the beginning of the hour. ## Connecting a LibreView patient account (`abbott_libreview`) ### Vital Link Widget Abbott LibreView is automatically available as an option in the [Vital Link Widget](/wearables/connecting-providers/launching_link). ### Vital Link API (custom widget) If you intend to build a [custom widget on top of the Vital Link API](/wearables/connecting-providers/custom_widget), Abbott LibreView is exposed as a **password provider**: Start the linking process by [generating a short-lived Link Token](/api-reference/link/generate-link-token). Submit the username and password of the LibreView patient account to the [Link Password Provider](/api-reference/link/link-password-provider) endpoint. You need not specify any region in your request. Vital will match the correct LibreView region transparently. The [Link Password Provider](/api-reference/link/link-password-provider) endpoint responds with `state: "pending_provider_mfa"`, alongside the MFA method being used, as well as a MFA hint. This means the user must complete multi-factor authentication before Vital can establish the connection. You should prompt the user to input the verification code they received out-of-band. Submit the MFA code to the [Complete Password Provider MFA](/api-reference/link/complete-password-provider-mfa) endpoint. You must use the same Link Token you generated in Step 1. The linking process is completed. Historical pulls are scheduled to start as soon as possible. ## Setting up practice-based connections (`freestyle_libre`) ### Adding users to a Practice When integrating with Vital, you have two options: use Vital's practice, or yours. We call the latter a custom practice. Depending on the approach, you need to instruct your user's to connect their Libre account to a practice. This allows Vital to start pulling data from their account. This is done through the Libre app: ### Supported Regions #### 🇺🇸 Vital US region | Region Code | Region | | ----------- | ------------- | | `ca` | Canada | | `hk` | Hong Kong | | `in` | India | | `kr` | South Korea | | `ph` | Philippines | | `sg` | Singapore | | `tw` | Taiwan | | `us`\* | United States | *(\*) Default if region is unspecified.* #### 🇪🇺 Vital EU region | Region Code | Region | | ----------- | -------------- | | `ar` | Argentina | | `at` | Austria | | `au` | Australia | | `be` | Belgium | | `bh` | Bahrain | | `br` | Brazil | | `ch` | Switzerland | | `cl` | Chile | | `co` | Colombia | | `cz` | Czechia | | `de` | Germany | | `dk` | Denmark | | `eg` | Egypt | | `es` | Spain | | `fi` | Finland | | `fr` | France | | `gb`\* | United Kingdom | | `gr` | Greece | | `hk` | Hong Kong | | `hr` | Croatia | | `ie` | Ireland | | `il` | Israel | | `in` | India | | `it` | Italy | | `jo` | Jordan | | `kr` | South Korea | | `kw` | Kuwait | | `lb` | Lebanon | | `lu` | Luxembourg | | `mx` | Mexico | | `nl` | Netherlands | | `no` | Norway | | `nz` | New Zealand | | `om` | Oman | | `ph` | Philippines | | `pl` | Poland | | `pt` | Portugal | | `qa` | Qatar | | `sa` | Saudi Arabia | | `se` | Sweden | | `sg` | Singapore | | `si` | Slovenia | | `sk` | Slovakia | | `tr` | Türkiye | | `tw` | Taiwan | | `za` | South Africa | *(\*) Default if region is unspecified.* ### Bring Your Own Practice `freestyle_libre` custom practice connections is available for [the Scale plan](https://tryvital.io/pricing). Get in touch with [Vital Support](/home/getting-support) to request the region-specific Vital practice email address. Invite the Vital practice email address to join your LibreView practice as a **Care Team Admin**. Inform Vital support of the invitation, and wait for the support acknowledgement of successful practice connection. Once the connection is established, Vital Link would recognize all patient emails having connected to your LibreView practice. These patients do not need to connect separately to the [Vital practices](#using-vital-practices). ### Using Vital Practices You can use a Vital practice to test the integration with Freestyle Libre. This modality is available to all subscriptions. Vital has two LibreView Practices that are available in all supported regions: | Environment | Practice Name | | ----------- | ---------------- | | Sandbox | tryVital-sandbox | | Production | tryVital | You need to ask your user to add one of the above practices in their Libre App. Once that's done, data will start flowing. ### Vital Link Widget To connect your user to Vital via the Link Widget you can follow the guide [here](/wearables/connecting-providers/launching_link). ### Vital Link API (custom widget) 1. To connect to Freestyle Libre, you must create a Vital Link token. ```python Python token_response = client.link.token(user_id="") ``` ```javascript Node const request: LinkTokenExchange = { userId: " } const data = await client.link.token(request) ``` ```java Java LinkTokenExchange request = LinkTokenExchange .builder() .userId("") .build(); var tokenResponse = vital.link().token(request); ``` ```go Go request := &vital.LinkTokenExchange{ UserId: " } tokenResponse, err := client.Link.Token(context.TODO(), request) if err != nil { return err } fmt.Printf("Received data %s\n response) ``` 2. Libre is an [email](/api-reference/link/link-email-provider) provider. This means that when connecting a user we have to use the connect email provider method, passing in the email of the user's Libre account and region in short form. ```python Python linkResponse = client.link.connect_email_auth_provider( provider="freestyle_libre email=" vital_link_token=token_response.link_token ) ``` ```javascript Node const linkRequest: EmailProviderAuthLink = { vitalLinkToken: tokenResponse.linkToken, email: "" } const connectEmailResponse = await client.link.connectEmailAuthProvider( "freestyle_libre linkRequest ) ``` ```java Java EmailProviderAuthLink request = EmailProviderAuthLink .builder() .vitalLinkToken(tokenResponse.getLinkToken()) .email("") .build(); var data = vital.link().connectEmailAuthProvider(" } emailAuth, err := client.Link.ConnectEmailAuthProvider(context.TODO(), "freestyle_libre emailAuthRequest) if err != nil { return err } fmt.Printf("Received data %s\n emailAuth) ``` Once this is complete we will start syncing data from their [Libre account](/api-details/data_flow). # Android Health Connect Source: https://docs.tryvital.io/wearables/guides/android-health-connect Android Health Connect is an SDK-based provider. Your Android consumer app would embed the Vital Mobile SDKs on a supported stack. Data are then pulled from the Health Connect data store on the user's Android device. Refer to the [Mobile SDK Installation](/wearables/sdks/installation) and [Vital Health SDK](/wearables/sdks/vital-health) guides for SDK installation instructions and general API usage. This guide contains information on the the behaviour and required configuration specific to the Android Health Connect integration. Learn about the minimum runtime and build-time requirements of Vital Mobile SDKs, and how to add them into your project through your package dependency manager. Learn about the initial setup required by Vital Health SDK, and the API exposed by Vital Health SDK for managing and inspecting health data permissions and automatic data sync. ## Getting Started To enable this integration, you would need to integrate [Vital Core SDK](/wearables/sdks/vital-core) and [Vital Health SDK](/wearables/sdks/vital-core) into your Native iOS, React Native or Flutter mobile app. Review the [installation requirements and package installation instructions](/wearables/sdks/installation). Follow the [SDK Authentication](/wearables/sdks/authentication) guidance to integrate your app with the authentication mechanism of Vital Core SDK. Follow the [Vital Health SDK](/wearables/sdks/vital-health) starting guidance to [configure the Health SDK](/wearables/sdks/vital-health#sdk-configuration) and to [ask for health data read permissions](/wearables/sdks/vital-health#ask-user-for-health-data-permissions) from your user. Health data sync is activated only on `VitalResource`s which you have asked permissions from your user for. If you did not ask for permissions, data sync would not occur. Follow the [Running as Foreground Service](#running-as-foreground-service) guidance below to customize the Foreground Service notification which may be shown by the Android OS during any prolonged health data sync attempts. Follow the [Background Sync](#background-sync) guidance below to configure regular background sync. ## Review the Google Play Store policy Apps using Android Health Connect **require prior Google approval** before they can be released on Google Play Store. Check out the official [Google Play Store policy on Android Health Connect](https://support.google.com/googleplay/android-developer/answer/12991134?hl=en-GB). Note that the application form is located under the "How do I access data through Health Connect?" section. ## Configure your AndroidManifest.xml ### Health Connect privacy dialogue Missing manifest declarations may result in Health Connect ignoring your app. Check [the Health Connect Getting Started guide](https://developer.android.com/health-and-fitness/guides/health-connect/develop/get-started#show-privacy-policy) for the official requirements. Here is a minimum declaration example: ```xml ``` ### Health Connect permissions Your app manifest (`AndroidManifest.xml`) must declare all the read or write permissions for all the data types you intend to sync. For example, if you intend to sync Blood Pressure records and Blood Glucose reocrds, your app manifest must contain the following `` declarations: ```xml ``` | SDK `VitalResource` type | Read permissions required | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Profile` | `android.permission.health.READ_HEIGHT` | | `Body` | `android.permission.health.READ_BODY_FAT`
    `android.permission.health.READ_WEIGHT` | | `Workout` | `android.permission.health.READ_EXERCISE`
    `android.permission.health.READ_HEART_RATE`
    `android.permission.health.READ_RESPIRATORY_RATE`
    `android.permission.health.READ_DISTANCE`
    `android.permission.health.READ_ACTIVE_CALORIES_BURNED`
    `android.permission.health.READ_ELEVATION_GAINED`
    `android.permission.health.READ_POWER`
    `android.permission.health.READ_SPEED` | | `Activity` | `android.permission.health.READ_ACTIVE_CALORIES_BURNED`
    `android.permission.health.READ_BASAL_METABOLIC_RATE`
    `android.permission.health.READ_TOTAL_CALORIES_BURNED`
    `android.permission.health.READ_DISTANCE`
    `android.permission.health.READ_STEPS`
    `android.permission.health.READ_FLOORS_CLIMBED`
    `android.permission.health.READ_DISTANCE`
    `android.permission.health.READ_VO2_MAX` | | `Sleep` | `android.permission.health.READ_SLEEP`
    `android.permission.health.READ_HEART_RATE`
    `android.permission.health.READ_RESPIRATORY_RATE`
    `android.permission.health.READ_HEART_RATE_VARIABILITY`
    `android.permission.health.READ_OXYGEN_SATURATION`
    `android.permission.health.READ_RESTING_HEART_RATE` | | `Glucose` | `android.permission.health.READ_BLOOD_GLUCOSE` | | `BloodPressure` | `android.permission.health.READ_BLOOD_PRESSURE` | | `HeartRate` | `android.permission.health.READ_HEART_RATE` | | `Steps` | `android.permission.health.READ_STEPS` | | `ActiveEnergyBurned` | `android.permission.health.READ_ACTIVE_CALORIES_BURNED` | | `BasalEnergyBurned` | `android.permission.health.READ_ACTIVE_CALORIES_BURNED`
    `android.permission.health.READ_BASAL_METABOLIC_RATE`
    `android.permission.health.READ_TOTAL_CALORIES_BURNED` | | `Water` | `android.permission.health.READ_HYDRATION` | ### Foreground Service permissions Vital Health SDK runs all its data sync workers inside a short-service Foreground Services. At build time, Vital Health SDK injects all the required `shortService` Foreground Services declarations into your AndroidManifest.xml. You need not modify your app's Manifest to enable this. Vital Health SDK can run work inside the [AndroidX WorkManager](https://developer.android.com/develop/background-work/background-tasks/persistent/how-to/long-running#declare-foreground-service-types-manifest) foregorund service, as well as our own `SyncOnExactAlarmService`. The merged AndroidManifest.xml of your app would include both of them. ## Prepare your app architecture Incompliance to the guidelines below may result in Health Connect permission flow failing to launch on specific Android OS versions. When requesting permission using the `ActivityResultContract` created by Vital Health `createPermissionRequestContract()`, you must launch the contract using AndroidX Activity Result API. Attempts to launch the contract manually via the legacy `Activity.startActivityForResult` API would result in an `android.content.ActivityNotFoundException` exception on Android 14 and later. The `MainActivity` of your Android host app project must be augmented with either of the two options below: 1. Subclassing `VitalHealthReactActivity` provided by the Vital Health SDK `com.vitalhealthreactnative` package, instead of the default `ReactActivity`. 2. Overriding `onRequestPermissionsResult(...)` so as to manually propagate permission request results to the Vital Health SDK. ```kotlin // MainActivity in your host app project open class MainActivity: ReactActivity() { override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) // Propagate permission flow results to Vital Health SDK. VitalHealthReactNativeModule.onRequestPermissionsResult( this.reactInstanceManager, requestCode, permissions, grantResults ) } } ``` The argumentation above is a workaround to React Native internals AndroidX-incompatible handling of permission request result callbacks, which breaks the AndroidX Activity Result API required by Android Health Connect. The `MainActivity` of your Android host app project must subclass `FlutterFragmentActivity` instead of `FlutterActivity`. This is because `FlutterFragmentActivity` is AndroidX compatible and therefore supports the Activity Result API as required by Android Health Connect, whilst `FlutterActivity` is not. ## Synchronization ### Sync On App Launch When your app resumes from background (i.e., having no Activity), Vital Health SDK triggers a data sync on all the resources to which the user has granted read permission. The SDK imposes a **2-minute** throttle on automatic data sync. This is to prevent rapid app switching from causing an excessive amount of data sync work being scheduled. Vital Health SDK relies on the [AndroidX ProcessLifecycleOwner](https://developer.android.com/reference/androidx/lifecycle/ProcessLifecycleOwner) to get notified of your app's resumption. ### Running as Foreground Service You cannot opt-out of this behaviour. Vital Health SDK runs all its data sync workers inside a Foreground Service. Running as a foreground service helps in two ways: 1. It ensures ongoing data sync can run to completion even if the user switches away from your app; and 2. data sync workers have to be **in foreground** to read data from Health Connect. Health Connect prohibits reading data from background. Android requires every Foreground Service to [be associated with a user-visible notification](https://developer.android.com/develop/background-work/services/foreground-services#user-dismiss-notification). The OS typically grants a grace period of about 10 seconds before making the user aware of this notification. In other words, if the data sync worker completes within the grace period, no notification would be posted. Vital Health SDK installs these notification copies by default: | Item | Copy | | -------------------- | ---------------------------------------------------------------- | | Notification Title | Health Data Sync | | Notification Content | `{APP_NAME}` is synchronizing with Health Connect... | | Channel Title | Health Data Sync | | Channel Description | Notifies when `{APP_NAME}` is synchronizing with Health Connect. | You can customize it through two ways: Register your custom `SyncNotificationBuilder` through `VitalHealthConnectManager.syncNotificationBuilder`. You should register it as soon as your app process is created. One way to ensure this is through the AndroidX Startup library. You can define an `Initializer` of your own that depends on the SDK `VitalHealthConnectInitializer`. #### An example App Startup Initializer implementation ```kotlin package com.example.app import android.content.Context import androidx.startup.Initializer import io.tryvital.vitalhealthconnect.VitalHealthConnectManager import io.tryvital.vitalhealthconnect.VitalHealthConnectInitializer class ExampleSyncNotificationBuilderInitializer: Initializer { override fun create(context: Context) { val manager = VitalHealthConnectManager.getOrCreate(context) manager.syncNotificationBuilder = ExampleSyncNotificationBuilder } override fun dependencies(): MutableList>> = mutableListOf( VitalHealthConnectInitializer::class.java, ) } ``` #### An example `SyncNotificationBuilder` implementation ```kotlin package io.tryvital.sample import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.content.Context.NOTIFICATION_SERVICE import androidx.core.app.NotificationCompat import io.tryvital.vitalhealthconnect.SyncNotificationBuilder import io.tryvital.vitalhealthconnect.model.VitalResource object ExampleSyncNotificationBuilder: SyncNotificationBuilder { override fun build(context: Context, resources: Set): Notification { return NotificationCompat.Builder(context, createChannel(context)) .setContentTitle("Example Sync") .setContentText("Syncing your data") .setOngoing(true) .setSmallIcon(android.R.drawable.ic_popup_sync) .build() } fun createChannel(context: Context): String { val importance = NotificationManager.IMPORTANCE_MIN val mChannel = NotificationChannel("ExampleSyncNotification", "Example Sync", importance) mChannel.description = "Notifies when Example is syncing your data" val notificationManager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(mChannel) return mChannel.id } } ``` Register your custom copies through `VitalHealth.setSyncNotificationContent` as early as possible in your React Native root component. ```typescript import { VitalHealth } from '@tryvital/vital-health-react-native'; await VitalHealth.setSyncNotificationContent({ "notificationTitle": "Example Sync", "notificationContent": "Syncing your data", "channelName": "Example Sync", "channelDescription": "Notifies when Example is syncing your data" }); ``` Register your custom copies through `vital_health.setSyncNotificationContent` as early as possible in your Flutter app startup logic. ```typescript import 'package:vital_health/vital_health.dart' as vital_health; vital_health.setSyncNotificationContent(vital_health.SyncNotificationContent( "Example Sync", "Syncing your data", "Example Sync", "Notifies when Example is syncing your data")); ``` ## Background Sync Vital Health SDK supports an **opt-in** Android Background Sync feature. It provides a continuous monitoring experience with Android Health Connect, siimlar to [Background Delivery for Apple HealthKit](/wearables/guides/apple-healthkit#background-delivery) on iOS. ### Sync Frequency The [Sync On App Launch](#sync-on-app-launch) behaviour is always-on. When Background Sync is enabled, the SDK additionally schedules hourly sync using the Android [Exact Alarms](https://developer.android.com/develop/background-work/services/alarms/schedule) mechanism. The OS has full discretion on whether to honour or defer the time as scheduled by the Vital Health SDK. This includes the policies of the base Android OS, as well as any vendor-specific OS augmentation. For example, the device may enter [Doze mode](https://developer.android.com/training/monitoring-device-state/doze-standby) during device inactivity. Doze mode would batch and defer most Exact Alarms and other background work to run at a lower frequency. Not to be confused with Alarm Clock apps, **Exact Alarm** is the technical name of an Android framework for scheduling app wake-ups in background at certain wall clock time. It is the Vital Health SDK being silently "alarmed" in background. Your user **will not be alarmed hourly** as a result of enabling Background Sync. However, if the data sync took longer than 10 seconds, the OS might nudge them of this occurrence with a user-visible notification. The notification content is configurable by you — see the ["Running as Foreground Service"](#running-as-foreground-service) section for more details. ### Runtime Permission Request Since Android 12 (API Level 31), scheduling Exact Alarm (`SCHEDULE_EXACT_ALARM`) requires a runtime permission request from the user. More specifically: 1. The user must go through an interactive flow to grant the *"Alarms & Reminders"* permission. * This flow is also reachable through the *"Alarms & Reminders"* section in Android Settings. 2. The user may refuse to grant the *"Alarms & Reminders"* permission. 3. The user may revoke the *"Alarms & Reminders"* permission through Android Settings at any time. Since Android 13 (API Level 33), a new mutually exclusive `USE_EXACT_ALARM` permission was introduced. This permits your app to schedule Exact Alarms without interactive permission requests. `USE_EXACT_ALARM` attracts scrutiny during Google Play Store review. Per the Google Play [Exact alarm permission policy](https://support.google.com/googleplay/android-developer/answer/9888170?hl=en-GB): > USE\_EXACT\_ALARM is a restricted permission and apps must only declare this permission if their core functionality supports > the need for an exact alarm. Apps that request this restricted permission are subject to review, and those that do not meet > the acceptable use case criteria will be disallowed from publishing on Google Play. > > *(excerpted on 20 March 2024)* If you choose to incorporate `USE_EXACT_ALARM`, you should prepare to justify to Google Play Store: 1. Why hourly background sync is quintessential to your product experience; and 2. Why the interactive permission request required by `SCHEDULE_EXACT_ALARM` is non-optimal to your product experience. Here is a matrix summarizing the Exact Alarm permission request requirements: | Android OS | API Level | Requirements | | -------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Android 11 or below | \<=30 | No interactive permission request. | | Android 12 | 31, 32 | `SCHEDULE_EXACT_ALARM`: Requires a runtime permission request. Revokable. | | Android 13 and above | >=33 | App must declare either:
    • `SCHEDULE_EXACT_ALARM`: Requires runtime permission request. Revokable.
    • `USE_EXACT_ALARM`: No interactive permission request.
    | ### Updating your AndroidManifest.xml Your app's AndroidManifest.xml must include the following `uses-permission` claims: Make sure you read and understand [Runtime User Permission](#runtime-user-permission-android-12) before moving forward. This option is mutually exclusive with `USE_EXACT_ALARM` in the second tab. ```xml ``` Make sure you read and understand [Runtime User Permission](#runtime-user-permission-android-12) before moving forward, especially the Google Play Store review policy surrounding the `USE_EXACT_ALARM` permission. This option is mutually exclusive with `SCHEDULE_EXACT_ALARM` in the first tab. ```xml ``` ### Enabling Background Sync You can enable Background Sync through `enableBackgroundSyncContract()` (an AndroidX `ActivityResultContract`). If the runtime [requires an interactive permission request](#runtime-user-permission-android-12), this contract will launch an Android Intent to request the *"Alarms & Reminders"* permission. The contract result is a boolean, indicating whether or not the Background Sync has been enabled successfully. For example, if the user did not grant the permission during the said Android Intent, the contract returns `false`. Otherwise, the contract returns `true` synchronously when no user interaction is required. You must launch this `ActivityResultContract` either through the in-built Android Compose support, or through the AndroidX Activity Result API if you do not use Android Compose. You can also inspect if Background Sync has been enabled through the `isBackgroundSyncEnabled` property. ```kotlin Android Compose import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.material3.* import io.tryvital.vitalhealthconnect.VitalHealthConnectManager val manager: VitalHealthConnectManager = ... val permissionsLauncher = rememberLauncherForActivityResult( manager.enableBackgroundSyncContract() ) { success -> Log.i("VitalBackgroundSync", "Enabled? $success") val enabled = manager.isBackgroundSyncEnabled Log.i("VitalBackgroundSync", "Enabled? $enabled") } Button(onClick = { permissionsLauncher.launch(Unit) }) { Text("Enable Background Sync") } ``` ```kotlin AndroidX Activity Result API import androidx.activity.ComponentActivity import io.tryvital.vitalhealthconnect.VitalHealthConnectManager val activity: ComponentActivity = ... val manager: VitalHealthConnectManager = ... activity.registerForActivityResult(contract) { success -> Log.i("VitalBackgroundSync", "Enabled? $success") val enabled = manager.isBackgroundSyncEnabled Log.i("VitalBackgroundSync", "Enabled? $enabled") } ``` You can enable Background Sync through `enableBackgroundSync()`. If the runtime [requires an interactive permission request](#runtime-user-permission-android-12), `enableBackgroundSync()` will launch an Android Intent to request the *"Alarms & Reminders"* permission. The promise result is a boolean, indicating whether or not the Background Sync has been enabled successfully. For example, if the user did not grant the permission during the said Android Intent, the contract returns `false`. Otherwise, `enableBackgroundSync()` returns `true` immediately when no user interaction is required. You can also inspect if Background Sync has been enabled through the `isBackgroundSyncEnabled` property. When running on iOS, both `enableBackgroundSync()` and `isBackgroundSyncEnabled` always return `true` immediately. ```typescript import { VitalHealth } from "@tryvital/vital-health-react-native"; const success = await VitalHealth.enableBackgroundSync(); console.log("background sync enabled?", success); const enabled = await VitalHealth.isBackgroundSyncEnabled; console.log("background sync enabled?", enabled); ``` You can enable Background Sync through `enableBackgroundSync()`. If the runtime [requires an interactive permission request](#runtime-user-permission-android-12), `enableBackgroundSync()` will launch an Android Intent to request the *"Alarms & Reminders"* permission. The promise result is a boolean, indicating whether or not the Background Sync has been enabled successfully. For example, if the user did not grant the permission during the said Android Intent, the contract returns `false`. Otherwise, `enableBackgroundSync()` returns `true` immediately when no user interaction is required. You can also inspect if Background Sync has been enabled through the `isBackgroundSyncEnabled` property. When running on iOS, both `enableBackgroundSync()` and `isBackgroundSyncEnabled` always return `true` immediately. ```typescript import 'package:vital_health/vital_health.dart' as vital_health; final success = await vital_health.enableBackgroundSync(); log("background sync enabled? ${success}") final enabled = await vital_health.isBackgroundSyncEnabled; log("background sync enabled? ${enabled}") ``` ### Disabling Background Sync You can disable Background Sync through the `disableBackgroundSync()` method. You can also inspect if Background Sync has been disabled through the `isBackgroundSyncEnabled` property. ```kotlin import io.tryvital.vitalhealthconnect.VitalHealthConnectManager val manager: VitalHealthConnectManager = ... manager.disableBackgroundSync() val enabled = manager.isBackgroundSyncEnabled Log.i("VitalBackgroundSync", "Enabled? $enabled") ``` You can disable Background Sync through the `disableBackgroundSync()` method. You can also inspect if Background Sync has been disabled through the `isBackgroundSyncEnabled` property. When running on iOS, this API **is a no-op**. HealthKit Background Delivery is always-on on iOS. ```typescript import { VitalHealth } from "@tryvital/vital-health-react-native"; await VitalHealth.disableBackgroundSync(); const enabled = await VitalHealth.isBackgroundSyncEnabled; console.log("background sync enabled?", enabled); ``` You can disable Background Sync through the `disableBackgroundSync()` method. You can also inspect if Background Sync has been disabled through the `isBackgroundSyncEnabled` property. When running on iOS, this API **is a no-op**. HealthKit Background Delivery is always-on on iOS. ```dart import 'package:vital_health/vital_health.dart' as vital_health; await vital_health.disableBackgroundSync(); final enabled = await vital_health.isBackgroundSyncEnabled; log("background sync enabled? ${enabled}") ``` ### Miscellaneous When you `signOut()` a user, Vital SDK will automatically disable Background Sync. # Apple HealthKit Source: https://docs.tryvital.io/wearables/guides/apple-healthkit Apple HealthKit is an SDK-based provider. Your iOS consumer app would embed the Vital Mobile SDKs on a supported stack. Data are then pulled from the Apple HealthKit data store on the user's iOS device. Refer to the [Mobile SDK Installation](/wearables/sdks/installation) and [Vital Health SDK](/wearables/sdks/vital-health) guides for SDK installation instructions and general API usage. This guide contains information on the the behaviour and required configuration specific to the Apple HealthKit integration. Learn about the minimum runtime and build-time requirements of Vital Mobile SDKs, and how to add them into your project through your package dependency manager. Learn about the initial setup required by Vital Health SDK, and the API exposed by Vital Health SDK for managing and inspecting health data permissions and automatic data sync. ## Sync Frequency | App state | Behaviour | | ---------- | ------------------------------------------------------------------------- | | Foreground | Apple HealthKit delivers any buffered and new changes immediately. | | Background | Hourly batch delivery of changes, subject to operating system throttling. | While the SDK schedules hourly data sync with Apple HealthKit, iOS has full discretion to defer the scheduled time based on factors like CPU usage, battery usage, connectivity, and Low Power Mode. In other words, the Vital SDK — or any third-party HealthKit apps — cannot guarantee the sync to happen on a strict schedule. The hourly schedule is advisory and non-binding from the persective of iOS. The actual delivery frequency can therefore fluctuate from hourly, to once a day, or when certain opportunity arises (e.g., when Sleep Mode ends, or when the phone starts charging). ## Getting Started To enable this integration, you would need to integrate [Vital Core SDK](/wearables/sdks/vital-core) and [Vital Health SDK](/wearables/sdks/vital-core) into your Native iOS, React Native or Flutter mobile app. Review the [installation requirements and package installation instructions](/wearables/sdks/installation). Follow the [SDK Authentication](/wearables/sdks/authentication) guidance to integrate your app with the authentication mechanism of Vital Core SDK. Follow the [Vital Health SDK](/wearables/sdks/vital-health) guidance to [configure your App Delegate](/wearables/sdks/vital-health#configure-your-app-delegate-ios-only), [configure the Health SDK](/wearables/sdks/vital-health#sdk-configuration) and to [ask for health data read permissions](/wearables/sdks/vital-health#ask-user-for-health-data-permissions) from your user. Health data sync is activated only on `VitalResource`s which you have asked permissions from your user for. If you did not ask for permissions, data sync would not occur. Follow the [Apple HealthKit Background Delivery guidance](#setting-up-apple-healthkit-background-delivery) below to setup your iOS app target as an eligible target for Apple HealthKit Background Delivery. ## Setting up Apple HealthKit Background Delivery You **MUST** [configure your App Delegate](/wearables/sdks/vital-health#configure-your-app-delegate-ios-only) for Apple HealthKit Background Delivery to function as expected. Vital Health SDK can subscribe to Apple HealthKit Background Delivery, so that data sync can happen automatically in background. The subscription persists even when: 1. Your app user does not open your app regularly; 2. Your app user forces quit your app; or 3. The iPhone has been restarted for any reason. Enabling Apple HealthKit will attract additional scrutiny in the App Store Review process. You should be prepared to explain and demonstrate your usage of these health and fitness data. ### 1. Setup app entitlements Enable the "HealthKit > Background Delivery" entitlement: Enable the "Background Modes > Background Processing entitlement: ### 2. Update your Info.plist In your "Info > Custom iOS Target Properties" section — also known as the `Info.plist` file — these entries should be configured: | Key | Value | | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | Privacy - Health Share Usage Description
    (`NSHealthShareUsageDescription`) | An explanation of your usage of the user's HealthKit data. | | Permitted background task scheduler identifiers
    (`BGTaskSchedulerPermittedIdentifiers`) | Include `io.tryvital.VitalHealthKit.ProcessingTask` in the array, alongside any other BGTask identifiers of yours. | If you request write permissions, you must also specify: | Key | Value | | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | Privacy - Health Update Usage Description
    (`NSHealthUpdateUsageDescription`) | An explanation of what user health data your app is writing to Apple HealthKit. | ### 3. Epilogue You are all set! Note that there is no need to call `syncData()` manually at all. Once you have asked the user for permission on resources, sync would automatically starts every app launch, as well as whenever your app is woken up by Apple HealthKit to respond to newly available data. As per the [documentation](https://developer.apple.com/documentation/healthkit/hkhealthstore/1614175-enablebackgrounddelivery): > HealthKit wakes your app whenever a process saves or deletes samples of the specified type. The system wakes your app at most once per time period defined by the specified frequency. Some sample types have a maximum frequency of `HKUpdateFrequency.hourly`. The system enforces this frequency transparently. > > For example, on iOS, `stepCount` samples have an hourly maximum frequency. This means that although we have background delivery's frequency set to `.hourly`, we cannot guarantee hourly syncing on the dot. # Dexcom (G6 and older) Source: https://docs.tryvital.io/wearables/guides/dexcom `dexcom` is the provider slug for Dexcom (G6 and older).
    For the OAuth-based `dexcom_v3` integration, see [Dexcom (OAuth based)](./dexcom_v3).
    This provider is being phased out in favour of the new [OAuth-based Dexcom](./dexcom_v3) integration. Consider migrating to the new API at your earliest convinence. ## Enabling Dexcom Share Your user needs to enable [Dexcom Share](https://www.dexcom.com/faqs/how-do-i-share-my-dexcom-g6-glucose-data-followers) for this connection to work: They **must** use the Dexcom G6 mobile app ([iOS](https://apps.apple.com/us/app/dexcom-g6/id1209262925), [Android](https://play.google.com/store/apps/details?id=com.dexcom.g6)). Note that the Dexcom Clarity app would not work for this integration. Please refer to the [official help article on how to enable Dexcom Share](https://www.dexcom.com/faqs/how-do-i-share-my-dexcom-g6-glucose-data-followers). They **must** have at least one follower\[1] on Dexcom Share to keep the sharing activated. Otherwise, the API would stop reporting data to Vital. \[1] You can setup your own Dexcom account for your user to add as a follower in the Dexcom G6 mobile app. Alternatively, your user can invite `support@tryvital.io` as a follower. They **must** connect to Vital using their *own* Dexcom account — the one which their Dexcom G6 app is signed into. This should **not** be the accounts of the followers. ## Vital Widget To connect your device with the *widget*, follow the link wizard ensuring and enter your username and password. ## Vital Link You can also connect a Dexcom user to vital using vital client. For additional information please consult the API reference. ```python Python link_token = client.Link.create(user_id) client.Link.password_provider( link_token, "dexcom", username, password ) ``` ```javascript JavaScript const { link_token } = await client.Link.create(user_id); await client.Link.password_provider( link_token, "dexcom", username, password ); ``` ## Webhooks On successful connection, you will receive a [Connection create](/webhooks/data_flow#connection-created) wehbook. [Daily webhooks](/webhooks/data_flow#daily-data-flow) `data.daily.glucose.created` and `data.daily.glucose.updated` will also be issued to your team as new or more up-to-date data becomes available during the day. For this provider however, Vital will begin to aggregate the data for the last 24 hours and will not go beyond that. As a result, there will be no [Historical webhooks](/webhooks/data_flow#historical-data-flow) issued. If you would like to pull the data immediately after connection, you can use the `user/refresh` endpoint. # Dexcom (OAuth based) Source: https://docs.tryvital.io/wearables/guides/dexcom_v3 `dexcom_v3` is the provider slug for Dexcom (OAuth based).
    For the password-based `dexcom` integration, see [Dexcom (G6 and older)](./dexcom).
    Bring Your Own OAuth (BYOO) is available for [the Grow and Scale plans](https://tryvital.io/pricing). WHOOP BYOO is available for [the Launch, Grow and Scale plans](https://tryvital.io/pricing). See [our WHOOP Guide](/wearables/guides/whoop) for more information. ## Connecting Dexcom to Vital You can connect your dexcom device to vital through Vital Link Widget or programatically like any other [OAuth](/api-reference/link/link-oauth-provider) provider. We recommend you obtain custom OAuth credentials from [Dexcom](https://developer.dexcom.com) with production access and follow the guide for [setting up custom credentials](/wearables/connecting-providers/bring_your_own_oauth) to get started. ## Webhooks On successful connection, you will receive a [Connection create](/event-catalog/provider.connection.created) webhook, and subsequently, [Daily](/event-catalog/daily.data.glucose.created) and [Historical](/event-catalog/historical.data.glucose.created) Glucose webhooks. The histrical webhook informs you that the last **30 days** of data prior to connection is available and the daily webhooks follow the usual semantics for all vital daily webhooks. # Fitbit Source: https://docs.tryvital.io/wearables/guides/fitbit ## Webhooks There is one caveat with Fitbit's webhooks. Namely, Fitbit computes the basal metabolic rate (BMR) server-side. This is a fairly static value because it depends on data such as height, weight, gender, age, etc. The fact this is computed value server-side is important to keep in mind, because it means you will always receive `daily.data.activity.updated` webhooks even if you are not wearing the device. This can be confusing at times, as someone who is not aware of this might wonder why are they getting only this one webhook and not the others. If you are only getting `daily.data.activity.updated`, the most likely case is that the user is neither wearing the device, or they did not sync their device with the Fitbit app. If you are sure this is not the case, contact us through our support channels. ## Respiratory Rate timeseries Fitbit does not provide detailed breakdown beyond average breaths-per-minute summary on a per sleep stage basis, i.e., REM average, Deep average, Light average and the whole session average. Given this constraint, Vital constructs a respiratory rate timeseries for Fitbit as follows: * Vital creates a respiratory rate data point for each hypnogram segment reported. * The data point value will be one of the four average values mentioned, based on the stage represented by the segment. * The data point will be timestamped to the **midnpoint** of the segment. ## Time Zone resolution Fitbit stores all their data in floating time. Moreover, They do not tag most of their data points or summaries with time zone information. Since Vital uses [UTC time basis](/wearables/providers/timestamps-and-time-zones), Vital has to convert the floating time basis to the UTC time basis, before we can ingest the data and forward it onwards to you. To facilitate this conversion, we must make an educated guess on the user's time zone. Vital makes the best time zone guess in the following precedences: If the user has granted the Profile scope, Vital uses the timezone offset as provided by their Fitbit Profile. If the user has granted the Activity scope, Vital uses the timezone offset from the most recently recorded activity (if any). If neither Fitbit data sources are available, Vital would use [User Fallback Time Zone](/wearables/providers/timestamps-and-time-zones#user-fallback-time-zone) if it has been set. If none of above is available, Vital assumes naively that everything happens in UTC (+00:00). This is usually incorrect, except for users living an all-year UTC (+00:00) region. When your user refuses to grant both the activity and the profile scope, Fitbit data are more likely be timestamped incorrectly in UTC time basis due to inaccurate or unavailable time zone information. # Garmin Source: https://docs.tryvital.io/wearables/guides/garmin ## Webhooks & Events Garmin connections have a non-standard [webhook lifecycle](/webhooks/introduction): You will receive a [Provider Connection Created event](/webhooks/introduction#connection-created-stage) normally. You would receive a [Historical Pull Completion event](/webhooks/introduction#historical-data-backfill-stage) **immediately** after the connection creation. However, if you query Vital API immediately, you would observe **no historical data**. The Historical Pull Completion event for Garmin connections does not indicate availability of historical data through the Vital API. All **historical and new data** are delivered incrementally as [Data Events](/webhooks/introduction#incremental-data-stage) (`daily.data.*.*`). This behaviour is exclusive to Garmin, as a downstream effect of Garmin's unique historical data access mechanism for third-party integrations. ## Re-running the historical data backfill stage Normally, when a user with an existing connection re-authenticates with the provider through [Vital Link](https://docs.tryvital.io/wearables/connecting-providers/introduction), Vital re-runs the [Historical Data Backfill stage](/webhooks/introduction#historical-data-backfill-stage) automatically. However, this **does not apply to Garmin** connections — you must first [deregister](/api-reference/user/deregister-a-provider) said users' existing Garmin connection for the re-run to take effect. This behaviour is exclusive to Garmin, as a downstream effect of Garmin's unique historical data access mechanism for third-party integrations. ## Vital Widget To connect your device with the *widget*, follow the link wizard ensuring and enter your username and password.
    # Google Fit Source: https://docs.tryvital.io/wearables/guides/google_fit ## Delays in Syncing Data ### Intro Google Fit via Cloud can have significant delay in data availability, more specifically between Google's Google Fit apps & Google's cloud data store - neither of which Vital has any control over. This is unlike using the Google Fit Android SDK directly, which can serve the latest local data immediately from the Google Fit data store, regardless of whether said local data have been uploaded to the Cloud already. Vital isn't certain how/when Google Fit decides to upload new local data to cloud, and also whether or not there is additional delay caused by Google's internal data ingestion pipelines in the cloud. But one thing for sure is that it cannot be forced, and it seems to be managed by Android's standard WorkManager framework. This means connectivity, battery usage, Doze mode and App Standby will all dynamically affect the actual upload frequency. At worst case, this can mean uploads only happen when one is sleeping (the typical favourable time for the OS to run background work — the phone is on charger, has been idle extensively, and probably on WiFi). ### Why not use Google Fit Android SDK directly? One way to avoid data delay is to use the Google Fit native SDK directly. However, Google Fit Android API has been deprecated by Google since May 2022 and support is set to end by end of 2024. For this reason we advise on using its successor: [HealthConnect](https://developer.android.com/guide/health-and-fitness/health-connect). You can find our docs [here](/wearables/sdks/android). Keep in mind that HealthConnect has its own set of quirks. The most notable one is that Google decided to ban any data access when apps are in background. So it is unlikely to have the same smooth background monitoring experience like Apple HealthKit out of the box. # Omron Source: https://docs.tryvital.io/wearables/guides/omron ## Permissions with Omron Omron's API does not provide information on what permissions users granted when connecting. Additionally it gives the same error for unauthorized resources and when a user uses one account in multiple environments, so we can't tell these apart. Because of this, we only accept Omron connections with all permissions granted. # Polar Source: https://docs.tryvital.io/wearables/guides/polar ## Historical backfill with Polar Polar unlike most other providers doesn't offer historical data for most of their resources. The only resources that do have it are Sleep and Sleep Stream - 28 days each. # WHOOP Source: https://docs.tryvital.io/wearables/guides/whoop WHOOP [Bring Your Own OAuth](/wearables/connecting-providers/bring_your_own_oauth) is available for [the Launch, Grow and Scale plans](https://tryvital.io/pricing). ## Connecting WHOOP to Vital WHOOP is a [BYOO](/wearables/connecting-providers/bring_your_own_oauth) only provider, so you will need your own app in [WHOOP's developer dashboard](https://developer-dashboard.WHOOP.com/). Then, you can configure the WHOOP integration in the Vital dashboard by navigating to the Config > Custom Credentials page and clicking "Setup" next to WHOOP V2. ![WHOOP V2 config](https://mintlify.s3.us-west-1.amazonaws.com/vital/img/whoop_config.png) ## Limitations WHOOP sets forth the following limitation: > Apps can be used for development immediately with a limit of 10 WHOOP members. To launch your app to all WHOOP members, you must first submit your app for approval. Learn more about app approval in the [WHOOP developer documentation](https://developer.whoop.com/docs/developing/app-approval/). WHOOP reviews apps on a monthly cadence. # Data Attributions Source: https://docs.tryvital.io/wearables/providers/data-attributions ## Source Type For summary data types, you can identify the Source Type through `source.type`. This is available in both summary data events and API responses. For timeseries data types: * You can identify the Source Type through `$.data.source.type` in timeseries data events you received. * You can request timeseries data grouped by their Source Type through [the Grouped Timeseries Data API](/api-reference/timeseries-grouped/steps). ### Common | Source Type | Description | | ------------------ | --------------------------------------------------------------------------- | | `unknown` | The default value. Vital does not know how the provider collects this data. | | `app` | The user manually enters this data through an app. | | `multiple_sources` | This data is derived from multiple sources. | ### Wearables | Source Type | Description | | ------------- | ----------------------------------------------------- | | `watch` | A smart watch collects this data automatically. | | `phone` | A smart phone collects this data automatically. | | `ring` | A smart ring collects this data automatically. | | `chest_strap` | A smart chest strap collects this data automatically. | ### Health devices | Source Type | Description | | ------------- | ------------------------------------------------------------------ | | `manual_scan` | A biosensor that needs to be manually scanned. | | `automatic` | A biosensor that uploads data continuously in background. | | `fingerprick` | A glucose testing device which analyzes fingerprick blood samples. | | `cuff` | A blood pressure cuff. | ### Supported providers The following provider integrations would tag data with Source Type: | Provider | Source Types | | --------------- | ----------------------------------------------------------- | | Apple HealthKit | `phone`, `watch`, `app`, `multiple_sources`, `unknown` \[1] | | Fitbit | `watch`, `scale`, `app`, `multiple_sources` | | Oura | `ring`, `app`, `multiple_sources` | | Garmin | `watch`, `scale`, `cuff`, `app`, `multiple_sources` | | Freestyle Libre | `automatic`, `manual_scan` | \[1] Only data from Apple Watch, iPhone and Apple Health app are tagged. Data from third-party apps (like the Oura iOS app) are tagged as `unknown` currently. ## App ID App ID indicates the origin application of the data. For summary data types, it is available at `source.app_id`. Note that App ID is unavailable on timeseries data at this time. ### Supported providers The following provider integrations would tag data with App ID: | Provider | Remarks | | ---------------------- | ----------------------------------------------------------------------------------------- | | Apple HealthKit | App Store Bundle ID, or Unique Device ID (`com.apple.health.{UUID}`) for first-party data | | Android Health Connect | Android Application Package Name | # Data Ingestion Bounds Source: https://docs.tryvital.io/wearables/providers/data-ingestion-bounds ## Overview By default, Vital: * pulls historical data as far back as indicated by the [pull preferences](/wearables/providers/introduction#customizing-historical-data-pull-range) you specified (if any), or the [default historical pull range](/wearables/providers/introduction#historical-data-pull-range). * accepts **all** new and changed data discovered through polling, incoming provider webhooks and Vital Mobile SDK integrations. If you need to impose a date bounds on the data being collected, you can set **data ingestion bounds** on a per-user basis: * The data ingestion bounds restricts the historical pull range of any new connections of the user. * The data ingestion bounds restricts data ingestion from any connections of the user — out of bound data are dropped. * When the user ingestion end date is reached, a 7-day late delivery catchment period starts. Once the catchment period expires, all connections of the user are automatically de-registered and marked as *paused*. Because *dates* are in floating time, you should expect an error margin of ±1 days on ingestion bound enforcement. If you cannot tolerate an error margin, offset the user data ingestion bounds you set accordingly. ## Setting Ingestion Start and End Dates You can specify the user data ingestion bounds (`ingestion_start` and `ingestion_end`) when you: * [Create a new user](/api-reference/user/create-user); or * [Update an existing user](/api-reference/user/patch-user) ```json Example { "user_id": "409d9870-21fb-443a-8daa-c5222659f40e", "team_id": "3aac677c-557f-40b7-9251-0315c1f48e77", "client_user_id": "d734e32e-dd43-4b77-ab56-692524279531", /** snipped **/ "ingestion_start": "2024-01-01", "ingestion_end": "2024-12-15", } ``` ## Changing Ingestion Bounds You can change the ingestion bounds at any time. However, do note that it has no retrospective effect on data already ingested — Vital does not delete collected data in response to ingestion bound changes. As long as the connection has not been marked as *paused*, you can change the ingestion end date to extend the ingestion bounds at any time, even during the (post ingestion end) 7-day late delivery catchment period. Otherwise, the ingestion end date has no effect on any *paused* cloud-based connections — since these connections have been deregistered with the data provider, the end user needs to go through the Vital Link flow to re-connect. # Primary Key Source: https://docs.tryvital.io/wearables/providers/data-primary-key ## Summary types Each summary is uniquely identified by its ID (`$.id`). Given the same ID, the latest version of a summary you received **replaces** all its previous versions. ## Timeseries types Each timeseries sample is uniquely identified by a compound key of: * the timeseries resource type; * the source provider; * the [Source Type](/wearables/providers/data-attributions#source-type); and * the sample timestamp. Given the same compound key, the latest value of a sample you received **replaces** all the previous values. # Heart Rate Zones Source: https://docs.tryvital.io/wearables/providers/heart-rate-zones ## Heart Rate Zones Calculation Heart rate zones are segments in a workout session. They are based on the percentage brackets of the user's maximum heart rate. \[under 50%, 50-60%, 60-70%, 70-80%, 80-90%, above 90%] The maximum heart rate for a person is calculated with a straightforward formula: `220 - age`. ## Fallback Birth Date Some providers may not share the user's birth date, making it impossible for Vital to know the user's age. To solve this, users can provide a Fallback Birth Date as an alternative option. You can specify the Fallback Birth Date when: * [Creating a new user](/api-reference/user/create-user); or * [Patching an existing user](/api-reference/user/patch-user) You will also get information about the source (by slug) and the last updated time of the Fallback Birth Date [when getting an existing user](/api-reference/user/get-user). Fallback Birth Date supplied via the REST API would always have a source slug of `manual`. If no birth date information is available, the assumed age of 30 is used in the calculations. ```json Example { "user_id": "409d9870-21fb-443a-8daa-c5222659f40e", "team_id": "3aac677c-557f-40b7-9251-0315c1f48e77", "client_user_id": "d734e32e-dd43-4b77-ab56-692524279531", /** snipped **/ "fallback_birth_date": { "value": "1980-09-12", "source_slug": "manual", "updated_at": "2022-09-11T13:45:56+00:00" } } ``` # Supported providers Source: https://docs.tryvital.io/wearables/providers/introduction We currently Support over 300+ devices, a full list can be seen below. ## Cloud Based Providers Some OAuth providers are unavailable unless you can [Bring Your Own OAuth (BYOO)](/wearables/connecting-providers/bring_your_own_oauth). You will have to apply directly to the provider for an OAuth application, and provide Vital your assigned credentials once the application is accepted. ### Generally Available | Provider (Slug) | Description | Availability | Remarks | | ------------------------------------------------------------------------- | ------------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------- | | [Beurer](https://www.beurer.com/web/gb/)
    `beurer_api` | Beurer Blood Pressure monitors | All Vital Teams | Password Auth | | [Dexcom (G6 And Older)](https://www.dexcom.com)
    `dexcom` | Dexcom CGM Glucose monitors | All Vital Teams | Password Auth
    [↗️ Guide](/wearables/guides/dexcom) | | [8Sleep](https://www.eightsleep.com)
    `eight_sleep` | Smart Mattress | All Vital Teams | Password Auth | | [Abbott LibreView](https://www.libreview.com/)
    `abbott_libreview` | Abbott CGM Glucose monitor | All Vital Teams | Password Auth
    [↗️ Guide](/wearables/guides/abbott-libreview) | | [Freestyle Libre](https://www.libreview.com/)
    `freestyle_libre` | Abbott CGM Glucose monitor | All Vital Teams | LibreView Practice
    [↗️ Guide](/wearables/guides/abbott-libreview) | | [Fitbit](https://www.fitbit.com/global/uk/home)
    `fitbit` | Activity Trackers (all devices) | All Vital Teams | OAuth
    [↗️ Guide](/wearables/guides/fitbit) | | [Garmin](https://www.garmin.com)
    `garmin` | Fitness watches (all devices) | All Vital Teams | OAuth
    [↗️ Guide](/wearables/guides/garmin) | | [Hammerhead](https://www.hammerhead.io)
    `hammerhead` | Cycling computers | All Vital Teams | Password Auth | | [Oura](https://ouraring.com)
    `oura` | Smart Sleep tracking ring | All Vital Teams | OAuth | | [Peloton](https://www.onepeloton.com)
    `peloton` | Popular Indoor Exercise bike | All Vital Teams | Password Auth | | [Renpho](https://renpho.com)
    `renpho` | Fitness Scales | All Vital Teams | Password Auth | | [Strava](https://www.strava.com)
    `strava` | Running & Cycling Social Network | All Vital Teams | OAuth | | [Wahoo](https://wahoofitness.com)
    `wahoo` | Cycling Equipment | All Vital Teams | OAuth | | [Withings](https://www.withings.com)
    `withings` | Fitness scales, watches and health monitors | All Vital Teams | OAuth | | [Zwift](https://zwift.com)
    `zwift` | Virtual cycling and running | All Vital Teams | Password Auth | | [Polar](https://www.polar.com/accesslink-api/)
    `polar` | Finnish sports tech pioneer | All Vital Teams | OAuth
    [↗️ Guide](/wearables/guides/polar) | | [Cronometer](https://www.cronometer.com)
    `cronometer` | Nutrition data | All Vital Teams | OAuth | | [Ultrahuman](https://www.cronometer.com)
    `ultrahuman` | Real-time nutrition and fitness tracking | 🧪 **Open Beta**
    All Vital Teams | OAuth | ### BYOO-only Bring Your Own OAuth (BYOO) is available for [the Grow and Scale plans](https://tryvital.io/pricing). WHOOP BYOO is available for [the Launch, Grow and Scale plans](https://tryvital.io/pricing). See [our WHOOP Guide](/wearables/guides/whoop) for more information. Some BYOO providers do not have a developer program with open sign-up. | Provider (Slug) | Description | Availability | Remarks | | --------------------------------------------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------- | | [Dexcom](https://www.dexcom.com)
    `dexcom_v3` | Dexcom CGM Glucose monitors | ⚠️ [BYOO only](/wearables/connecting-providers/bring_your_own_oauth) | Password Auth
    [↗️ Guide](/wearables/guides/dexcom_v3) | | [iHealth](https://ihealthlabs.com)
    `ihealth` | Blood pressure, Fitness scales, Glucometers & Fitness watches | ⚠️ [BYOO only](/wearables/connecting-providers/bring_your_own_oauth) | OAuth | | [WHOOP](https://www.whoop.com)
    `whoop_v2` | Your Personal Digital Fitness and Health Coach | ⚠️ [BYOO](/wearables/connecting-providers/bring_your_own_oauth) only | OAuth
    [↗️ Guide](/wearables/guides/whoop) | | [MyFitnessPal API](https://www.myfitnesspalapi.com)
    `my_fitness_pal_v2` | Nutrition data | ⚠️ [BYOO](/wearables/connecting-providers/bring_your_own_oauth) only | OAuth | | [MapMyFitness](https://developer.mapmyfitness.com)
    `map_my_fitness` | Technology for all atheletes | ⚠️ [BYOO](/wearables/connecting-providers/bring_your_own_oauth) only | OAuth | ### Deprecated providers | Provider (Slug) | Status | Availability | Remarks | | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------ | | [Google Fit](https://www.google.com/fit/)
    `google_fit` | Google will shut down Google Fit on June 30, 2025. Android Health Connect is the successor. | All Vital Teams | OAuth
    [↗️ Guide](/wearables/guides/google_fit) | | [MyFitnessPal](https://www.myfitnesspal.com)
    `my_fitness_pal` | Deprecated | All Vital Teams | MyFitnessPal Diary Share Key
    [↗️ Guide](/wearables/guides/my_fitness_pal) | ## SDK Based Providers | Provider (Slug) | Description | Guide | | ------------------------------------------------------------------------------------------------ | ------------------------------------------------- | ---------------------------------------------------- | | [Accu-Chek](https://www.accu-chek.com) (Bluetooth)
    `accuchek_ble` | Glucose Strips/MySugr App | | | [Apple HealthKit](https://www.apple.com/uk/ios/health/)
    `apple_health_kit` | Health and fitness data on iPhone and Apple Watch | [↗️ Guide](/wearables/guides/apple-healthkit) | | [Beurer](https://www.beurer.com/web/gb/) (Bluetooth)
    `beurer_ble` | Beurer Blood Pressure monitors | | | [Contour](https://www.diabetes.ascensia.com) (Bluetooth)
    `contour_ble` | Glucometers | | | [Freestyle Libre BLE](https://www.freestylelibre.co.uk/libre/) (NFC)
    `freestyle_libre_ble` | Abbott CGM Glucose monitor readings via SDK | | | [Omron](https://www.omron-healthcare.com) (Bluetooth)
    `omron_ble` | Blood Pressure monitors and scales | | | [Android Health Connect](https://developer.android.com/health-connect)
    `health_connect` | Health and fitness data on Android devices | [↗️ Guide](/wearables/guides/android-health-connect) | ## Providers under beta **Beta**: Providers under beta, these are providers that have been recently added. All providers here are available in sandbox, any feedback you have is greatly appreciated! | Provider | Slug | Description | | ------------------------------------------------- | -------- | ---------------------------------- | | [Omron](https://www.omron-healthcare.com) (Cloud) | `omron` | Blood Pressure monitors and scales | | [Kardia](https://kardia.com/) | `kardia` | Electrocardiogram readings | **Planned**: On our roadmap | Provider | Slug | Description | Stage | | ----------------------------------------------- | ---------- | ------------------------- | ------- | | [Xiaomi](https://www.xiaomi.com) | `xiaomi` | All data | Enquire | | [Suunto](https://www.suunto.com) | `suunto` | Fitness Watch | Planned | | [iGlucose](https://smartmeterrpm.com/iglucose/) | `iglucose` | Glucose Strips | Planned | | [KetoMojo](https://keto-mojo.com) | TBD | Glucose, Ketones and more | Planned | ## Full Device Support List ## Data Frequency Whenever Vital detects that new data is available, [data events](/event-catalog) are always sent to your configured endpoints, regardless of the provider being push-based or polling-based. ### Cloud Based Providers Cloud Based Providers can be divided in two main categories: * Push-based * When a cloud-based provider supports a push notify mechanism (typically webhooks), Vital would prefer to use it to drive data fetches. In other words, as soon as the provider notifies Vital of new data through the said mechanism, Vital fetches the latest data. * Polling-based * For providers and/or resources without any push notify mechanism, Vital polls these resources at a regular schedule, typically every 15 minutes or so. | Provider | Description | | --------------------------------------------------------------- | ----------- | | [Fitbit](https://www.fitbit.com/global/uk/home) | Push | | [Garmin](https://www.garmin.com) | Push | | [Strava](https://www.strava.com) | Push | | [Wahoo](https://wahoofitness.com) | Push | | [Withings](https://www.withings.com) | Push | | [iHealth](https://ihealthlabs.com) | Push | | [Freestyle](https://www.freestylelibre.co.uk/libre/)(API + SDK) | Polling | | [Google Fit](https://www.google.com/fit/) | Polling | | [Oura](https://ouraring.com) | Polling | | [Peloton](https://www.onepeloton.com) | Polling | | [Renpho](https://renpho.com) | Polling | | [WHOOP](https://www.whoop.com) | Push | | [Zwift](https://zwift.com) | Polling | | [8Sleep](https://www.eightsleep.com) | Polling | | [Hammerhead](https://www.hammerhead.io) | Polling | | [Dexcom](https://www.dexcom.com) | Polling | | [MyFitnessPal](https://www.myfitnesspal.com) | Polling | | [Cronometer](https://www.cronometer.com) | Push | ### SDK Based Providers SDK Based Providers are all push-based, where data are pushed from the Vital SDK embedded inside your Android or iOS app. | Provider | Description | | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | [Apple HealthKit](https://www.apple.com/uk/ios/health/) |
    • Auto sync on app launch and in foreground
    • Hourly Background Sync, subject to OS throttling
    | | [Android Health Connect](https://developer.android.com/health-connect) |
    • Sync On App Launch
    • Opt-in Hourly Background Sync, subject to OS throttling
    | | [Omron](https://www.omron-healthcare.com) | Manual Post | | [Contour](https://www.diabetes.ascensia.com) | Manual Post | | [Accu-Chek](https://www.accu-chek.com) | Manual Post | For more information on Apple HealthKit and Android Health Connect, please refer to the specific guides: * [Vital Health SDK: Automatic Data Sync](/wearables/sdks/vital-health#automatic-data-sync) * [Apple HealthKit: Sync Frequency](/wearables/guides/apple-healthkit#sync-frequency) * [Android Health Connect: Sync Frequency](/wearables/guides/android-health-connect#sync-frequency) ## Historical Data Pull Range Vital fetches the historical data immediately when the connection is established. The pull range can be estimated by `[now - days_to_pull, now]`. | Provider | Default | Configurable | Remarks | | ---------------------------------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Abbott LibreView](https://www.libreview.com) | 90 days | ❌ | - | | [Fitbit](https://www.fitbit.com/global/uk/home) | 90 days | ⚠️ | Activity and heartrate timeseries data are fixed to 14 days. | | [Garmin](https://www.garmin.com) | 90 days | ✅ | - | | [Google Fit](https://www.google.com/fit/) | 90 days | ✅ | - | | [Oura](https://ouraring.com) | 180 days | ✅ | - | | [Peloton](https://www.onepeloton.com) | 180 days | ✅ | - | | [Renpho](https://renpho.com) | 180 days | ✅ | - | | [Strava](https://www.strava.com) | 14 days | ✅ | - | | [Wahoo](https://wahoofitness.com) | 180 days | ✅ | - | | [WHOOP](https://www.whoop.com) | 180 days | ✅ | - | | [Zwift](https://zwift.com) | 270 days | ✅ | - | | [Withings](https://www.withings.com) | 90 days | ✅ | - | | [8Sleep](https://www.eightsleep.com) | 90 days | ✅ | - | | [Apple HealthKit](https://www.apple.com/uk/ios/health/) (SDK) | 30 days | ✅ | - | | [Android Health Connect](https://developer.android.com/health-connect) (SDK) | 30 days | ❌ | Google restricts access to historical data to [30 days before the first successful permission request](https://developer.android.com/health-and-fitness/guides/health-connect/develop/frequently-asked-questions#time-range). | | [Hammerhead](https://www.hammerhead.io) | 30 days | ✅ | - | | [Dexcom](https://www.dexcom.com) | 30 days | ✅ | - | | [Dexcom (G6 And Older)](https://www.dexcom.com) | 1 day | ❌ | - | | [MyFitnessPal](https://www.myfitnesspal.com) | 14 days | ✅ | - | | [Polar](https://www.polar.com/accesslink-api/#polar-accesslink-api) | 28 days | ❌ | Polar only supports historical backfill for Sleep and Sleep Stream resources. | | [Cronometer](https://www.cronometer.com) | 28 days | ✅ | - | The configurable maximum is 365 days at this time. ### Customizing historical data pull range Org Management API is available for [the Scale plan](https://tryvital.io/pricing). You can configure the historical pull range using the [Team Data Pull Preferences](/api-reference/org-management/team-data-pull-preferences/upsert-team-data-pull-preferences) feature of the Org Management API. You can specify a number of days to backfill that uniformly applies to all resources of a provider, as well as overriding the the number of days to backfill for specific resources. The Team Data Pull Preferences you specified is *advisory*. There are scenarios in which Vital systems may not adhere strictly to your stated preferences. # Resources Source: https://docs.tryvital.io/wearables/providers/resources ## Overview There are two broad resource categories provided by Vital: * A **Summary** type is a summarization of a specific calendar day or a session. * A **Timeseries** type is a time-ordered collection of data samples. There is no relation in resource availability among these two categories. Some providers do offer both, while some providers offer only one category. For example: * Fitbit provides daily activity summaries, as well as **some** high-frequency activity timeseries data. * Oura provides only daily activity summary, without any complementary high-frequency activity timeseries data. There is also no strict relation between a field in a *Summary* type and a *Timeseries* type. For example: * The existence of a `sleep_efficiency` summary field does not imply that there is a corresponding `sleep_efficiency` timeseries. * The existence of a Glucose Timeseries type does not imply that there is a corresponding `glucose` summary field. * A *Timeseries* type could derive multiple summary fields, e.g., heart rate statistics and HR zones. ### Summary types | Granularity | Resources | | ----------- | --------------------------------------------------------------------------------------------- | | Daily | `activity` | | Session | `sleep`, `workouts`, `body`, `meal`, `menstrual_cycle`, `electrocardiogram`, `workout_stream` | | Single | `profile` | ### Discrete Timeseries types Each value is associated with a single point in time. | Category | Resources | | ----------------- | -------------------------------------------------------------------------------------------- | | Cardiorespiratory | `respiratory_rate` | | Vitals | `blood_oxygen`, `blood_pressure`, `glucose`, `heartrate`, `hrv`, `electrocardiogram_voltage` | | Wellness | `stress_level` | | Body | `fat`, `weight` | | Lab Testing | `cholesterol`, `igg`, `ige` | ### Interval Timeseries types Each value is associated with a half-open time interval (`[start, end[`). | Category | Resources | | ----------------- | ---------------------------------------------------------------------------------------------- | | Activity | `calories_active`, `calories_basal`, `distance`, `floors_climbed`, `steps`, `workout_duration` | | Body | `body_temperature`, `body_temperature_delta`, `insulin_injection` | | Vitals | `afib_burden`, `heart_rate_alert` | | Cardiorespiratory | `vo2_max` | | Nutrition | `carbohydrates`, `caffeine`, `water` | | Wellness | `mindfulness_minutes` | | Diary | `note` | ## Timeseries Data Sampling Rates Junction records and forwards timeseries data from the data provider, without any upsampling or downsampling applied. Even within the same data provider, these data may come from different devices or data sources with varying sampling rates. Because of this, you should not make assumptions of any particular timeseries resource having a fixed sampling rate. For technical design and capacity planning purposes, you may model after the following three categories of timeseries resources: ### Very High Frequency Timeseries Second-level peak sampling rate; typically 1000s to low 10000s samples per day. | Peak rate | Example Scenario | | -------------------------- | -------------------- | | 1 sample every 3 seconds | Apple Watch workouts | | 1 sample every 15 seconds | Garmin | | 1 sample every 1-15 minute | Apple Watch, Fitbit | Resources that fall into this category: | Resource | Events | Notable Providers | | ----------------- | ------------------------------ | --------------------- | | `heartrate` | `daily.data.heartrate.*` | Apple, Fitbit, Garmin | | `calories_active` | `daily.data.calories_active.*` | Apple, Fitbit, Garmin | ### High frequency Timeseries Minute-level peak sampling rate; typically 100s samples per day. | Peak rate | Example Scenario | | ------------------------- | -------------------------------------------------- | | 1 sample every 1 minute | Fitbit Intraday timeseries data | | 1 sample every 15 minutes | Garmin all-day activity timeseries data ("epochs") | Resources that fall into this category: | Resource | Events | Notable Providers | | ------------------- | -------------------------------- | --------------------- | | `steps` | `daily.data.steps.*` | Apple, Fitbit, Garmin | | `calories_basal` | `daily.data.calories_basal.*` | Apple | | `distance` | `daily.data.distance.*` | Apple, Fitbit, Garmin | | `glucose` | `daily.data.glucose.*` | LibreView, Dexcom | | `stress_level` | `daily.data.stress_level.*` | Garmin | | `hrv` | `daily.data.hrv.*` | Garmin, Fitbit | | `respiratory_rate` | `daily.data.respiratory_rate.*` | Garmin, Fitbit | | `blood_oxygen` | `daily.data.blood_oxygen.*` | Fitbit | | `daylight_exposure` | `daily.data.daylight_exposure.*` | Apple | | `stand_duration` | `daily.data.stand_duration.*` | Apple | ### Sparse Timeseries All timeseries resources not specified as (Very) High Frequency fall into this category. Day-level peak sampling rate; typically no more than 5 per day. | Peak rate | Example Scenario | | ------------------------- | -------------------------------------------------- | | 1 sample every day | Apple Watch wrist temperature delta | | 1 sample every 15 minutes | Garmin all-day activity timeseries data ("epochs") | | Unpredictable / Never | Heart Rate Alerts, Sleep Apnea Alerts | ## Summary types by providers ## Timeseries types by providers ## Fetching via the API Summary resources can be fetched from our `/summary//` API, while everything else via `/timeseries//`. For example, `Workouts` is made up of fields specific to an workout, like calories, distance and duration. When you make a request to `/summary/workouts/` you get the following fields: ```json { "workouts": [ { "user_id": "a6fea8eb-402f-4578-96ba-189df1a8ca75", "user_key": "a6fea8eb-402f-4578-96ba-189df1a8ca75", "id": "12398c74-ea41-4d9c-a1b7-8c529ee67e46", "title": null, "timezone_offset": 3600, "average_hr": 147, "max_hr": 178, "distance": 31557.85, "time_start": "2022-07-29T12:05:49+00:00", "time_end": "2022-07-29T13:26:08+00:00", "calories": 729.0, "sport": { "id": 108, "name": "Road Biking", "slug": "road_biking" }, "hr_zones": [ 5, 76, 288, 1467, 2148, 835 ], "moving_time": 4819, "total_elevation_gain": 374.0, "elev_high": null, "elev_low": null, "average_speed": 6.548, "max_speed": 17.448, "average_watts": null, "device_watts": null, "max_watts": null, "weighted_average_watts": null, "map": null, "provider_id": "9297103021", "source": { "name": "Garmin", "slug": "garmin", "logo": "https://storage.googleapis.com/vital-assets/garmin.png" } } } ``` `Heartrate` on the other hand, is a timeseries resources. You can fetch it via `timeseries//heartrate` and looks like this: ```json [ { "timestamp": "2022-04-29T08:35:57+00:00", "timezone_offset": 3600, "value": 174.0, "type": null, "unit": "bpm" }, { "timestamp": "2022-04-30T11:22:38+00:00", "timezone_offset": 3600, "value": 79.0, "type": null, "unit": "bpm" }, { "timestamp": "2022-04-30T11:22:39+00:00", "timezone_offset": 3600, "value": 80.0, "type": null, "unit": "bpm" }, { "timestamp": "2022-04-30T11:22:40+00:00", "timezone_offset": 3600, "value": 78.0, "type": null, "unit": "bpm" }, { "timestamp": "2022-04-30T11:22:41+00:00", "timezone_offset": 3600, "value": 78.0, "type": null, "unit": "bpm" } ] ``` Different providers (e.g. Garmin), have different resources available. As an example, Garmin doesn't have `Glucose` as a resource. ## Fields by Provider Resources alone are not the whole story. Depending on the provider, a field might not be available. For example both Garmin and Strava provide `Workouts`, yet the former doesn't have `ele_high` nor `ele_low`. Providers mark with `*` means that Vital calculates the field's value. In some cases, they might came as `null`, since we weren't able to make the calculation. Even if a field is available for a provider, it doesn't mean it's available. For example, Fitbit is capable of generating HRV data. However HRV data is only available for specific high-end devices. Another common scenario is Apple HealthKit data. If the user doesn't give permission to a particular field, it won't be available. **Be mindful of these situations and similar ones**. ### Activity ### Body ### Sleep ### Sleep Stream ### Workout ### Workout Stream Soon. Work in progress. ### Profile Soon. Work in progress. # Synthetic Data (Sandbox) Source: https://docs.tryvital.io/wearables/providers/test_data Using demo users to build and test your Vital integration This feature is currently available for the following providers: * Apple Health Kit * Fitbit * Oura * Freestyle Libre The others will be coming soon. If there is a particular provider you would like us to add please do not hesitate to contact us at [support@tryvital.io](mailto:support@tryvital.io)! When integrating with Vital, you may not own a physical device for each provider you're planning to support. For this, we provide demo users with test data so you can build and test the whole integration without touching a real device. A demo user gives you the same experience as using a real one, the only difference is that you won't go through the [Vital Link](/wearables/vital-link/introduction) flow. Demo users have certain limitations to keep in mind: * Only available in sandbox. * A demo user cannot have connections to a real device. If you already have a user with real data, you need to create a new one to connect a demo account. * There is no limitation on the number of demo users you can create, but they do count towards the overall users limit in sandbox. Up to date limits can be found [on this page](/home/environments). * Demo users expire after 7 days, meaning the user and its data will be deleted. ## Usage First, you need a Vital user. You can create one either through the [dashboard](https://app.tryvital.io) or through the [API](/api-reference/user/create-user). After creating the user, the `Users` page of your dashboard will look like this: Now you can create a demo connection for the Vital user you just created, in this case `150db84c-537c-4cad-a6e9-24dc589d7fa2`. You can directly hit the [API endpoint](/api-reference/link/link-demo-provider) or using one of our client libraries, as shown below: ```bash Creating a demo connection curl --request POST \ --url https://api.sandbox.tryvital.io/v2/link/connect/demo \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --header 'x-vital-api-key: ' \ --data '{"user_id": "", "provider": ""}' ``` ```javascript Node import { VitalClient, VitalEnvironment } from '@tryvital/vital-node'; import { DemoConnectionCreationPayload, DemoProviders } from '@tryvital/vital-node/api'; const client = new VitalClient({ apiKey: '', environment: VitalEnvironment.Sandbox, }); const request: DemoConnectionCreationPayload = { userId: "", provider: DemoProviders., } const data = await client.link.connectDemoProvider(request); ``` ```python Python from vital.client import Vital from vital.environment import VitalEnvironment client = Vital( api_key="YOUR_API_KEY", environment=VitalEnvironment.SANDBOX ) data = client.link.connect_demo_provider(user_id="", provider=DemoProviders.) ``` ```java Java import com.vital.api.Vital; import com.vital.api.core.Environment; import com.vital.api.resources.link.requests.DemoConnectionCreationPayload; import com.vital.api.types.DemoProviders; Vital vital = Vital.builder() .apiKey("YOUR_API_KEY") .environment(Environment.SANDBOX) .build(); DemoConnectionCreationPayload request = DemoConnectionCreationPayload .builder() .userId("") .provider(DemoProviders.) .build(); var data = vital.link().connectDemoProvider(request); ``` ```go Go import ( "context" vital "github.com/tryVital/vital-go" vitalclient "github.com/tryVital/vital-go/client" "github.com/tryVital/vital-go/option" ) client := vitalclient.NewClient( option.WithApiKey(""), option.WithBaseURL(vital.Environments.Sandbox), ) request := &vital.DemoConnectionCreationPayload{ UserId: "", Provider: vital.DemoProviders, } response, err := client.Link.ConnectDemoProvider(context.TODO(), request) if err != nil { return err } fmt.Printf("Received data %s\n", response) ``` After the demo connection is created, a Fitbit logo will appear beside your user. From here, everything works exactly the same as if you connected a real device. This means you will receive the following webhook updates: * [Connection created](/webhooks/data_flow#connection-created), as soon as the connection is created. * [Historical webhooks](/webhooks/data_flow#historical-data-flow). We simulate historical data for the demo device and send the corresponding webhook updates, as in the real-world scenario. * [Daily webhooks](/webhooks/data_flow#daily-data-flow). We also simulate updates to the device data every couple of hours so you can test receiving the data when a user recorded a workout or other activity. * [Refresh user data](/api-reference/user/refresh-user-data). You can also instantaneously refresh a user's data through this endpoint. ## Backfill Data For demo connections there will be 30 days of historic data backfilled. This is in contrast with data backfill for non demo providers, where the number of days data backfilled varies according to the provider. You can find out more [here](/wearables/providers/introduction#historical-days-retrieved) ## Data Format When connecting to the live Vital client, there are three data response structures available for data. Raw, Stream and Summary When connecting to the demo Vital client data is only available in the Summary structure. Summary restructures provider data fields, to have a consistent response structure across all providers. This is the easiest way to use provider data across an application which connects to multiple providers. # Timestamps and Time Zones Source: https://docs.tryvital.io/wearables/providers/timestamps-and-time-zones ## Timestamps All Vital data timestamps are ISO 8601 formatted as follows: | Providers | Time Basis | Pattern | | --------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------- | | `abbott_libreview`
    `freestyle_libre` \[1] | Floating Time \[2] | `YYYY-mm-ddTHH:mm:ss+00:00`
    The `+00:00` TZ specifier should be **ignored** \[3]. | | *Everyone else* | UTC | `YYYY-mm-ddTHH:mm:ss+00:00`
    Always specified as `+00:00` (UTC). | \[1] `freestyle_libre` specific: Some older teams may see floating time data (Freestyle Libre) without the `+00:00` TZ specifier, as part of an earlier iteration of the feature. Contact Vital support if you wish to disable this behaviour, or if you intend to adopt the Vital backend SDKs. \[2] [Floating time](https://www.w3.org/International/wiki/FloatingTime) is not affixed to any time zone. This is not the same as time relative to a local time zone, i.e., a known UTC offset or geographical location. \[3] Vital sends timestamps in floating time with `+00:00` for OpenAPI 3.x interoperability. OAS 3 requires all timestamps to be [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339), which in turn requires TZ specifier to be mandatory. When processing data from Freestyle Libre, you should [**reinterpret literally**](#data-point-in-floating-time-freestyle-libre) the date and time components as local date-time in the desired time zone. Do not convert from UTC/Zulu. ## Time Zones Vital formats time zones in data as a *nullable* integer offset to UTC: * A positive offset indicates east of UTC. * A negative offset indicates west of UTC. * `null` indicates the time zone information is absent. Each provider has their own affinity of time zone information: | Affinity | Time Basis | Time Zone | | ------------------------ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | ✅ | UTC | The data comes with time zone information. | | User Fallback, or UTC | UTC | We assume the data were captured in the [User Fallback Time Zone](#user-fallback-time-zone). We fallback to UTC if the former is not set. | | User Fallback, or Absent | UTC | We assume the data were captured in the [User Fallback Time Zone](#user-fallback-time-zone). Mark as no time zone (`null`) if the former is not set. | | Inferred, or Absent | UTC | We infer the time zone from the data. Mark as no time zone (`null`) if the inferrence fails. | | Absent | UTC | We do not know the exact time zone the recording took place in. Mark as no time zone (`null`). | | Floating Time | Floating Time | We do not know the exact time zone the recording took place in. Mark as no time zone (`null`). | ### Cloud-based Providers | Provider | Activity Summary (Daily) | Session Summary (e.g. Sleep) | Timeseries Sample | Remarks | | ---------------------------------------------------------------------------- | ------------------------ | ---------------------------- | ------------------- | -------------------------------------------------------------- | | [Freestyle Libre](https://www.freestylelibre.co.uk/libre/) `freestyle_libre` | *N/A* | *N/A* | Floating Time | | | [Abbott LibreView](https://www.libreview.com/) `abbott_libreview` | *N/A* | *N/A* | Floating Time | | | [Fitbit](https://www.fitbit.com/global/uk/home) `fitbit` | ✅ | ✅ | ✅ | ⚠️ [Time Zones](/wearables/guides/fitbit#time-zone-resolution) | | [Garmin](https://www.garmin.com) `garmin` | ✅ | ✅ | ✅ | | | [Google Fit](https://www.google.com/fit/) `google_fit` | User Fallback, or UTC | User Fallback, or Absent | Absent | | | [Oura](https://ouraring.com) `oura` | ✅ | ✅ | ✅ | | | [Peloton](https://www.onepeloton.com) `peloton` | *N/A* | ✅ | *N/A* | | | [Renpho](https://renpho.com) `renpho` | *N/A* | ✅ | Absent | | | [Strava](https://www.strava.com) `strava` | *N/A* | ✅ | ✅ | | | [Wahoo](https://wahoofitness.com) `wahoo` | *N/A* | Absent | *N/A* | | | [WHOOP](https://www.whoop.com) `whoop_v2` | ✅ | ✅ | *N/A* | | | [Zwift](https://zwift.com) `zwift` | *N/A* | User Fallback, or Absent | *N/A* | | | [Withings](https://www.withings.com) `withings` | ✅ | ✅ | ✅ | | | [iHealth](https://ihealthlabs.com) `ihealth` | *N/A* | ✅ | Absent | | | [8Sleep](https://www.eightsleep.com) `eight_sleep` | *N/A* | ✅ | ✅ | | | [Hammerhead](https://www.hammerhead.io) `hammerhead` | *N/A* | Inferred, or Absent | Inferred, or Absent | | | [Dexcom](https://www.dexcom.com) `dexcom_v3` | *N/A* | *N/A* | ✅ | | | [Polar](https://www.polar.com) `polar` | ✅ | ✅ | ✅ | | | [Kardia](https://kardia.com/) `kardia` | *N/A* | *N/A* | ✅ | | | [Omron](https://www.omron-healthcare.com) `omron` | ✅ | *N/A* | ✅ | | ### SDK-based Providers On-device data often do not capture the time zone at recording time. Vital SDK uses the device's current time zone as the fallback of closest approximation. | Provider | Activity Summary (Daily) | Session Summary | Timeseries Samples | | --------------------------------------------------------------------------------------------------- | ------------------------ | --------------- | ------------------ | | [Apple HealthKit](https://www.apple.com/uk/ios/health/) `apple_health_kit` | ✅ | ✅ | ✅ | | [Android Health Connect](https://developer.android.com/health-connect) `health_connect` | ✅ | ✅ | ✅ | | [Omron](https://www.omron-healthcare.com) `omron_ble` | *N/A* | N/A | ✅ | | [Contour](https://www.diabetes.ascensia.com) `contour_ble` | *N/A* | N/A | ✅ | | [Accu-Chek](https://www.accu-chek.com) `accuchek_ble` | *N/A* | N/A | ✅ | | [Freestyle Libre BLE](https://www.freestylelibre.co.uk/libre/) `freestyle_libre_ble` | N/A | N/A | ✅ | ## Examples ### Data point with time zone offset The data point was recorded on July 7, 2023 at 1:00 PM in UTC-07:00. ```json { "source": { "provider": "apple_health_kit", }, "data": [ { "timestamp": "2023-07-07T20:00:00+00:00", "timezone_offset": -25200 } ] } ``` ### Data point with no time zone offset (`null`) The data point was timestamped to Aug 13, 2023 at 8:12 AM in UTC+00:00 (Zulu). We do not know the exact time zone the recording took place in. ```json { "source": { "provider": "apple_health_kit", }, "data": [ { "timestamp": "2023-08-13T08:12:00+00:00", "timezone_offset": null } ] } ``` ### Data point in floating time (Abbott Libreview & Freestyle Libre) This Freestyle Libre data point was recorded on Sep 27, 2023 at 7:48 AM in floating time (whichever time zone the user was in). We do not know the exact time zone the recording took place in. You can display the date and time components literally to the user, e.g., `2023/09/27 07:48 AM`, as they are relative to their perception of local time. If you need to convert it to UTC for persistence, you need to pick a time zone based on your understanding of the user. Then you should reinterpret the date and time components **literally** in said time zone. For example, we knew that this example user is based in `America/New_York`. So we would interpret this data point to be in `2023/09/27 07:48 AM ET`. We then finally convert this to UTC, resulting in `2023/09/27 11:48 AM UTC`. ```json { "source": { "provider": "freestyle_libre", }, "data": [ { "timestamp": "2023-09-27T07:48:00+00:00", "timezone_offset": null } ] } ``` ## User Fallback Time Zone Some providers neither expose nor even capture time zone information at source. So Vital can only request data and interpret them strictly in UTC. If you prefer the data to be contextualize to the geographical location of a user, a Fallback Time Zone (denoted by an [IANA tz database identifier](https://data.iana.org/time-zones/tz-link.html)) can be specified on a per-user basis. Once specified, Vital would use the time zone to pull data and interpret timestamps from any time zone unware providers from that point onwards. You can specify the Fallback Time Zone when: * [Creating a new user](/api-reference/user/create-user); or * [Patching an existing user](/api-reference/user/patch-user) You will also get information about the source (by slug) and the last updated time of the Fallback Time Zone [when getting an existing user](/api-reference/user/get-user). Fallback Time Zone manually supplied via the REST API would always have a source slug of `manual`. ```json Example { "user_id": "409d9870-21fb-443a-8daa-c5222659f40e", "team_id": "3aac677c-557f-40b7-9251-0315c1f48e77", "client_user_id": "d734e32e-dd43-4b77-ab56-692524279531", /** snipped **/ "fallback_time_zone": { "id": "Europe/London", "source_slug": "manual", "updated_at": "2022-09-11T13:45:56+00:00" } } ``` # Mobile SDK Authentication Source: https://docs.tryvital.io/wearables/sdks/authentication ## Vital Sign-In Token In the *Vital Sign-In Token* scheme, each mobile app installation signs in with Vital Mobile SDK as an individual user in your Team. The app installation would be granted only Vital API access scoped to that specific user. Instead of hardcoding a [Vital API Key](#vital-api-keys), your app would request your own backend to generate a **Vital Sign-In Token** for the user, through your own API authentication and authorization systems. In this model, your Vital API Keys are kept strictly as a server-side secret. The signed-in user is persistent across app relaunch and device reboots. Only user-level resources of the signed-in user can be accessed. ### Overview In typical circumstances, within each app installation, you would sign in your authenticated user only once with the Vital Mobile SDK. You would do so as part of your app's user lifecycle. There are some known circumstances where you would have to sign in the user again: * You have signed out your user in response to your app's user lifecycle changes; or * The Vital SDK sign-in state is out of sync with your app's user lifecycle; or * The SDK automatically signed out, because it had detected the deletion of the Vital User. It is unnecessary to request and sign-in with the Vital Sign-In Token every time your app launches. You should sign-in and sign-out of the Vital Mobile SDK as an integral part of your app's user lifecycle: 1. When the user signs in to your app, you would request the Vital Sign-In Token from your backend, and then sign in with the Vital Mobile SDK. 2. When the user signs out from your app, you would also sign out from the Vital Mobile SDK. Vital Mobile SDK persists the signed-in Vital User — as well as any settings and sync state — across app relaunches and device reboot. We recommend regularly reconciling the state of the Vital Mobile SDK and your app's user lifecycle. This is because: 1. The SDK sign-in process involves remote API calls, which can fail due to Internet connectivity; 2. Unknown edge cases in your integration of Vital Core SDK into your app's user lifecycle may result in the state being out of sync; and 3. If you are integrating Vital Mobile SDK into an existing production app, this reconciliation can serve as a one-off migration for app installations upgrading from an older version of your app. You can achieve this through the [Core SDK](/wearables/sdks/vital-core#core-sdk-status) API in three steps: 1. Query the [Core SDK](/wearables/sdks/vital-core#core-sdk-status) Status and the Current User ID; 2. Compare these against your app's user state; and 3. Only if a discrepancy is detected, perform a Vital SDK sign-in or sign-out in accordance to the discrepancy. You would typically schedule this as an asynchronous task that spawns when your application process launches. Do not sign in and sign out every time your application process launches. **Vital Sign-In Token** is a short-lived bearer token issued for a specific Vital User. Vital Mobile SDK uses it to sign in with Vital's Identity Provider as the Vital User represented by the token. Each app installation can only have one active signed-in Vital User. Internally, the Vital Mobile SDK: 1. **exchanges** the short-lived Vital Sign-In Token for a permanent sign-in, which is in the form of an OAuth 2.0 access token and an OAuth 2.0 refresh token. 2. discards the Vital Sign-In Token upon a successful exchange. 3. stores these token secrets securely in the device persistent storage. 4. calls the Vital API using the OAuth 2.0 access token. 5. transparently manages the OAuth 2.0 refresh token flow to keep the access token fresh and valid. This is why the documentation emphasizes the "sign in once" expectation. The SDK does not rely on the Vital Sign-In Token for day-to-day operation and API calls. It only needs this artifact once upfront to exchange for the token secrets it needs. ### Case Study: A typical Backend - Mobile App flow ```mermaid sequenceDiagram participant Your App actor Your Identity Provider autonumber Your App->>Your Identity Provider: User authentication Your Identity Provider-->>Your App: Token Secret for authenticating to your Backend API ``` Your end user signs in with your Identity Provider — a separate IdP, or your own API acting as one — through password authentication, Social Login, Single Sign-On (SSO) or any other authentication methods. ```mermaid sequenceDiagram participant Your App actor Your Backend autonumber Your App->>Your Backend: Request the user profile Your Backend-->>Your App: User profile Your App->>Your Backend: Request a Vital Sign-In Token for the user ``` Once your user has successfully authenticated, your app is issued with token secrets for accessing your backend API. Your app stores these token secrets securely. It then makes some API requests to your backend API, in order to gather all information required to setup your app into an authenticated state. Among these API requests, one would be requesting your backend API to issue a Vital Sign-In Token for your authenticated user. ```mermaid sequenceDiagram actor Your Backend actor Vital API autonumber Your Backend->>Vital API: Token Request via API Key Vital API-->>Your Backend: Issued a Vital Sign-In Token ``` Your backend API receives and validates the token issuance request. Once validated, your backend service: * Look up the Vital User associated with this authenticated user; * If one does not yet exist, call the [Create User](/api-reference/user/create-user) endpoint to create a Vital User; and * Call the [Create Sign-In Token](/api-reference/user/create-sign-in-token) endpoint to obtain a Vital Sign-In Token for this Vital user. Only this step would use a [Vital API Key](/api-details/authentication). Because this step happens in your backend service, this enables you to keep the Vital API Key strictly as a server-side secret. ```bash curl --request POST --url '{{BASE_URL}}/v2/user/{{USER_ID}}/sign_in_token' --header 'X-Vital-API-Key: ' ``` Your backend API then responds to your app with the obtained Vital Sign-In Token: ```json { "user_id": "e209947b-323e-4108-8e18-1518b80da0bd", "sign_in_token": "XYZ==" } ``` ```mermaid sequenceDiagram participant Your App participant Vital Mobile SDK actor Your Backend autonumber Your Backend-->>Your App: The Vital Sign-In Token Your App->>Vital Mobile SDK: Sign in with Vital Sign-In Token ``` Your app receives a response from your backend API, which includes the Vital Sign-In Token for the authenticated user. Your app then calls the Vital Mobile SDK `signIn` method with the Vital Sign-In Token: ```swift Native iOS import VitalCore let response: MyAPIResponse = // .... do { try await VitalClient.signIn(withRawToken: response.signInToken) print("Signed in with Vital successfully") } catch let error { print("Error signing in with Vital", error) } ``` ```kotlin Native Android import io.tryvital.client.VitalClient import android.content.Context import android.util.Log val response: MyAPIResponse val applicationContext: Context try { VitalClient.signIn(applicationContext, response.signInToken) Log.i("vital_sdk", "Signed in with Vital successfully") } catch (e: Throwable) { Log.e("vital_sdk", "Error signing in with Vital", e) } ``` ```typescript React Native import { VitalCore } from "@tryvital/vital-core-react-native"; const response: MyAPIResponse do { await VitalCore.signIn(response.signInToken) console.log("Signed in with Vital successfully") } catch let error { console.error("Error signing in with Vital", error) } ``` ```dart Flutter import 'package:vital_core/vital_core.dart' as vital_core; MyAPIResponse response; try { await vital_core.signIn(response.signInToken) Fimble.i("Signed in with Vital successfully") } catch (error) { Fimble.e("Error signing in with Vital", error) } ```
    `signIn()` throws an error when you have already signed in with the Vital SDK. You can [inspect the Core SDK status](/wearables/sdks/vital-core#core-sdk-status) at any time for troubleshooting. You can also rely on the Core SDK status to reconcile differences between your own user session state and Vital SDK sign-in.
    The Vital Mobile SDK is now signed-in with the Vital User associated with your authenticated user. You can now proceed to use all the Vital SDK features.
    ## Vital API Keys API Key is **discouraged** for production mobile apps, since it would be distributed as cleartext. API Key support is intended only for customer early evaluation in Sandbox. Use [Vital Sign-In Token](#vital-sign-in-token) whenever possible. Vital Mobile SDK can be configured to authenticate using API Key alongside a target user ID. ```swift Native iOS import VitalCore VitalClient.configure( apiKey: "sk_us_...", environment: .sandbox(.us) ) VitalClient.setUserId(UUID(uuidString: "ccd6f98d-3a2a-433b-a114-8fe064f301ed")!) ``` ```kotlin Native Android import io.tryvital.client.VitalClient import android.content.Context val applicationContext: Context VitalClient.configure( applicationContext, Region.US, Environment.Sandbox, "sk_us_..." ) VitalClient.setUserId(applicationContext, "ccd6f98d-3a2a-433b-a114-8fe064f301ed") ``` ```typescript React Native import { VitalCore } from "@tryvital/vital-core-react-native"; await VitalCore.configure( "sk_us_...", "sandbox", "us", true, ); await VitalCore.setUserId("ccd6f98d-3a2a-433b-a114-8fe064f301ed"); ``` ```dart Flutter import 'package:vital_core/vital_core.dart' as vital_core; await vital_core.configure( "sk_us_...", vital_core.Environment.sandbox, vital_core.Region.us ); await vital_core.setUserId("ccd6f98d-3a2a-433b-a114-8fe064f301ed"); ```
    When running on iOS, the SDK must be **explicitly** configured before other SDK methods can be invoked. Alternatively, follow the [Apple HealthKit guide](https://docs.tryvital.io/wearables/guides/apple-healthkit#2-integrate-with-ios-system-callbacks) to enable SDK Automatic Configuration during app launch. This auto-configures the SDK using the last known configuration you supplied — including but not limited to the API Key, the environment and the target User ID. Not only is this a prerequisite to enable HealthKit Background Delivery, this allows you also to only call `configure(...)` and `setUserId(...)` once to "sign-in" the user persistently when using the Vital API Key scheme. ## Sign Out Regardless of the authentication scheme you used, you can sign out with the Vital SDK using `signOut()`. This erases any persistent user session and configuration stored by the Vital Core and Health SDKs. ```swift Native iOS import VitalCore await VitalClient.shared.signOut() ``` ```kotlin Native Android import io.tryvital.client.VitalClient import android.content.Context val applicationContext: Context VitalClient.getOrCreate(applicationContext).signOut() ``` ```typescript React Native import { VitalCore } from "@tryvital/vital-core-react-native"; await VitalCore.signOut(); ``` ```dart Flutter import 'package:vital_core/vital_core.dart' as vital_core; await vital_core.signOut(); ``` ## Migrate from Vital API Keys to Vital Sign-In Tokens Always use [Vital Sign-In Token](#vital-sign-in-tokens) for your production mobile apps. An existing app installation signed-in with Vital API Key + User ID can be seamlessly migrated to use Vital Sign-In Tokens. It is as simple as performing a one-off migration logic during app launch: Check whether the Vital SDK status includes `useApiKey` (i.e., the user is signed in using Vital API Key). Similar to the new user sign-in flow, your app needs to obtain a Vital Sign-In Token through your backend service. Your app can simply sign-in with the Vital Sign-In Token. Note that it is **unnecessary to reset** the SDK beforehand — the SDK `signIn` method would automatically migrate, as long as the supplied Sign-In Token is compatible with the existing API Key sign-in (i.e., having the same Vital user ID, same Vital environment, and same Vital region). ```swift Native iOS import VitalCore if VitalClient.status.contains(.useApiKey) { do { let response = await callBackend(...) try await VitalClient.signIn(withToken: response.signInToken) } print("Signed in with Vital successfully") } catch let error { print("Error signing in with Vital", error) } } ``` ```kotlin Native Android import io.tryvital.client.VitalClient import android.content.Context import android.util.Log val applicationContext: Context VitalClient.getOrCreate(applicationContext) if (VitalClient.Status.useApiKey in VitalClient.status) { try { val response = callBackend(...) VitalClient.signIn(applicationContext, response.signInToken) Log.i("vital_sdk", "Signed in with Vital successfully") } catch (e: Throwable) { Log.e("vital_sdk", "Error signing in with Vital", e) } } ``` ```typescript React Native import { VitalCore } from "@tryvital/vital-core-react-native"; const status = await VitalCore.status(); if (status.includes("useApiKey")) { do { const response = await callBackend(...); await VitalCore.signIn(response.signInToken); console.log("Signed in with Vital successfully"); } catch let error { console.error("Error signing in with Vital", error); } } ``` ```dart Flutter import 'package:vital_core/vital_core.dart' as vital_core; Set status = await vital_core.clientStatus(); if (status.contains(vital_core.ClientStatus.useApiKey)) { try { MyAPIResponse response = await callBackend(...); await vital_core.signIn(response.signInToken) Fimble.i("Signed in with Vital successfully") } catch (error) { Fimble.e("Error signing in with Vital", error) } } ``` # Installation Source: https://docs.tryvital.io/wearables/sdks/installation ## Project requirements ### iOS Applies also to Flutter and React Native projects. | Parameter | Requirement | | ------------------------- | ----------- | | Minimum deployment target | iOS 14.0 | If you are integrating on React Native or Flutter, or integrating on Native iOS via CocoaPods, make sure you have updated the minimum iOS deployment target in: * your `Podfile`; and * your iOS App Target. ```diff Podfile # e.g., React Native default -platform :ios, min_ios_version_supported # Set minimum deployment target to be iOS 14.0. +platform :ios, '14.0' ``` ### Android Applies also to Flutter and React Native projects. | Parameter | Requirement | | ----------------------------- | --------------- | | Minimum SDK Level | 26 | | Compile SDK Level | 34 | | Kotlin compiler version | 1.8.22 or above | | Android Gradle Plugin version | 8.2.0 or above | | Gradle version | 8.2.0 or above | ### Flutter | Parameter | Requirement | | ----------- | ------------- | | Flutter SDK | 3.13 or above | ### React Native | Parameter | Requirement | | ----------------- | --------------- | | React Native Core | 0.72.0 or above | | Node.js | 18.0 or above | ## Setup package dependencies ## iOS Vital Mobile SDKs are available through both Swift Package Manager and CocoaPods. ### Swift Package Manager Add the vital-ios package (`https://github.com/tryVital/vital-ios`) as a dependency of your project. Link `VitalHealthKit` and `VitalDevices` as appropriate. Note that `VitalCore` is mandatory. ### CocoaPods Add the following declarations to your app target in your Podfile: ```ruby pod 'VitalCore' pod 'VitalDevices' pod 'VitalHealthKit' ``` ## Android Make sure Maven Central is included in the list of repositories in your `build.gradle`. ```groovy repositories { mavenCentral() } ``` Then include our Android SDK artifacts as dependencies of your modules as needed: ```groovy def vital_version = '3.2.1' implementation 'io.tryvital:vital-client:$vital_version' implementation 'io.tryvital:vital-health-connect:$vital_version' implementation 'io.tryvital:vital-devices:$vital_version' ``` ## React Native Install Vital SDK packages through NPM or Yarn: ```commandline npm install @tryvital/vital-core-react-native npm install @tryvital/vital-health-react-native npm install @tryvital/vital-devices-react-native ``` Pull all dependencies for your React Native iOS project: ```commandline cd ios pod install ``` `pod install` on your iOS project might error due to version lock conflicts on the Vital iOS libraries. This usually happens after you have bumped the React Native SDK package versions. Use `pod update` to resolve the conflict: ```commandline pod update VitalCore VitalDevices VitalHealthKit --repo-update ``` ## Flutter Add Vital SDK packages to your `pubspec.yaml`: ```yaml dependencies: vital_core: ^3.1.1 vital_health: ^3.1.1 vital_devices: ^3.1.1 ``` `pod install` on your iOS project might error due to version lock conflicts on the Vital iOS libraries. This usually happens after you have bumped the Flutter SDK package versions. Use `pod update` to resolve the conflict: ```commandline pod update VitalCore VitalDevices VitalHealthKit --repo-update ``` # Vital Core SDK Source: https://docs.tryvital.io/wearables/sdks/vital-core ## Overview Check out the [SDK Authentication guide](/wearables/sdks/authentication) on the available SDK authentication schemes, and how to authenticate with the Vital Mobile SDK. Learn how to authenticate your mobile app user with Vital Mobile SDK. ## Accessing Vital API You can access Vital API through `VitalClient` with typed request methods and response models. ```swift Native iOS // e.g. Link Service let linkService = VitalClient.shared.link ``` ```kotlin Native Android // e.g. Link Service val linkService = VitalClient.getOrCreate(context).linkService ``` ```dart Flutter import 'package:vital_core/vital_core.dart' as vital_core; vital_core.VitalClient client; // For customers using Vital API Keys for early evaluation. client = vital_core.VitalClient() ..init(region: Region.eu, environment: Environment.sandbox, apiKey: 'sk_eu_...'); // For customers using the Vital Sign-In Tokens scheme client = vital_core.VitalClient.forSignedInUser(environment: Environment.sandbox, region: Region.eu); // e.g. Link Service final linkService = client.linkService; ```
    Typed API access is not yet available in React Native. ## Generating Vital Link URL You can obtain an URL to the Vital Link Widget through `VitalClient.linkWidgetUrl`. Note that the URL contains a short-lived token with 10-minute expiry. For the `redirectUrl`, you should set it to a URL with a custom URL scheme which your app would register to handle. ```swift Native iOS let widgetUrl = await VitalClient.shared.link.createProviderLink( redirectURL: "x-vital-app://" ) ``` ```dart Flutter Uri widgetUrl = await client.linkWidgetUrl(redirectUrl: "x-vital-app://"); ``` ## Core SDK Status You can inspect the status of the Core SDK for troubleshooting, as well as for your own business logic. For example, you may reconcile your user session and the Vital SDK sign-in based on the Core SDK status. The SDK normally reports one of the following status combinations: | Combinations | Auth Scheme | Remarks | | ------------------------------------------- | ------------------- | ----------------------------------------------------------------------------- | | *Empty* | N/A | The SDK has neither been configured nor an active sign-in. | | `Configured` | API Key | The SDK has been configured, but does not have an active sign-in. | | `Configured`, `SignedIn` & `UseApiKey` | API Key | The active sign-in was done by setting an API Key and a target Vital User ID. | | `Configured`, `SignedIn` & `UseSignInToken` | Vital Sign-In Token | The active sign-in was done by consuming a Vital Sign-In Token. | ### Get the current status ```swift Native iOS import VitalCore let status: VitalClient.Status = VitalClient.status ``` ```kotlin Native Android import io.tryvital.client.VitalClient import android.content.Context val applicationContext: Context VitalClient.getOrCreate(applicationContext) val status = VitalClient.status ``` ```typescript React Native import { VitalCore } from "@tryvital/vital-core-react-native"; await VitalCore.status(); ``` ```dart Flutter import 'package:vital_core/vital_core.dart' as vital_core; await vital_core.clientStatus(); ``` ### Observe status changes over time ```swift Native iOS import VitalCore // Combine Publisher let cancellable = VitalClient.statusDidChange.sink { _ in } // AsyncStream Task { for status in VitalClient.statuses { // ... } } ``` ```kotlin Native Android import io.tryvital.client.VitalClient import android.content.Context val applicationContext: Context val statuses: Flow> statuses = VitalClient.statuses(applicationContext) val job = statuses .onEach { /** ... **/ } .launchIn(...) ``` ```typescript React Native import { VitalCore } from "@tryvital/vital-core-react-native"; VitalCore.observeStatusChange((statuses) => { // ... }); ``` ```dart Flutter import 'package:vital_core/vital_core.dart' as vital_core; // NOTE: This stream does not yield the current statuses on listen. Stream> statuses; statuses = vital_core.clientStatusStream; ``` ## Get Current Vital User ID Aside from the Core SDK Status, you can also query the Vital User ID of the signed-in user: ```swift Native iOS import VitalCore let userId: String? = VitalClient.currentUserId ``` ```kotlin Native Android import io.tryvital.client.VitalClient import android.content.Context val applicationContext: Context VitalClient.getOrCreate(applicationContext) val userId = VitalClient.currentUserId ``` ```typescript React Native import { VitalCore } from "@tryvital/vital-core-react-native"; const userId = await VitalCore.currentUserId(); ``` ```dart Flutter import 'package:vital_core/vital_core.dart' as vital_core; final userId = await vital_core.currentUserId(); ``` ## Reset the SDK (Sign Out) Refer to [the Sign Out section in the SDK Authentication guide](/wearables/sdks/authentication#sign-out). ## Verbose Logging to Console/Logcat You can enable verbose logging to have a deeper look into how Vital Mobile SDK is reacting to your calls, as well as all the automatic behaviours that the SDK is performing in background. To avoid any missing log entry, you should enable verbose logging as early as possible — preferably before any usage of any Vital Mobile SDKs. On Android, verbose logging is enabled by default when running as a debuggable build (i.e., with `ApplicationInfo.FLAG_DEBUGGABLE`). ```swift Native iOS (Swift) import VitalCore class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil ) -> Bool { VitalLogger.stdOutEnabled = true // ... return true } } ``` ```objc Native iOS (Objective-C) #import "AppDelegate.h" @import VitalCore - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { VitalLoggerObjC.stdOutEnabled = YES; // ... return YES; } ``` ```kotlin Native Android import io.tryvital.client.utils.VitalLogger VitalLogger.getOrCreate().enabled = true ``` # Vital Devices SDK Source: https://docs.tryvital.io/wearables/sdks/vital-devices ## Prerequisites Your app project must declare the required permission usage. This applies equally to both native, Flutter and React Native app projects. Add the following to your `Info.plist` file: ```xml NSBluetoothAlwaysUsageDescription Our app uses bluetooth to find, connect and transfer data between different devices ``` Bluetooth permission on iOS is automatically requested on the first scanning attempt. Add the following to your `AndroidManifest.xml`: ```xml ``` You will have to request the appropriate permissions at in your app before you can call the Vital Devices SDK. ## Bluetooth devices ### Choose Device Model The SDK provides a list of supported Bluetooth devices. Select the matching instance to prepare for scanning. ```swift Native iOS import VitalDevices let devicesManager = DevicesManager() /// Get the brands let brands = DevicesManager.brands() /// Each brand has devices let devices = DevicesManager.devices(for: brands[0]) ``` ```kotlin Native Android import io.tryvital.vitaldevices.* val deviceManager = VitalDeviceManager.create(context) val deviceModels = devices(Brand.AccuChek) val deviceModel = deviceModels[0] ``` ```typescript React Native import { VitalDevicesManager } from "@tryvital/vital-devices-react-native"; const devicesManager = new VitalDevicesManager(); let deviceModels = VitalDevicesManager.supportedDevices; let deviceModel = deviceModels[0]; ``` ```dart Flutter import 'package:vital_devices/vital_devices.dart'; final deviceManager = DeviceManager(); final deviceModels = await deviceManager.getDevices(Brand.accuChek); final deviceModel = deviceModels[0]; ``` ### Scanning for devices First you have to scan for one of the supported devices. ```swift Native iOS let cancellable = devicesManager .search(for: state.device) .sink { scannedDevice in print("Discovered: \(scannedDevice)") } // Stop scanning cancellable.cancel() ``` ```kotlin Native Android val job = deviceManager .search(deviceModel) .catch { Log.i("Devices", "Error scanning ${it.message}", it.cause) } .onEach { scannedDevice -> Log.i("Devices", "Discovered ${scannedDevice}") } .launchIn(Dispatchers.Main) // Stop scanning job.cancel() ``` ```typescript React Native let scanner = deviceManager.scanForDevice(deviceModel, { onDiscovered: (device) => { console.log("Discovered: " + device) }, onError: (error) => console.log(error) }) // Stop scanning scanner.cancel() ``` ```dart Flutter deviceManager.scanForDevices(deviceModel).listen((newDevice) { Fimber.i("Discovered: $newDevice"); }); // Stop scanning await deviceManager.stopScan(); ``` Depending on the type of device you are connecting to, you will have to call different methods to connect to it. ### Blood Pressure Monitor ```swift Native iOS let scannedDevice: ScannedDevice let reader = deviceManager.bloodPressureReader(for: scannedDevice) reader.read(device: scannedDevice).sink { samples in print("Read samples: \(samples)") } ``` ```kotlin Native Android val scannedDevice: ScannedDevice val samples = deviceManager.bloodPressure(context, scannedDevice).read() ``` ```typescript React Native let scannedDevice: ScannedDevice let samples = await devicesManager.readBloodPressure(scannedDevice.id) ``` ```dart Flutter List bloodPressureSamples; bloodPressureSamples = await deviceManager.readBloodPressureData(scannedDevice); ``` ### Glucose Meter ```swift Native iOS let scannedDevice: ScannedDevice let reader = deviceManager.glucoseMeter(for: scannedDevice) reader.read(device: scannedDevice).sink { samples in print("Read samples: \(samples)") } ``` ```kotlin Native Android val scannedDevice: ScannedDevice val samples = deviceManager.glucoseMeter(context, scannedDevice).read() ``` ```typescript React Native let scannedDevice: ScannedDevice let samples = await devicesManager.readGlucoseMeter(scannedDevice.id) ``` ```dart Flutter List glucoseSamples; glucoseSamples = await deviceManager.readGlucoseMeterData(scannedDevice); ``` After you have received samples depending on the type of device you might need to star scanning again to receive the next set of samples. ## Freestyle Libre 1 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. 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: ```swift Native iOS let reader = Libre1Reader( readingMessage: "Ready for reading", errorMessage: "Failed reading from sensor", completionMessage: "Completed successfully!", queue: mainQueue ) let reading = try await reader.read() ``` ```kotlin Native Android import android.app.Activity import io.tryvital.vitaldevices.devices.Libre1Reader let currentActivity: Activity val reading = Libre1Reader.create(currentActivity).read() ``` ```typescript React Native import { VitalDevicesManager } from "@tryvital/vital-devices-react-native"; let devicesManager: VitalDeviceManager let result = await devicesManager.readLibre1("reading", "errored", "completed") ``` `read()` returns a result object with two fields: 1. the sensor information; and 2. a list of glucose samples. ## Upload device samples to Vital This automatic behaviour is available since Vital iOS 1.0.0, Vital Android 3.0.0, Vital Flutter SDK 4.0.0 and Vital React Native 4.0.0. Vital Devices SDK automatically uploads any blood pressure and glucose samples discovered during the read operation, provided that the Vital Core SDK [has a signed-in user](/wearables/sdks/vital-core#core-sdk-status). The SDK does not currently buffer samples that fail to upload. You might want to ensure that the device has Internet connectivity before initiating the read operation. # Vital Health SDK Source: https://docs.tryvital.io/wearables/sdks/vital-health ## Overview Make sure you have successfully integrated with a Vital Mobile SDK authentication scheme, before you proceed to try out Vital Health SDK. Learn how to authenticate your mobile app user with Vital Mobile SDK. This Vital Health SDK guidance page focuses on the inital setup and API usage of the Health SDK that is common to both the Apple HealthKit and Android Health Connect integration. Please consult the following guides on requirements and restrictions of Apple HealthKit and Android Health Connect: Learn how to integrate with Apple HealthKit through Vital Mobile SDKs, available in Native iOS, React Native and Flutter. Learn how to integrate with Android Health Connect through Vital Mobile SDKs, available in Native Android, React Native and Flutter. ## Initial Setup ### iOS apps: Configure your App Delegate This section applies to: * 🔘 Native iOS apps * 🔘 React Native iOS apps This section does not apply to: * ❌ Fluter iOS apps (using Vital Flutter SDK 4.3.8 or later). Vital **Flutter** SDK (4.3.8 and later) automatically integrates with your App Delegate through the Flutter Plugin machinery. You need not manual edit your AppDelegate in **Flutter** iOS projects. You must configure your App Delegate as instructed if you use Vital Health SDK on iOS. Not doing so may result in missing background delivery, or app process termination by the operating system. In your AppDelegate's `application(_:didFinishLaunchingWithOptions:)` implementation, it **must** call Vital Health SDK `automaticConfiguration()` synchronously before it returns. Vital needs to register various handlers with the system frameworks like Apple HealthKit and BackgroundTasks. These frameworks require said registrations to be completed **before** the "app finished launching" moment [\[1\]](https://developer.apple.com/documentation/healthkit/hkhealthstore/1614175-enablebackgrounddelivery#3801028) [\[2\]](https://developer.apple.com/documentation/backgroundtasks/bgtaskscheduler/register\(fortaskwithidentifier:using:launchhandler:\)#Discussion). For React Native apps, the AppDelegate is part of the template Swift or Objective-C code in your generated Xcode project. ```swift Swift import VitalHealthKit class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil ) -> Bool { VitalHealthKitClient.automaticConfiguration() // ... return true } } ``` ```objc Objective-C / React Native #import "AppDelegate.h" #import "VitalHealthKitConfiguration.h" - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [VitalHealthKitConfiguration automaticConfiguration]; // ... return YES; } ``` ### Configure the Vital Health SDK The Health SDK must be configured before use. The configuration is persistent, so you can do this only once, immediately after [a successful authentication with the Core SDK](/wearables/sdks/authentication). ```swift Native iOS import VitalHealthKit VitalHealthKitClient.configure( backgroundDeliveryEnabled: true ) ``` ```kotlin Native Android import io.tryvital.vitalhealthconnect.VitalHealthConnectManager val manager = VitalHealthConnectManager.configure(context) manager.configureHealthConnectClient( syncNotificationBuilder = null ) ``` ```typescript React Native import { VitalHealth, HealthConfig } from "@tryvital/vital-health-react-native"; let config = new HealthConfig(); config.iOSConfig.backgroundDeliveryEnabled = true; await VitalHealth.configure(config); ``` ```dart Flutter import 'package:vital_health/vital_health.dart' as vital_health; await vital_health.configure( config: const vital_health.HealthConfig( iosConfig: vital_health.IosHealthConfig( backgroundDeliveryEnabled: true, ), ), ); ``` ## Health Data Permissions ### Ask user for health data permissions Before the SDK can read any data, you need to ask the end user to grant health data permissions. ```swift Native iOS import VitalHealthKit await VitalHealthKitClient.shared.ask( readPermissions: [.activity, .workout, .sleep], writePermissions: [] ) ``` ```kotlin Native Android // // Android Compose // import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.material3.* import io.tryvital.vitalhealthconnect.VitalHealthConnectManager val manager: VitalHealthConnectManager = ... val contract = manager.createPermissionRequestContract( readResources = set( VitalResource.Activity, VitalResource.Workout, VitalResource.Sleep, ), writeResources = emptySet(), ) val coroutineScope = rememberCoroutineScope() val permissionsLauncher = rememberLauncherForActivityResult(contract) { outcomeAsync -> coroutineScope.launch { val outcome = outcomeAsync.await() Log.i("VitalPermissionOutcome", outcome.toString()) } } Button(onClick = { permissionsLauncher.launch(Unit) }) { Text("Ask for permission") } // // AndroidX Activity Result API // import androidx.activity.ComponentActivity import io.tryvital.vitalhealthconnect.VitalHealthConnectManager val activity: ComponentActivity = ... val manager: VitalHealthConnectManager = ... activity.registerForActivityResult(contract) { outcomeAsync -> CoroutineScope(Dispatchers.Main).launch { val outcome = outcomeAsync.await() Log.i("VitalPermissionOutcome", outcome.toString()) } } ``` ```typescript React Native import { VitalHealth, VitalResource } from "@tryvital/vital-health-react-native"; await VitalHealth.ask( [VitalResource.Activity, VitalResource.Workout, VitalResource.Sleep], [] ) ``` ```dart Flutter import 'package:vital_health/vital_health.dart' as vital_health; final List readResources = [ vital_health.HealthResource.activity, vital_health.HealthResource.sleep, vital_health.HealthResource.workout, ]; await vital_health.askForPermission(readResources, []); ``` Regardless of whether the user has granted or denied the permissions, any subsequent re-request would be non-interactive and returns instantly. This is because the operating system only prompts the end user once for each resource type. If you wish to provide a way for users to review their permission grants or denials, you can [inform them of the system app location where they can do so](#check-if-a-read-permission-was-granted-or-denied-spoiler-you-cant). The health data read permission prompted is managed by the operating system. Vital cannot customize or alter its behaviour. ### Check if your app has asked for permissions before You can check if your app has already asked the user *at least once* before, for permissions on a specific `VitalResource`. Because [Ask for Permission](#ask-user-for-health-data-permissions) are gracefully ignored by the operating system beyond the first time, checking before asking can help you skip the parts of your UX journey that only makes sense to users going through [Ask for Permission](#ask-user-for-health-data-permissions) for the first time. The only exception to this behaviour is when a `VitalResource` pulls a new data type introduced in a new OS release. In this scenario, calling [Ask for Permission](#ask-user-for-health-data-permissions) would result in a OS permission prompt that asks for permissions on *specifically* those new data types. If you wish to provide a way for users to review their permission grants or denials, you can [inform them of the system app location where they can do so](#check-if-a-read-permission-was-granted-or-denied-spoiler-you-cant). ```swift Native iOS import VitalHealthKit VitalHealthKitClient.shared.hasAskedForPermission(resource: .activity) ``` ```kotlin Native Android import io.tryvital.vitalhealthconnect.VitalHealthConnectManager val manager = VitalHealthConnectManager.configure(context) manager.hasAskedForPermission(VitalResource.Activity) ``` ```typescript React Native import { VitalHealth, VitalResource } from "@tryvital/vital-health-react-native"; await VitalHealth.hasAskedForPermission(VitalResource.Activity); ``` ```dart Flutter import 'package:vital_health/vital_health.dart' as vital_health; await vital_health.hasAskedForPermission(vital_health.HealthResource.Activity); ``` ### Check if a read permission was granted or denied (⚠️ Spoiler: You can't) Vital Health SDK allows you to [ask for permission](#ask-user-for-health-data-permissions) and [check if your app has asked for permission before](#check-if-your-app-has-asked-for-permissions-before). However, Vital Health SDK **cannot** tell you: 1. Whether or not a user has granted or denied a read permission after the [Ask for Permission](#ask-user-for-health-data-permissions) has concluded. 2. The state of a read permission at any exact moment — granted or denied. Apple made a deliberate choice to conceal the state of HealthKit data type read permissions from third-party apps. Quoting [Apple's own words](https://developer.apple.com/documentation/healthkit/hkhealthstore/1614154-authorizationstatus) (as at 27 September 2024): > To help prevent possible leaks of sensitive health information, your app cannot determine whether or not a user has granted permission to read data. > > If you are not given permission, it simply appears as if there is no data of the requested type in the HealthKit store. > > \[...] In other words, all third-party apps — including anyone using the Vital Health SDK — would: 1. not be able to determine if a particular read permission has been granted or denied by the user; 2. not be able to manage or review read permissions inside their app. The best course of action out of this is to inform your users of the location where they can review and manage their health data read permissions: | Operating System | System App | Location | | -------------------- | ------------------ | ------------------------------------- | | iOS 8.0 and above | Health app | Sharing > Apps and Services | | iOS 15.0 and above | Settings app | Privacy & Security > Health | | Android 13 or below | Health Connect app | Apps and Permissions | | Android 14 and above | Settings app | Health Connect > Apps and Permissions | When designing your user experience, **assume** you would have zero knowledge of permission grants and denials after the user has gone through the [Ask for Permission](#ask-user-for-health-data-permissions) flow. ## Automatic Data Sync Vital Health SDK automates data sync for you on both Android and iOS. Please first consult the provider-specific guides for the requirements and setup instructions: * [Apple HealthKit guide](/wearables/guides/apple-healthkit) * [Android Health Connect guide](/wearables/guides/android-health-connect) ### iOS | Sync is automatically... | | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Activated on | All resource types you have [asked permission for](#ask-user-for-health-data-permissions). | | Triggered by | Two behaviours:
    • Foreground: Apple HealthKit immediately delivers buffered and new changes.
    • Background: Hourly batch delivery of changes, subject to OS throttling.
    | HealthKit change notification is an always-on behaviour. If you did not configure [Apple HealthKit Background Delivery](/wearables/guides/apple-healthkit) as instructed, your app will not receive any HealthKit change notification while it is in background. In turn, background sync would not occur. ### Android | Sync is automatically... | | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Activated on | All resource types you have [asked permission for](#ask-user-for-health-data-permissions). | | Triggered by | Two mechanisms:
    • [Sync On App Launch](/wearables/guides/android-health-connect#sync-on-app-launch)
    • [Background Sync](/wearables/guides/android-health-connect#background-sync) if it is enabled.
    | Sync On App Launch is an always-on behaviour. Background Sync is an **opt-in** behaviour. Please refer to the [Android Health Connect guide](/wearables/guides/android-health-connect) for the full context and required configuration of these two behaviours. ### Background Sync Frequency Vital Health SDK schedules hourly background sync with both Apple HealthKit and Android Health Connect, provided that you have: 1. Configured [Apple HealthKit Background Delivery](/wearables/guides/apple-healthkit) as per instruction; and 2. Configured [Health Connect Background Sync](/wearables/guides/android-health-connect#background-sync) and request user permissions (when necessary) as per instruction. However, this schedule is only advisory. The OS has full discretion to defer the scheduled time based on runtime constraints like battery power, as well as platform background execution policies. For more detailed explanation, please refer to: * [Apple HealthKit: Sync Frequency](/wearables/guides/apple-healthkit#sync-frequency) * [Android Health Connect: Sync Frequency](/wearables/guides/android-health-connect#sync-frequency) In other words, your product experience should not assume near real-time availability of wearable data, since the norm is a variable delay ranging from an hour to a day. Longer delay is also possible due to the lack of Internet connectivity, e.g., a long-haul flight, or an off-the-grid trip. The only possible exception is when your product experience does involve **active usage** of your consumer app with an active Internet connection. For example, iOS 17+ supports third-party apps initiating an Apple Watch workout session that would be live synced with your iPhone app. ### Pausing Data Synchronization You can pause and unpause data sync at any time, without having to sign-out the user from the Vital Mobile SDK. Note that pausing data sync does not reset the incremental sync progress and does not trigger a historical stage upon unpause. It only "freezes" the sync progress. ```swift Native iOS import VitalHealthKit // Pause Synchronization VitalHealthKitClient.shared.pauseSynchronization = true // Unpause Synchronization VitalHealthKitClient.shared.pauseSynchronization = false // Check if synchronization is paused print("Paused? \(VitalHealthKitClient.shared.pauseSynchronization)") ``` ```kotlin Native Android import io.tryvital.vitalhealthconnect.VitalHealthConnectManager val manager = VitalHealthConnectManager.getOrCreate(context) // Pause Synchronization manager.pauseSynchronization = true // Unpause Synchronization manager.pauseSynchronization = false // Check if synchronization is paused Log.i("sync", "Paused? ${manager.pauseSynchronization}") ``` ```typescript React Native import { VitalHealth } from "@tryvital/vital-health-react-native"; // Pause Synchronization await VitalHealth.setPauseSynchronization(true); // Unpause Synchronization await VitalHealth.setPauseSynchronization(false); // Check if synchronization is paused const isPaused = await VitalHealth.pauseSynchronization; ``` ```dart Flutter import 'package:vital_health/vital_health.dart' as vital_health; // Pause Synchronization await vital_health.setPauseSynchronization(true); // Unpause Synchronization await vital_health.setPauseSynchronization(false); // Check if synchronization is paused final isPaused = await vital_health.pauseSynchronization; ``` ## Manually start data sync You can force data sync to start with `syncData()`. ```swift Native iOS import VitalHealthKit VitalHealthKitClient.shared.syncData() ``` ```kotlin Native Android import io.tryvital.vitalhealthconnect.VitalHealthConnectManager val manager = VitalHealthConnectManager.getOrCreate(context) manager.syncData() ``` ```typescript React Native import { VitalHealth } from "@tryvital/vital-health-react-native"; await VitalHealth.syncData(); ``` ```dart Flutter import 'package:vital_health/vital_health.dart' as vital_health; await vital_health.syncData(); ``` ## Writing data You can also write data back to Apple HealthKit and Android Health Connect: ```swift Native iOS import VitalHealthKit try await VitalHealthKitClient.shared.write( input: .water(milliliters: 1000), startDate: Date(), endDate: Date() ) ``` ```kotlin Native Android import io.tryvital.vitalhealthconnect.VitalHealthConnectManager import io.tryvital.vitalhealthconnect.model.WritableVitalResource val manager = VitalHealthConnectManager.getOrCreate(context) manager.writeRecord( WritableVitalResource.Water, Instant.now(), Instant.now(), 100.0 ) ``` ```typescript React Native await VitalHealth.writeHealthData( VitalWriteResource.Water, 100.0, new Date(), new Date(), ) ``` ```dart Flutter import 'package:vital_health/vital_health.dart' as vital_health; await vital_health.writeHealthData(vital_health.HealthResourceWrite.water, startDate, endDate, 1000); ``` Not all resources are writable in both iOS and Android. Please refer to the `health_resource.dart` file. ## Sync Status You can observe the `syncStatus` stream for continuous updates on the health data sync status. ```swift Native iOS import VitalHealthKit VitalHealthKitClient.shared.status.sink { status in print("Sync Status: \(status)") } ``` ```kotlin Native Android import io.tryvital.vitalhealthconnect.VitalHealthConnectManager import kotlinx.coroutines.* import kotlinx.coroutines.flow.* val coroutineScope: CoroutineScope val manager = VitalHealthConnectManager.getOrCreate(context) manager.status .onEach { status -> Log.i("HealthDataSync", "$status") } .launchIn(coroutineScope) ``` ```typescript React Native import { VitalHealthEvents, VitalHealthReactNativeModule } from "@tryvital/vital-health-react-native"; const healthEventEmitter = new NativeEventEmitter(VitalHealthReactNativeModule); healthEventEmitter.addListener(VitalHealthEvents.statusEvent, (event: any) => { console.log(VitalHealthEvents.statusEvent, event); }); ``` ```dart Flutter import 'package:vital_health/vital_health.dart' as vital_health; Stream status = vital_health.syncStatus; ``` ## Sync Progress Debugging UI (iOS only) Vital Health SDK for iOS includes a `ForEachVitalResource` — a pre-baked SwiftUI View providing a live-updated sync progress feed of all the `VitalResource`s whose permissions have been asked for. You can use `ForEachVitalResource()` in any position which you would use `ForEach`. For example, you can use it inside a `Section` of a `List`: ```swift Native iOS import VitalHealthKit struct SyncProgressView: View { var view: some View { List { Section(header: Text("Sync Progress")) { ForEachVitalResource() } } } } ``` If you are using UIKit, you can still access it through `UIHostingController`: ```swift Native iOS let viewController: UIViewController let hostingController = UIHostingController(rootView: SyncProgressView()) viewController.present(hostingController, animated: true, completion: nil) ``` You can open the Apple HealthKit Sync Progress Inspector UI through a simple call: ```dart Flutter import 'package:vital_health/vital_health.dart' as vital_health; unawaited(vital_health.openSyncProgressView()); ``` ```typescript React Native import { VitalHealth } from "@tryvital/vital-health-react-native"; await VitalHealth.openSyncProgressView(); ``` This Sync Progress Inspector UI provides a live-updated sync progress feed of all the VitalResources whose permissions have been asked for. Note that it is currently a no-op on Android. # Introduction Source: https://docs.tryvital.io/wearables/vital-app/introduction Vital Connect iOS app is available for [the Grow and Scale plans](https://tryvital.io/pricing). The app is available on the App Store, and you can download it [here](https://apps.apple.com/us/app/vital-connect/6444167196). The Vital iOS app allows your customers to quickly sync their Apple HealthKit data with you. This is quite handy if you don't have an iOS app, but you still want to analyse the data stored in the Apple Health app.
    ## How does it work? The flow is quite straighforward: 1. Your user, is already part of our system (e.g. they have a `userId`). If they aren't, you can create one by using our [Create User](/api-reference/user/create-user) endpoint. 2. You create a code, against a `userId`. You can use our [Create Code](/api-reference/link/create-code) endpoint for this. 3. You provide this code to your user. They will then go to the Settings and input this code. Once succesfully, their data will be synced. A Vital code can only be used once. If your user is facing issues, please ask them if they tried the code multiple times. ## Stop sharing data After the user enters the code successfully they will have the option to: 1. Stop sharing data. 2. Add other providers (e.g. Garmin, Oura). If the user stops sharing data, the app will clean the user's credentials stored locally. It will also reset the SDK. However it won't delete the connection between that user and Apple HealthKit on our servers. This means that even if the user stops sharing data, the already collected data won't be deleted. This is true for both Apple HealthKit but also for any other provider the user might be connected to (e.g. Garmin, Oura). If you want to disconnect your user from a provider, you need to call the [deregister provider endpoint](/api-reference/user/deregister-a-provider). # Debugging Source: https://docs.tryvital.io/webhooks/debugging You can debug webhook events in one of two ways: the Dashboard or using the Svix API. ### [Vital Dashboard](https://app.tryvital.io) The Vital [Dashboard](https://app.tryvital.io) has a Webhooks section that allows you to test and observe webhook events. # Azure Event Hubs Source: https://docs.tryvital.io/webhooks/etl-pipelines/azure-event-hubs ETL Pipelines integration is available for [the Scale plan](https://tryvital.io/pricing). ## Event Structure Each event is published with these [Azure EventData Properties](https://learn.microsoft.com/en-us/dotnet/api/azure.messaging.eventhubs.eventdata.properties?view=azure-dotnet#azure-messaging-eventhubs-eventdata-properties): | Type | Key | Value | | ------------- | ---------------- | ---------------------------------------------------------------------------------------------------- | | Property | event-type | Vital event type, e.g., `daily.data.activity.created` | | Property | user-id | Vital User ID of the event subject | | Property | team-id | Vital Team ID of the event subject | | Property | content-encoding | `gzip`: The blob is compressed by gzip. absent: The blob is uncompressed. | | Property | idempotency-key | The event delivery ID; only unique amongst all events from the same user and of the same event type. | | Partition Key | - | Vital User ID | | Data | - | The JSON blob. This may or may not be compressed — see `content-encoding`. | By default, payload smaller than 1KiB would not be compressed. You must check `content-encoding` to identify if decompression of the event payload blob is necessary. To force Vital to always compress the payload, set `compression: always` on your [Team ETL Pipeline](https://docs.tryvital.io/api-reference/org-management/team-etl-pipeline/upsert-team-etl-pipelines) configuration. At this time, Vital publishes exclusively JSON blobs (`blob_type == json`). Having said that, you are strongly encouraged to check for and drop any events with unrecognized `Content-Type` and `Content-Encoding`. ## Broker authentication Vital connects to your Azure Event Hub namespace through a connection URL you supplied in your [Team ETL Pipeline](https://docs.tryvital.io/api-reference/org-management/team-etl-pipeline/upsert-team-etl-pipelines) configuration: * It must embed a Event Hub Shared Access Signature (SAS); and * It must **not** specify an Event Hub — the default Event Hub should be declared separately as part of your [Team ETL Pipeline](https://docs.tryvital.io/api-reference/org-management/team-etl-pipeline/upsert-team-etl-pipelines) configuration. ## Configuration Org Management API is available for [the Scale plan](https://tryvital.io/pricing). You can manage your Azure Event Hub destination through the Org Management API [Team ETL Pipeline](https://docs.tryvital.io/api-reference/org-management/team-etl-pipeline/upsert-team-etl-pipelines) endpoints. A basic configuration would look as such: ```json { "team_id": "TEAM_ID", "preferences": { "enabled": ["azure_amqp"], "preferred": "azure_amqp", }, "azure_amqp": { "connection_string": "Endpoint://...;...", "default_event_hub": "vital_event_ingestion" }, "push_historical_data": true } ``` ### Multiple Event Hubs You can [configure your Azure Event Hub destination](https://docs.tryvital.io/api-reference/org-management/team-etl-pipeline/upsert-team-etl-pipelines) to route certain events to designated Event Hubs that are *not* the default Event Hub. You can do so by declaring a list of Event Hub Matchers (JSON Path: `$.azure_amqp.event_hub_matchers`) . For every outbound event, Vital sends it to the first ever Event Hub which has a matching Event Type prefix, or the default Event Hub if none matches. ```json Example "azure_amqp": { ..., "event_hub_matchers": [ { "event_type_prefix": "labtest.", "event_hub": "labtest-events" }, { "event_type_prefix": "daily.", "event_hub": "wearable-events" }, { "event_type_prefix": "historical.", "event_hub": "wearable-events" }, { "event_type_prefix": "provider.", "event_hub": "wearable-events" } ] } ``` ### Azure Blob Storage You can [configure your Azure Event Hub destination](https://docs.tryvital.io/api-reference/org-management/team-etl-pipeline/upsert-team-etl-pipelines) to route certain events to an Azure Blob Storage container. This is useful if you have enabled the Provider Raw Data firehose, which may publish raw data events beyond the 1 MiB size limit of Azure Event Hub. Vital writes each matched event as a Block Blob. The blob name follows the standard format of: ``` # uncompressed {team-id}/{user-id}/{timestamp_rfc3339}#{event-type}#{idempotency-key}.json # gzip compressed {team-id}/{user-id}/{timestamp_rfc3339}#{event-type}#{idempotency-key}.json.gz ``` You can do so by declaring an Event Hub Matcher that targets a Blob Storage container instead of an Event Hub: ```json Example "azure_amqp": { ..., "event_hub_matchers": [ { "event_type_prefix": "raw.garmin.", "blob_storage": { "connection_string": "Endpoint://...;...", "container": "garmin-raw-data" } }, { "event_type_prefix": "raw.", "blob_storage": { "connection_string": "Endpoint://...;...", "container": "provider-raw-data" } }, { "event_type_prefix": "daily.", "event_hub": "wearable-events" }, ... ] } ``` Some polling-based providers can write 96 writes per day per user on certain event types, e.g., like `daily.data.activity.*` and `raw.fitbit.daily_activity_summary`. Azure Blob Storage typically charges based on volume and classes of operations, in addition to the retention cost. # Google Cloud Pub/Sub Source: https://docs.tryvital.io/webhooks/etl-pipelines/google-cloud-pubsub ETL Pipelines integration is available for [the Scale plan](https://tryvital.io/pricing). # Event Structure Each event is published with these Cloud Pub/Sub message attributes: | Type | Key | Value | | ------------ | ----------------- | ---------------------------------------------------------------------------------------------------- | | Attribute | `event-type` | Vital event type, e.g., `daily.data.activity.created` | | Attribute | `blob-type` | `json` : UTF8 JSON document | | Attribute | `blob-codec` | `gzip`: The blob is compressed by gzip. `null` or absent: The blob is uncompressed. | | Attribute | `idempotency-key` | The event delivery ID; only unique amongst all events from the same user and of the same event type. | | Ordering Key | - | Vital User ID (If you have enabled Event Ordering) | Payload smaller than 1KiB would not be compressed. You must check `blob_codec` to identify if decompression of the message payload blob is necessary. At this time, Vital publishes exclusively JSON blobs (`blob-type == json`). Having said that, you are strongly encouraged to check for and drop any events with unrecognized `blob-type` and `blob-codec`. ## Pub/Sub event ordering If you wish to receive event for each Vital user in their publication order, Vital supports Pub/Sub Event Ordering, and uses the Vital User ID as the event ordering key. Vital publishes all events through the `us-central1` and `europe-west1` PubSub regional endpoints. Pub/Sub Exactly Once Delivery is **discouraged** to be used together with Event Ordering. Our own trial suggested that the combination would struggle to move high volume of events. ## IAM Permission Grant the relevant IAM Principals on your topic with these pre-defined roles: * Pub/Sub Publisher (`roles/pubsub.publisher`) * Pub/Sub Viewer (`roles/pubsub.viewer`) Or more specifically these permissions if you wish to create a custom IAM role with a minimized grant: * `pubsub.topics.get` * `pubsub.topics.publish` | Environment | Region | Service Account | | ----------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- | | Sandbox | US | [customer-topics-us@vital-sandbox.iam.gserviceaccount.com](mailto:customer-topics-us@vital-sandbox.iam.gserviceaccount.com) | | Sandbox | Europe | [customer-topics-eu@vital-sandbox.iam.gserviceaccount.com](mailto:customer-topics-eu@vital-sandbox.iam.gserviceaccount.com) | | Production | US | [customer-topics-us@vital-prod-307415.iam.gserviceaccount.com](mailto:customer-topics-us@vital-prod-307415.iam.gserviceaccount.com) | | Production | Europe | [customer-topics-eu@vital-prod-307415.iam.gserviceaccount.com](mailto:customer-topics-eu@vital-prod-307415.iam.gserviceaccount.com) | ## Configuration Org Management API is available for [the Scale plan](https://tryvital.io/pricing). You can manage your RabbitMQ destination through the Org Management API [Team ETL Pipeline](https://docs.tryvital.io/api-reference/org-management/team-etl-pipeline/upsert-team-etl-pipelines) endpoints. A basic configuration would look as such: ```json { "team_id": "TEAM_ID", "preferences": { "enabled": ["cloud_pubsub"], "preferred": "cloud_pubsub", }, "cloud_pubsub": { "project": "my-gcp-project-123456", "topic": "event-ingestion", "message_ordering": true }, "push_historical_data": true } ``` # ETL Pipelines Source: https://docs.tryvital.io/webhooks/etl-pipelines/overview ETL Pipelines integration is available for [the Scale plan](https://tryvital.io/pricing). With ETL Pipelines, Vital streams all [Vital events](/webhooks/event-structure) continuously to a supported destination over direct authenticated TLS connections. Compared to [Webhooks](/webhooks/introduction), this simplifies your operational complexity by removing the need to operate public unauthenticated HTTPS endpoints for receiving incoming Webhooks. Offload the pressure on your public HTTP services. Events are published through authenticated channels over TLS. Data events larger than 1 KiB are compressed before publication. Publication order can be preserved for select destination types. ## Destinations The following destinations are supported: ## Event Schema Webhooks, ETL Pipelines and our API endpoints all share the same JSON object schema. Webhooks and ETL Pipelines use identical JSON event payload structure. Check out our [Event Catalog](/event-catalog). ## Features ### Data compression Vital does not compress payload blobs that are smaller than 1 KiB. Please refer to the destination-specific documentation below on how to detect a compressed blob versus an uncompressed blob. ### Static outbound IPs Vital establish connections to your ETL Pipeline destination through a static pool of IPs. Contact Vital support to obtain the up-to-date static IP list. ### Double writing for migration To ensure smooth switchover from Webhooks to your new ETL Pipelines destination, Vital supports double writing events to the combination of: * all your Webhook endpoints; and * your ETL Pipelines destination. This is not a proper fan-out feature, and is only intended to aid customers evaluating and one-off migrating between destinations. There can only be one *preferred* destination, and the secondary destination(s) do not enjoy the same reliability guarantee as the *preferred* one. ### Pushed Historical Data When you use ETL Pipelines, Vital can *push* all the historical data as `daily.data.*` data events to your ETL Pipelines destination. The data-less `historical.data.*.created` event would still be emitted to denote the completion of historical pull. ## Configuring ETL Pipelines You can change the ETL Pipelines configuration on your Teams at any time through the [Set Team ETL Pipelines](https://docs.tryvital.io/api-reference/org-management/team-etl-pipeline/upsert-team-etl-pipelines) endpoint of the Org Management API. Your new configuration is typically active as soon as the endpoint has acknowledged your request. Refer to the destination-specific documentation for configuration examples. Feel free to reach out Vital support if you need assisted setup, or if you need a Vital Org Key to access the Org Management API. # RabbitMQ Source: https://docs.tryvital.io/webhooks/etl-pipelines/rabbitmq ETL Pipelines integration is available for [the Scale plan](https://tryvital.io/pricing). ## Event Structure Each event is published with these RabbitMQ message custom headers: | Key | Value | | ------------------ | ---------------------------------------------------------------------------- | | Routing Key | Vital event type, e.g., `daily.data.activity.created` | | `Content-Type` | application/json : UTF8 JSON document | | `Content-Encoding` | gzip: The blob is compressed by gzip. null/absent: The blob is uncompressed. | | Data | Encoded JSON blob. This may or may not be compressed — see blob\_codec. | Payload smaller than 1KiB would not be compressed. You must check `Content-Encoding` to identify if decompression of the message payload blob is necessary. At this time, Vital publishes exclusively JSON blobs (`blob_type == json`). Having said that, you are strongly encouraged to check for and drop any events with unrecognized `Content-Type` and `Content-Encoding`. ## Broker authentication Vital only supports password authentication at this time. Vital publishes events with the following settings: * Publisher Confirm mode is required * Events are published with the Mandatory flag but without the Immediate flag. * Events are published with Event Type as the Routing Key. Please get in touch if these need to be configured differently for your RabbitMQ exchange. ## Configuration Org Management API is available for [the Scale plan](https://tryvital.io/pricing). You can manage your RabbitMQ destination through the Org Management API [Team ETL Pipeline](https://docs.tryvital.io/api-reference/org-management/team-etl-pipeline/upsert-team-etl-pipelines) endpoints. A basic configuration would look as such: ```json { "team_id": "TEAM_ID", "preferences": { "enabled": ["rabbitmq"], "preferred": "rabbitmq", }, "rabbitmq": { "uri": "amqps://...", "exchange": "default" }, "push_historical_data": true } ``` # Event Structure Source: https://docs.tryvital.io/webhooks/event-structure All our webhook events have the following standard structure: * A top-level `event_type` field, specifying the payload type contained in the `data` field. * A top-level `data` field containing the event-specific payload. * A set of top-level fields describing the event subject: * `team_id`: Your Vital Team ID. * `user_id` and `client_user_id`: The unique Vital User ID. * `client_user_id`: Your application user reference you declared for [the Vital User](/api-reference/user/create-user). Schemas and examples are available through [the Event Catalog tab in the Webhooks section of our Web app](https://app.tryvital.io). ```json Basic event structure { "data": { # ... event specific data }, "event_type": "daily.data.glucose.created", "user_id": "4a29dbc7-6db3-4c83-bfac-70a20a4be1b2", "client_user_id": "01HW3FSNVCHC3B2QB5N0ZAAAVG", "team_id": "6b74423d-0504-4470-9afb-477252ccf67a" } ``` ```json Example: Provider connection created { "data": { "source": { "logo": "https://storage.googleapis.com/vital-assets/freestyle.png", "name": "Freestyle Libre BLE", "slug": "freestyle_libre_ble" }, "user_id": "4a29dbc7-6db3-4c83-bfac-70a20a4be1b2" }, "event_type": "provider.connection.created", "user_id": "4a29dbc7-6db3-4c83-bfac-70a20a4be1b2", "client_user_id": "01HW3FSNVCHC3B2QB5N0ZAAAVG", "team_id": "6b74423d-0504-4470-9afb-477252ccf67a" } ``` ## Wearable data events Wearable data events have the following extended standard structure: * An `event_type` field for differentiating the payload contained in the data field. * A `data` field, which comprises of: * The UUID of the Vital user for which this event is intended — note that this is not the `client_user_id` of the Vital user. (JSON path: `$.data.user_id`) * The metadata about the data source of this event (JSON path: `$.data.source`) * `name`: The human-readable name of the source * `slug`: The stable identifier Vital assigned to the source — see [Supported providers](/wearables/providers/introduction) for the complete list. * `logo`: The URL to an icon representing the source. * Any number of event-specific fields. ```json Wearable data event structure { "data": { # ... event specific fields # Common wearable data event fields "source": { "logo": "https://path/to/example", "name": "Fitbit", "slug": "fitbit" }, "user_id": "4a29dbc7-6db3-4c83-bfac-70a20a4be1b2" }, "event_type": "daily.data.glucose.created", "user_id": "4a29dbc7-6db3-4c83-bfac-70a20a4be1b2", "client_user_id": "01HW3FSNVCHC3B2QB5N0ZAAAVG", "team_id": "6b74423d-0504-4470-9afb-477252ccf67a" } ``` ### Summary types Each time a [summary object](/wearables/providers/resources) is created or updated, Vital emits a data event for the occurrence. The object is made available directly at the top-level `data` field (JSON path: `$.data`), alongside the standard fields listed above. Each summary is uniquely identified by its ID (`$.id`). Given the same ID, the latest version of a summary you received **replaces** all its previous versions. ```json Summary data event structure { "data": { # ... All summary object fields # Common wearable data event fields "source": { "logo": "https://path/to/example", "name": "Fitbit", "slug": "fitbit" }, "user_id": "4a29dbc7-6db3-4c83-bfac-70a20a4be1b2" }, "event_type": "daily.data.activity.created", "user_id": "4a29dbc7-6db3-4c83-bfac-70a20a4be1b2", "client_user_id": "01HW3FSNVCHC3B2QB5N0ZAAAVG", "team_id": "6b74423d-0504-4470-9afb-477252ccf67a" } ``` ```json Example: Activity updated { "data": { "calendar_date": "2023-05-17", "calories_active": 134, "calories_total": 1006, "daily_movement": 1162, "date": "2023-05-17T00:00:00+00:00", "distance": 1162, "floors_climbed": 2, "high": null, "id": "715dd6a5-0c67-471c-b5fe-9681a980a2c2", "low": null, "medium": null, "steps": 1496, "timezone_offset": 3600, # Common wearable data event fields "source": { "logo": "https://storage.googleapis.com/vital-assets/apple_health.png", "name": "Apple HealthKit", "slug": "apple_health_kit" }, "user_id": "4a29dbc7-6db3-4c83-bfac-70a20a4be1b2" }, "event_type": "daily.data.activity.updated", "user_id": "4a29dbc7-6db3-4c83-bfac-70a20a4be1b2", "client_user_id": "01HW3FSNVCHC3B2QB5N0ZAAAVG", "team_id": "6b74423d-0504-4470-9afb-477252ccf67a" } ``` ### Timeseries types (except for Workout Streams) Each time a batch of [timeseries samples](/wearables/providers/resources) has been created or updated, Vital emits a data event for the batch of samples. The samples are made available under an inner `data` field under the top-level `data` field (JSON path: `$.data.data`). Each timeseries sample is uniquely identified by a compound key of: * the timeseries resource type; * the source provider; * the [Source Type](/wearables/providers/data-attributions#source-type); and * the sample timestamp. Given the same compound key, the latest value of a sample you received **replaces** all the previous values. While most timeseries data are immutable, there have been exceptions of some sources aggregating some data types using time bucketing, and some may even send updates ASAP on time buckets that have incomplete data. For example, activity timeseries data from Apple HealthKit (integration in production) and Garmin (integration planned) do exhibit such behaviour.

    These exceptions are the rationale behind the recommended deduplication semantic as stated above.
    ```json Timeseries data event structure { "data": { "data": [ { "timestamp": "2023-05-16T10:00:00+00:00", "timezone_offset": 3600, "type": null, "unit": "count", "value": 237 }, { "timestamp": "2023-05-16T11:00:00+00:00", "timezone_offset": 3600, "type": null, "unit": "count", "value": 451 }, # ... other timeseries samples in the batch ], "source_id": 16, # Common wearable data event fields "source": { "logo": "https://storage.googleapis.com/vital-assets/apple_health.png", "name": "Apple HealthKit", "slug": "apple_health_kit" }, "user_id": "4a29dbc7-6db3-4c83-bfac-70a20a4be1b2" }, "event_type": "daily.data.steps.created", "user_id": "4a29dbc7-6db3-4c83-bfac-70a20a4be1b2", "client_user_id": "01HW3FSNVCHC3B2QB5N0ZAAAVG", "team_id": "6b74423d-0504-4470-9afb-477252ccf67a" } ``` ```json Example: Calories Active created { "data": { "data": [ { "timestamp": "2023-05-17T07:00:00+00:00", "start": "2023-05-17T07:00:00+00:00", "end": "2023-05-17T08:00:00+00:00", "timezone_offset": 3600, "type": "", "unit": "kcal", "value": 39.518054167148996 }, { "timestamp": "2023-05-17T08:00:00+00:00", "start": "2023-05-17T08:00:00+00:00", "end": "2023-05-17T09:00:00+00:00", "timezone_offset": 3600, "type": "", "unit": "kcal", "value": 31.270257990822234 }, { "timestamp": "2023-05-17T09:00:00+00:00", "start": "2023-05-17T09:00:00+00:00", "end": "2023-05-17T09:01:00+00:00", "timezone_offset": 3600, "type": "", "unit": "kcal", "value": 5.733999999999999 } ], "source_id": 16, # Common wearable data event fields "source": { "logo": "https://storage.googleapis.com/vital-assets/apple_health.png", "name": "Apple HealthKit", "slug": "apple_health_kit" }, "user_id": "4a29dbc7-6db3-4c83-bfac-70a20a4be1b2" }, "event_type": "daily.data.calories_active.created", "user_id": "4a29dbc7-6db3-4c83-bfac-70a20a4be1b2", "client_user_id": "01HW3FSNVCHC3B2QB5N0ZAAAVG", "team_id": "6b74423d-0504-4470-9afb-477252ccf67a" } ``` ### Workout Streams While Workout Streams are classified as a timeseries type, it contains a huge amount of timeseries data which cannot be delivered by our webhook message transport. So events for Webhook Streams are shallow, with only some minimal metadata about the workout. Successful reception of the event indicates that the data are available to be read via our REST API, e.g., [the Workout Stream endpoint](/api-reference/workouts/get-stream) `GET /v2/timeseries/workouts/{workout_id}/stream`. ```json Workout Stream created { "data": { "message": "Due to payload size limits, to access the workout stream, please use the /workouts/edc80dd0-8cc8-4ec8-8b80-931fd9a3309a/stream endpoint.", "provider_id": "51278201336", "sport": { "id": 57, "name": "Other", "slug": "other" }, "workout_id": "fdc70dd0-8cc8-4ec8-8b80-121fd9a3302a" # Common wearable data event fields "source": { "logo": "https://storage.googleapis.com/vital-assets/fitbit.png", "name": "Fitbit", "slug": "fitbit" }, "user_id": "bf6222fb-3b81-4201-9630-118bfee01e03", }, "event_type": "daily.data.workout_stream.created", "user_id": "4a29dbc7-6db3-4c83-bfac-70a20a4be1b2", "client_user_id": "01HW3FSNVCHC3B2QB5N0ZAAAVG", "team_id": "6b74423d-0504-4470-9afb-477252ccf67a" } ``` # Introduction Source: https://docs.tryvital.io/webhooks/introduction With Webhooks and [ETL Pipelines](/webhooks/etl-pipelines/overview), Vital pushes any new data or updates to your endpoint or your ETL destination as soon as they are discovered. ## Connection Created stage When a user successfully connects a provider through [Vital Link](/wearables/connecting-providers/introduction), Vital sends a Provider Connection Created event to acknowledge it: `provider.connection.created` Check out the [Provider Connection Created event](/event-catalog/provider.connection.created) in the Event Catalog. ```json Webhook Event { "event_type": "provider.connection.created", "data": { "user_id": "4eca10f9-...", "provider": { "name": "Strava", "slug": "strava", "logo": "https://storage.googleapis.com/vital-assets/strava.png" }, "resource_availability": { ... } } } ``` ## Historical Data Backfill stage Vital also schedules jobs to backfill *historical* data upon connection establishment. Historical data are data that exist prior to the connection establishment. Because this can [amount to months of data](/wearables/providers/introduction#historical-data-pull-range) — and might potentially run into temporary provider-side API rate limits — these backfill jobs may take minutes to hours to complete. You can inspect the status of historical data backfill jobs of each individual user through [Vital Dashboard](https://app.tryvital.io) or [Vital API](/api-reference/data/introspection/historical-pulls). Once the historical data backfill job of a specific [resource](/wearables/providers/resources) has **fully** completed, Vital sends a **Historical Pull Completion** event for the resource: `historical.data.{RESOURCE}.created` This event is a data-less notification. Use the [Vital Data Access API](/api-reference/data/sleep/get-summary) to fetch the resource you are interested in. Constrain your API call using the information in the Historical Pull Completion event. For example, the event includes the [provider](/wearables/providers/introduction) slug as well as full datetime range which Vital has looked through. Check out the [Historical Workout Data Pull Completion event](/event-catalog/historical.data.workouts.created) in the Event Catalog. ```json { "event_type": "historical.data.workouts.created", "data": { "user_id": "e9e072e8-...", "start_date": "2020-06-21T08:23:01+00:00", "end_date": "1996-11-02T14:39:28+00:00", "provider": "zwift" } } ``` [ETL Pipelines](/webhooks/etl-pipelines/overview) can opt-in to receive historical data as *Vital Data Events* (as outlined below in the [Incremental Data stage](#incremental-data-stage) section). Note that Vital would still send the Historical Pull Completion event to signal the completion of the historical data backfill stage. ## Incremental Data stage Once the historical data backfill stage completes, Vital [monitors for new data and changes](/wearables/providers/introduction#data-frequency). Whenever new data or changes are discovered, Vital sends them out as **Data Events** as soon as possible: You can inspect the state of incremental data updates of each individual user through [Vital Dashboard](https://app.tryvital.io) or [Vital API](/api-reference/data/introspection/user-resources). * Initial discovery — `daily.data.{RESOURCE}.created` * Subsequent updates — `daily.data.{RESOURCE}.updated` Treat the `daily.data` prefix as a misnomer for *Incremental Data*. It **does not** imply a daily delivery schedule. Vital sends out *Data Events* as soon as we discover new or updated entries as reported by a provider. This also implies: 1. Vital does not prioritize or aggregate Data Events across providers: * You may receive multiple events of the same type from a single provider, because the provider supports multiple sources, devices or apps (e.g., iPhone sleeps and Apple Watch sleeps from Apple HealthKit); * You may receive multiple events of the same type from multiple providers, because the user has connected to multiple providers. 2. There is no standard Data Event delivery frequency or schedule across providers *and* resources: * Some may be sent only once per day; * Some may be sent multiple times at irregular times; * Some may appear to be sent on a regular schedule throughout the day. Your system typically would ingest relevant Vital Data Events into a database. You can query the database with any business rules and query constraints suitable to your product experience. This may include your own data prioritization rules based on, e.g., [Source Type](/wearables/providers/data-attributions#source-type). Vital also offers the [Horizon AI Aggregation API](/api-reference/horizon-ai/aggregation/overview) for your data aggregation and consolidation needs. Check out the [Workout Created event](/event-catalog/daily.data.workouts.created) in the Event Catalog. The event structure and deduplication semantic of [summary types](/wearables/providers/resources) and [timeseries types](/wearables/providers/resources) have a few key differences. Please refer to [Event Structure](/webhooks/event-structure) for a more in-depth discussion. ```json { "event_type": "daily.data.workouts.created", "data": { "id": "c90bd6fb-...", "average_hr": 198, "distance": 8544, "time_start": "2012-11-24T22:57:01+00:00", "time_end": "2017-04-22T04:57:31+00:00", "calories": 4470, "hr_zones": [8279], "user_id": "ab3247dc-...", ..., "provider_id": "dolor ipsum reiciendis Lorem veniam elit. esse", "source": { "provider": "zwift", "type": "unknown" } } } ``` ## Deep Dive into Vital Webhooks