@healthcloudai/hc-safe-cdx
v0.6.2
Published
Healthcheck Safe CDX connector.
Maintainers
Readme
Healthcheck Safe CDX Connector
SDK client for the Safe CDX diagnostic test workflow. HCSafeCDXClient handles authenticated requests to the Safe CDX service on behalf of a logged-in patient.
Installation
npm install @healthcloudai/hc-safe-cdx @healthcloudai/hc-http @healthcloudai/hc-login-connectorImport
import { HCSafeCDXClient } from "@healthcloudai/hc-safe-cdx";
import { HCLoginClient } from "@healthcloudai/hc-login-connector";
import { FetchClient } from "@healthcloudai/hc-http";Setup
const httpClient = new FetchClient();
const loginClient = new HCLoginClient(httpClient);
loginClient.configure("healthcheck", "dev");
await loginClient.login(email, password);
const safeCdx = new HCSafeCDXClient(httpClient, loginClient);Safe CDX derives its own service URL from loginClient.getEnvironment(). It does not use loginClient.getBaseUrl().
API Key
When the environment requires an API key:
safeCdx.setApiKey("x-api-key", apiKeyValue);Service URL
Safe CDX calls a different backend from all other connectors.
| Environment | Base URL |
|---|---|
| dev | https://dev-api-hcs.healthcloud-services.com/api/console/hcservice/safecdx |
| uat | https://uat-api-hcs.healthcloud-services.com/api/console/hcservice/safecdx |
| prod | https://api-hcs.healthcloud-services.com/api/console/hcservice/safecdx |
Response Shapes
Safe CDX methods return one of three distinct shapes. Read them differently depending on the method — check the table at the bottom.
Shape A — APIResponse<T> (standard Health Cloud envelope)
{
IsOK: boolean;
ErrorMessage: string | null;
Data: T | null; // actual payload directly in Data
}Access:
const r = await safeCdx.getTestProfileByGTIN(gtin);
if (r.IsOK) {
const id = r.Data?.ID;
}Shape B — APIResponse<SafeAPIResponse<T>> (double-wrapped)
The Health Cloud gateway wraps the SafeCDX vendor response without normalising it. Data is itself a vendor envelope.
{
IsOK: boolean;
ErrorMessage: string | null;
Data: {
success: boolean; // vendor-level success flag
data: T | null; // actual payload
message: string | null;
code: number;
} | null;
}Access — always check both levels:
const r = await safeCdx.listTestProfilesByAccount();
if (r.IsOK && r.Data?.success) {
const profiles = r.Data.data; // T is here
}
// r.IsOK can be true while r.Data?.success is false
// r.Data?.message contains the vendor error descriptionShape C — SafeAPIResponse<T> (vendor envelope only, no outer wrapper)
Four methods return the vendor structure directly — no APIResponse outer layer.
{
success: boolean;
data: T | null;
message: string | null;
code: number;
}Access:
const r = await safeCdx.getCvmlResults(imageOfCaptureId);
if (r.success) {
const cvmlStatus = r.data?.cvmlStatus;
}Typical Patient Workflow
// 1. List available test profiles
const profilesResponse = await safeCdx.listTestProfilesByAccount();
if (!profilesResponse.IsOK || !profilesResponse.Data?.success) {
throw new Error(profilesResponse.Data?.message ?? profilesResponse.ErrorMessage ?? "Failed to list profiles");
}
const profiles = profilesResponse.Data.data; // TestProfileByAccountItem[]
// 2. Resolve profile by GTIN
const profileResponse = await safeCdx.getTestProfileByGTIN(profiles[0].gtin);
if (!profileResponse.IsOK || !profileResponse.Data) {
throw new Error(profileResponse.ErrorMessage ?? "Failed to resolve profile");
}
const userTestResultId = profileResponse.Data.ID!;
const gtin = profileResponse.Data.DiagnosticProfile?.TestInfo.gtin ?? profiles[0].gtin;
// 3. Create upload URL
const uploadResponse = await safeCdx.createUploadUrl(userTestResultId, gtin);
if (!uploadResponse.IsOK || !uploadResponse.Data?.preSignedURL) {
throw new Error(uploadResponse.ErrorMessage ?? "Failed to get upload URL");
}
const preSignedURL = uploadResponse.Data.preSignedURL;
const imageOfCaptureId = uploadResponse.Data.Metadata!.UploadId!;
// 4. Upload image (direct to S3 — no auth headers)
await safeCdx.uploadImage(preSignedURL, imageBody, "image/jpeg");
// 5. Notify CVML processing started
await safeCdx.updateCvmlStatus(imageOfCaptureId, "ImageProcessing");
// 6. Get CVML analysis results
const cvml = await safeCdx.getCvmlResults(imageOfCaptureId);
if (!cvml.success) throw new Error(cvml.message ?? "CVML failed");
// 7. Submit analyte answers
const submitResponse = await safeCdx.submitAnswers({
UserTestResultId: userTestResultId,
Result: [{ Analyte: "Purchase", ReportedValue: "drug store", StoredValue: "drug store", Score: "0" }],
});
if (!submitResponse.IsOK || !submitResponse.Data?.success) {
throw new Error(submitResponse.Data?.message ?? submitResponse.ErrorMessage ?? "Submit failed");
}
// 8. Finalize
await safeCdx.finalizeTest(userTestResultId);Methods
getTestProfileByGTIN(gtin) — GET
Resolves the test profile for a GTIN barcode. Returns Shape A.
safeCdx.getTestProfileByGTIN(gtin: string): Promise<APIResponse<GetTestProfileByGTINData>>const r = await safeCdx.getTestProfileByGTIN("850024942325");
// r.IsOK, r.Data?.ID, r.Data?.DiagnosticProfileExample response:
{
"Data": {
"ID": "6a104bba6068dd9a213db810",
"TestName": "UTI Test + Treat",
"CustomTestName": "UTI Test + Treat",
"IsDeactivated": false,
"IsOrderable": true,
"CaptureMethod": 0,
"DiagnosticProfile": {
"_id": "6780031a991814469e88237f",
"TestInfo": {
"gtin": "860002060439",
"productName": "Winx UTI Test + Treat",
"cvmlTestName": "winx-uti",
"cvmlDuration": 20,
"isCVMLResult": true,
"cvmlRetryLimit": 2
},
"Analyte": [
{
"_id": "1",
"Name": "Leukocyte",
"Question": "What is the Leukocyte reading?",
"Responses": [
{ "value": "Negative -", "storedValue": "Neg", "score": "0" },
{ "value": "Positive 70+", "storedValue": "70+", "score": "4" }
]
}
]
}
},
"ErrorMessage": null,
"IsOK": true
}listTestProfilesByAccount(includeRegisterTestDetails?) — POST
Lists test profiles for the patient's account. Returns Shape B — read r.Data.data for the array.
| Parameter | Type | Default | Description |
|---|---|---|---|
| includeRegisterTestDetails | boolean | true | Include registration UI details |
safeCdx.listTestProfilesByAccount(
includeRegisterTestDetails?: boolean
): Promise<APIResponse<SafeAPIResponse<TestProfileByAccountItem[]>>>const r = await safeCdx.listTestProfilesByAccount();
if (r.IsOK && r.Data?.success) {
const profiles = r.Data.data; // TestProfileByAccountItem[]
}Example response:
{
"Data": {
"success": true,
"data": [
{
"gtin": "860002060439",
"productName": "Winx UTI Test + Treat",
"testKitImageURL": "https://safe-content-cache-us-west-2-quality-speed.s3-us-west-2.amazonaws.com/...",
"language": "en",
"registerTestDetail": {
"title": "",
"buttonTitle": "Scan Barcode on the Box"
}
}
],
"message": "Success",
"code": 0
},
"ErrorMessage": null,
"IsOK": true
}createUploadUrl(userTestResultId, gtin, imageType?) — POST
Creates a pre-signed S3 URL for uploading the test image. Returns Shape A.
| Parameter | Type | Default | Description |
|---|---|---|---|
| userTestResultId | string | — | From getTestProfileByGTIN response (Data.ID) |
| gtin | string | — | GTIN of the selected test |
| imageType | string | "jpg" | Image format for upload metadata |
safeCdx.createUploadUrl(
userTestResultId: string,
gtin: string,
imageType?: string
): Promise<APIResponse<CreateUploadUrlData>>const r = await safeCdx.createUploadUrl(userTestResultId, gtin);
const preSignedURL = r.Data?.preSignedURL;
const imageOfCaptureId = r.Data?.Metadata?.UploadId;Example response:
{
"Data": {
"preSignedURL": "https://sf-us-west-2-lower-neural-cdx-img-uploads-quality.s3.us-west-2.amazonaws.com/healthcheck/winx-uti/...?X-Amz-Expires=1200&...",
"Metadata": {
"UploadId": "860002060439_6a104bba6068dd9a213db810-1",
"Type": "Rapid Test Kit Upload",
"File": "860002060439_6a104bba6068dd9a213db810-1.jpg"
}
},
"ErrorMessage": null,
"IsOK": true
}uploadImage(preSignedURL, image, contentType?) — PUT (direct S3)
Uploads the image directly to the pre-signed storage URL. Returns Promise<void>.
Does not call a Safe CDX API route. Does not send Health Cloud auth headers. The pre-signed URL provides its own credential. Invalid preSignedURL, image, or contentType input throws ValidationError. A non-2xx storage response throws an HTTP-status-mapped connector error.
| Parameter | Type | Default | Description |
|---|---|---|---|
| preSignedURL | string | — | From createUploadUrl response |
| image | BodyInit | — | Raw image — File, Blob, Buffer, etc. |
| contentType | string | "image/jpeg" | MIME type |
await safeCdx.uploadImage(preSignedURL, imageBody, "image/jpeg");updateCvmlStatus(imageOfCaptureId, cvmlStatus) — POST
Notifies the CVML service of the processing status for an image capture. Returns Shape C — read r.success and r.data directly.
| Parameter | Type | Description |
|---|---|---|
| imageOfCaptureId | string | UploadId from createUploadUrl metadata |
| cvmlStatus | string | Processing status, e.g. "ImageProcessing" |
safeCdx.updateCvmlStatus(
imageOfCaptureId: string,
cvmlStatus: string
): Promise<UpdateCvmlStatusResponse> // = SafeAPIResponse<EntityReferenceData>const r = await safeCdx.updateCvmlStatus(imageOfCaptureId, "ImageProcessing");
if (r.success) {
const id = r.data?._id;
}Example response:
{
"success": true,
"data": { "_id": "6a104bba6068dd9a213db810" },
"message": "Success",
"code": 0
}getCvmlResults(imageOfCaptureId) — GET
Returns CVML analysis results for a captured image. Returns Shape C.
safeCdx.getCvmlResults(
imageOfCaptureId: string
): Promise<GetCvmlResultsResponse> // = SafeAPIResponse<CvmlResultsData>const r = await safeCdx.getCvmlResults(imageOfCaptureId);
if (r.success) {
const cvmlStatus = r.data?.cvmlStatus; // e.g. "Retake", "Positive", "Negative"
}Example response:
{
"success": true,
"data": {
"_id": "6a104bba6068dd9a213db810",
"status": "Started",
"cvmlStatus": "Retake",
"gtin": "860002060439",
"isCVMLResult": true,
"capture": {
"retryCount": 0,
"retryLimit": 2,
"failoverEnabled": true,
"lastCvmlStatus": "Retake",
"failoverTriggered": false
},
"imageOfCaptureResult": {
"parsed": {
"outcome": "Retake",
"responseCode": "Err4"
}
}
},
"message": "Success",
"code": 0
}listPendingResults(excludeStatus?) — POST
Returns incomplete or pending test results for the patient. Returns Shape B.
| Parameter | Type | Default | Description |
|---|---|---|---|
| excludeStatus | string | "Invalid,Canceled" | Comma-separated statuses to exclude |
safeCdx.listPendingResults(
excludeStatus?: string
): Promise<APIResponse<SafeAPIResponse<TestResultSummary[]>>>const r = await safeCdx.listPendingResults();
if (r.IsOK && r.Data?.success) {
const pending = r.Data.data; // TestResultSummary[]
}Example response:
{
"Data": {
"success": true,
"data": [
{
"_id": "6a104bba6068dd9a213db810",
"status": "Started",
"cvmlStatus": "Retake",
"gtin": "860002060439",
"testName": "Winx UTI Test + Treat",
"recordedDate": "2026-05-22T12:49:26.047Z",
"resumable": true,
"resumeNext": "SelfReporting"
}
],
"message": "Success",
"code": 0
},
"ErrorMessage": null,
"IsOK": true
}getLastResults(excludeStatus?) — POST
Returns the most recent test result for the patient. Returns Shape B. Data.data may be null if no results exist.
| Parameter | Type | Default | Description |
|---|---|---|---|
| excludeStatus | string | "Invalid" | Status to exclude |
safeCdx.getLastResults(
excludeStatus?: string
): Promise<APIResponse<SafeAPIResponse<TestResultDetails>>>const r = await safeCdx.getLastResults();
if (r.IsOK && r.Data?.success && r.Data.data) {
const latest = r.Data.data; // TestResultDetails
}Example response:
{
"Data": {
"success": true,
"data": {
"_id": "6a104bba6068dd9a213db810",
"status": "Started",
"cvmlStatus": "Retake",
"gtin": "860002060439",
"testName": "Winx UTI Test + Treat",
"recordedDate": "2026-05-22T12:49:26.047Z",
"finalized": false
},
"message": "Success",
"code": 0
},
"ErrorMessage": null,
"IsOK": true
}listTestHistory(excludeStatus?) — POST
Returns the full test result history for the patient. Returns Shape B.
| Parameter | Type | Default | Description |
|---|---|---|---|
| excludeStatus | string | "Invalid" | Status to exclude |
safeCdx.listTestHistory(
excludeStatus?: string
): Promise<APIResponse<SafeAPIResponse<TestResultSummary[]>>>const r = await safeCdx.listTestHistory();
if (r.IsOK && r.Data?.success) {
const history = r.Data.data; // TestResultSummary[]
}Example response:
{
"Data": {
"success": true,
"data": [
{
"_id": "6a104bba6068dd9a213db810",
"status": "Started",
"testName": "Winx UTI Test + Treat",
"recordedDate": "2026-05-22T12:49:26.047Z",
"overallResult": "Unknown"
}
],
"message": "Success",
"code": 0
},
"ErrorMessage": null,
"IsOK": true
}getResultDetails(userTestResultId) — POST
Returns full details for a specific test attempt. Returns Shape C — read r.success and r.data directly.
safeCdx.getResultDetails(
userTestResultId: string
): Promise<GetResultDetailsResponse> // = SafeAPIResponse<TestResultDetails>const r = await safeCdx.getResultDetails(userTestResultId);
if (r.success) {
const details = r.data; // TestResultDetails
}Example response:
{
"success": true,
"data": {
"_id": "6a104bba6068dd9a213db810",
"status": "Started",
"cvmlStatus": "Retake",
"gtin": "860002060439",
"testName": "Winx UTI Test + Treat",
"finalized": false,
"result": [
{ "analyte": "Purchase", "reportedValue": "drug store", "storedValue": "drug store", "score": "0" }
]
},
"message": "Success",
"code": 0
}getResultPdf(userTestResultId) — POST
Returns a pre-signed S3 URL for the PDF result. Returns Shape B. Data.data is the URL string.
safeCdx.getResultPdf(
userTestResultId: string
): Promise<APIResponse<SafeAPIResponse<string>>>const r = await safeCdx.getResultPdf(userTestResultId);
if (r.IsOK && r.Data?.success) {
const pdfUrl = r.Data.data; // string — pre-signed S3 URL
}Example response:
{
"Data": {
"success": true,
"data": "https://safe-manual-test-results-us-west-2-quality-speed.s3.us-west-2.amazonaws.com/archive/2026/05/22/12/48/7ecfd730-c4c3-4cd6-8b12-4210b9a2db28.pdf?X-Amz-Expires=900&...",
"message": "Success",
"code": 0
},
"ErrorMessage": null,
"IsOK": true
}getImageCaptureUrl(userTestResultId) — POST
Returns image capture metadata for a test attempt. Returns Shape C.
safeCdx.getImageCaptureUrl(
userTestResultId: string
): Promise<GetImageCaptureUrlResponse> // = SafeAPIResponse<string>const r = await safeCdx.getImageCaptureUrl(userTestResultId);
if (r.success) {
const imageUrl = r.data; // string — pre-signed URL
}Example response:
{
"success": true,
"data": "https://sf-us-west-2-lower-neural-cdx-img-uploads-quality.s3.us-west-2.amazonaws.com/healthcheck/winx-uti/...?X-Amz-Expires=900&...",
"message": "Success",
"code": 0
}resumeFlow(userTestResultId, resumed?) — POST
Marks a test attempt as resumed or not. Returns Shape B.
| Parameter | Type | Default | Description |
|---|---|---|---|
| userTestResultId | string | — | Test attempt identifier |
| resumed | boolean | true | Resume state to set |
safeCdx.resumeFlow(
userTestResultId: string,
resumed?: boolean
): Promise<APIResponse<SafeAPIResponse<ResumeFlowResult>>>const r = await safeCdx.resumeFlow(userTestResultId, true);
if (r.IsOK && r.Data?.success) {
const result = r.Data.data; // ResumeFlowResult
}Example response:
{
"Data": {
"success": true,
"data": {
"_id": "6a02f6da6068dd9a213db7cb",
"cvmlStatus": "Retake",
"resumed": true,
"failoverTriggered": false
},
"message": "Success",
"code": 0
},
"ErrorMessage": null,
"IsOK": true
}submitAnswers(submission) — POST
Submits analyte answers for a test attempt. Returns Shape B.
| Parameter | Type | Required | Description |
|---|---|---|---|
| submission.UserTestResultId | string | Yes | Test attempt identifier |
| submission.Result | AnswerResult[] | Yes | At least one answer required |
safeCdx.submitAnswers(
submission: SubmitAnswersRequest
): Promise<APIResponse<SafeAPIResponse<EntityReferenceData>>>const r = await safeCdx.submitAnswers({
UserTestResultId: userTestResultId,
Result: [
{ Analyte: "Purchase", ReportedValue: "drug store", StoredValue: "drug store", Score: "0" }
]
});
if (r.IsOK && r.Data?.success) {
const id = r.Data.data?._id;
}Example response:
{
"Data": {
"success": true,
"data": { "_id": "6a104bba6068dd9a213db810" },
"message": "Success",
"code": 0
},
"ErrorMessage": null,
"IsOK": true
}finalizeTest(userTestResultId) — POST
Finalizes a test attempt. Returns Shape A.
Data is typed as unknown, so treat any returned payload as illustrative rather than a stable SDK contract. Check r.IsOK for success.
safeCdx.finalizeTest(userTestResultId: string): Promise<APIResponse<unknown>>const r = await safeCdx.finalizeTest(userTestResultId);
if (r.IsOK) {
// test finalized
}Method Summary
| Method | Shape | Return type | Access data via |
|---|---|---|---|
| getTestProfileByGTIN(gtin) | A | APIResponse<GetTestProfileByGTINData> | r.Data |
| listTestProfilesByAccount(...) | B | APIResponse<SafeAPIResponse<TestProfileByAccountItem[]>> | r.Data?.data |
| createUploadUrl(...) | A | APIResponse<CreateUploadUrlData> | r.Data |
| uploadImage(...) | — | Promise<void> | throws on failure |
| updateCvmlStatus(...) | C | SafeAPIResponse<EntityReferenceData> | r.data |
| getCvmlResults(imageOfCaptureId) | C | SafeAPIResponse<CvmlResultsData> | r.data |
| listPendingResults(...) | B | APIResponse<SafeAPIResponse<TestResultSummary[]>> | r.Data?.data |
| getLastResults(...) | B | APIResponse<SafeAPIResponse<TestResultDetails>> | r.Data?.data |
| listTestHistory(...) | B | APIResponse<SafeAPIResponse<TestResultSummary[]>> | r.Data?.data |
| getResultDetails(userTestResultId) | C | SafeAPIResponse<TestResultDetails> | r.data |
| getResultPdf(userTestResultId) | B | APIResponse<SafeAPIResponse<string>> | r.Data?.data |
| getImageCaptureUrl(userTestResultId) | C | SafeAPIResponse<string> | r.data |
| resumeFlow(...) | B | APIResponse<SafeAPIResponse<ResumeFlowResult>> | r.Data?.data |
| submitAnswers(submission) | B | APIResponse<SafeAPIResponse<EntityReferenceData>> | r.Data?.data |
| finalizeTest(userTestResultId) | A | APIResponse<unknown> | r.IsOK |
Shape legend:
- A —
APIResponse<T>— checkr.IsOK, readr.Data - B —
APIResponse<SafeAPIResponse<T>>— checkr.IsOK && r.Data?.success, readr.Data.data - C —
SafeAPIResponse<T>— checkr.success, readr.data
Shape B is a backend pass-through: the Health Cloud gateway returns the SafeCDX vendor response as-is inside
APIResponse.Data, without normalising it.r.IsOKandr.Data?.successare independent checks — both must be true before readingr.Data.data.
