Skip to content

Consolidated assessments API

Get an AI skin-cancer assessment with one POST (inline mode) or one POST + N uploads + one submit (slot mode). Both modes return the same aggregated response shape via GET or webhook.

Prerequisites

  • OAuth2 client credentials with scopes assessments:write (to create/submit) and assessments:read (to read).
  • The platform host root, e.g. https://platform.example.com. All paths below are relative to that root.

If you have not set up OAuth2 credentials yet, see direct-api.md for the token-exchange walkthrough.

Two modes at a glance

Mode Calls Use when
Inline 1 — POST /v1/clinical-api/assessments with images[].data Images are already in-process / small enough to embed as base64
Slot POSTPUT × N → POST …/submit Images are large, come from a camera SDK, or you want to pipeline uploads

Both modes accept the same request body shape. In inline mode you add base64 bytes in images[].data. In slot mode you omit data and the platform returns a presigned upload_url per image; when all uploads are done, call submit to finalise.


Authentication

Authorization: Bearer <access_token>

Obtain a token from POST /v1/auth/oauth/token (see direct-api.md, Step 1).

Idempotency

Both POST /v1/clinical-api/assessments and POST /v1/clinical-api/assessments/:id/submit accept an Idempotency-Key header. If the same key is received twice, the second call returns the existing record unchanged instead of creating a duplicate.

Idempotency-Key: <client-generated-uuid>

Minimum required fields

Field Type Description
patient or patient_id object | UUID string Either embed patient data inline or reference an existing patient record
patient.given_name string Required if using inline patient
patient.family_name string Required if using inline patient
patient.dob string (ISO 8601 date) Required if using inline patient, e.g. "1985-06-15"
finding.finding_type lesion | rash | patch | blemish | other Type of skin finding
images[] array, min 1 item At least one image declaration
images[].capture_type dermoscopic | macroscopic | other How the image was captured
images[].mime_type string IANA media type, e.g. image/jpeg
images[].data base64 string Inline mode only. Omit for slot mode.

Optional fields

Field Description
patient.identifiers[] Array of { scheme, value } external identifiers (e.g. NHS number)
external_reference Your own case reference (max 255 chars); passed through to the result
clinical_context Free-form JSON object for clinical metadata (e.g. patient history notes)
custom_fields Per-(org, product) key/value string pairs (configured in the admin UI)
finding.body_site_id UUID of a body site record from the platform's body-site catalogue

Mode 1 — Inline (single POST)

Embed base64 image bytes directly in the request. The platform stores the bytes, attaches them to the case, and begins processing immediately.

Request

POST /v1/clinical-api/assessments
Authorization: Bearer <access_token>
Content-Type: application/json
Idempotency-Key: f47ac10b-58cc-4372-a567-0e02b2c3d479

{
  "patient": {
    "given_name": "Alice",
    "family_name": "Anderson",
    "dob": "1985-06-15",
    "identifiers": [{ "scheme": "NHS", "value": "999 999 9999" }]
  },
  "finding": { "finding_type": "lesion" },
  "external_reference": "EHR-12345",
  "images": [
    {
      "capture_type": "dermoscopic",
      "mime_type": "image/jpeg",
      "data": "/9j/4AAQSkZJRgABAQAAAQABAAD..."
    }
  ]
}

Response — 202 Accepted

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "processing",
  "patient_id": "11112222-3333-4444-5555-666677778888",
  "case_id": "aaaabbbb-cccc-dddd-eeee-ffff00001111",
  "external_reference": "EHR-12345",
  "images": [
    {
      "id": "img-0001-0002-0003-0004-000500060007",
      "capture_type": "dermoscopic",
      "status": "received"
    }
  ],
  "result": null,
  "error": null,
  "next_poll_after_ms": 5000
}

Because you provided image bytes, the assessment moves directly to processing. No further action is required — await the webhook or poll the GET endpoint.


Mode 2 — Slot (POST + uploads + submit)

Declare images without bytes. The platform reserves slots and returns a presigned PUT URL for each image. Upload directly to storage, then call submit.

Step 1 — Create the assessment (declare images, no bytes)

POST /v1/clinical-api/assessments
Authorization: Bearer <access_token>
Content-Type: application/json
Idempotency-Key: b8e1f2a3-9c4d-5e6f-7a8b-9c0d1e2f3a4b

{
  "patient": {
    "given_name": "Bob",
    "family_name": "Baker",
    "dob": "1972-11-30"
  },
  "finding": { "finding_type": "lesion", "body_site_id": "bsite-arm-upper-left" },
  "images": [
    { "capture_type": "dermoscopic", "mime_type": "image/jpeg" },
    { "capture_type": "macroscopic", "mime_type": "image/png" }
  ]
}

Response — 202 Accepted

