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.