{
  "openapi": "3.1.0",
  "info": {
    "title": "Omni-AI API",
    "version": "1.1.0",
    "summary": "Turn a medical dictation into a structured, template-filled report in one request.",
    "description": "Omni-AI (by Omnimed AI) transcribes a doctor's dictation, detects which study it belongs to, matches it against the medical center's report templates, and returns the finished report as HTML.\n\n## When to use this API\n\nReach for Omni-AI when you need to produce a structured radiology or diagnostic-imaging report from spoken or written dictation, inside your own RIS, HIS, PACS viewer or reporting tool. Typical jobs:\n\n- Generate a report from an audio dictation (`POST /api/omni-ai` with `file`).\n- Generate a report from dictation you already have as text (`POST /api/omni-ai` with `transcription_text`).\n- Force the report onto a specific template you already know (`template_html`), skipping study detection.\n- Apply a spoken correction to a report you generated earlier (`POST /api/omni-ai/refine`).\n\nIf you do not need control of the recording UI, the embeddable widget at `https://api.omniai.com.ar/static/widget/v1/omni-widget.min.js` wraps this same API and is the faster integration.\n\n## Authentication\n\nEvery call carries a `medical_center_id` provisioned by Omni; it selects the templates, language and report style used to generate the output. There is no bearer token today. Request a center ID at santiago@omniai.com.ar.\n\n## Versioning and deprecation\n\nThis document describes v1 of the API. The paths are unversioned; the version of record is `info.version` here and the `v1` segment in the widget URL. Breaking changes are announced by email to provisioned integration partners before they ship, and the previous behaviour stays available for at least 90 days. Additive changes (new optional parameters, new response fields) can ship at any time, so parse responses tolerantly.\n\nv2 of the contract (`https://www.omniai.com.ar/openapi-v2.json`) adds Partner Credential authentication, the Widget token exchange and addenda over the same paths, and is recommended for new integrations.\n\n## Errors\n\nEvery non-2xx response uses the same shape: `{ \"success\": false, \"error\": \"...\", \"error_code\": 400 }`. See the `ErrorResponse` schema. `error_code` is usually the HTTP status; some errors carry a more specific code instead (e.g. `1011`: the center has no templates; `1006`: the report model is unavailable).\n\n## Rate limits\n\nThere is no published per-partner rate limit today. Retry 5xx responses with exponential backoff.",
    "termsOfService": "https://www.omniai.com.ar/terms-of-service",
    "contact": {
      "name": "Omnimed AI - integraciones",
      "email": "santiago@omniai.com.ar",
      "url": "https://www.omniai.com.ar/api-documentation/radiologia/v1"
    }
  },
  "externalDocs": {
    "description": "Omni-AI integration guide (widget + direct API)",
    "url": "https://www.omniai.com.ar/api-documentation/radiologia/v1"
  },
  "servers": [
    {
      "url": "https://api.omniai.com.ar",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Reports",
      "description": "Generate and refine structured medical reports from dictation."
    },
    {
      "name": "Service",
      "description": "Connectivity and status checks."
    }
  ],
  "paths": {
    "/api/omni-ai": {
      "post": {
        "operationId": "generateReport",
        "tags": [
          "Reports"
        ],
        "summary": "Generate a structured report from a dictation",
        "description": "Accepts an audio dictation (`file`) or the dictation already transcribed (`transcription_text`) and returns the structured report as HTML. `data.reports` always has one entry: when the doctor dictates several studies, their reports come concatenated in that entry and `data.metadata.is_multi_study` is true.\n\nWithout `template_html`, the study is detected automatically from the dictation and matched against the center's stored templates — you do not need to send the study name. With `template_html`, detection and matching are skipped and the report deterministically follows the template you sent, always producing exactly one report.\n\nAudio always wins: if both `file` and `transcription_text` are sent, the audio is transcribed and the text is ignored, with no fallback if transcription fails. A blank value in either field is treated as absent, and sending neither returns 400.\n\nSend `username` to match against that doctor's visible templates (center-wide + assigned); without it only the center-wide templates are matched. Send the ordered study in `dicom_metadata` to get laterality / modality / region mismatch warnings in `data.warnings`; they never change or block the report.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/GenerateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Report(s) generated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GenerateResponse"
                }
              }
            }
          },
          "400": {
            "description": "Neither `file` nor `transcription_text` was sent, the audio format is unsupported, `metadata` is not valid JSON or exceeds 10KB, `transcription_text` exceeds 50KB, or `template_html` exceeds 100KB or is empty after sanitisation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The `medical_center_id` is not provisioned or has been disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "The center has no templates configured (`error_code` 1011). Contact Omni.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Transcription or report generation failed, including audio that transcribes to empty text (e.g. muted microphone). Retry with exponential backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "The report generation model is temporarily unavailable (`error_code` 1006). Retry with exponential backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/omni-ai/refine": {
      "post": {
        "operationId": "refineReport",
        "tags": [
          "Reports"
        ],
        "summary": "Apply a dictated correction to an existing report",
        "description": "Takes a report previously returned by `generateReport` plus a dictated correction (\"cambiar menisco interno por menisco externo\") and returns the same report with only the targeted element changed — plus the Conclusion when the change affects it. The whole report is never regenerated, and refinement is not billed.\n\n`data.report_html` is the full corrected report, ready to use. `data.patch` describes the exact regions that changed if you prefer to splice them into your own editor. To chain refinements, feed the returned `report_html` into the next call.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/RefineRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Report refined.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RefineResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `report_html`, missing both `file` and `transcription_text`, or an oversized `template_html`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The `medical_center_id` is not provisioned or has been disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Transcription or refinement failed. Retry with exponential backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "The refinement model is temporarily unavailable (`error_code` 1006). Retry with exponential backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "Service"
        ],
        "summary": "Check API connectivity",
        "description": "Returns the status of the API and its dependent services. Use it to verify connectivity from your backend before going live.",
        "responses": {
          "200": {
            "description": "Service status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "GenerateRequest": {
        "type": "object",
        "description": "Multipart form. Exactly one of `file` or `transcription_text` is required.",
        "required": [
          "medical_center_id"
        ],
        "properties": {
          "file": {
            "type": "string",
            "format": "binary",
            "description": "Audio dictation. Supported: wav, mp3, m4a, ogg, flac, webm. Required unless `transcription_text` is sent; if both are sent the audio wins."
          },
          "transcription_text": {
            "type": "string",
            "maxLength": 51200,
            "description": "The dictation as plain text, used instead of audio. Max 50KB. Skips the transcription step; study detection, template matching and generation behave identically."
          },
          "medical_center_id": {
            "type": "string",
            "description": "Center identifier provisioned by Omni. Selects the templates, language and report style."
          },
          "user_id": {
            "type": "string",
            "description": "Doctor or user identifier, recorded for analytics."
          },
          "username": {
            "type": "string",
            "description": "Doctor's username in the center (same login as the desktop app). Template matching uses that doctor's visible templates (center-wide + assigned directly or through a group). Absent or unknown to the center: center-wide templates only. Ignored when `template_html` is sent."
          },
          "language": {
            "type": "string",
            "default": "spa",
            "description": "Accepted for compatibility and ignored: dictation and report use the language of the center (`medical_center_id`)."
          },
          "metadata": {
            "type": "string",
            "format": "json",
            "maxLength": 10240,
            "description": "Free-form JSON metadata, max 10KB."
          },
          "dicom_metadata": {
            "type": "string",
            "format": "json",
            "description": "Study Context from the order / DICOM headers, as a JSON object (see `DicomMetadata`). Compared with the dictation to produce `data.warnings`; never changes the report.",
            "contentMediaType": "application/json"
          },
          "template_html": {
            "type": "string",
            "maxLength": 102400,
            "description": "Preselected template. When present, study detection and template matching are skipped and the report follows this template exactly, always yielding a single report. Sanitised server-side; max 100KB."
          },
          "template_name": {
            "type": "string",
            "description": "Label for the preselected template. Echoed back as `study_name` and `template_used`."
          },
          "include_template": {
            "type": "boolean",
            "default": false,
            "description": "Return the sanitised template the server actually used, for diff highlighting."
          },
          "include_rtf": {
            "type": "boolean",
            "default": false,
            "description": "Also return an RTF rendering of each report."
          }
        }
      },
      "RefineRequest": {
        "type": "object",
        "description": "Multipart form. Exactly one of `file` or `transcription_text` is required.",
        "required": [
          "medical_center_id",
          "report_html"
        ],
        "properties": {
          "file": {
            "type": "string",
            "format": "binary",
            "description": "Audio with the dictated correction. Required unless `transcription_text` is sent."
          },
          "transcription_text": {
            "type": "string",
            "maxLength": 51200,
            "description": "The correction as text instead of audio. Max 50KB; audio wins if both are sent."
          },
          "medical_center_id": {
            "type": "string",
            "description": "Center identifier provisioned by Omni."
          },
          "report_html": {
            "type": "string",
            "description": "The last generated report, exactly as returned by the API or as last refined."
          },
          "template_html": {
            "type": "string",
            "maxLength": 102400,
            "description": "The report's original template, used as a read-only style and format reference. Max 100KB."
          },
          "template_name": {
            "type": "string",
            "description": "Template or study-type name. Enables per-study-type refinement rules."
          },
          "selected_html": {
            "type": "string",
            "description": "The exact HTML element to refine. Omit to let the model locate the target from the dictation."
          },
          "study_id": {
            "type": "integer",
            "description": "`data.metadata.study_id` from the generation call, so the refinement is tracked against it."
          },
          "user_id": {
            "type": "string",
            "description": "Doctor or user identifier, recorded for analytics."
          },
          "language": {
            "type": "string",
            "default": "spa",
            "description": "Accepted for compatibility and ignored: dictation and report use the language of the center (`medical_center_id`)."
          }
        }
      },
      "Report": {
        "type": "object",
        "required": [
          "study_name",
          "template_used",
          "template_match_confidence",
          "report_html"
        ],
        "properties": {
          "study_name": {
            "type": "string",
            "description": "Detected (or preselected) study, e.g. \"IRM de Rodilla\"."
          },
          "template_used": {
            "type": "string",
            "description": "Name of the template the report was generated against."
          },
          "template_match_confidence": {
            "type": "number",
            "format": "float",
            "minimum": 0,
            "maximum": 1,
            "description": "Confidence of the template match. Always 1.0 when `template_html` was supplied."
          },
          "report_html": {
            "type": "string",
            "description": "The generated report as HTML."
          },
          "report_rtf": {
            "type": [
              "string",
              "null"
            ],
            "description": "RTF rendering, present only when `include_rtf` was true."
          },
          "transcription_segment": {
            "type": [
              "string",
              "null"
            ],
            "description": "The portion of the transcription this report was generated from."
          },
          "template_html": {
            "type": [
              "string",
              "null"
            ],
            "description": "The sanitised template the server used, present only when `include_template` was true. Diff against this, not against your original."
          }
        }
      },
      "GenerateResponse": {
        "type": "object",
        "required": [
          "success",
          "message",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary, e.g. \"Successfully processed audio and generated 1 report(s)\"."
          },
          "data": {
            "type": "object",
            "required": [
              "transcription",
              "reports"
            ],
            "properties": {
              "transcription": {
                "type": "string",
                "description": "Full transcription of the dictation."
              },
              "language_detected": {
                "type": "string",
                "description": "Language detected in the audio (dictation is transcribed in the center's language). Echoes the `language` parameter when the input was text."
              },
              "duration_seconds": {
                "type": "number",
                "format": "float",
                "description": "Length of the audio. 0.0 when the input was text."
              },
              "speaker_count": {
                "type": "integer",
                "description": "Speakers detected in the audio. 1 when the input was text."
              },
              "reports": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Report"
                },
                "description": "Always exactly one entry. Several dictated studies are concatenated into it (`study_name` / `template_used` comma-separated, `metadata.is_multi_study` true)."
              },
              "warnings": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ContextMismatchWarning"
                },
                "description": "Discrepancies between `dicom_metadata` and the dictation. Informational: the report is generated the same."
              },
              "context_checks": {
                "type": "object",
                "description": "Outcome of each mismatch check, keyed by laterality, modality, region.",
                "additionalProperties": {
                  "$ref": "#/components/schemas/ContextCheckStatus"
                }
              },
              "processing_time_ms": {
                "type": "integer",
                "description": "Total server-side processing time for the request."
              },
              "metadata": {
                "type": "object",
                "additionalProperties": true,
                "properties": {
                  "study_id": {
                    "type": "integer",
                    "description": "Keep this if you plan to refine the report later."
                  },
                  "detection_source": {
                    "type": "string",
                    "enum": [
                      "auto_detect",
                      "preselected_template"
                    ],
                    "description": "How the study was determined."
                  },
                  "input_source": {
                    "type": "string",
                    "enum": [
                      "audio",
                      "text"
                    ],
                    "description": "Whether the pipeline started from audio or from `transcription_text`."
                  },
                  "is_multi_study": {
                    "type": "boolean",
                    "description": "True when the dictation covered several studies and their reports were concatenated."
                  }
                },
                "description": "Request-scoped details. Additional keys may be added over time, so parse tolerantly."
              }
            },
            "description": "The transcription and the report(s) generated from it."
          }
        }
      },
      "RefineResponse": {
        "type": "object",
        "required": [
          "success",
          "message",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary, e.g. \"Successfully refined report\"."
          },
          "data": {
            "type": "object",
            "required": [
              "transcription",
              "report_html"
            ],
            "properties": {
              "transcription": {
                "type": "string",
                "description": "The dictated correction as transcribed."
              },
              "report_html": {
                "type": "string",
                "description": "The full report with the correction already applied."
              },
              "patch": {
                "type": "object",
                "description": "The exact regions that changed, for splicing into your own editor: replace the first occurrence of `target_html` with `refined_text`, and of `conclusion_original_text` with `conclusion_text` when non-null.",
                "properties": {
                  "refined_text": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The replacement HTML for `target_html`."
                  },
                  "target_html": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The HTML element in the original report that was replaced."
                  },
                  "conclusion_text": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The rewritten Conclusion, or null when the correction did not affect it."
                  },
                  "conclusion_original_text": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The Conclusion as it was before the correction, or null when unchanged."
                  }
                }
              },
              "processing_time_ms": {
                "type": "integer",
                "description": "Total server-side processing time for the request."
              },
              "metadata": {
                "type": "object",
                "additionalProperties": true,
                "properties": {
                  "medical_center_id": {
                    "type": "string",
                    "description": "The center the refinement was attributed to."
                  },
                  "study_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "The study this refinement was tracked against, when `study_id` was sent."
                  },
                  "event_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Identifier of the refinement event, for support requests."
                  },
                  "model_used": {
                    "type": "string",
                    "description": "Model that produced the correction, e.g. \"gemini-2.5-flash\"."
                  }
                },
                "description": "Request-scoped details. Additional keys may be added over time, so parse tolerantly."
              }
            },
            "description": "The transcribed correction and the updated report."
          }
        }
      },
      "HealthResponse": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "healthy",
              "degraded",
              "unhealthy"
            ],
            "description": "Overall service status."
          },
          "services": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "service": {
                  "type": "string",
                  "description": "Service name, repeated inside the entry."
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "healthy",
                    "degraded",
                    "unhealthy"
                  ],
                  "description": "Status of this individual service."
                },
                "available": {
                  "type": "boolean",
                  "description": "Whether this service is currently reachable."
                }
              }
            },
            "description": "Per-service status, keyed by service name."
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "description": "Every non-2xx response from this API uses this shape.",
        "required": [
          "success",
          "error",
          "error_code"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false,
            "description": "Always false on an error response."
          },
          "error": {
            "type": "string",
            "description": "Human-readable error message."
          },
          "error_code": {
            "type": "integer",
            "description": "Machine-readable code. Mirrors the HTTP status.",
            "examples": [
              400,
              403,
              500
            ]
          }
        }
      },
      "DicomMetadata": {
        "type": "object",
        "description": "Ordered study sent as `dicom_metadata`. Drives the Context Mismatch checks.",
        "required": [
          "dicom_modality"
        ],
        "properties": {
          "dicom_modality": {
            "type": "string",
            "description": "DICOM Modality: CT, MR, DX/CR, US, MG."
          },
          "dicom_bodypart": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "DICOM Body Part Examined, one value (HAND, KNEE, SHOULDER, FOOT, BREAST, ...). Region is checked for extremities and breast only."
          },
          "dicom_laterality": {
            "type": "string",
            "description": "R, L or B (derecha / izquierda / bilateral also accepted). If absent, read from `dicom_series_description` when it names the side."
          },
          "dicom_series_description": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Series descriptions, e.g. \"TC MANO DERECHA\"."
          }
        },
        "additionalProperties": true,
        "examples": [
          {
            "dicom_modality": "CT",
            "dicom_bodypart": [
              "HAND"
            ],
            "dicom_laterality": "R",
            "dicom_series_description": [
              "TC MANO DERECHA"
            ]
          }
        ]
      },
      "ContextMismatchWarning": {
        "type": "object",
        "required": [
          "code",
          "context_value",
          "dictated_value",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "LATERALITY_MISMATCH",
              "MODALITY_MISMATCH",
              "REGION_MISMATCH"
            ],
            "description": "Which check found the discrepancy."
          },
          "study_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Study the warning refers to."
          },
          "context_value": {
            "type": "string",
            "description": "Value from `dicom_metadata`: laterality R/L; modality RX, TC, RM, ECO, MAMO; region a Spanish key (mano, pie, rodilla, muneca, ...)."
          },
          "dictated_value": {
            "type": "string",
            "description": "Value the doctor dictated, same vocabulary as `context_value`."
          },
          "evidence": {
            "type": [
              "string",
              "null"
            ],
            "description": "Literal fragment of the dictation, when available."
          },
          "message": {
            "type": "string",
            "description": "Spanish message, ready to show the radiologist."
          }
        }
      },
      "ContextCheckStatus": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "mismatch",
              "skipped"
            ],
            "description": "ok: checked and consistent; mismatch: a warning was emitted; skipped: not checked (see reason)."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "no_context",
              "unmapped",
              "not_dictated",
              "bilateral_context",
              "llm_unavailable",
              null
            ],
            "description": "Why a check was skipped. Skipped means not verified, not verified OK."
          }
        }
      }
    }
  }
}
