# Connect a patient's wearable

Canonical page: https://docs.ovok.com/wearables/connect-a-patient

A patient connects their own account, from your patient app, while signed in as themselves. Every patient route reads the project and the Patient from the bearer token; a practitioner or admin token answers [`403 PATIENT_PROFILE_REQUIRED`](/errors/patient-profile-required).

## Before you start [#before-you-start]

- The provider is [set up and enabled](/wearables/set-up-providers) in the project.
- [`PATIENT_APP_URL`](/settings-and-features/settings/patient-app-url) is set. Ovok appends `/wearables/<provider>` to it as written, so `https://app.example.com` returns to `https://app.example.com/wearables/oura`, and `myapp://` returns to `myapp:///wearables/oura`.
- Your app can handle that URL: a route in a web app, or a deep link in a native app.

## The flow [#the-flow]

```mermaid
sequenceDiagram
  participant App as Patient app
  participant Ovok
  participant Provider
  App->>Ovok: POST /v1/wearables/connections/oura/authorize
  Ovok-->>App: { authorizationUrl }
  App->>Provider: Open authorizationUrl in a browser or auth session
  Provider->>Ovok: GET /v1/wearables/oauth/oura/callback?code&state
  Ovok->>Provider: Exchange the code for tokens
  Ovok-->>App: 302 to <PATIENT_APP_URL>/wearables/oura?result=connected
  App->>Ovok: GET /v1/wearables/connections
  Ovok-->>App: oura: { connectedAt, syncedThrough, reconnectRequired }
```

1. **Ask for a consent link.** `authorizationUrl` is valid for 10 minutes and for one use. Ask for a new one each time the patient taps "Connect".
2. **Open it** in the system browser or an auth session: `ASWebAuthenticationSession` on iOS, Custom Tabs on Android, `openAuthSessionAsync` from `expo-web-browser`, or a full-page redirect on the web. Do not use an embedded WebView: the patient should see the provider's own page in a real browser.
3. **Ovok handles the callback.** You never call it. It exchanges the code, stores the tokens encrypted and redirects to your app with `result`:
   - `connected`: the tokens are stored;
   - `denied`: the patient refused, or the provider returned an error;
   - `failed`: the code exchange or the save failed, an admin disabled the provider meanwhile, or Polar refused to register the patient because they have not accepted Polar's data-sharing consents. Nothing stays connected.
4. **Re-read the connections.** `result` is for display only. `GET /v1/wearables/connections` is the state of record.

