{"openapi":"3.1.0","info":{"title":"Files Extraction API","version":"1.6.0","description":"Extract structured data from documents, audio recordings and raw text. Ships with templates for Argentine fiscal documents (Factura A/B/C, Nota de Crédito, Nota de Débito) and lets you define your own — or skip the template entirely and let the model discover the structure (freeform mode).\n\n**Sources:** `POST /api/v1/extract` (PDF/images), `POST /api/v1/audio` (recordings — always returns transcript + summary + metadata), `POST /api/v1/text` (raw text — always returns summary + metadata).\n\n**Freeform mode:** omit `template` on any extraction endpoint and the model extracts every structured datum it finds, choosing its own keys, plus a `suggestedTemplate` you can save via `POST /api/v1/templates` and reuse for stable production extraction. Freeform output keys are model-chosen and may vary between different documents — for pinned output shapes, graduate to a template.\n\n**Authentication:** every endpoint requires an `Authorization: Bearer fx_live_…` header. Create keys from the `/api-keys` dashboard.\n\n**Sync vs async:** extraction endpoints run synchronously by default (they block on the LLM). Opt into async via `?async=true`, `X-Async: true`, or `async: true` in the JSON body: you'll get back `202` with a `jobId`; poll `GET /api/v1/jobs/{id}` until `status` flips to `success` or `failed`. Long audio (>20 min) is forced onto the async path automatically — the 202 carries `forcedAsync: true`.\n\n**Idempotency:** mutating endpoints (POST/PUT/DELETE) honour an `Idempotency-Key: <opaque-token>` header. Retrying with the same key replays the original response. Same key + different body → 422.\n\n**Dedupe:** re-sending the same content (SHA-256) with the same template (or both freeform) under the same API key within the last 30 days returns the cached result — the LLM is not called again.\n\n**Rate limiting:** each API key has a 60 request/minute token bucket. Responses include `X-RateLimit-{Limit,Remaining,Reset}`; 429s include `Retry-After`.\n\n**CORS:** all endpoints accept cross-origin requests."},"servers":[{"url":"https://archivos.tandemdgt.com"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Extracción","description":"Procesar un archivo y obtener datos estructurados."},{"name":"Plantillas","description":"Administrar los esquemas de extracción."}],"paths":{"/api/v1/extract":{"post":{"tags":["Extracción"],"summary":"Extraer datos de un archivo","description":"Acepta un PDF o imagen y devuelve los campos estructurados según la plantilla elegida. Dos modos de entrada: multipart/form-data (recomendado para clientes que ya tienen el archivo en disco) o JSON con el archivo en base64.\n\n**Sin plantilla (freeform):** si omitís `template`, el modelo descubre la estructura solo — devuelve `data` con las keys que eligió, un `summary` del documento y un `suggestedTemplate` listo para guardar vía `POST /api/v1/templates` y reusar. Ideal para explorar un tipo de documento nuevo; para producción, graduá a una plantilla (las keys del freeform pueden variar entre corridas).\n\n**Una o varias plantillas:** `template` acepta un slug o varios (máx 5). Con varios, el modelo (o el QR de AFIP, de forma determinística) elige la ÚNICA que mejor coincide y devuelve sólo esa — nunca una mezcla. En multipart, repetí el campo `template` o usá una lista separada por comas; en JSON, pasá un array. La respuesta incluye `matchedTemplate` y, en `meta`, `candidates` + `matchReason`.\n\nSi el mismo archivo (mismo SHA-256) ya fue extraído con la misma plantilla (o ambos freeform) con esta API key en los últimos 30 días, la respuesta se sirve del caché de dedupe — `meta.deduped: true`.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Token opaco para reintentar de forma segura. La primera respuesta se cachea por 24h."},{"name":"X-Async","in":"header","required":false,"schema":{"type":"string","enum":["true"]},"description":"Si se manda con valor `true`, la extracción corre en segundo plano: el endpoint devuelve 202 con `jobId` y la respuesta final se consulta vía `GET /api/v1/jobs/{id}`. Alternativas: `?async=true` en la URL o `\"async\": true` en el body JSON."},{"name":"async","in":"query","required":false,"schema":{"type":"boolean"},"description":"Alternativa al header `X-Async`."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"template":{"type":"string","description":"Slug de la plantilla. Para varias candidatas, repetí el campo o usá una lista separada por comas (máx 5). Omitilo para modo freeform.","example":"accounting-factura"},"file":{"type":"string","format":"binary","description":"Archivo a procesar."}}}},"application/json":{"schema":{"type":"object","required":["fileName","mimeType","fileBase64"],"properties":{"template":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"},"maxItems":5}],"description":"Un slug, o un array de hasta 5 candidatos. Omitilo para modo freeform.","example":"accounting-factura"},"fileName":{"type":"string","example":"factura-001.pdf"},"mimeType":{"type":"string","example":"application/pdf"},"fileBase64":{"type":"string","description":"Contenido del archivo en base64 (con o sin prefijo `data:...,`)."}}}}}},"responses":{"200":{"description":"Extracción exitosa (modo sincrónico).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractionResponse"}}}},"202":{"description":"Extracción aceptada (modo asincrónico). El cliente debe hacer polling a `statusUrl` hasta que `status` sea `success` o `failed`.","headers":{"Cache-Control":{"schema":{"type":"string"},"description":"`no-store` para evitar cachés intermedios."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAccepted"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"415":{"$ref":"#/components/responses/UnsupportedMedia"},"422":{"$ref":"#/components/responses/IdempotencyMismatch"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/audio":{"post":{"tags":["Extracción"],"summary":"Transcribir y extraer datos de un audio","description":"Acepta una grabación de audio (mp3, m4a, wav, webm, ogg, flac, aac, aiff — máx 100MB) y SIEMPRE devuelve `transcript` (transcripción completa), `summary` (resumen) y `metadata` (`durationSecs`, `language`, `speakers`) — el audit trail de todo lo que caiga en `data`.\n\n**Con `template`:** `data` sigue los campos de la plantilla, extraídos de lo que el audio efectivamente dice (una sola pasada del modelo: escucha el audio directo, sin cadena transcribir-después-extraer).\n\n**Sin `template` (freeform):** el modelo elige las keys de `data` y devuelve además un `suggestedTemplate` reutilizable.\n\n**Audios largos:** grabaciones de más de 20 minutos (o >25MB sin duración legible) corren async automáticamente — recibís `202` con `forcedAsync: true` y hacés polling en `GET /api/v1/jobs/{id}`.\n\nDedupe: mismo audio + misma plantilla (o ambos freeform) en los últimos 30 días se sirve del caché sin llamar al modelo.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Token opaco para reintentar de forma segura. La primera respuesta se cachea por 24h."},{"name":"X-Async","in":"header","required":false,"schema":{"type":"string","enum":["true"]},"description":"Corre en segundo plano: 202 con `jobId`, polling vía `GET /api/v1/jobs/{id}`. Alternativas: `?async=true` o `\"async\": true` en el body JSON."},{"name":"async","in":"query","required":false,"schema":{"type":"boolean"},"description":"Alternativa al header `X-Async`."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"template":{"type":"string","description":"Slug de la plantilla (una sola — /audio no soporta multi-match). Omitilo para modo freeform.","example":"resumen-consulta"},"file":{"type":"string","format":"binary","description":"Audio a procesar (máx 100MB)."}}}},"application/json":{"schema":{"type":"object","required":["fileBase64","mimeType"],"properties":{"template":{"type":"string","description":"Slug de la plantilla. Omitilo para modo freeform."},"fileName":{"type":"string","example":"reunion-2026-07-28.mp3"},"mimeType":{"type":"string","example":"audio/mpeg"},"fileBase64":{"type":"string","description":"Audio en base64 (con o sin prefijo `data:...,`)."},"async":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Extracción exitosa (modo sincrónico).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioExtractionResponse"}}}},"202":{"description":"Extracción aceptada (async — pedido o forzado por duración). Polling en `statusUrl`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAccepted"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"description":"Archivo demasiado grande (máx 100MB).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"$ref":"#/components/responses/UnsupportedMedia"},"422":{"$ref":"#/components/responses/IdempotencyMismatch"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/text":{"post":{"tags":["Extracción"],"summary":"Extraer datos de texto plano","description":"Acepta texto crudo (hasta 500.000 caracteres) y SIEMPRE devuelve `summary` y `metadata` (`language`, `textLength`).\n\n**Con `template`:** `data` sigue los campos de la plantilla.\n\n**Sin `template` (freeform):** el modelo elige las keys de `data` y devuelve además un `suggestedTemplate` reutilizable.\n\nDedupe: mismo texto + misma plantilla (o ambos freeform) en los últimos 30 días se sirve del caché sin llamar al modelo.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Token opaco para reintentar de forma segura. La primera respuesta se cachea por 24h."},{"name":"X-Async","in":"header","required":false,"schema":{"type":"string","enum":["true"]},"description":"Corre en segundo plano: 202 con `jobId`."},{"name":"async","in":"query","required":false,"schema":{"type":"boolean"},"description":"Alternativa al header `X-Async`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":500000,"description":"El texto a analizar."},"template":{"type":"string","description":"Slug de la plantilla. Omitilo para modo freeform."},"fileName":{"type":"string","description":"Etiqueta opcional; se muestra en el dashboard.","example":"notas-reunion.txt"},"async":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Extracción exitosa (modo sincrónico).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TextExtractionResponse"}}}},"202":{"description":"Extracción aceptada (modo asincrónico). Polling en `statusUrl`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAccepted"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"description":"Texto demasiado largo (máx 500.000 caracteres).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"$ref":"#/components/responses/UnsupportedMedia"},"422":{"$ref":"#/components/responses/IdempotencyMismatch"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/extractions/batch":{"post":{"tags":["Extracción"],"summary":"Subir múltiples archivos a la vez","description":"Hasta 50 archivos en una sola llamada — documentos (PDF/imágenes), audios y .txt pueden mezclarse en el mismo batch; cada archivo se enruta a su pipeline. Siempre async: la respuesta es 202 con un array de `jobId`s; el cliente hace polling de cada uno por separado en `/api/v1/jobs/{id}`.\n\n**`template` es opcional**: omitilo para modo freeform (el modelo descubre la estructura y sugiere una plantilla por archivo). Con plantilla, los audios/.txt del batch usan extracción de plantilla única (sin multi-match).\n\nDedupe corre por archivo: subir el mismo contenido dos veces dentro del batch (o contra el historial reciente) devuelve resultados cacheados sin gastar tokens.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Cubre todo el batch — reintentos con la misma clave replican la respuesta original."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"template":{"type":"string","example":"accounting-factura","description":"Opcional — omitilo para modo freeform."},"file":{"type":"array","description":"Repetí el campo `file` por cada archivo (curl: `-F file=@a.pdf -F file=@b.pdf`).","items":{"type":"string","format":"binary"}}}}},"application/json":{"schema":{"type":"object","required":["files"],"properties":{"template":{"type":"string","example":"accounting-factura","description":"Opcional — omitilo para modo freeform."},"files":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"object","required":["fileName","mimeType","fileBase64"],"properties":{"fileName":{"type":"string"},"mimeType":{"type":"string"},"fileBase64":{"type":"string"}}}}}}}}},"responses":{"202":{"description":"Batch aceptado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchAccepted"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"415":{"$ref":"#/components/responses/UnsupportedMedia"},"422":{"$ref":"#/components/responses/IdempotencyMismatch"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/extractions/{id}":{"delete":{"tags":["Extracción"],"summary":"Eliminar una extracción","description":"Borra permanentemente la extracción: el registro y el archivo original en storage. Sólo alcanza extracciones creadas con la misma API key (las ajenas devuelven 404). No se puede deshacer.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255}}],"responses":{"200":{"description":"Extracción eliminada.","content":{"application/json":{"schema":{"type":"object","required":["id","deleted"],"properties":{"id":{"type":"string","format":"uuid"},"deleted":{"type":"boolean","enum":[true]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/jobs/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"ID del job devuelto por `POST /api/v1/extract` en modo async."}],"get":{"tags":["Extracción"],"summary":"Poll del estado de un job","description":"Devuelve el estado actual de la extracción. Mientras está en proceso devuelve `202` con `status: \"pending\"`; cuando termina devuelve `200` con el resultado completo o un `error` si falló.\n\nEl job sólo es accesible para la API key que lo creó (otras keys reciben 404).","responses":{"200":{"description":"Job terminado (éxito o fallo).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractionResponse"}}}},"202":{"description":"Job todavía en curso.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAccepted"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/templates":{"get":{"tags":["Plantillas"],"summary":"Listar plantillas","description":"Devuelve plantillas personalizadas primero, luego las incluidas por el sistema.","responses":{"200":{"description":"Lista de plantillas.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TemplateSummary"}}}}}}}}},"post":{"tags":["Plantillas"],"summary":"Crear una plantilla","description":"Crea una plantilla personalizada. El slug no puede coincidir con una plantilla incluida ni con otra existente.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateUpsert"}}}},"responses":{"201":{"description":"Plantilla creada.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Template"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/v1/templates/{slug}":{"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Identificador de la plantilla.","example":"accounting-factura"}],"get":{"tags":["Plantillas"],"summary":"Obtener una plantilla","responses":{"200":{"description":"Plantilla encontrada.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Template"}}}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"tags":["Plantillas"],"summary":"Actualizar una plantilla","description":"Sólo plantillas personalizadas. Si se cambia el slug, no puede colisionar.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateUpsert"}}}},"responses":{"200":{"description":"Plantilla actualizada.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Template"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Plantillas"],"summary":"Eliminar una plantilla","description":"Sólo plantillas personalizadas. Las extracciones que la usaron no se eliminan.","responses":{"200":{"description":"Plantilla eliminada.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"slug":{"type":"string"}}}}}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"fx_live_…","description":"API key in the `Authorization: Bearer …` header."}},"schemas":{"TemplateField":{"type":"object","required":["key","label","type","description"],"properties":{"key":{"type":"string","description":"Identificador del campo en el JSON de salida.","pattern":"^[a-zA-Z_][a-zA-Z0-9_]*$","example":"emisorCuit"},"label":{"type":"string","example":"CUIT del emisor"},"type":{"type":"string","enum":["string","number","date","boolean","enum","array"]},"description":{"type":"string","description":"Instrucción para el modelo: dónde encontrar este dato, qué formato esperar."},"required":{"type":"boolean"},"options":{"type":"array","items":{"type":"string"},"description":"Valores permitidos cuando `type=enum`."},"items":{"type":"array","description":"Sub-campos cuando `type=array`. Soporta un nivel de anidación.","items":{"$ref":"#/components/schemas/TemplateField"}}}},"TemplateUpsert":{"type":"object","required":["slug","name","prompt","fields"],"properties":{"slug":{"type":"string","description":"Identificador. Sólo minúsculas, números y guiones medios.","pattern":"^[a-z0-9][a-z0-9-]*[a-z0-9]$","minLength":2,"maxLength":64,"example":"recibo-x"},"name":{"type":"string","maxLength":120,"example":"Recibo X"},"description":{"type":"string","maxLength":500},"prompt":{"type":"string","description":"Instrucciones extra que recibe el modelo además del esquema."},"fields":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/TemplateField"}}}},"Template":{"allOf":[{"$ref":"#/components/schemas/TemplateUpsert"},{"type":"object","properties":{"builtin":{"type":"boolean","description":"Si es una plantilla incluida (read-only)."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}]},"TemplateSummary":{"type":"object","required":["slug","name","fieldCount","builtin"],"properties":{"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"fieldCount":{"type":"integer","minimum":0},"builtin":{"type":"boolean"},"updatedAt":{"type":"string","format":"date-time","nullable":true}}},"BatchAccepted":{"type":"object","required":["count","template","jobs"],"properties":{"count":{"type":"integer","description":"Cantidad de archivos en el batch."},"template":{"type":"string","nullable":true,"example":"accounting-factura","description":"`null` en modo freeform."},"mode":{"type":"string","enum":["template","freeform"]},"jobs":{"type":"array","items":{"type":"object","required":["id","jobId","fileName","status"],"properties":{"id":{"type":"string","format":"uuid"},"jobId":{"type":"string","format":"uuid"},"fileName":{"type":"string"},"status":{"type":"string","enum":["pending","success","failed"],"description":"`pending` → el archivo se está procesando; hacé polling. `success` (con `deduped: true`) → resultado replicado del caché. `failed` → archivo rechazado (MIME, etc.)."},"statusUrl":{"type":"string","description":"Sólo en `pending`. URL para hacer polling."},"deduped":{"type":"boolean","description":"true cuando este resultado vino del caché de dedupe."},"dedupedFromId":{"type":"string","format":"uuid","description":"ID de la extracción original cuando `deduped: true`."},"error":{"type":"string","description":"Mensaje de error cuando `status: \"failed\"`."}}}}}},"JobAccepted":{"type":"object","required":["id","jobId","status","statusUrl"],"properties":{"id":{"type":"string","format":"uuid"},"jobId":{"type":"string","format":"uuid","description":"Alias de `id` — usalo con `GET /api/v1/jobs/{jobId}`."},"template":{"type":"string","nullable":true,"example":"accounting-factura","description":"`null` en modo freeform."},"mode":{"type":"string","enum":["template","freeform"]},"status":{"type":"string","enum":["pending"]},"statusUrl":{"type":"string","example":"/api/v1/jobs/f2e4b1aa-...","description":"URL relativa para hacer polling del estado."},"forcedAsync":{"type":"boolean","description":"Sólo /audio: `true` cuando la grabación superó el límite sync y se forzó el modo asincrónico."},"reason":{"type":"string","description":"Explicación humana cuando `forcedAsync: true`."}}},"SourceMetadata":{"type":"object","description":"Propiedades de la FUENTE (a diferencia de `meta`, que describe la corrida).","properties":{"durationSecs":{"type":"integer","nullable":true,"description":"Duración del audio en segundos (parseada del contenedor). Sólo audio."},"language":{"type":"string","nullable":true,"description":"Idioma principal detectado (ISO 639-1: 'es', 'en', …)."},"speakers":{"type":"integer","nullable":true,"description":"Cantidad de hablantes distintos detectados. Sólo audio, best-effort."},"textLength":{"type":"integer","nullable":true,"description":"Longitud del texto de entrada en caracteres. Sólo /text."}}},"SuggestedTemplate":{"type":"object","description":"Plantilla inferida por el modelo en modo freeform — lista para guardar vía `POST /api/v1/templates` (mismo shape que `TemplateUpsert`).","required":["slug","name","prompt","fields"],"properties":{"slug":{"type":"string","example":"orden-medica"},"name":{"type":"string","example":"Orden médica"},"description":{"type":"string"},"prompt":{"type":"string"},"fields":{"type":"array","items":{"$ref":"#/components/schemas/TemplateField"}}}},"AudioExtractionResponse":{"type":"object","required":["id","mode","status","transcript","summary","metadata","data"],"properties":{"id":{"type":"string","format":"uuid"},"template":{"type":"string","nullable":true,"description":"`null` en modo freeform."},"mode":{"type":"string","enum":["template","freeform"]},"status":{"type":"string","enum":["success","failed"]},"transcript":{"type":"string","description":"Transcripción completa del audio, con hablantes prefijados ('Hablante 1:', …) cuando hay varios."},"summary":{"type":"string","description":"Resumen del contenido (3-6 oraciones)."},"metadata":{"$ref":"#/components/schemas/SourceMetadata"},"data":{"type":"object","additionalProperties":true,"description":"Campos extraídos: los de la plantilla (modo template) o keys elegidas por el modelo (freeform)."},"suggestedTemplate":{"$ref":"#/components/schemas/SuggestedTemplate"},"warnings":{"type":"array","items":{"type":"string"}},"error":{"type":"string","description":"Sólo cuando `status=failed`."},"meta":{"type":"object","description":"Datos de la corrida: modelo, latencia, tokens, costo, dedupe, confianza.","additionalProperties":true}}},"TextExtractionResponse":{"type":"object","required":["id","mode","status","summary","metadata","data"],"properties":{"id":{"type":"string","format":"uuid"},"template":{"type":"string","nullable":true,"description":"`null` en modo freeform."},"mode":{"type":"string","enum":["template","freeform"]},"status":{"type":"string","enum":["success","failed"]},"summary":{"type":"string","description":"Resumen del contenido (3-6 oraciones)."},"metadata":{"$ref":"#/components/schemas/SourceMetadata"},"data":{"type":"object","additionalProperties":true,"description":"Campos extraídos: los de la plantilla (modo template) o keys elegidas por el modelo (freeform)."},"suggestedTemplate":{"$ref":"#/components/schemas/SuggestedTemplate"},"warnings":{"type":"array","items":{"type":"string"}},"error":{"type":"string","description":"Sólo cuando `status=failed`."},"meta":{"type":"object","description":"Datos de la corrida: modelo, latencia, tokens, costo, dedupe, confianza.","additionalProperties":true}}},"ExtractionResponse":{"type":"object","required":["id","template","status"],"properties":{"id":{"type":"string","format":"uuid"},"template":{"type":"string","nullable":true,"example":"accounting-factura","description":"`null` en modo freeform."},"matchedTemplate":{"type":"string","description":"El slug de la plantilla que coincidió (igual a `template` salvo en multi-plantilla).","example":"accounting-factura"},"mode":{"type":"string","enum":["template","freeform"],"description":"`freeform` cuando la request no traía `template`."},"status":{"type":"string","enum":["success","failed"]},"data":{"type":"object","description":"Campos extraídos. Con plantilla, la forma la define la plantilla; en freeform, las keys las elige el modelo.","additionalProperties":true},"summary":{"type":"string","description":"Resumen del documento. Sólo en modo freeform."},"metadata":{"allOf":[{"$ref":"#/components/schemas/SourceMetadata"}],"description":"Sólo en modo freeform (idioma detectado, etc.)."},"suggestedTemplate":{"$ref":"#/components/schemas/SuggestedTemplate"},"warnings":{"type":"array","items":{"type":"string"},"description":"Avisos de validación (CUIT inválido, totales que no cierran, etc.)."},"error":{"type":"string","description":"Sólo cuando `status=failed`."},"meta":{"type":"object","properties":{"model":{"type":"string","example":"gemini-2.5-flash"},"latencyMs":{"type":"integer"},"fileHash":{"type":"string","description":"SHA-256 hex del archivo."},"attempts":{"type":"integer","description":"Cuántas veces se invocó el modelo (≥1, sube cuando se gatilla el retry de razonamiento)."},"tokens":{"type":"object","properties":{"input":{"type":"integer"},"output":{"type":"integer"}}},"costMicros":{"type":"integer","description":"Costo total en micro-USD (USD × 1.000.000)."},"deduped":{"type":"boolean","description":"true cuando la respuesta provino del caché de dedupe (no se llamó al modelo)."},"dedupedFromId":{"type":"string","format":"uuid","description":"ID de la extracción original cuando deduped=true."},"confidence":{"type":"object","nullable":true,"description":"Autoevaluación de certeza del modelo. Útil para decidir qué auto-aceptar y qué enviar a revisión humana.","properties":{"overall":{"type":"number","description":"Confianza global de 0 a 1 sobre toda la extracción."},"lowFields":{"type":"array","items":{"type":"string"},"description":"Keys de los campos sobre los que el modelo tiene poca certeza (conviene revisarlos a mano)."}}},"candidates":{"type":"array","items":{"type":"string"},"description":"Sólo en multi-plantilla: los slugs que se consideraron."},"matchReason":{"type":"string","description":"Sólo en multi-plantilla: por qué se eligió matchedTemplate."}}}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"code":{"type":"string","description":"Código de error machine-readable."},"issues":{"type":"array","description":"Detalle por campo cuando `code=VALIDATION`.","items":{"type":"object"}}}}},"responses":{"Unauthorized":{"description":"Missing, invalid, or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Too many requests. Retry after the seconds in `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"IdempotencyMismatch":{"description":"An Idempotency-Key was reused with a different request body. Use a fresh key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"BadRequest":{"description":"Pedido inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Operación no permitida sobre este recurso.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Recurso no encontrado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"Colisión de slug o estado inconsistente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"UnsupportedMedia":{"description":"MIME no soportado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InternalError":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}