{
  "id": "c1d2e3f4-a5b6-7890-cdef-012345678901",
  "status": "awaiting_images",
  "patient_id": "22223333-4444-5555-6666-777788889999",
  "case_id": null,
  "images": [
    {
      "id": "img-1111-2222-3333-4444-555566667777",
      "capture_type": "dermoscopic",
      "status": "awaiting_upload",
      "upload_url": "https://storage.example.com/uploads/img-1111...?X-Amz-Signature=..."
    },
    {
      "id": "img-aaaa-bbbb-cccc-dddd-eeeeffff0000",
      "capture_type": "macroscopic",
      "status": "awaiting_upload",
      "upload_url": "https://storage.example.com/uploads/img-aaaa...?X-Amz-Signature=..."
    }
  ],
  "result": null,
  "error": null
}

The upload_urls are presigned S3 PUT URLs valid for approximately 5 minutes.

Step 2 — PUT image bytes to storage (one call per image)

PUT https://storage.example.com/uploads/img-1111...?X-Amz-Signature=...
Content-Type: image/jpeg

<raw image bytes>

Expected response: 200 OK (no body). Repeat for every image in the images[] array.

No auth header needed on the PUT. The presigned URL encodes all required credentials. Any extra Authorization header will break the request.

Step 3 — Submit

POST /v1/clinical-api/assessments/c1d2e3f4-a5b6-7890-cdef-012345678901/submit
Authorization: Bearer <access_token>
Idempotency-Key: d9e2f3a4-0b5c-6d7e-8f9a-0b1c2d3e4f50

No request body is required.

Response — 200 OK

{
  "id": "c1d2e3f4-a5b6-7890-cdef-012345678901",
  "status": "processing",
  "patient_id": "22223333-4444-5555-6666-777788889999",
  "case_id": "bbbbcccc-dddd-eeee-ffff-000011112222",
  "images": [
    {
      "id": "img-1111-2222-3333-4444-555566667777",
      "capture_type": "dermoscopic",
      "status": "received"
    },
    {
      "id": "img-aaaa-bbbb-cccc-dddd-eeeeffff0000",
      "capture_type": "macroscopic",
      "status": "received"
    }
  ],
  "result": null,
  "error": null,
  "next_poll_after_ms": 5000
}

The status transitions to processing and the case is created in the background.


Result delivery

Subscribe once to receive push notifications when assessments complete or fail:

POST /v1/clinical-api/webhook-subscriptions
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "url": "https://your-domain.example/hooks/skin-analytics",
  "event_types": ["assessment.completed", "assessment.failed"]
}

The platform will POST the aggregated assessment payload (see GET response shape below) to your endpoint as the webhook body, within seconds of completion.

Webhook delivery uses exponential back-off. See scopes.md for the webhooks:write scope requirement.

Option B — Poll

GET /v1/clinical-api/assessments/:id
Authorization: Bearer <access_token>

Suggested strategy: on processing, wait next_poll_after_ms (returned in the previous response) before polling again. On terminal statuses (completed, failed) stop polling.


GET response shape

Both polling and webhook delivery use the same JSON shape:

{
  "id": "<assessment-uuid>",
  "status": "completed",
  "patient_id": "<patient-uuid>",
  "case_id": "<case-uuid>",
  "external_reference": "EHR-12345",
  "images": [
    {
      "id": "<image-uuid>",
      "capture_type": "dermoscopic",
      "status": "processed",
      "download_url": "https://storage.example.com/images/...?X-Amz-Signature=..."
    }
  ],
  "result": {
    "ai_provider": "derm",
    "outcome": {
      "malignancy_score": 0.82,
      "recommendation": "refer"
    },
    "completed_at": "2026-06-02T14:37:00.000Z"
  },
  "error": null
}

When the assessment fails:

{
  "id": "<assessment-uuid>",
  "status": "failed",
  "patient_id": "<patient-uuid>",
  "case_id": null,
  "images": [],
  "result": null,
  "error": {
    "code": "IMAGE_QUALITY_INSUFFICIENT",
    "message": "At least one image did not meet the minimum quality threshold."
  }
}

Status values

Status Meaning
awaiting_images Slot mode: waiting for all image uploads before proceeding
processing Images received; AI pipeline running
completed AI assessment done; result is populated
failed Processing failed; error is populated

Error handling

HTTP status Meaning Action
400 Validation failure (missing required field, bad enum value) Fix the request body
401 Token expired or invalid Re-issue via POST /v1/auth/oauth/token
403 Token lacks assessments:write or assessments:read Check granted scopes with your platform operator
404 Assessment ID not found or belongs to a different org Verify the id
409 Idempotency-Key collision with a different payload Use a fresh key or retry with the same payload
4xx (application/problem+json) RFC-7807 error with title, detail, status Log detail for diagnosis
5xx Transient server error Retry with exponential back-off + jitter

Reference

The Postman collection downloadable from the Integration tab (/orgs/:orgId/products/:productId/integration) includes pre-built requests for all three assessment endpoints (POST assessments, POST assessments/:id/submit, GET assessments/:id) with sample bodies matching both modes.

See README.md for the full endpoint catalogue and Postman download instructions.