The first data arrives with the next daily sync at 08:00 UTC. To show data straight away, [sync on demand](/wearables/sync-and-read-data#sync-on-demand) after `connected`.

Connecting a provider the patient is already connected to replaces the stored tokens. This is also how a patient clears `reconnectRequired`.

For Withings, Ovok also subscribes the patient to Withings' notifications at connect time, so new measurements and nights arrive within minutes of reaching Withings.

<Callout type="info" title="Polar: consents first">


Tell Polar users to accept Polar's mandatory data-sharing consents in their Polar account before connecting. Without them, Polar refuses to register the patient and the flow returns `result=failed`.

</Callout>

## With `@ovok/core` [#with-ovokcore]

`@ovok/core` 0.4.46 and later has the provider and connection operations. Install or update it:

```bash
npm install @ovok/core@latest
```

Start the flow from a client signed in as the patient:

```ts
const { authorizationUrl } = await client.authorizeWearableConnection('oura');
window.location.assign(authorizationUrl);
```

On the return route, re-read the list:

```ts
const params = new URLSearchParams(window.location.search);
const result = params.get('result'); // 'connected' | 'denied' | 'failed', for display only
const { connections } = await client.listWearableConnections();
```

Disconnect:

```ts
await client.deleteWearableConnection('oura');
```

The SDK does not wrap the sync route yet. Call it through the same client:

```ts
await client.post('/v1/wearables/connections/oura/sync', undefined);
```

<Callout type="info" title="SDK types lag the API">


In `@ovok/core` 0.4.46, the `WearableConnection` type declares only `connectedAt`. The API also returns `syncedThrough` and `reconnectRequired`; read them from the response until the types catch up.

</Callout>

### React hooks [#react-hooks]

`@ovok/core` also exports React hooks for the same operations: `useWearableConnections()`, `useAuthorizeWearableConnection()` and `useDeleteWearableConnection()` for the patient, and `useWearableProviders()`, `useCreateWearableProvider()`, `useUpdateWearableProvider()` and `useDeleteWearableProvider()` for admin screens.

```tsx
import { useWearableConnections } from '@ovok/core';

const { data, reload } = useWearableConnections();

// After the app returns from the provider:
reload();
```

A successful `deleteWearableConnection()` refreshes mounted `useWearableConnections()` hooks that use the same client.

### React Native SDK [#react-native-sdk]

`@ovok/native` has no wearable connect flow yet. In a React Native or Expo app, use the `@ovok/core` client as above and open `authorizationUrl` with `expo-web-browser`, with your `PATIENT_APP_URL` deep link as the return URL:

```ts
import * as WebBrowser from 'expo-web-browser';

const { authorizationUrl } = await client.authorizeWearableConnection('oura');
await WebBrowser.openAuthSessionAsync(authorizationUrl, 'myapp:///wearables/oura');
const { connections } = await client.listWearableConnections();
```

## With plain HTTP [#with-plain-http]

```bash
export OVOK_TOKEN='<access token of a patient>'
```

Start the flow:

```bash
curl --request POST \
  --url 'https://api.sandbox.ovok.com/v1/wearables/connections/oura/authorize' \
  --header "Authorization: Bearer ${OVOK_TOKEN}"
```

```json
{
  "provider": "oura",
  "authorizationUrl": "https://cloud.ouraring.com/oauth/authorize?response_type=code&client_id=…&redirect_uri=…&state=…&scope=…"
}
```

Read the connections:

```bash
curl --request GET \
  --url 'https://api.sandbox.ovok.com/v1/wearables/connections' \
  --header "Authorization: Bearer ${OVOK_TOKEN}"
```

```json
{
  "connections": [
    {
      "provider": "oura",
      "connection": {
        "connectedAt": "2026-10-21T09:00:00.000Z",
        "syncedThrough": "2026-10-22T08:00:00.000Z",
        "reconnectRequired": false
      }
    },
    { "provider": "whoop", "connection": null }
  ]
}
```

Disconnect:

```bash
curl --request DELETE \
  --url 'https://api.sandbox.ovok.com/v1/wearables/connections/oura' \
  --header "Authorization: Bearer ${OVOK_TOKEN}"
```

The answer is `204`. Where the provider allows it, Ovok first undoes what it set up there: Withings stops notifying Ovok about the patient, and Polar deregisters the patient, which also revokes your app's access. Other providers keep your app's access: tell the patient they can remove it in their provider account. A failure at the provider never stops the disconnect.

## Build the screen from the list [#build-the-screen-from-the-list]

- Render the providers from `connections`, not from a hard-coded list. It holds every provider the project has enabled, plus any provider the patient is still connected to after an admin disabled it.
- `connection: null`: offer **Connect**.
- `reconnectRequired: true`: the provider no longer accepts the connection, usually because the patient removed your app's access at the provider. Offer **Reconnect**, which is the same authorize call.
- `syncedThrough: null`: connected, not synced yet.

## Errors [#errors]

| Status | Code | What to do |
| --- | --- | --- |
| [`403`](/errors/forbidden) | [`PATIENT_PROFILE_REQUIRED`](/errors/patient-profile-required) | Sign in as the patient. Admins and practitioners cannot connect on a patient's behalf. |
| [`404`](/errors/not-found) | [`PROVIDER_NOT_AVAILABLE`](/errors/provider-not-available) | The project has not set up the provider or disabled it. Hide it; re-read the list. |
| [`404`](/errors/not-found) | [`CONNECTION_NOT_FOUND`](/errors/connection-not-found) | On disconnect: the patient is not connected to this provider. Re-read the list. |
| [`409`](/errors/conflict) | [`APP_URL_NOT_CONFIGURED`](/errors/app-url-not-configured) | The project has no usable `PATIENT_APP_URL`. Show it as a setup problem and tell the project admin. |
| [`422`](/errors/validation-failed) | [`INVALID_STATE`](/errors/invalid-state) | On the callback: the link expired (10 minutes), was already used, or belongs to another provider. Start again from **Connect**. |
| [`501`](/errors/not-implemented) | [`FEATURE_NOT_CONFIGURED`](/errors/feature-not-configured) | Wearables are not set up on this Ovok server. Contact Ovok support. |
| [`503`](/errors/service-unavailable) | [`STATE_STORE_UNAVAILABLE`](/errors/state-store-unavailable) | Ovok could not store or read the pending authorization. Retry after `Retry-After`. |
