ovok_sdk (Dart)

The pure-Dart Ovok SDK follows the @ovok/native/next contract, so its method names and results match across SDKs. It works in Flutter apps, servers and command-line tools.

Flutter-specific plugins and UI belong in the separate ovok_flutter package. Keep server and CLI dependencies Flutter-free by depending on ovok_sdk directly.

Install

dart pub add ovok_sdk

Or in pubspec.yaml:

dependencies:
  ovok_sdk: ^0.1.0
  flutter_secure_storage: ^9.2.0 # or any store you adapt to OvokSecureStore

Development builds (0.1.0-dev.<N>+<sha7>) and release candidates (0.1.0-rc.<n>) are pre-releases on pub.dev, so they are never picked by default. Pin one to try it:

dart pub add "ovok_sdk:0.1.0-dev.<N>+<sha7>"

Create a client

The session lives in the device's secure store. OvokSecureStore has the same read, write and delete methods as flutter_secure_storage, so the adapter only forwards them:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:ovok_sdk/ovok_sdk.dart';

class SecureStore implements OvokSecureStore {
  SecureStore(this._storage);

  final FlutterSecureStorage _storage;

  @override
  Future<String?> read({required String key}) => _storage.read(key: key);

  @override
  Future<void> write({required String key, required String value}) =>
      _storage.write(key: key, value: value);

  @override
  Future<void> delete({required String key}) => _storage.delete(key: key);
}

final ovok = Ovok(
  baseUrl: 'https://api.sandbox.ovok.com',
  tenantCode: 'your-tenant',
  secureStore: SecureStore(const FlutterSecureStorage(
    // Lets a background launch read the session after the first unlock.
    iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
  )),
);
Option Required Default Notes
baseUrl Yes An absolute http(s) URL.
tenantCode Yes Picks the tenant at sign-in, and names the storage key.
secureStore Yes Any OvokSecureStore. Platform options go on the store itself.
fhirPath No fhir The FHIR base under baseUrl, such as fhir/R4.
clientId No Your client application's ID. Social sign-in needs it, and password resets use it.
httpClient No http.Client() Pass your own for proxies, certificate pinning or tests.
timeout No 30 s Per request.
maxNetworkRetries No 2 For GET, PUT, DELETE and conditional creates only.
onUnauthenticated No Called when a refresh token is refused and the user has been signed out.
  • Errors from options: a bad option throws OvokConfigException from the constructor, not from the first request.
  • No clientSecret: anything shipped inside an app can be extracted from it, so there is no machine client.

Secure storage

  • Key: the session is one JSON value under ovok:<tenantCode>:session, in the same shape @ovok/core/next writes. The client never touches other keys.
  • Locked keychain: a read that throws, such as one before the first unlock, counts as "no session". The client never deletes anything because a read failed.
  • No chunking: @ovok/native splits values because Expo SecureStore warns past 2048 bytes. flutter_secure_storage has no such limit, so each value is stored whole.

Sign users in

final result = await ovok.auth.login(
  profileType: ProfileType.practitioner,
  email: 'ada@example.com',
  password: password,
);

if (result case MfaRequired(:final challenge)) {
  await ovok.auth.verifyMfa(challenge, codeFromAuthenticatorApp);
}

await ovok.auth.logout();
  • Login flow: login runs the tenant login with PKCE and signs in to the configured tenantCode.
  • Refresh: access tokens refresh a minute before they expire, through /oauth2/token. Concurrent requests share one refresh.
    • A refused refresh token signs the user out and calls onUnauthenticated.
    • An outage keeps the session, so a later call can retry.
  • 401: a request answered 401 refreshes once and is sent once more.
  • Logout: logout always clears the local session, even when the server cannot be reached.

ovok.auth also has:

  • register(Registration(...)): returns SignedIn, or ClinicianInvite when the project routes the email to a clinician invitation.
  • registerPractitioner, and socialLogin(SocialProvider.google, internalId: ..., credential: {...}).
  • current(), me(), changePassword, resetPassword, completePasswordReset and deleteAccount(days: 30).
  • sessions.list() and sessions.revoke('current' | 'other' | 'all' | sessionId).

Realtime

Listen for matching FHIR updates with an authenticated WebSocket. The client reconnects after a dropped connection and refreshes its short-lived binding.

final stop = await ovok.realtime.subscribe(
  'Observation?subject=Patient/$patientId',
  (notification) => refreshObservationList(),
);

await stop();

See the realtime guide for session lifecycle and cleanup details. The Flutter example app includes a sandbox sign-in and notification counter.

FHIR

Resources are decoded JSON (Map<String, dynamic>).

final patient = await ovok.fhir.read('Patient', id);

final page = await ovok.fhir.search('Observation', {'patient': 'Patient/$id', '_count': 50});
await for (final observation in page.autoPaging()) {
  // every page, fetched as you go
}

