Omnimed AI
    Documentación de la API

    Guía de integración de OmniMed Consulta

    Enviá la grabación de una consulta y recibí la nota, los pedidos con su borrador de orden y las sugerencias para el médico, en un solo resultado.

    OpenAPI 3.1: /openapi-consulta-v1.json

    OmniMed Consulta procesa la grabación completa de una consulta médica (médico, paciente y acompañantes) y devuelve, en un único JSON:

    • la nota de la consulta en el formato elegido (SOAP por defecto), donde cada oración cita el momento de la conversación del que sale;
    • los pedidos que hizo el médico (laboratorio, imágenes, otros estudios, derivaciones, terapias, procedimientos), cada uno con un borrador de orden listo para completar en tu plataforma;
    • las sugerencias para el médico (diagnósticos para considerar con razones a favor y en contra, controles de seguridad de los pedidos, chequeos de guías clínicas), que nunca cambian la nota por sí solas;
    • los códigos CIE-10 propuestos para la evaluación;
    • la transcripción con el rol de cada hablante y sus tiempos.

    Si enviás la historia clínica del paciente, el resultado la usa como contexto: trae los antecedentes relevantes citados y marca las discrepancias entre lo dicho hoy y lo registrado (por ejemplo, "no toma medicación" frente a un tratamiento activo). La nota sale solo de lo dicho en la consulta de hoy: la historia nunca entra en sus secciones como si se hubiera dicho.

    Las decisiones del médico (aceptar una sugerencia, editar la nota, firmar las órdenes) ocurren en tu sistema, no en el nuestro.

    El contrato completo está en OpenAPI 3.1.

    Inicio rápido

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

    1. Pedí tu Partner Credential (credencial de partner) a santiago@omniai.com.ar, habilitada para Consulta en tus centros. Es un secreto omp_...: guardalo como variable de entorno de tu servidor. Si ya integrás el dictado de radiología, la misma credencial sirve cuando el centro tiene Consulta habilitada.

    2. Enviá la consulta. Desde tu servidor, con la grabación y el consentimiento del paciente:

      curl -X POST https://api.omniai.com.ar/api/consulta/v1/consultations \
        -H "Authorization: Bearer $OMNI_PARTNER_CREDENTIAL" \
        -H "Idempotency-Key: 6f1c2a4e-0b9d-4d61-a3f2-7c5e8b1d9a20" \
        -F "audio=@consulta.m4a" \
        -F "medical_center_id=YOUR_CENTER_ID" \
        -F "doctor_id=med-1042" \
        -F "consent=true"

      Responde 202 enseguida, con el id de la consulta y status: "processing".

    3. Consultá el resultado hasta que status sea done (o failed):

      curl https://api.omniai.com.ar/api/consulta/v1/consultations/3f6c1d2e-8a4b-4c7e-9f10-2b5d6e7a8c90 \
        -H "Authorization: Bearer $OMNI_PARTNER_CREDENTIAL"
    4. Mostrá el resultado en tu sistema: la nota en note, los pedidos en orders, las sugerencias en suggestions (ver El resultado, campo por campo).

    El procesamiento es asíncrono porque una consulta larga tarda más de lo que una conexión HTTP espera.

    Autenticación

    Partner Credential (servidor a servidor)

    Cada llamada lleva la Partner Credential como Bearer:

    Authorization: Bearer omp_...
    • Es un secreto de servidor a servidor: nunca debe llegar al navegador ni a una app instalada. Guardala en una variable de entorno o en un gestor de secretos.
    • Está habilitada para una lista de centros y, en cada centro, para los productos contratados. Una credencial de Dictado no sirve para Consulta salvo que el centro tenga Consulta habilitada para esa credencial.
    • Cada llamada que crea una consulta nombra el centro con medical_center_id (su id o su slug en Omnimed AI).
    • Esta API no tiene un token para el navegador: todas las llamadas salen de tu servidor.

    Rotación

    Si la credencial se filtra, pedinos rotarla a santiago@omniai.com.ar: la credencial vieja deja de funcionar en menos de 60 segundos. Una credencial rotada o revocada responde 401 con partner_credential_revoked.

    Médico declarado

    Tu sistema ya autenticó al médico; en cada consulta nos decís quién es. No creamos usuarios ni le pedimos al médico que inicie sesión con nosotros: confiamos en tu palabra dentro de los centros para los que tu credencial está habilitada.

    CampoRequeridoMáximoDescripción
    doctor_idSí255 caracteresTu identificador del médico que grabó la consulta
    doctor_nameNo255 caracteresNombre del médico, como debe figurar en el borrador de orden
    doctor_licenseNo100 caracteresMatrícula del médico. Sin ella, el borrador de orden la marca como faltante (missing incluye doctor_license)
    doctor_specialtyNo255 caracteresEspecialidad del médico

    Los cuatro vuelven en la consulta como doctor (id, name, license, specialty). Un campo que supera su largo máximo responde 422 con invalid_doctor.

    Decidí el médico en tu backend a partir de tu propia sesión, nunca a partir de algo que mande el navegador: así un médico no puede actuar a nombre de otro.

    Datos del paciente

    No hace falta identificar al paciente. Podés enviar en el campo patient (JSON) datos opcionales que mejoran el resultado y una referencia opaca tuya para correlacionar:

    CampoTipoDescripción
    refString (hasta 255)Referencia opaca de tu sistema (por ejemplo, el número de historia clínica). Vuelve como patient_ref; no la usamos para unir consultas
    birth_dateFecha (AAAA-MM-DD)Fecha de nacimiento. Si la enviás, se usa en lugar de age
    ageEntero, 0 a 130Edad en años
    sexfemale, male, other, unknownSexo
    coverage.nameStringObra social o prepaga
    coverage.planStringPlan
    coverage.affiliate_numberStringNúmero de afiliado
    {"ref": "hc-998877", "birth_date": "1988-04-12", "sex": "female", "coverage": {"name": "Obra social ejemplo", "plan": "210"}}
    • Sin identidad. No mandes nombre ni documento: los datos de identidad del paciente los completa tu sistema sobre el borrador de orden.
    • Tolerante. Los campos que no conocemos se ignoran, y también un campo conocido cuyo valor no respeta el formato (por ejemplo sex: "F", age: 200 o una fecha mal escrita): se descarta ese campo y se usa el resto. Guardamos solo los campos de esta lista.
    • Hasta 1 MB. Un patient que no es JSON, no es un objeto o supera 1 MB responde 422 con invalid_patient.

    Historia clínica

    Opcional, en el campo history (JSON), organizada por secciones. Todas las secciones son opcionales. Cada registro lleva tu propio id: las citas del resultado (source_ids) usan esos mismos ids, así podés enlazar cada antecedente o discrepancia al registro de tu sistema.

    SecciónCampos requeridosCampos opcionales
    problemsid, text (el problema, como lo lista tu sistema, ej. "Hipertensión arterial")status (active o resolved; por defecto active), code (código con su sistema, ej. "CIE-10 I10"), onset (fecha)
    allergiesid, substancereaction
    medicationsid, namedose (dosis y frecuencia en texto, ej. "10 mg cada 12 h"), status (active o suspended; por defecto active), since (fecha)
    labsid, name, value, dateunit, ref_range (rango de referencia en texto, ej. "70-110")
    imaging_reportsid, study, date, text (texto del informe)conclusion
    visits (consultas previas)id, datereason, summary (resumen o evolución de esa consulta)

    Las fechas van como AAAA-MM-DD.

    {
      "problems": [{"id": "pb-3", "text": "Hipertensión arterial", "code": "CIE-10 I10", "onset": "2019-05-01"}],
      "allergies": [{"id": "alg-17", "substance": "Penicilina", "reaction": "Urticaria"}],
      "medications": [{"id": "med-8", "name": "Enalapril", "dose": "10 mg cada 12 h", "since": "2019-05-01"}],
      "labs": [{"id": "lab-51", "name": "Glucemia", "value": "98", "unit": "mg/dL", "ref_range": "70-110", "date": "2026-08-20"}],
      "imaging_reports": [
        {"id": "img-4", "study": "Ecografía abdominal", "date": "2025-03-10", "text": "Hígado de forma y tamaño conservados...", "conclusion": "Sin hallazgos patológicos."}
      ],
      "visits": [{"id": "vis-22", "date": "2026-08-20", "reason": "Control de presión", "summary": "Buen control con enalapril."}]
    }
    • La historia que enviás se usa solo para esa consulta: nunca se mezcla con otra consulta ni con otro paciente.
    • Hasta 1 MB de JSON. Los textos de más de 8.000 caracteres se recortan.
    • Responde 422 con invalid_history si no es JSON, supera 1 MB o un registro no respeta su formato: campos requeridos, fechas, valores permitidos o ids repetidos dentro de una sección.

    Consentimiento

    Grabar una consulta requiere el consentimiento del paciente. Al enviar consent=true tu sistema declara que el paciente aceptó ser grabado. Sin eso la consulta se rechaza con 422 y consent_required.

    Endpoints

    Todas las llamadas llevan Authorization: Bearer omp_... y salen de tu servidor.

    MétodoRutaPara qué
    POST/api/consulta/v1/consultationsEnviar una consulta para procesar
    GET/api/consulta/v1/consultations/{consultation_id}Obtener el estado y el resultado
    POST/api/consulta/v1/consultations/{consultation_id}/instructionsGenerar indicaciones para el paciente
    GET/api/consulta/v1/note-formatsListar los formatos de nota

    Enviar una consulta

    POST https://api.omniai.com.ar/api/consulta/v1/consultations
    Authorization: Bearer omp_...
    Idempotency-Key: <UUID de este envío>
    Content-Type: multipart/form-data

    Recibe la grabación completa y responde de inmediato con el id; el procesamiento sigue en segundo plano.

    Parámetros

    DóndeCampoTipoRequeridoDescripción
    HeaderIdempotency-KeyString (hasta 255)No, recomendadoIdentificador único de este envío, generado por tu sistema (recomendado un UUID). Ver Idempotencia
    FormaudioArchivoSíLa grabación completa de la consulta (médico, paciente y acompañantes)
    FormdictationArchivoNoLo que el médico dicta solo, después de que el paciente se fue. Se suma a la misma consulta; si contradice a la conversación, prevalece el dictado
    Formmedical_center_idStringSíEl centro por el que actuás (su id o su slug en Omnimed AI). Tiene que estar habilitado para Consulta en tu credencial
    Formdoctor_idString (hasta 255)SíTu identificador del médico. Ver Médico declarado
    Formdoctor_nameString (hasta 255)NoNombre del médico, como debe figurar en el borrador de orden
    Formdoctor_licenseString (hasta 100)NoMatrícula del médico
    Formdoctor_specialtyString (hasta 255)NoEspecialidad del médico
    FormconsentBooleanSíTiene que ser true. Declara que el paciente aceptó ser grabado
    Formnote_formatStringNoUno de los id de GET /api/consulta/v1/note-formats. Por defecto soap
    Formnum_speakersEntero, 1 a 10NoCantidad esperada de personas que hablan. Si no se envía, se detecta sola
    FormpatientJSONNoVer Datos del paciente
    FormhistoryJSONNoVer Historia clínica

    audio y dictation aceptan m4a, mp3, wav, webm y ogg, hasta 100 MB cada uno. Mandá patient e history como partes JSON del formulario (type=application/json en curl).

    Respuesta 202: consulta aceptada

    {
      "id": "3f6c1d2e-8a4b-4c7e-9f10-2b5d6e7a8c90",
      "status": "processing",
      "stage": "transcribing",
      "medical_center_id": "9b2f4c1a-6d3e-4f5a-8b7c-1d2e3f4a5b6c",
      "doctor": {"id": "med-1042", "name": "Dra. Ana Pérez", "license": "MP 12345", "specialty": "Clínica médica"},
      "patient_ref": "hc-998877",
      "note_format": "soap",
      "created_at": "2026-11-03T13:05:12Z",
      "completed_at": null
    }
    • Guardá el id: es lo que vas a consultar.
    • medical_center_id viene siempre como id del centro, aunque lo hayas enviado por su slug.
    • Un reenvío con una Idempotency-Key ya usada responde 200 con la consulta existente, con la misma forma; si esa consulta había fallado o estaba trabada, se reprocesa y responde 202 con el mismo id (ver Idempotencia).

    Ejemplo completo

    Con dictado posterior, datos del médico, datos del paciente e historia clínica:

    curl -X POST https://api.omniai.com.ar/api/consulta/v1/consultations \
      -H "Authorization: Bearer $OMNI_PARTNER_CREDENTIAL" \
      -H "Idempotency-Key: 6f1c2a4e-0b9d-4d61-a3f2-7c5e8b1d9a20" \
      -F "audio=@consulta.m4a" \
      -F "dictation=@dictado-posterior.m4a" \
      -F "medical_center_id=YOUR_CENTER_ID" \
      -F "doctor_id=med-1042" \
      -F "doctor_name=Dra. Ana Pérez" \
      -F "doctor_license=MP 12345" \
      -F "doctor_specialty=Clínica médica" \
      -F "consent=true" \
      -F "note_format=soap" \
      -F 'patient={"ref": "hc-998877", "age": 38, "sex": "female"};type=application/json' \
      -F "history=@historia.json;type=application/json"

    En Python:

    import json
    import os
    import uuid
    
    import requests
    
    API = "https://api.omniai.com.ar"
    headers = {
        "Authorization": f"Bearer {os.environ['OMNI_PARTNER_CREDENTIAL']}",
        "Idempotency-Key": str(uuid.uuid4()),  # guardala para reintentar con la misma
    }
    
    with open("consulta.m4a", "rb") as audio:
        response = requests.post(
            f"{API}/api/consulta/v1/consultations",
            headers=headers,
            files={
                "audio": ("consulta.m4a", audio, "audio/mp4"),
                "patient": (None, json.dumps({"ref": "hc-998877", "age": 38}), "application/json"),
            },
            data={
                "medical_center_id": "YOUR_CENTER_ID",
                "doctor_id": "med-1042",
                "consent": "true",
            },
            timeout=120,
        )
    
    response.raise_for_status()
    consultation_id = response.json()["id"]

    Obtener el estado y el resultado

    GET https://api.omniai.com.ar/api/consulta/v1/consultations/{consultation_id}
    Authorization: Bearer omp_...

    consultation_id es el id que devolvió el envío.

    Estados

    statusQué significaQué hacer
    processingSe está procesando. stage dice en qué paso va y los campos del resultado vienen vacíosSeguí consultando
    doneTerminó. Trae todo el resultadoMostralo en tu sistema
    failedFalló. error dice por quéReenviá con la misma Idempotency-Key

    Mientras status es processing, stage va pasando por transcribing, identifying_speakers y writing_note; al terminar vale done o failed. La lista de pasos puede crecer: tratá un valor que no conozcas como "procesando".

    Con failed, error trae uno de estos códigos:

    error.codeQué significaerror.message
    audio_emptyLa grabación no tiene voz"La grabación no tiene voz. Revisá que el micrófono no haya estado silenciado."
    processing_failedEl procesamiento falló"No se pudo procesar la consulta; reenviala con la misma Idempotency-Key."
    processing_timeoutEl procesamiento llevaba más de 10 minutos sin avanzar"El procesamiento se interrumpió; reenviala con la misma Idempotency-Key."

    Un código que no conozcas trae el mensaje "No se pudo procesar la consulta.". Compará por error.code; error.message es para mostrarle al médico y puede cambiar.

    {
      "id": "3f6c1d2e-8a4b-4c7e-9f10-2b5d6e7a8c90",
      "status": "failed",
      "stage": "failed",
      "error": {"code": "audio_empty", "message": "La grabación no tiene voz. Revisá que el micrófono no haya estado silenciado."},
      "...": "..."
    }

    En los tres casos podés reenviar con la misma Idempotency-Key: la consulta se procesa de nuevo con el audio de ese envío y responde 202 con el mismo id.

    Cada cuánto consultar

    Recomendamos consultar cada 5 segundos durante el primer minuto y después cada 15 segundos. El tiempo de procesamiento depende de la duración del audio; no hay un tiempo garantizado.

    import time
    
    def wait_for(consultation_id: str) -> dict:
        started = time.monotonic()
        while True:
            response = requests.get(
                f"{API}/api/consulta/v1/consultations/{consultation_id}",
                headers={"Authorization": f"Bearer {os.environ['OMNI_PARTNER_CREDENTIAL']}"},
                timeout=30,
            )
            response.raise_for_status()
            consultation = response.json()
            if consultation["status"] != "processing":
                return consultation  # "done" o "failed"
            time.sleep(5 if time.monotonic() - started < 60 else 15)

    Respuesta 200 con status: "done"

    {
      "id": "3f6c1d2e-8a4b-4c7e-9f10-2b5d6e7a8c90",
      "status": "done",
      "stage": "done",
      "error": null,
      "medical_center_id": "9b2f4c1a-6d3e-4f5a-8b7c-1d2e3f4a5b6c",
      "doctor": {"id": "med-1042", "name": "Dra. Ana Pérez", "license": "MP 12345", "specialty": "Clínica médica"},
      "patient_ref": "hc-998877",
      "note_format": "soap",
      "created_at": "2026-11-03T13:05:12Z",
      "completed_at": "2026-11-03T13:06:40Z",
      "audio_seconds": 312.4,
      "note": {
        "format": "soap",
        "sections": [
          {
            "key": "subjective",
            "title": "Subjetivo",
            "sentences": [
              {"text": "Dolor abdominal de 2 días de evolución, con diarrea, sin vómitos.", "turn_ids": ["t2", "t4"]},
              {"text": "Refiere cefalea leve.", "turn_ids": ["t4"]}
            ]
          },
          {
            "key": "objective",
            "title": "Objetivo",
            "sentences": [{"text": "Dolor que se irradia a fosa ilíaca derecha.", "turn_ids": ["t8"]}]
          },
          {
            "key": "assessment",
            "title": "Evaluación",
            "sentences": [{"text": "Se sospecha cuadro quirúrgico a descartar.", "turn_ids": ["t9"]}]
          },
          {
            "key": "plan",
            "title": "Plan",
            "sentences": [
              {"text": "Laboratorio con enzimas y ecografía abdominal; control con resultados.", "turn_ids": ["t9"]},
              {"text": "Pautas de alarma: fiebre alta, dolor abdominal intenso o abdomen duro.", "turn_ids": ["t11"]}
            ]
          }
        ],
        "history_context": [{"text": "Alergia a penicilina registrada.", "source_ids": ["alg-17"]}],
        "discrepancies": []
      },
      "orders": [
        {
          "id": "o1",
          "kind": "lab",
          "name": "Laboratorio",
          "said_as": "te vamos a sacar sangre, quiero ver si tenés elevadas algunas enzimas",
          "details": "Enzimas",
          "indication": "Descartar cuadro quirúrgico",
          "status": "firm",
          "needs_detail": true,
          "detail_question": "¿Qué determinaciones?",
          "detail_options": ["Hemograma", "Hepatograma", "Amilasa", "Lipasa"],
          "turn_ids": ["t9"],
          "missing": ["detail"],
          "draft": {
            "study": "Laboratorio, Enzimas",
            "presumptive_diagnosis": "Dolor abdominal, se sospecha cuadro quirúrgico",
            "clinical_data": "Dolor abdominal de 2 días con irradiación a fosa ilíaca derecha.",
            "cie10_codes": ["R10.3"]
          }
        },
        {
          "id": "o2",
          "kind": "imaging",
          "name": "Ecografía abdominal",
          "said_as": "vamos a hacer un eco rápido como para descartar que no sea el apéndice",
          "details": "",
          "indication": "Descartar apendicitis",
          "status": "firm",
          "needs_detail": false,
          "detail_question": "",
          "detail_options": [],
          "turn_ids": ["t9"],
          "missing": [],
          "draft": {
            "study": "Ecografía abdominal",
            "presumptive_diagnosis": "Dolor abdominal, se sospecha cuadro quirúrgico",
            "clinical_data": "Dolor abdominal de 2 días con irradiación a fosa ilíaca derecha.",
            "cie10_codes": ["R10.3"]
          }
        }
      ],
      "suggestions": [
        {
          "id": "s1",
          "kind": "diagnosis_to_consider",
          "severity": "info",
          "title": "Gastroenteritis aguda",
          "body": "",
          "question": "",
          "order_id": null,
          "proposed_actions": [
            {"key": "add_to_assessment", "label": "Agregar a la evaluación"},
            {"key": "dismiss", "label": "Descartar"}
          ],
          "reasons_for": [{"text": "Diarrea de 2 días", "turn_ids": ["t4"]}],
          "reasons_against": [{"text": "Dolor que se irradia a fosa ilíaca derecha", "turn_ids": ["t8"]}],
          "sources": {"turn_ids": ["t4", "t8"], "source_ids": [], "guideline": null},
          "ai_generated": true
        }
      ],
      "diagnosis_codes": [{"code": "R10.3", "description": "Dolor localizado en otras partes inferiores del abdomen"}],
      "transcript": {
        "turns": [
          {"id": "t1", "speaker_id": "speaker_0", "role": "doctor", "segment": "conversation", "text": "Hola, ¿cómo andás?", "start": 0.4, "end": 1.6},
          {"id": "t2", "speaker_id": "speaker_1", "role": "patient", "segment": "conversation", "text": "Vengo porque tengo dolor de panza hace dos días.", "start": 1.8, "end": 5.2}
        ]
      },
      "patient_instructions": null
    }

    La transcripción del ejemplo está recortada a sus dos primeros turnos. Cada campo se explica en El resultado, campo por campo.

    Generar indicaciones para el paciente

    POST https://api.omniai.com.ar/api/consulta/v1/consultations/{consultation_id}/instructions
    Authorization: Bearer omp_...

    Sin cuerpo. Reescribe el Plan de la nota en lenguaje simple para que el paciente se lo lleve (por ejemplo, por WhatsApp o impreso). No agrega nada que no esté en la nota.

    • Solo con una consulta en done; antes responde 409 con consultation_not_done.
    • Si la nota no tiene Plan del que partir, responde 422 con plan_empty.
    • Llamarlo de nuevo genera una versión nueva, que reemplaza a la anterior en patient_instructions de la consulta.
    • No cuenta como consulta.

    Respuesta 200

    {
      "patient_instructions": "Vas a hacerte un análisis de sangre y una ecografía de abdomen. Cuando tengas los resultados, volvé a la consulta.\nConsultá a la guardia si tenés fiebre alta, dolor fuerte en la panza o si la panza se pone dura de golpe.\n"
    }

    patient_instructions es texto plano, en segunda persona, para el paciente.

    Ejemplo

    curl -X POST https://api.omniai.com.ar/api/consulta/v1/consultations/3f6c1d2e-8a4b-4c7e-9f10-2b5d6e7a8c90/instructions \
      -H "Authorization: Bearer $OMNI_PARTNER_CREDENTIAL"

    Listar los formatos de nota

    GET https://api.omniai.com.ar/api/consulta/v1/note-formats
    Authorization: Bearer omp_...

    Los formatos en los que se puede escribir la nota, con sus secciones en orden. El id de cada formato es el valor de note_format al enviar la consulta, y las key de sus secciones son las de note.sections en el resultado.

    Respuesta 200

    {
      "formats": [
        {
          "id": "soap",
          "name": "SOAP",
          "sections": [
            {"key": "subjective", "title": "Subjetivo"},
            {"key": "objective", "title": "Objetivo"},
            {"key": "assessment", "title": "Evaluación"},
            {"key": "plan", "title": "Plan"}
          ]
        },
        {
          "id": "pediatric_checkup",
          "name": "Control pediátrico",
          "sections": [
            {"key": "reason", "title": "Motivo de consulta"},
            {"key": "growth", "title": "Crecimiento y desarrollo"},
            {"key": "feeding", "title": "Alimentación y hábitos"},
            {"key": "vaccines", "title": "Vacunas"},
            {"key": "exam", "title": "Examen físico"},
            {"key": "plan", "title": "Indicaciones"}
          ]
        }
      ]
    }

    Idempotencia

    Una red que se corta a mitad de un envío de 15 MB deja la duda de si la consulta llegó. Con Idempotency-Key podés reenviar sin crear otra consulta ni pagarla dos veces.

    • Generá un identificador único por consulta (recomendado un UUID, hasta 255 caracteres) y guardalo junto a la consulta en tu sistema antes de enviar.
    • Reenvío con la misma key:
      • si la primera sigue en proceso o terminó, responde 200 con esa misma consulta;
      • si falló, o lleva más de 10 minutos sin avanzar, se procesa de nuevo con el audio de este envío y responde 202 con el mismo id: seguí consultando ese id.
    • Fallida o trabada: para reintentar una consulta en failed (o una que no avanza), reenviala con la misma key.
    • Keys por credencial: cada credencial tiene sus propias keys. Una key que ya se usó para otro centro responde 409 con idempotency_key_conflict.
    • Sin key, cada envío crea una consulta nueva.
    • Una key de más de 255 caracteres responde 422 con invalid_idempotency_key.

    El resultado, campo por campo

    Consulta

    CampoTipoDescripción
    idUUIDIdentificador de la consulta
    statusprocessing, done, failedEstado. Ver Estados
    stageStringPaso actual: transcribing, identifying_speakers, writing_note, done, failed. Puede crecer; tratá un valor desconocido como "procesando"
    errorObjeto o nullSolo con failed: {"code", "message"}
    medical_center_idStringEl id del centro, aunque lo hayas enviado por su slug
    doctorObjetoEl médico declarado: id, name, license, specialty (estos tres pueden ser null)
    patient_refString o nullEl ref que mandaste en patient
    note_formatStringEl formato de la nota
    created_atFecha y horaCuándo se recibió
    completed_atFecha y hora o nullCuándo terminó
    audio_secondsNúmero o nullDuración del audio procesado
    noteObjeto o nullLa nota
    ordersListaLos pedidos
    suggestionsListaLas sugerencias
    diagnosis_codesListaLos códigos CIE-10
    transcriptObjeto o nullLa transcripción
    patient_instructionsString o nullLas últimas indicaciones generadas con POST .../instructions, si las hay

    Parseá la respuesta de forma tolerante: pueden sumarse campos nuevos.

    Nota (note)

    CampoDescripción
    formatEl formato de la nota (soap, pediatric_checkup, ...)
    sectionsLas secciones del formato, en orden. Cada una tiene key, title y sentences
    sections[].sentences[]Cada oración: text y turn_ids, los turnos de transcript.turns de los que sale. Cada oración sale de lo dicho hoy
    history_contextAntecedentes de la historia enviada que son relevantes hoy: text y source_ids, los id de tus registros en history. Vacío sin historia
    discrepanciesLo dicho hoy que contradice la historia enviada: text, turn_ids (el turno) y source_ids (el registro). Nunca se resuelve sola: la resuelve el médico

    history_context y discrepancies van aparte de sections: la historia nunca entra en la nota como si se hubiera dicho hoy.

    Pedidos (orders)

    Un pedido es algo que indicó el médico, citado a sus propias palabras. Lo que pide el paciente sin que el médico lo indique no es un pedido.

    CampoDescripción
    idIdentificador del pedido dentro de la consulta (lo cita suggestions[].order_id)
    kindlab (laboratorio), imaging (imágenes), other_study (otro estudio), referral (derivación), therapy (terapia), procedure (procedimiento)
    nameNombre clínico, como en una orden (ej. "Ecografía abdominal")
    said_asLas palabras textuales del médico
    detailsLateralidad, región, contraste, determinaciones, sesiones; solo si se dijeron
    indicationEl motivo del pedido, solo si el médico lo dijo
    statusfirm: lo pide ahora. conditional: depende de algo futuro ("si sale positivo..."). in_visit: se hace durante la consulta
    needs_detailtrue si falta algo imprescindible para emitir la orden
    detail_questionQué falta, como pregunta corta para el médico (ej. "¿Qué determinaciones?")
    detail_optionsEntre 2 y 4 respuestas posibles a detail_question, para mostrar como opciones
    turn_idsLos turnos donde el médico lo pidió
    missingLo que falta para que el borrador de orden esté completo: detail, diagnosis, clinical_data, doctor_license
    draftEl borrador de orden. Solo para pedidos firm; si no, null

    Borrador de orden (draft)

    Lo que una plataforma de órdenes necesita, armado a partir del pedido y la nota. Nunca es una orden válida por sí misma: el médico la completa y la firma en tu sistema, y los datos del paciente (nombre, documento, cobertura) los agrega tu sistema.

    CampoDescripción
    studyEstudio con sus detalles
    presumptive_diagnosisDiagnóstico presuntivo, de la evaluación de la nota; null si falta
    clinical_dataDatos clínicos que justifican el pedido; null si faltan
    cie10_codesHasta 2 códigos CIE-10

    Sugerencias (suggestions)

    Una sugerencia es una propuesta para el médico sobre la consulta de hoy. No cambia nada por sí sola: el médico decide en tu sistema. Cada una explica por qué y cita sus fuentes.

    kindQué propone
    diagnosis_to_considerUn diagnóstico para considerar, con razones a favor y en contra
    safetyUn pedido contraindicado para este paciente (ej. contraste tras una reacción previa)
    duplicateEl mismo estudio ya figura en la historia enviada
    low_valueUna guía desaconseja ese pedido en esta situación
    missing_checkUn chequeo de una guía clínica que no aparece en la consulta
    unrecorded_problemAlgo dicho hoy que no figura como problema en la historia
    pending_follow_upUn control que la historia deja pendiente

    La lista de tipos puede crecer: ignorá los que no conozcas.

    CampoDescripción
    idIdentificador de la sugerencia
    kindEl tipo, de la lista de arriba
    severityred: revisar antes de cerrar la consulta. amber: conviene revisar. info: informativa
    titleTítulo corto
    bodyExplicación
    questionPregunta para el médico, si la sugerencia la tiene
    order_idEl pedido al que se refiere, si se refiere a uno; si no, null
    proposed_actionsAcciones que la sugerencia propone (key, label), para que las muestres. Aplicarlas es decisión del médico en tu sistema
    reasons_forSolo en diagnosis_to_consider: razones a favor, cada una con text y turn_ids
    reasons_againstSolo en diagnosis_to_consider: razones en contra, con la misma forma
    sourcesSus fuentes: turn_ids (turnos de la transcripción), source_ids (tus registros en history) y guideline (title, url, version de la guía clínica, o null)
    ai_generatedtrue cuando la sugerencia la redacta un modelo de IA (hoy, los diagnósticos para considerar); false cuando sale de una regla clínica explícita

    Códigos CIE-10 (diagnosis_codes)

    Hasta 4 códigos CIE-10 propuestos para la evaluación, cada uno con code (normalizado, ej. "K52.9") y description.

    Transcripción (transcript)

    transcript.turns es la conversación, turno por turno:

    CampoDescripción
    idLo que citan los turn_ids de la nota, los pedidos y las sugerencias
    speaker_idIdentificador del hablante dentro de la consulta (ej. speaker_0)
    roledoctor, patient, companion (quien acompaña al paciente; lo que dice entra en la nota como referido por un familiar) o unknown (no se pudo asignar)
    segmentconversation, o dictation para el dictado posterior del médico
    textLo dicho
    start, endSegundos desde el inicio de su audio, o null

    Cómo seguir una cita

    • turn_ids apunta a transcript.turns[].id: con su role sabés quién lo dijo y con start y end, en qué momento.
    • source_ids apunta a los id que vos pusiste en history: enlazá el antecedente o la discrepancia a tu propio registro.
    • order_id apunta a orders[].id.

    Errores

    Todo error responde con un status no-2xx y este cuerpo:

    {
      "detail": {
        "code": "consent_required",
        "message": "Falta el consentimiento del paciente (consent=true)."
      }
    }

    detail.code es estable y es lo que tu código debe comparar; detail.message es para personas y puede cambiar.

    Credencial y disponibilidad (todos los endpoints)

    Statusdetail.codeQué significaQué hacer
    401unauthenticatedFalta Authorization: Bearer omp_...Mandá la credencial desde tu servidor
    401invalid_partner_credentialCredencial desconocida (mal copiada o rotada)Revisá el valor; si se rotó, usá la nueva
    401partner_credential_revokedCredencial revocadaPedinos una credencial nueva
    503partner_lookup_unavailableNo pudimos verificar la credencial en este momento. No es un rechazoReintentá con espera creciente
    503llm_unavailableEl modelo de IA no está disponible por un momentoReintentá con espera creciente

    Los 401 traen además el header WWW-Authenticate.

    POST /api/consulta/v1/consultations

    Statusdetail.codeQué significa
    400invalid_audioEl audio falta, pesa menos de 1 KB, su formato no está soportado o su extensión no coincide con el contenido
    403center_not_enabledTu credencial no está habilitada para ese centro
    403product_not_enabledEl centro no tiene Consulta habilitada para tu credencial
    409idempotency_key_conflictLa Idempotency-Key ya se usó para otro centro
    413—El archivo supera los 100 MB. Esta respuesta puede llegar sin cuerpo JSON: ramificá por el status
    422medical_center_id_requiredFalta medical_center_id
    422consent_requiredFalta consent=true
    422doctor_id_requiredFalta doctor_id
    422invalid_doctorUn campo del médico supera su largo máximo
    422unknown_note_formatnote_format no es uno de los de GET /api/consulta/v1/note-formats
    422invalid_patientpatient no es JSON, no es un objeto o supera 1 MB (un campo con un valor inválido no da error: se descarta)
    422invalid_historyhistory no es JSON, supera 1 MB o un registro no respeta su formato (campos requeridos, fechas, valores permitidos o ids repetidos en una sección)
    422invalid_num_speakersnum_speakers no es un entero entre 1 y 10
    422invalid_idempotency_keyLa Idempotency-Key supera 255 caracteres

    Los 403 son configuración de nuestro lado: escribinos. Los 400, 409, 413 y 422 no se reintentan sin corregir la llamada.

    GET /api/consulta/v1/consultations/{consultation_id}

    Statusdetail.codeQué significa
    404consultation_not_foundLa consulta no existe, no es de tu credencial o su centro ya no tiene Consulta habilitada para tu credencial

    Una consulta que falló no es un error HTTP: responde 200 con status: "failed" y el motivo en error (ver Estados).

    POST /api/consulta/v1/consultations/{consultation_id}/instructions

    Statusdetail.codeQué significaQué hacer
    404consultation_not_foundIgual que en el GET
    409consultation_not_doneLa consulta todavía no terminó de procesarse o fallóEsperá a done
    422plan_emptyLa nota no tiene Plan del que partirNo hay indicaciones que generar
    502generation_failedNo se pudieron generarReintentá

    GET /api/consulta/v1/note-formats

    Solo los errores de credencial y disponibilidad.

    Límites

    LímiteValor
    Audio (audio, dictation)Hasta 100 MB por archivo; mínimo 1 KB. Formatos m4a, mp3, wav, webm y ogg
    Historia clínica (history)Hasta 1 MB de JSON; los textos de más de 8.000 caracteres se recortan
    Datos del paciente (patient)Hasta 1 MB de JSON
    Idempotency-Key255 caracteres
    doctor_id, doctor_name, doctor_specialty255 caracteres
    doctor_license100 caracteres
    num_speakers1 a 10
    • Una consulta de 15 minutos en m4a pesa alrededor de 15 MB.
    • El tiempo de procesamiento depende de la duración del audio; no hay un tiempo garantizado. Consultá cada 5 segundos durante el primer minuto y después cada 15.
    • Reintentá los 5xx con espera creciente; los 4xx no se reintentan sin corregir la llamada.

    Datos y seguridad

    • Todo el tráfico va cifrado (TLS).
    • Nuestros servidores y bases de datos están alojados en la Unión Europea.
    • El audio se procesa y no se conserva salvo que el contrato del centro lo pida.
    • Qué más se conserva (transcripción, nota, resultado) y por cuánto tiempo se define en el contrato con cada centro.
    • La lista de subprocesadores y sus condiciones se entrega bajo acuerdo de confidencialidad.

    Facturación

    Cada consulta procesada con éxito cuenta como una consulta. No cuentan:

    • reenviar con la misma Idempotency-Key;
    • una consulta que falla por un problema de nuestro lado;
    • generar indicaciones para el paciente.

    Changelog

    v1 (2026-10-30)

    • Primera versión: POST /api/consulta/v1/consultations, GET /api/consulta/v1/consultations/{consultation_id}, POST .../instructions y GET /api/consulta/v1/note-formats.
    • Partner Credential (omp_...) habilitada por centro y producto, con el médico declarado en cada consulta.
    • Historia clínica opcional con citas a tus propios ids (source_ids) y discrepancias.
    • Idempotencia con Idempotency-Key.

    Soporte

    Consultas, Partner Credentials y alta de centros: santiago@omniai.com.ar