{"openapi":"3.1.0","info":{"title":"SonoScribe Partner API","description":"Machine-to-machine API for PACS, RIS and teleradiology integrations. See /api/integration for the conformance statement and interface specs.","version":"0.1.2"},"paths":{"/health":{"get":{"tags":["meta"],"summary":"Health","description":"Liveness probe. No DB call, no AI call — must be fast and dependency-free.\n\n`templates` is deliberately included and is NOT decoration. On 2026-07-17 this\nservice was deployed, healthy, and 200-ing on this very endpoint while shipping\nZERO report templates: render.yaml used `dockerContext: ./backend`, so the\nrepo-root `templates/` folder was never in the build context and /templates did\nnot exist in the image. A missing folder only logs `templates_dir_missing` and\ndegrades to an empty list, so the app could not draft a single report and every\nhealth check still passed.\n\nCounting them here makes that class of failure OBSERVABLE FROM OUTSIDE, without\na login: `curl /health` now distinguishes \"the server is up\" from \"the product\ncan actually work\". scripts/verify_production.py asserts this equals the number\nof .docx files in the repo, so dev->prod drift fails loudly instead of silently.\n\nCheap (a directory glob, no I/O per file) and safe (a count and a bool — no\nnames, no paths, no internals).","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Health Health Get"}}}}}}},"/api/integration":{"get":{"tags":["integration"],"summary":"List the integration documents","description":"Everything a partner engineer needs, in one place, with no credential required.","operationId":"index_api_integration_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Index Api Integration Get"}}}}}}},"/api/integration/{slug}":{"get":{"tags":["integration"],"summary":"Fetch one integration document as Markdown","operationId":"document_api_integration__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/config":{"get":{"tags":["config"],"summary":"Runtime Config","description":"Public runtime configuration. No secrets, no pricing, no user data — safe unauthenticated.","operationId":"runtime_config_api_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Runtime Config Api Config Get"}}}}}}},"/api/v1/sites":{"post":{"tags":["partner"],"summary":"Create Site","description":"Register a site. Idempotent: re-registering an existing site_ref returns it with created=false.\n\nNOTE the chicken-and-egg: `require_partner` resolves X-Site-Id, so provisioning the FIRST site\nof a partner is done out-of-band via `scripts/provision_partner.py`. This endpoint is for a\npartner adding further sites once one exists.","operationId":"create_site_api_v1_sites_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"X-Site-Id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Site-Id"}},{"name":"X-User-Ref","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Ref"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteIn"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Site Api V1 Sites Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["partner"],"summary":"List Sites","operationId":"list_sites_api_v1_sites_get","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"X-Site-Id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Site-Id"}},{"name":"X-User-Ref","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Sites Api V1 Sites Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reports":{"post":{"tags":["partner"],"summary":"Create Report","description":"Generate a report from study context. Billable once per unique study examination.\n\n202 + a report id; the partner polls `GET /reports/{id}` for the drafted text. Fire-and-poll\nmatches the existing consumer path and keeps the request short, which matters over the flaky\nclinic links this will run on.","operationId":"create_report_api_v1_reports_post","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Idempotency-Key"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"X-Site-Id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Site-Id"}},{"name":"X-User-Ref","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Ref"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudyContext"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Report Api V1 Reports Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reports/{report_id}":{"get":{"tags":["partner"],"summary":"Get Report","description":"Poll drafting state and fetch the report. Scoped to the calling partner AND site — a partner\ncan never read another partner's report, and one hospital cannot read another's.","operationId":"get_report_api_v1_reports__report_id__get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"X-Site-Id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Site-Id"}},{"name":"X-User-Ref","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Report Api V1 Reports  Report Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reports/{report_id}/document":{"get":{"tags":["partner"],"summary":"Get Report Document","description":"The RENDERED report — the same .docx and .pdf a doctor downloads, for partners who want the\ndocument rather than the text.\n\nBuilt on the CONSUMER pipeline deliberately (`render_report_docx` / `render_report_pdf` with the\nidentical `GenerationInputs`), not a partner-specific renderer. A second renderer would drift,\nand the drift would be invisible: the partner's copy and the doctor's copy of one signed report\nwould slowly stop agreeing on house style, PCPNDT wording and every coherence fix. One pipeline,\nmany transports — the same rule the text surfaces already follow.\n\nSIGNED STATE IS NOT ASSUMED. A partner report is drafted under a machine service account, which\nhas no clinic letterhead and no sonologist signature. Rendering it as though it were signed\nwould produce the single most dangerous artefact this codebase can emit: a document that LOOKS\nlike a signed report and carries no real signer. So an unsigned report renders through the\nexisting `preview=True` path — logo and signature stripped, PREVIEW banner stamped — exactly as\nan unsigned consumer draft does, and the response says which it is. This mirrors the DICOM SR's\nCompletionFlag/VerificationFlag rule rather than inventing a second answer to the same question.","operationId":"get_report_document_api_v1_reports__report_id__document_get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","default":"docx","title":"Format"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"X-Site-Id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Site-Id"}},{"name":"X-User-Ref","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Report Document Api V1 Reports  Report Id  Document Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/usage":{"get":{"tags":["partner"],"summary":"Usage","description":"Usage for the partner, filterable by site and date range.\n\nPartners must WATCH the number accumulate rather than meeting it at invoice time — that is what\nprevents billing disputes, so this is a first-class endpoint, not a report we email monthly.","operationId":"usage_api_v1_usage_get","parameters":[{"name":"site_ref","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Ref"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Since"}},{"name":"until","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Until"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"X-Site-Id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Site-Id"}},{"name":"X-User-Ref","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Usage Api V1 Usage Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/balance":{"get":{"tags":["partner"],"summary":"Balance","description":"Current balance and what may be topped up right now.\n\nFirst-class rather than a field buried in /usage: a client must be able to see the number that\ndecides whether their radiologists can work, and see it before it becomes a problem.\n\n🔴 REPORTED IN CURRENCY, NOT A PERCENTAGE — the deliberate opposite of the consumer path. A\nB2C subscriber sees only `percentage_used`, because their allowance is derived from confidential\ncost figures that must never leave the server. A B2B client has PREPAID actual money: showing\nthem a percentage of an amount they funded themselves would be absurd and unauditable. They see\nrupees in, rupees spent, rupees remaining.\n\nStill carries NO cost or margin data. What a client spent is theirs to know; what it cost us to\nserve them is not.","operationId":"balance_api_v1_balance_get","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"X-Site-Id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Site-Id"}},{"name":"X-User-Ref","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Balance Api V1 Balance Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/topup/order":{"post":{"tags":["partner"],"summary":"Create Topup Order","description":"Create a Razorpay order for a self-serve top-up.\n\nRecords INTENT only. The balance moves when Razorpay's signed webhook confirms capture — a\nbrowser callback can be replayed, spoofed, or simply lost when a phone dies mid-redirect, and\nnone of those may create money.","operationId":"create_topup_order_api_v1_topup_order_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"X-Site-Id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Site-Id"}},{"name":"X-User-Ref","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Ref"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopupIn"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Topup Order Api V1 Topup Order Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/outbox":{"get":{"tags":["partner"],"summary":"Claim Outbox","description":"Claim the next finalized reports awaiting delivery for this site.\n\nClaiming is idempotent-ish by design: re-claiming an unacked delivery returns it again rather\nthan skipping it. A connector that crashes between claim and write MUST see the report again,\nbecause the alternative — a signed report silently never reaching the RIS — is far worse than\ndelivering it twice. HL7 receivers deduplicate on MSH-10, so a duplicate is a non-event.","operationId":"claim_outbox_api_v1_outbox_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":25,"minimum":1,"default":10,"title":"Limit"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"X-Site-Id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Site-Id"}},{"name":"X-User-Ref","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Claim Outbox Api V1 Outbox Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/outbox/{report_id}/ack":{"post":{"tags":["partner"],"summary":"Ack Delivery","description":"The RIS accepted the message (HL7 AA). Stop redelivering.","operationId":"ack_delivery_api_v1_outbox__report_id__ack_post","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"X-Site-Id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Site-Id"}},{"name":"X-User-Ref","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Ack Delivery Api V1 Outbox  Report Id  Ack Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/outbox/{report_id}/nack":{"post":{"tags":["partner"],"summary":"Nack Delivery","description":"Delivery failed (RIS returned AE/AR, or the socket died). Record why; leave it queued.\n\nStays `pending` so the next poll retries it. Only the attempt ceiling moves it to `failed`,\nand even then the row survives for an operator — a signed report is never dropped.","operationId":"nack_delivery_api_v1_outbox__report_id__nack_post","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"X-Site-Id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Site-Id"}},{"name":"X-User-Ref","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Ref"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NackIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Nack Delivery Api V1 Outbox  Report Id  Nack Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/launch":{"post":{"tags":["launch"],"summary":"Mint Launch","description":"Mint a one-time launch URL for a study. The partner puts this behind their worklist button.\n\nDeliberately NOT billable: nothing has been generated yet. Billing happens when a report is\nactually drafted, so a radiologist who opens a study and changes their mind costs nothing —\ncharging for a click would make the partner reluctant to put the button anywhere prominent.\n\n🔴 BUT IT IS FUNDS-GATED, and this is the subtle part. Minting is free, yet it is the ONLY\npartner-authenticated step in this flow: redemption is done by a radiologist's browser holding\na bearer token, and drafting then proceeds through the ordinary workspace where a partner\nservice account has no Subscription to check. So if the balance is not tested HERE, an\nexhausted client can keep feeding studies in through the worklist button indefinitely — the\nexact AI cost the owner's rule forbids us to absorb, arriving by the one door that had no lock.\n\nBounded on the other side too: already-minted tokens stay valid for their few remaining\nminutes, which is a deliberate trade — invalidating live links would strand a radiologist\nmid-study for a balance that ran out while they were dictating.","operationId":"mint_launch_api_v1_launch_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"X-Site-Id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Site-Id"}},{"name":"X-User-Ref","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Ref"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LaunchIn"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mint Launch Api V1 Launch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LaunchIn":{"properties":{"study_uid":{"type":"string","maxLength":128,"minLength":1,"title":"Study Uid"},"template":{"type":"string","maxLength":64,"minLength":1,"title":"Template"},"user_ref":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"User Ref"},"patient_id":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Patient Id"},"accession":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Accession"},"opaque_key":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Opaque Key"},"study_date":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Study Date"},"modality":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Modality"},"study_description":{"anyOf":[{"type":"string","maxLength":256},{"type":"null"}],"title":"Study Description"},"body_part":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Body Part"},"contrast_agent":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Contrast Agent"},"series_descriptions":{"items":{"type":"string"},"type":"array","title":"Series Descriptions"},"indication":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Indication"},"prior_report_text":{"anyOf":[{"type":"string","maxLength":20000},{"type":"null"}],"title":"Prior Report Text"}},"type":"object","required":["study_uid","template"],"title":"LaunchIn"},"NackIn":{"properties":{"error":{"type":"string","maxLength":500,"title":"Error","default":""}},"type":"object","title":"NackIn"},"SiteIn":{"properties":{"site_ref":{"type":"string","maxLength":128,"minLength":1,"title":"Site Ref"},"rate_limit_per_min":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rate Limit Per Min"},"monthly_report_cap":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Monthly Report Cap"},"rate_per_report_inr":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rate Per Report Inr"}},"type":"object","required":["site_ref"],"title":"SiteIn"},"StudyContext":{"properties":{"study_uid":{"type":"string","maxLength":128,"minLength":1,"title":"Study Uid"},"template":{"type":"string","maxLength":64,"minLength":1,"title":"Template"},"dictation":{"type":"string","maxLength":20000,"title":"Dictation","default":""},"patient_id":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Patient Id"},"accession":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Accession"},"study_date":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Study Date"},"modality":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Modality"},"opaque_key":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Opaque Key"},"study_description":{"anyOf":[{"type":"string","maxLength":256},{"type":"null"}],"title":"Study Description"},"body_part":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Body Part"},"protocol_name":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Protocol Name"},"contrast_agent":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Contrast Agent"},"series_descriptions":{"items":{"type":"string"},"type":"array","title":"Series Descriptions"},"indication":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Indication"},"prior_report_text":{"anyOf":[{"type":"string","maxLength":20000},{"type":"null"}],"title":"Prior Report Text"},"biometry":{"additionalProperties":{"type":"number"},"type":"object","title":"Biometry"},"biometry_units":{"additionalProperties":{"type":"string"},"type":"object","title":"Biometry Units"}},"type":"object","required":["study_uid","template"],"title":"StudyContext","description":"What the partner sends about the study.\n\nIdentifiers are consumed to compute a billing fingerprint and are NEVER persisted raw — see\n`partner_billing.study_fingerprint`.\n\n`biometry` is the ultrasound-specific field and the reason this differs from the CT/MRI build:\nunlike CT and MRI, an obstetric ultrasound machine DOES emit a structured report, so the\nmeasurements already exist in machine-readable form and nobody should retype them. See\n`integrations/adapters/dicom_sr.py` for the extraction and, importantly, for the boundary it\nenforces."},"TopupIn":{"properties":{"amount_inr":{"type":"number","exclusiveMinimum":0.0,"title":"Amount Inr"}},"type":"object","required":["amount_inr"],"title":"TopupIn","description":"A top-up request. `amount_inr` must be the activation minimum or a published pack — the\nserver re-validates, so a client calling the API directly hits the same floor as the UI."}}}}