# SonoScribe — Partner Integration Guide For PACS/RIS vendors and teleradiology providers embedding SonoScribe reporting into an existing workflow. Everything here is live API surface, not roadmap. Anything not yet built is marked **NOT IMPLEMENTED** rather than described as if it worked. Base URL: `https://sonoscribe-api.onrender.com` (production). Custom hostnames on request. --- ## 1. Pick an integration depth You do not have to start deep. Each tier reuses the same engine and the same billing; you can move up later without changing anything you already built. | Tier | What you build | Effort | You get | |---|---|---|---| | **1. Launch-in-context** | One hyperlink in your worklist | An afternoon | Radiologist clicks a study, lands in a workspace already loaded with it | | **2. REST/JSON** | Call `POST /api/v1/reports` | 1–2 days | Reports generated from your own study data, returned to you | | **3. Site connector** | Run one small process on-site | ~half a day of IT time | Studies pulled from your PACS automatically; signed reports written back into your RIS | Tier 1 exists because a three-month integration before anyone sees value is how these projects die. Start there, prove it on real studies, deepen only if it earns it. --- ## Getting the rendered document Most partners render our JSON in their own viewer, and the DICOM SR / HL7 ORU put the report where the PACS and the RIS read it. If you instead want the **document** — the same styled Word file and PDF a sonologist downloads — fetch it: ``` GET /api/v1/reports/{report_id}/document?format=docx (or format=pdf) ``` ```json { "report_id": "...", "format": "docx", "url": "https://...", "signed": true, "preview": false } ``` `url` is a short-lived pre-signed link. Download it promptly; do not store the URL as a permanent reference to the report. **Check `signed` before you file it.** A report only renders as a signed record once a sonologist has signed it. Until then it renders as a PREVIEW — the clinic logo and signature are stripped and the page carries a banner — and `signed` is `false`. We will not hand you a document that looks signed when nobody has signed it, and you should not attach one to a patient record. This is the same distinction the DICOM SR carries in `CompletionFlag` / `VerificationFlag`. `409` means the report is still drafting; poll `GET /api/v1/reports/{id}` until `draft_state` is `drafted`. `400` means the `format` was neither `docx` nor `pdf`. PDF rendering requires native libraries on our host; if it is unavailable you get `503` and Word still works. The document comes off the SAME rendering pipeline as the doctor's own copy, so house style, regulatory wording and every content fix reach you and the sonologist identically. There is no partner-specific renderer to drift out of step. ## 2. Authentication Every machine-to-machine call carries three headers: ``` X-API-Key: cs_live_. X-Site-Id: X-User-Ref: (optional but recommended) ``` - **Keys are per-partner**, issued by us. The secret is shown once at creation and stored only as a hash — we cannot recover it, only reissue. - **Sites must be pre-registered** (`POST /api/v1/sites`). An unknown `X-Site-Id` is rejected with **403**, never auto-created, so a misconfigured deployment cannot silently fan out into hundreds of unbilled sites. - `site_ref` and `user_ref` are **opaque strings you assign**. Do not send hospital names or radiologist names — we do not want them and will not store them. You keep the mapping. ## 3. Identifiers: what we take and what we keep We never persist raw patient identifiers. `patient_id`, `accession` and `study_date` are consumed in-request to compute a keyed HMAC billing fingerprint, and the originals are dropped. If you would rather send nothing at all, send `opaque_key` instead and leave the other three blank. Billing then keys off that, and we ingest **zero** identifiers. Pixel data is never requested, never transmitted and never stored. We generate text. --- ## 4. Tier 1 — Launch-in-context ```http POST /api/v1/launch ``` ```json { "study_uid": "1.2.840...", "template": "ct_brain_plain", "user_ref": "rad-7", "modality": "CT", "body_part": "BRAIN", "series_descriptions": ["Ax Plain", "Ax Post"], "indication": "headache", "prior_report_text": "CT 2025: normal." } ``` Returns a `launch_url`. Put it behind a button on the worklist row. The token is **single-use, 10 minutes, scoped to one study, and is not a session** — a leaked URL in browser history is already spent and can never become account access. Minting is **not billable**; a radiologist who opens a study and changes their mind costs nothing. ## 5. Tier 2 — REST/JSON ```http POST /api/v1/reports Idempotency-Key: # required on every mutating call ``` Returns **202** and a `report_id`; poll `GET /api/v1/reports/{id}`. Two fields are clinically load-bearing and must be sent accurately: - **`series_descriptions`** — the sequences actually acquired. The engine treats this as a whitelist. A report describing a sequence that was never run is a serious clinical error, so if you cannot supply this reliably, send an empty list rather than a guess. - **`contrast_agent`** — omit it if you genuinely do not know. `null` means *"not told"*, which the engine handles differently from *"no contrast given"*. It will never infer contrast. **Idempotency:** replaying a key returns the original report. Reusing a key with a *different* body returns **409** — that is a bug on your side, not a transient failure, so do not retry it. ## 5a. Your first call, end to end Nothing below is pseudo-code — substitute your key and site and it runs. ```bash # 1. Submit a study. 202 + a report_id comes straight back; drafting happens after. curl -X POST https://sonoscribe-api.onrender.com/api/v1/reports -H "X-API-Key: cs_live_." -H "X-Site-Id: " -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" -d '{ "study_uid": "1.2.840.113619.2.55.3.1", "template": "growth_scan_singleton", "dictation": "Single live intrauterine fetus in cephalic presentation. Placenta anterior.", "modality": "US", "study_description": "OBSTETRIC USG", "series_descriptions": ["OB Growth", "Doppler"], "biometry": {"BPD": 88.4, "HC": 312.0, "AC": 305.0, "FL": 64.2}, "biometry_units": {"BPD": "mm", "HC": "mm", "AC": "mm", "FL": "mm"}, "indication": "Third trimester growth scan" }' # 2. Poll until draft_state is "drafted" (a few seconds to a couple of minutes). curl https://sonoscribe-api.onrender.com/api/v1/reports/ -H "X-API-Key: ..." -H "X-Site-Id: ..." # 3. Fetch the rendered document if you want the file rather than the text. curl "https://sonoscribe-api.onrender.com/api/v1/reports//document?format=docx" -H "X-API-Key: ..." -H "X-Site-Id: ..." ``` **Both headers are required on every call.** `X-Site-Id` is not optional — a partner may have many sites and the meter, the caps and the report scoping are all per site. Omitting it returns `400 Missing X-Site-Id.` rather than guessing which site you meant. `GET /api/v1/templates` is not a route; the template keys are agreed during onboarding and are stable. Send one you were given. ### Status codes you should handle | Code | Meaning | Retry? | |---|---|---| | `202` | Accepted; drafting started | — | | `400` | Missing `Idempotency-Key`, missing `X-Site-Id`, or an unsupported `format` | **No** — fix the call | | `401` | Bad or unknown API key | **No** | | `403` | Unknown `X-Site-Id` for this partner | **No** — register the site first | | `409` | Same `Idempotency-Key`, different body — or the report is still drafting when you asked for the document | **No** for the first; poll for the second | | `422` | The body failed validation; the response names the field | **No** | | `429` | Per-site rate limit | **Yes**, with backoff | | `5xx` | Ours | **Yes**, with backoff — and the `Idempotency-Key` makes the retry safe | ### Polling, not webhooks — a deliberate choice There is no callback URL to register. Drafting finishes in seconds to a couple of minutes, and a webhook would mean us holding an outbound URL for your network, retrying into it, and you operating a public endpoint to receive it. Polling `GET /api/v1/reports/{id}` keeps every connection outbound-from-you, which is what makes a firewalled PACS site possible at all. If callbacks matter for your volume, say so and we will discuss it — we would rather add it deliberately than have you discover its absence mid-build. ## 6. Tier 3 — Site connector (firewalled PACS) One process, on a machine inside the hospital network. **It makes outbound HTTPS only.** No inbound port, no firewall rule, no NAT — which is what makes it deployable without a security review. ``` PACS --(DICOMweb, LAN)--> connector --(HTTPS out)--> SonoScribe --> workspace --> sign | RIS <--(MLLP, LAN)-- connector <--(HTTPS out)-- /api/v1/outbox ``` - **Intake:** polls your PACS over DICOMweb for new CT/MR studies. **Metadata only** — `/frames` and the rendered-image endpoints are never called. - **Local pseudonymisation:** identifiers are replaced before anything leaves the building, using a secret generated on that machine that is never transmitted. Run `--self-check` to print the exact outbound payload and confirm for yourself that nothing identifying leaves. - **Store-and-forward:** a durable local queue with backoff. A dropped link delays reports; it never loses them. - **Return leg:** finished reports are pulled and written into your RIS as **HL7 v2 ORU^R01** over MLLP. Only reports the radiologist has **signed** are ever delivered — an unsigned draft is never written into a RIS. Delivery is acknowledged on an explicit `MSA|AA`; anything else is retried and recorded with a reason. **Also available (built and tested, 2026-07-30):** - **DIMSE C-STORE receiver.** We can be configured as a DICOM destination — AE title, IP, port — which is the one way most installed scanners and every ultrasound machine can push data out. Only AE titles you list are accepted; an unknown calling AE is rejected at association time, before any patient data transfers. Pixel data is discarded on arrival: we generate text, never image analysis. A dropped association delivers nothing, so a half-received study cannot be persisted. Plain DIMSE is unencrypted, so this runs on the edge connector inside your network or across a VPN; binding it to a public interface is refused. - **DIMSE C-STORE sender + STOW-RS.** The signed report goes back to the PACS as a DICOM object over either transport. STOW-RS partial success (HTTP 202 carrying `FailedSOPSequence`) is parsed rather than trusted, so a partially-stored batch is reported as a failure with the reason per instance — never as a silent success. - **DICOM SR write-back (TID 1500 Comprehensive3D).** The report becomes a first-class DICOM object that inherits the study's own `StudyInstanceUID`, so it files into the same study and opens in the same viewer as the images. `CompletionFlag`/`VerificationFlag` are `COMPLETE`/`VERIFIED` **only** for a report a radiologist has signed; a draft is explicitly `PARTIAL`/`UNVERIFIED`, so a viewer can never present an unsigned draft as signed. - **FHIR R4 DiagnosticReport**, for a RIS/EHR that speaks FHIR rather than HL7 v2 — including retrieval of prior reports, which is the highest-value input for comparative ultrasound reporting. `status` is `final` only when signed; a draft is `preliminary`. `dicom_listen_port` in the site config now configures the receiver rather than logging a warning. **Still not implemented:** DICOM Modality Worklist (MWL) SCP, and QIDO/WADO *serving* (we consume those, we do not host them). Tell us if you need either. ## 7. Metering and billing - The billing unit is the **study**, not the API call. - **Regenerations, section regenerations and chat on the same study are free.** A genuinely different examination bills again. The window is 7 days. - `GET /api/v1/usage` gives a live per-site count, so you watch the number accumulate rather than meeting it at invoice time. - Per-site rate limits and monthly caps are checked **before** any work — a site past its agreed volume is refused up front rather than having a report generated and then be told it was not allowed. ## 8. Your IP and ours - The engine, prompts and clinical logic run **on our servers**. Nothing of ours is deployed on your hardware — the connector contains no prompts, no model credentials and no drafting logic, and there is a test in our suite that fails the build if any appear in it. - Equally, we hold no images and no patient identifiers of yours. ## 9. What the radiologist still does Every report is a **draft until a radiologist reviews and signs it**. SonoScribe does not diagnose and does not produce final reports unattended. Time-critical findings are **flagged in the UI** for the radiologist; they are never auto-communicated and never auto-routed. Acting on them is the radiologist's decision and responsibility. --- ## 10. Getting started 1. Tell us your site count and expected monthly study volume. 2. We issue a partner key and register your sites. 3. Build Tier 1 against staging — usually the same day. 4. Run real studies. Deepen to Tier 2 or 3 only once the output has earned it.