await ovok.fhir.create(observation, ifNoneExist: 'identifier=urn:device|42');
await ovok.fhir.update(patient);
await ovok.fhir.patch('Patient', id, [PatchOperation.replace('/active', false)]);
await ovok.fhir.delete('Patient', id);
await ovok.fhir.batch(bundle);
final first = await ovok.fhir.searchOne('Patient', {'identifier': 'urn:mrn|123'});
  • Search parameters: a list value repeats its parameter, which FHIR reads as AND, as in 'date': ['ge2026-01-01', 'le2026-02-01']. For OR, write one comma-separated string.
  • Conditional create: with ifNoneExist, a retry cannot create a duplicate, so the request is retried like a GET.

ovok.health.check() reads /healthcheck without a token; it always answers 200, so read ok. ovok.consent has legal.retrieve(), legal.set(accepted: true), the same pair under medical, and requestSignature(consentId).

API namespaces

The Flutter client also follows the @ovok/native/next platform and domain namespaces. Responses use decoded JSON maps, and paged lists return Page<JsonMap>.

final patients = await ovok.slim.patients.list({'count': 25});
await for (final patient in patients.autoPaging()) {
  // Each next page is loaded only when it is needed.
}

final locales = await ovok.locales.retrieve();
final legalPage = await ovok.cms.legalPage('terms-and-conditions');
  • ovok.slim includes patients, devices, notifications and notes, thresholds, trends, sleep, locations, zones, exports, change history, projects, policies, organizations, practitioners, users, and health.
  • ovok.signals enables Signals for a patient and lists or repairs undeliverable readings.
  • ovok.projects, accounts, locales, localizations, and email cover the platform project, account, language, translation, and template routes.
  • ovok.invites exposes record-share and care invite flows. ovok.sharing manages patient-sharing invites, shares, and settings.
  • ovok.cms reads tenant-scoped Payload collections, legal pages, translations, and release notes without requiring a signed-in session.
  • ovok.patient reads the signed-in patient's FHIR records, latest observations, appointments, and dashboard.
  • ovok.measurements builds FHIR Observations from supported measurement maps, saves them conditionally, and queues retryable saves in secure storage.
  • ovok.questionnaires populates questionnaires and submits responses with their observations in one transaction.
  • ovok.content reads and edits CMS content; ovok.documents manages document uploads and builds signed or public download URLs.
  • ovok.ai, ovok.videoCalls, ovok.wearables, and ovok.bots expose the corresponding AI, video-call, wearable, and bot routes.
final dashboard = await ovok.patient.dashboard();
final result = await ovok.measurements.save(
  BuildObservationsParams(
    patientId: patientId,
    measurement: {
      'measurementTypeKey': 'body-weight',
      'bodyWeight': 72.4,
    },
  ),
);

if (result.status == 'queued') {
  await ovok.measurements.offlineQueue.flush();
}

See the API reference for each resource and method.

Paging

Lists return Future<Page<T>>. A Page has data, hasMore and total (null when the server does not count). page.autoPaging() is a Stream<T> that walks every item from that page on, fetching the next page only when it is needed. Which pagination the server uses (FHIR Bundle links, numbered pages, cursors or Payload pages) does not show.

Errors

Every failure is an OvokException with status, code, type, message, traceId, requestId, retryAfterMs and fieldErrors. Catch a subclass to handle one case:

Class When
OvokValidationException 400, 422
OvokAuthenticationException 401
OvokPermissionException 403: signed in, but not allowed
OvokNotFoundException 404
OvokConflictException 409
OvokRateLimitException 429
OvokServiceException 5xx
OvokConnectionException No answer: network failure or timeout
OvokConfigException A bad option, or an action this client cannot take
try {
  await ovok.fhir.read('Patient', id);
} on OvokNotFoundException {
  // show "not found"
} on OvokException catch (error) {
  log('Ovok failed', error: error.traceId);
}

Quote traceId when you report a problem.

  • Retries: GET, PUT, DELETE and conditional creates retry on 429, 502, 503, 504 or a connection failure. The wait is 300 ms, doubling each time; Retry-After wins, and no wait is longer than 5 s.
  • Timeouts: a timed-out request fails with OvokConnectionException. package:http cannot cancel it, so it may still reach the server.

Documentation

See the Flutter SDK documentation for setup and guides, and the API reference for every class and method.

Development

Run these commands from the repository root:

flutter pub get
dart format --output=none --set-exit-if-changed .
dart analyze packages/ovok_sdk
(cd packages/ovok_sdk && dart test)
(cd packages/ovok_flutter && flutter test)
(cd packages/ovok_flutter/example && flutter test)

Releases

Every push to a release branch tags a version, and the tag push publishes it to pub.dev:

Branch Tag = pub.dev version
development v0.1.0-dev.<N>+<sha7>
staging v0.1.0-rc.<n>
main v0.1.0

N is the commit count of development. The base version is version in packages/ovok_sdk/pubspec.yaml; keep it aligned with ovok_flutter. Bump it, and add a CHANGELOG.md entry, after a final release; the workflow refuses to tag dev or rc builds of a version that is already released.

License

GNU Affero General Public License v3.0.

Libraries

ovok_sdk
The Ovok SDK for Dart and Flutter, following the @ovok/native/next contract.