Skip to content

SDK integration

Use this mode when your integration is in TypeScript / JavaScript and you want typed methods + automatic OAuth2 token handling. The SDK ships as @sa-platform/client-sdk.

The SDK exposes methods for every public endpoint — it doesn't pre-filter by scope. Calls that lack the matching scope return 403 from the platform. Pass exactly the scopes you've been granted at construction time to keep behaviour explicit.

Install

npm install @sa-platform/client-sdk

Node 20+ recommended.

Construct the client

import { SkinAnalyticsClient } from '@sa-platform/client-sdk';

const client = new SkinAnalyticsClient({
  baseUrl: 'https://platform.example.com',
  clientId: process.env.SA_CLIENT_ID!,
  clientSecret: process.env.SA_CLIENT_SECRET!,
  scopes: [
    'patients:read',
    'patients:write',
    'cases:read',
    'cases:write',
    'images:write',
    'derm_review:read',
    'derm_review:write',
  ],
});

baseUrl is the platform host root (no path) — the SDK composes the per-service prefix (/v1/auth, /v1/clinical-api, /v1/orchestrator) automatically.

The SDK exchanges your credentials for an OAuth2 token on first use and caches it in memory until 30s before expiry. No manual token plumbing.

Available resources

Resource Methods Scopes required
client.patients create, get, update patients:read / patients:write
client.cases create, get, update cases:read / cases:write
client.images get images:read
client.workflows getDefinition, getInstance, advanceInstance orchestrator:read, orchestrator:read-instances, orchestrator:advance-instance
client.aiReview submit(caseId, opts?), listForCase(caseId) derm_review:read / derm_review:write
client.webhooks subscribe, list webhooks:read / webhooks:write

Example: end-to-end submission

// 1. Create a patient
const patient = await client.patients.create({
  given_name: 'Alice',
  family_name: 'Anderson',
  dob: '1985-06-15',
});

// 2. Open a case for them
const c = await client.cases.create({ patient_id: patient.id as string });

// 3. Submit images via multipart (low-level request — full image upload
//    helpers land in a follow-up release).
//    Use the direct-API path for now: POST /v1/clinical-api/images:initiate with multipart.

// 4. Submit the case (with its already-attached images) to DERM. The
//    actor and the image set come from the case server-side — no body.
const submitted = await client.aiReview.submit(c.id as string, {
  idempotencyKey: 'request-uuid', // optional but recommended for retries
});

// 5. Poll until the latest review for the case completes
while (true) {
  const { items } = await client.aiReview.listForCase(c.id as string);
  const latest = items[0]; // newest first
  if (latest?.status === 'completed' || latest?.status === 'failed') break;
  await new Promise((r) => setTimeout(r, 5_000));
}
void submitted;

Working with workflows

When the platform configures a client-driven workflow for your product, you advance it step-by-step:

const def = await client.workflows.getDefinition('<definition-id>');
const instance = await client.workflows.getInstance('<instance-id>');

// instance.current_step tells you what's parked; supply its output to advance.
await client.workflows.advanceInstance(instance.id as string, {
  step_id: 'collect_questions_1',
  output: { answers: { q1: 'yes', q2: 'no' } },
});

Errors

import { ApiError } from '@sa-platform/client-sdk';

try {
  await client.cases.create({ patient_id: 'does-not-exist' });
} catch (err) {
  if (err instanceof ApiError) {
    if (err.status === 403) console.error('Missing scope:', err.parsedBody());
    else if (err.status === 404) console.error('Patient not found');
    else throw err;
  } else throw err;
}

ApiError.parsedBody<T>() decodes the JSON body when one is present; falls back to null for non-JSON responses.

Versioning

Pre-1.0 the SDK ships breaking changes between minor versions; pin to a specific minor in production. After 1.0 the SDK follows semver, with backwards-compatible additions only on minor bumps.