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
OvokConfigExceptionfrom 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/nextwrites. 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/nativesplits values because Expo SecureStore warns past 2048 bytes.flutter_secure_storagehas 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:
loginruns the tenant login with PKCE and signs in to the configuredtenantCode. - 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.
- A refused refresh token signs the user out and calls
- 401: a request answered 401 refreshes once and is sent once more.
- Logout:
logoutalways clears the local session, even when the server cannot be reached.
ovok.auth also has:
register(Registration(...)): returnsSignedIn, orClinicianInvitewhen the project routes the email to a clinician invitation.registerPractitioner, andsocialLogin(SocialProvider.google, internalId: ..., credential: {...}).current(),me(),changePassword,resetPassword,completePasswordResetanddeleteAccount(days: 30).sessions.list()andsessions.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.slimincludes patients, devices, notifications and notes, thresholds, trends, sleep, locations, zones, exports, change history, projects, policies, organizations, practitioners, users, and health.ovok.signalsenables Signals for a patient and lists or repairs undeliverable readings.ovok.projects,accounts,locales,localizations, andemailcover the platform project, account, language, translation, and template routes.ovok.invitesexposes record-share and care invite flows.ovok.sharingmanages patient-sharing invites, shares, and settings.ovok.cmsreads tenant-scoped Payload collections, legal pages, translations, and release notes without requiring a signed-in session.ovok.patientreads the signed-in patient's FHIR records, latest observations, appointments, and dashboard.ovok.measurementsbuilds FHIR Observations from supported measurement maps, saves them conditionally, and queues retryable saves in secure storage.ovok.questionnairespopulates questionnaires and submits responses with their observations in one transaction.ovok.contentreads and edits CMS content;ovok.documentsmanages document uploads and builds signed or public download URLs.ovok.ai,ovok.videoCalls,ovok.wearables, andovok.botsexpose 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-Afterwins, and no wait is longer than 5 s. - Timeouts: a timed-out request fails with
OvokConnectionException.package:httpcannot 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
Libraries
- ovok_sdk
- The Ovok SDK for Dart and Flutter, following the
@ovok/native/nextcontract.