Omnimed AI

    Omnimed AI Integration Guide

    Integrate Omnimed AI report dictation into your RIS, HIS or viewer.

    1. Widget

    Drop a <script> tag into your page. A floating recording button is injected; the generated report can be auto-inserted into the active editor.

    Use this if: you want the fastest integration and are happy with the default recording UI.

    2. Direct API

    POST an audio file to /api/omni-ai and handle the response yourself.

    Use this if: you need full control over the recording UI or are integrating from a native/desktop client.

    Base URL: https://api.omniai.com.ar

    v1: authentication by medical_center_id only. For new integrations use v2, with a Partner Credential and a token-based Widget.

    Overview

    Omni-AI turns a medical dictation (audio) into a structured, template-filled report in a single request. You can integrate in two ways:

    1. Embeddable Widget — drop a <script> tag into your page. The doctor records from a floating button, the report is returned and can be auto-inserted into your editor.
    2. Direct API — call POST https://api.omniai.com.ar/api/omni-ai with the audio file yourself and handle the response.

    Both routes use the same omni-ai endpoint under the hood. Start with the Widget unless you need full control of the recording UI.

    Authentication

    Each integration partner receives a medical_center_id (provisioned by Omni). The center ID identifies the templates, language and report style used to generate the output. No bearer token is required today.

    Contact [email protected] to provision a center ID.

    Widget

    Embed

    <script src="https://api.omniai.com.ar/static/widget/v1/omni-widget.min.js"
            data-center-id="YOUR_CENTER_ID"
            data-username="DOCTOR_USERNAME"
            async></script>

    A floating recording button is injected in the bottom-right. The doctor clicks it, dictates, stops, and the widget shows the generated report.

    data-username is optional. It is the doctor's username in this center (the same login they use in the desktop app) and it decides which templates the dictation is matched against:

    data-usernameTemplates matched
    absentOnly the center's general templates
    a username of the centerThe general templates plus the ones assigned to that doctor (directly or through a group)
    a username the center does not knowOnly the general templates

    Auto-insert the report

    The widget emits an omniai:report event when the report is ready, and again each time the doctor clicks "Insertar". event.detail carries html, text and the dictation's recordingId; if you auto-insert, replace what you already inserted for that recordingId instead of appending a second copy. Listen for it and paste the HTML (or plain text) into your editor:

    window.addEventListener('omniai:report', (event) => {
      const { html, text } = event.detail;
      // e.g. paste into the focused contenteditable / rich-text field
      document.execCommand('insertHTML', false, html);
    });

    Or initialize manually with a callback:

    OmniWidget.init({
      centerId: 'YOUR_CENTER_ID',
      username: 'DOCTOR_USERNAME', // optional, same rules as data-username
      onReport: ({ html, text, recordingId }) => {
        // Insert into your report editor
      },
      onAudio: (audio) => {
        // Optional: keep your own copy of the recording (see below)
      }
    });

    Keep a copy of the audio

    If your system must retain the original dictation (e.g. PACS legal retention), the widget hands you the raw recording the moment the doctor stops, before uploading it, so your copy exists even if processing fails. Listen for omniai:audio or pass onAudio to init:

    window.addEventListener('omniai:audio', (event) => {
      const { blob, mimeType, kind, recordingId, parentRecordingId } = event.detail;
      // e.g. upload blob to your storage, keyed by your study ID + recordingId
    });
    FieldDescription
    blobThe recording (Blob), usually WebM/Opus (Ogg or MP4 on browsers without WebM)
    mimeTypeMIME type of the blob
    durationSecondsRecording length
    sizeBytesBlob size
    kind"dictation" or "refinement" (a spoken correction)
    recordingIdUnique ID of this recording. The omniai:report payload carries the dictation's recordingId, linking audio and report
    parentRecordingIdFor a refinement, the recordingId of the dictation it corrects; null for dictations
    recordedAtISO 8601 timestamp

    The blob never leaves your page through this event: storing it is up to your code.

    Widget lifecycle

    StateMeaning
    idleReady to record
    recordingMicrophone active
    pausedRecording paused
    processingAudio uploaded, report generating
    resultReport shown in panel
    errorProcessing failed; the doctor can record again

    Browser requirements

    • HTTPS (required for microphone access)
    • Permission granted for navigator.mediaDevices.getUserMedia
    • Max recording duration: 10 minutes (configurable)

    Direct API

    Generate a report

    POST https://api.omniai.com.ar/api/omni-ai
    Content-Type: multipart/form-data

    Parameters

    ParameterTypeRequiredDescription
    fileFileConditionalAudio file (wav, mp3, m4a, ogg, flac, webm). Required unless transcription_text is sent
    transcription_textStringConditionalDictation text used instead of audio (see below). Required unless file is sent
    medical_center_idStringYesCenter identifier provided by Omni
    user_idStringNoDoctor/user identifier for analytics
    usernameStringNoDoctor's username in the center. Matching uses that doctor's visible templates (general + assigned). Absent or unknown: general templates only. Ignored when template_html is sent
    languageStringNoAccepted for compatibility and ignored: dictation and report use the language of the center (medical_center_id)
    metadataString (JSON)NoFree-form JSON metadata, max 10KB
    dicom_metadataString (JSON)NoOrdered study from the order / DICOM headers; drives the mismatch warnings (see below)
    template_htmlString (HTML)NoPreselected template: generate the report against exactly this template (see below)
    template_nameStringNoLabel for the preselected template; echoed back as study_name / template_used
    include_templateBooleanNoReturn the template HTML the server used, for diff highlighting
    include_rtfBooleanNoReturn RTF version of each report

    Note: Without template_html, study detection is automatic from the transcription and the template is matched against the center's stored templates — you do not need to send the study name.

    Several studies in one dictation: reports always has one entry. When the doctor dictates several studies, their reports come concatenated in that single report_html, study_name / template_used list them comma-separated, and metadata.is_multi_study is true.

    Sending text instead of audio (transcription_text)

    If your system already has the dictation as text (typed, or transcribed on your side), send it as transcription_text and omit the audio — the transcription step is skipped and the text enters the pipeline exactly where the transcription would: study detection, template matching and generation behave identically. It combines naturally with template_html (no transcription and no matching — the fastest, fully deterministic path).

    • Audio always wins. If you send both, the audio is transcribed and transcription_text is ignored entirely. There is no fallback: if the audio fails to transcribe (e.g. muted microphone) the call errors even when text was sent.
    • A blank file field or a blank transcription_text ("" or whitespace) is treated as absent. Sending neither returns a 400.
    • Plain text, max 50KB.
    • In the response: duration_seconds is 0.0, speaker_count is 1, language_detected echoes your language parameter, and metadata.input_source is "text" (vs "audio").
    • Billing is unchanged — a report generated from text consumes credit exactly like one generated from audio.

    Sending your own template (template_html)

    When your system knows which template the report must follow (per clinic, per referring center, per modality), send it in the call:

    • Study detection and template matching are skipped entirely — the report deterministically follows your template.
    • The result is always a single report, even if the dictation mentions several studies.
    • The HTML is sanitised server-side before use; max size is 100KB. Oversized HTML, or markup that is empty after sanitisation, returns a 400. A blank field ("" or whitespace) is treated as absent: the call falls back to the normal matching path.
    • HTML is the recommended format: it preserves bold, headings and block structure.
    • In the response: template_match_confidence is 1.0 and metadata.detection_source is "preselected_template".
    • With include_template=true, template_html in the response is the sanitised template the server actually used — diff against this, not your original.

    Success response

    {
      "success": true,
      "message": "Successfully processed audio and generated 1 report(s)",
      "data": {
        "transcription": "...",
        "language_detected": "spa",
        "duration_seconds": 42.1,
        "speaker_count": 1,
        "reports": [
          {
            "study_name": "IRM de Rodilla",
            "template_used": "IRM de Rodilla",
            "template_match_confidence": 1.0,
            "report_html": "<p>...</p>",
            "report_rtf": null,
            "transcription_segment": "...",
            "template_html": null
          }
        ],
        "warnings": [],
        "context_checks": {
          "laterality": {"status": "ok"},
          "modality": {"status": "skipped", "reason": "not_dictated"},
          "region": {"status": "ok"}
        },
        "processing_time_ms": 7441,
        "metadata": {
          "study_id": 12345,
          "detection_source": "preselected_template",
          "input_source": "audio",
          "is_multi_study": false,
          "...": "..."
        }
      }
    }

    Tip: Keep data.metadata.study_id if you plan to refine this report later.

    Mismatch warnings (laterality, modality, region)

    Send the ordered study in dicom_metadata and the API compares it with what the doctor dictated. A discrepancy comes back in data.warnings; the report is generated exactly the same and is never changed or blocked by it. Neither side is assumed correct: the order can be wrong too.

    {
      "dicom_modality": "CT",
      "dicom_bodypart": ["HAND"],
      "dicom_laterality": "R",
      "dicom_series_description": ["TC MANO DERECHA"]
    }
    • dicom_modality (required when dicom_metadata is sent): DICOM Modality (CT, MR, DX/CR, US, MG).
    • dicom_bodypart: DICOM Body Part Examined, one value (HAND, KNEE, SHOULDER, FOOT, BREAST, ...). Region is checked for extremities and breast; other body parts are not checked yet.
    • dicom_laterality: R, L or B (derecha / izquierda / bilateral also accepted). If absent, the side is read from dicom_series_description when it says it in words (DER, IZQ, DERECHA, LEFT, ...).
    • Free-form context such as the clinical indication goes in metadata, which is context for the report, not input to these checks.

    Each warning:

    {
      "code": "LATERALITY_MISMATCH",
      "study_name": "custom",
      "context_value": "R",
      "dictated_value": "L",
      "evidence": "quinto metacarpiano del pie izquierdo",
      "message": "El estudio está indicado como derecho y se dictó izquierdo."
    }
    • code: LATERALITY_MISMATCH, MODALITY_MISMATCH or REGION_MISMATCH.
    • context_value / dictated_value: laterality R/L; modality RX, TC, RM, ECO, MAMO; region a Spanish key such as mano, pie, rodilla, muneca.
    • evidence: literal fragment of the dictation, when available. message: Spanish, ready to show the radiologist.

    context_checks says, per check, whether it ran: ok, mismatch, or skipped with a reason (no_context: not sent; unmapped: a value we do not check yet; not_dictated: the doctor did not say it; bilateral_context: the order is bilateral; llm_unavailable: the check could not finish in time). An empty warnings with skipped checks means "not verified", not "verified OK".

    The checks are tuned to avoid false alarms: when in doubt they do not warn. Mentions of the other side for comparison ("comparado con la rodilla izquierda") or of another modality as a suggestion ("se sugiere RM") are not mismatches.

    Examples

    cURL

    curl -X POST https://api.omniai.com.ar/api/omni-ai \
      -F "[email protected]" \
      -F "medical_center_id=YOUR_CENTER_ID" \
      -F "user_id=dr_martinez" \
      -F "language=spa"

    cURL (preselected template)

    curl -X POST https://api.omniai.com.ar/api/omni-ai \
      -F "[email protected]" \
      -F "medical_center_id=YOUR_CENTER_ID" \
      -F "template_html=<h2>IRM de Rodilla</h2><p>...</p>" \
      -F "template_name=IRM de Rodilla" \
      -F "include_template=true"

    Python

    import requests
    
    url = "https://api.omniai.com.ar/api/omni-ai"
    
    with open("audio.m4a", "rb") as f:
        response = requests.post(
            url,
            files={"file": f},
            data={
                "medical_center_id": "YOUR_CENTER_ID",
                "user_id": "dr_lopez",
                "language": "spa",
            },
        )
    
    response.raise_for_status()
    result = response.json()
    
    for report in result["data"]["reports"]:
        print(report["study_name"], "->", report["report_html"])

    JavaScript (fetch)

    async function processAudio(audioFile, centerId, userId) {
      const form = new FormData();
      form.append('file', audioFile);
      form.append('medical_center_id', centerId);
      form.append('user_id', userId);
      form.append('language', 'spa');
    
      const res = await fetch('https://api.omniai.com.ar/api/omni-ai', {
        method: 'POST',
        body: form,
      });
    
      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        throw new Error(err.error || `HTTP ${res.status}`);
      }
    
      const { data } = await res.json();
      return data.reports[0].report_html;
    }

    Refine a report

    POST https://api.omniai.com.ar/api/omni-ai/refine
    Content-Type: multipart/form-data

    The doctor dictates a correction to an already-generated report ("cambiar menisco interno por menisco externo"). The server transcribes the audio, changes only the targeted element — plus the report's Conclusion when the change affects it — and returns the updated report. Refinement never regenerates the whole report, and it is not billed.

    Parameters

    ParameterTypeRequiredDescription
    fileFileConditionalAudio with the dictated correction. Required unless transcription_text is sent
    transcription_textStringConditionalThe correction as text instead of audio (max 50KB, same rules as on generation: audio wins, blank = absent)
    medical_center_idStringYesCenter identifier provided by Omni
    report_htmlString (HTML)YesThe last generated report, exactly as returned by the API (or as last refined)
    template_htmlString (HTML)NoThe original template of the report; used as a read-only style/format reference (max 100KB)
    template_nameStringNoTemplate/study-type name, enables per-study-type rules
    selected_htmlString (HTML)NoExact HTML element to refine; omit to let the model locate the target from the dictation
    study_idIntegerNometadata.study_id from the generation call, to track the refinement
    user_idStringNoDoctor/user identifier for analytics
    languageStringNoAccepted for compatibility and ignored: the language of the center is used

    Success response

    {
      "success": true,
      "message": "Successfully refined report",
      "data": {
        "transcription": "cambiar menisco interno por menisco externo",
        "report_html": "<p>...full report with the correction applied...</p>",
        "patch": {
          "refined_text": "<p>Menisco externo sin alteraciones.</p>",
          "target_html": "<p>Menisco interno sin alteraciones.</p>",
          "conclusion_text": null,
          "conclusion_original_text": null
        },
        "processing_time_ms": 3120,
        "metadata": {
          "medical_center_id": "your-center",
          "study_id": 12345,
          "event_id": "678",
          "model_used": "gemini-2.5-flash",
          "...": "..."
        }
      }
    }
    • data.report_html is the full report with the correction already applied — use it directly.
    • data.patch describes the exact regions that changed, if you prefer to splice them 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).
    • To chain refinements, send the returned report_html as the input of the next refine call.

    Example

    curl -X POST https://api.omniai.com.ar/api/omni-ai/refine \
      -F "[email protected]" \
      -F "medical_center_id=YOUR_CENTER_ID" \
      -F "report_html=<p>...last generated report...</p>" \
      -F "template_html=<p>...original template...</p>" \
      -F "study_id=12345"

    Error response

    Errors return a non-2xx status with:

    {
      "success": false,
      "error": "Human-readable error message",
      "error_code": 400
    }

    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).

    StatusCauseFix
    400Missing input (neither file nor transcription_text), unsupported audio format, malformed metadata JSON, or oversized template_htmlSend wav/mp3/m4a/ogg/flac/webm or transcription_text (max 50KB); validate JSON under 10KB; keep template_html under 100KB
    403Center disabled / not provisionedContact Omni to enable your medical_center_id
    422The center has no templates configured (error_code 1011)Contact Omni to load your center's templates
    500Transcription or report generation failed (e.g. silent audio / muted microphone)Check the recording; retry with backoff; contact support if persistent
    503Report generation model temporarily unavailable (error_code 1006)Retry with backoff

    Highlighting AI changes (client-side diff)

    The Widget highlights what the AI changed by diffing the template against the generated report. You can do the same in your UI with jsdiff (diff on npm) — no server support needed:

    import { diffWords } from 'diff';
    
    function highlightChanges(templateText, reportText) {
      return diffWords(templateText, reportText)
        .map((part) =>
          part.added
            ? `<mark class="ai-change">${part.value}</mark>`
            : part.removed
              ? ''
              : part.value
        )
        .join('');
    }
    • Baseline: request include_template=true on generation and diff against the returned template_html.
    • After a refine, diff the previous report_html against the new one to flash only the correction.
    • Diff on visible text (e.g. from DOMParser) rather than raw HTML strings for cleaner highlights.

    Health check

    Use this endpoint to verify connectivity from your backend:

    curl https://api.omniai.com.ar/api/health

    Response:

    {
      "status": "healthy",
      "services": {
        "api": {
          "service": "api",
          "status": "healthy",
          "available": true
        }
      }
    }

    Support

    Questions and provisioning: [email protected]