{"openapi":"3.1.0","info":{"title":"API pública de facturación electrónica","version":"1.0.0","description":"API pública de la plataforma de facturación electrónica (CFE) para Uruguay.\nPermite a integradores (ERPs, e-commerce, sistemas de gestión) consultar comprobantes, crear y emitir CFE contra DGI, descargar el PDF y el XML firmado, y administrar contactos, productos e inventario — sobre la misma capa de aplicación que la app (mismas validaciones DGI, misma idempotencia, mismo pipeline).\n\n**Autenticación**: header `Authorization: Bearer afk_live_<secreto>`. Cada API key pertenece a exactamente una empresa y se administra en `/app/configuracion/api-keys`; la key resuelve el tenant, que nunca viaja en la URL ni en el body.\n\n**Scopes**: cada key puede restringirse por scopes (`documents:read`, `documents:write`, `documents:issue`, `contacts:read`, `contacts:write`, `products:read`, `products:write`, `inventory:read`, `inventory:write`, `payments:read`, `payments:write`). Cada operación declara su scope requerido en la extensión `x-required-scope`. Una key **sin scopes** no puede operar ninguna operación que requiera scope (deny por defecto). Operar sin el scope requerido devuelve `403 FORBIDDEN`.\n\n**Rate limiting**: 240 req/min por IP (pre-autenticación) y 120 req/min por API key; toda respuesta incluye `X-RateLimit-Limit`, `X-RateLimit-Remaining` y `X-RateLimit-Reset`, y los 429 agregan `Retry-After`.\n\n**Convenciones**: montos y cantidades son SIEMPRE strings decimales (nunca floats). Las respuestas usan coma decimal es-UY sin separador de miles (`\"12200,50\"`; los enteros van sin separador: `\"12200\"`). En la entrada se acepta coma o punto decimal (`\"10,80\"` ≡ `\"10.80\"`; con coma, los puntos previos solo valen como miles en grupos de 3: `\"1.234,56\"`). Instantes ISO-8601; fechas calendario `YYYY-MM-DD`. Errores con envelope estable `{ \"error\": { \"code\", \"message\", \"details?\" } }`."},"servers":[{"url":"https://facturar.uy","description":"Servidor de la plataforma"}],"tags":[{"name":"Identidad","description":"Verificación de la API key y su empresa."},{"name":"Documentos","description":"Comprobantes fiscales electrónicos (CFE): listado, creación, emisión a DGI, NC/ND y artefactos (PDF / XML firmado)."},{"name":"Contactos","description":"Clientes y proveedores (receptores de comprobantes)."},{"name":"Productos","description":"Catálogo de productos y servicios."},{"name":"Sucursales","description":"Sucursales (casa central y locales) de la empresa — datos de referencia para emitir documentos."},{"name":"Inventario","description":"Stock por sucursal, kardex y movimientos manuales. Requiere el módulo `inventory` en el plan."},{"name":"Cobros","description":"Cobros de clientes, cuentas por cobrar con aging y estado de cobro de comprobantes. Metadata comercial que nunca modifica el registro fiscal. Requiere el módulo «payments» habilitado en el plan de la empresa — sin él, todos estos endpoints responden 403. Scopes: `payments:read` / `payments:write`."}],"security":[{"bearerAuth":[]}],"paths":{"/api/v1/me":{"get":{"operationId":"getMe","tags":["Identidad"],"summary":"Identidad de la API key","description":"Devuelve la empresa (id, nombre, razón social, RUC) y los metadatos de la key (nombre, prefijo). Úsalo como health check de la integración antes de operar. No requiere ningún scope: cualquier key válida puede llamarlo.","responses":{"200":{"description":"Identidad de la key y su empresa.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"type":"object","properties":{"company":{"type":["object","null"],"description":"Empresa dueña de la API key (el tenant de todos los requests).","properties":{"id":{"type":"string","description":"Id de la empresa."},"name":{"type":"string","description":"Nombre de la empresa en la plataforma."},"legalName":{"type":"string","description":"Razón social."},"tradeName":{"type":["string","null"],"description":"Nombre comercial."},"ruc":{"type":["string","null"],"description":"RUC de la empresa (string, 12 dígitos)."}}},"apiKey":{"type":"object","description":"Metadatos de la key autenticada (nunca incluye el secreto).","properties":{"name":{"type":"string","description":"Nombre dado a la key al crearla."},"prefix":{"type":"string","description":"Prefijo visible de la key (p.ej. afk_live_a1b2c3)."}}}}},"example":{"company":{"id":"cmc0emp4h0000uy01a1b2c3d4","name":"Estudio Méndez","legalName":"Estudio Méndez SRL","tradeName":"Méndez & Asociados","ruc":"211234567890"},"apiKey":{"name":"Integración ERP","prefix":"afk_live_a1b2c3"}}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/documents":{"get":{"operationId":"listDocuments","tags":["Documentos"],"summary":"Listar comprobantes","description":"Listado de comprobantes de la empresa (emitidos y recibidos), más reciente primero. Nunca incluye plantillas de facturas recurrentes. Scope requerido: `documents:read`.","x-required-scope":"documents:read","parameters":[{"name":"status","in":"query","description":"Filtra por estado del documento.","schema":{"type":"string","enum":["DRAFT","READY_TO_ISSUE","ISSUING","SIGNED","SUBMITTED","ACCEPTED","REJECTED","OBSERVED","CANCELLED","FAILED","CONTINGENCY"]},"example":"ACCEPTED"},{"name":"direction","in":"query","description":"Emitidos o recibidos.","schema":{"type":"string","enum":["ISSUED","RECEIVED"]}},{"name":"type","in":"query","description":"Código DGI del tipo de comprobante (p.ej. 101 e-Ticket, 111 e-Factura).","schema":{"type":"integer","minimum":100,"maximum":299},"example":111},{"name":"search","in":"query","description":"Búsqueda por receptor, tipo o número (mínimo 3 caracteres). Un valor solo numérico busca por número de comprobante, RUT/CI y número interno; «A-112» por serie y número.","schema":{"type":"string","minLength":3,"maxLength":100}},{"name":"limit","in":"query","description":"Cantidad máxima de resultados (1-100).","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"example":50},{"name":"offset","in":"query","description":"Desplazamiento para paginar (0-10000). Para recorrer más allá usá paginación por `cursor` donde esté disponible.","schema":{"type":"integer","minimum":0,"maximum":10000,"default":0},"example":0},{"name":"cursor","in":"query","description":"Cursor opaco devuelto en `pagination.nextCursor` de la página anterior (paginación keyset, costo constante a cualquier profundidad). Excluyente con `offset` > 0.","schema":{"type":"string","maxLength":200}},{"name":"createdAfter","in":"query","description":"Solo comprobantes creados DESPUÉS de este instante ISO-8601 (sincronización incremental).","schema":{"type":"string","format":"date-time"},"example":"2026-07-01T00:00:00Z"},{"name":"updatedAfter","in":"query","description":"Solo comprobantes modificados DESPUÉS de este instante ISO-8601 — la forma eficiente de sondear cambios de estado (DGI, entrega) sin recorrer todo el listado.","schema":{"type":"string","format":"date-time"},"example":"2026-07-05T12:00:00Z"},{"name":"includeTotal","in":"query","description":"`false` omite `pagination.total` y ahorra el conteo del lado del servidor (recomendado al paginar por `cursor`). Default `true`.","schema":{"type":"boolean","default":true}}],"responses":{"200":{"description":"Página de comprobantes. `pagination.nextCursor` es el cursor de la página siguiente (null en la última); `pagination.total` se omite con `includeTotal=false`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DocumentSummary"},"description":"Página de comprobantes (más recientes primero)."},"pagination":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"nextCursor":{"type":["string","null"],"description":"Cursor opaco para pedir la página siguiente con `?cursor=`; null cuando no hay más resultados."}}}],"description":"Paginación: `total` (opcional), `limit`, `offset` y `nextCursor` (keyset)."}}},"example":{"data":[{"id":"cmc4x8p2h0001uy01a9k3f7d2","status":"ACCEPTED","direction":"ISSUED","documentType":{"code":111,"name":"e-Factura","kind":"CFE"},"series":"A","number":1284,"issueDate":"2026-07-01","currency":"UYU","total":"12200","receiver":{"legalName":"Estudio Méndez SRL","code":"CLI-001"},"receiverDeliveryStatus":"SENT","createdAt":"2026-07-01T14:30:00.000Z","updatedAt":"2026-07-01T14:35:12.000Z"}],"pagination":{"total":87,"limit":50,"offset":0,"nextCursor":"MjAyNi0wNy0wNVQxMjozNDo1Ni4wMDBafGNtYzBkb2M0aDAwMDB1eTAxYTFiMmMzZDQ"}}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`documents:read`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"Parámetros inválidos","details":{"limit":["El límite debe estar entre 1 y 100"]}}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}},"post":{"operationId":"createDocument","tags":["Documentos"],"summary":"Crear un comprobante","description":"Crea un comprobante en una llamada, con el mismo flujo que el sistema: la API compone los pasos del wizard (borrador → datos generales → líneas → datos adicionales) sobre la misma capa de aplicación que la UI. Nunca va directo a DGI: el resultado es un `DRAFT` normal, visible y editable en la app. Con `autoIssue: true` corre además el gate de validación DGI y encola el pipeline de emisión; si el borrador está incompleto responde `422` con el requisito exacto y `details.documentId` (el draft persiste y puede terminarse en la app). Idempotente vía header `Idempotency-Key` (o `idempotencyKey` en el body; el header gana): un retry devuelve el documento ya creado (`200`, `reused: true`). `series`/`number` se ignoran (la numeración la gestiona la reserva de CAE). Scope requerido: `documents:write` (más `documents:issue` cuando `autoIssue: true`).","x-required-scope":"documents:write","parameters":[{"name":"Idempotency-Key","in":"header","description":"Clave de idempotencia (8-128 caracteres, única por empresa). Un retry con la misma clave devuelve el documento ya creado sin duplicarlo.","schema":{"type":"string","minLength":8,"maxLength":128},"example":"erp-fact-2026-000123"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDocumentRequest"},"example":{"documentTypeCode":111,"branchCode":"1","receiverCode":"CLI-001","currency":"UYU","issueDate":"2026-07-05","operationType":"SERVICE","paymentType":"CREDIT","dueDate":"2026-08-04","lines":[{"description":"Servicio mensual","quantity":"1","unit":"N/A","unitPrice":"10000","billingIndicator":3,"productCode":"SRV-CONSULT","responsibleIndicator":"R","retentionPerceptions":[{"retentionCode":"2183114","rate":"5.000","subjectAmount":"10000.00","retentionValue":"540.00"}]}],"adjustments":[{"lineNumber":1,"movementType":"DISCOUNT","adjustmentType":2,"percent":"10.00","description":"Descuento por pronto pago","billingIndicator":3}],"autoIssue":false}}}},"responses":{"200":{"description":"Reuso idempotente: la clave de idempotencia ya había creado este documento.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDocumentResult"},"example":{"document":{"id":"cmc4x8p2h0001uy01a9k3f7d2","status":"DRAFT","direction":"ISSUED","documentType":{"code":111,"name":"e-Factura","kind":"CFE"},"series":null,"number":null,"issueDate":"2026-07-01","currency":"UYU","total":"12200","receiver":{"legalName":"Estudio Méndez SRL","code":"CLI-001"},"receiverDeliveryStatus":"PENDING","createdAt":"2026-07-01T14:30:00.000Z","updatedAt":"2026-07-01T14:35:12.000Z"},"reused":true,"issue":null}}}},"201":{"description":"Comprobante creado.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDocumentResult"},"example":{"document":{"id":"cmc4x8p2h0001uy01a9k3f7d2","status":"DRAFT","direction":"ISSUED","documentType":{"code":111,"name":"e-Factura","kind":"CFE"},"series":null,"number":null,"issueDate":"2026-07-01","currency":"UYU","total":"12200","receiver":{"legalName":"Estudio Méndez SRL","code":"CLI-001"},"receiverDeliveryStatus":"PENDING","createdAt":"2026-07-01T14:30:00.000Z","updatedAt":"2026-07-01T14:35:12.000Z"},"reused":false,"issue":null}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`documents:write`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"409":{"description":"Estado incompatible o `Idempotency-Key` reutilizada con otro payload / aún en curso (`IDEMPOTENCY_CONFLICT`, con `Retry-After` en el segundo caso).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"IDEMPOTENCY_CONFLICT","message":"La Idempotency-Key 'erp-op-2026-000123' ya se usó con un payload distinto."}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"El documento requiere sucursal para emitirse","details":{"documentId":"cmc4x8p2h0001uy01a9k3f7d2"}}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/documents/{id}":{"get":{"operationId":"getDocument","tags":["Documentos"],"summary":"Detalle de un comprobante","description":"Detalle completo: identidad fiscal (tipo, serie, número), totales (strings decimales, nunca floats), líneas, CAE (número, serie, rango, vencimiento), tracking DGI (`trackingId`, `submittedAt`, `ackSobreCode`), receptor y sucursal. Tras emitir, sondear este endpoint hasta `ACCEPTED` / `REJECTED` / `OBSERVED`. Scope requerido: `documents:read`.","x-required-scope":"documents:read","parameters":[{"name":"id","in":"path","required":true,"description":"Identificador del comprobante.","schema":{"type":"string","pattern":"^[a-zA-Z0-9]{20,32}$"},"example":"cmc4x8p2h0001uy01a9k3f7d2"},{"name":"If-None-Match","in":"header","description":"ETag recibido en una respuesta anterior; si el comprobante no cambió la respuesta es `304 Not Modified` sin body.","schema":{"type":"string"}}],"responses":{"200":{"description":"Detalle del comprobante.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}},"ETag":{"description":"Validador débil del recurso (cambia con cada modificación). Enviarlo en `If-None-Match` devuelve `304` si no cambió.","schema":{"type":"string","example":"W/\"cmc0doc4h0000uy01a1b2c3d4:1751718896000\""}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentDetail"},"example":{"id":"cmc4x8p2h0001uy01a9k3f7d2","status":"ACCEPTED","direction":"ISSUED","documentType":{"code":111,"name":"e-Factura","kind":"CFE"},"series":"A","number":1284,"issueDate":"2026-07-01","currency":"UYU","total":"12200","receiver":{"legalName":"Estudio Méndez SRL","code":"CLI-001"},"receiverDeliveryStatus":"SENT","createdAt":"2026-07-01T14:30:00.000Z","updatedAt":"2026-07-01T14:35:12.000Z","exchangeRate":null,"operationType":"SERVICE","paymentType":"CREDIT","dueDate":"2026-08-04","periodFrom":null,"periodTo":null,"createdVia":"API","subtotal":"10000","taxTotal":"2200","totalAmount":"12200.00","totalRetentionPerceptionAmount":"540.00","totalFiscalCreditAmount":"0.00","payableAmount":"11660.00","receiverDocument":{"legalName":"Estudio Méndez SRL","documentNumber":"211234567890"},"branch":{"code":"1","name":"Casa central"},"cae":{"number":"90230012345","series":"A","fromNumber":1,"toNumber":5000,"expirationDate":"2027-12-31"},"dgi":{"trackingId":"f2b7c9e4-6a1d-4c3b-9e8f-1a2b3c4d5e6f","submittedAt":"2026-07-01T14:32:05.000Z","ackSobreCode":"AS"},"lines":[{"lineNumber":1,"description":"Servicio mensual","quantity":"1","unit":"N/A","unitPrice":"10000","taxRate":"0.22","billingIndicator":3,"productCode":"SRV-CONSULT","subtotal":"10000","tax":"2200","total":"12200","retentionPerceptions":[{"code":"2183114","rate":"5.000","subjectAmount":"10000.00","additionalInfo":null,"retentionValue":"540.00"}]}],"payments":[],"adjustments":[{"lineNumber":1,"movementType":"DISCOUNT","adjustmentType":2,"description":"Descuento por pronto pago","code":null,"percent":"10.00","amount":"1000.00","billingIndicator":3}],"references":[]}}}},"304":{"description":"El comprobante no cambió desde el ETag enviado en `If-None-Match` (sin body)."},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`documents:read`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/documents/{id}/issue":{"post":{"operationId":"issueDocument","tags":["Documentos"],"summary":"Emitir a DGI","description":"Dispara la emisión a DGI. `READY_TO_ISSUE` → encola el pipeline (`202`, `enqueued: true`). `DRAFT` → corre primero la misma validación de completitud que la UI (campos DGI, snapshots fiscales, reserva de numeración CAE, tipo de cambio); si el borrador está incompleto responde `422` con el requisito exacto. Si la empresa tiene activado el ajuste por redondeo automático, al validar un `DRAFT` se agrega/recalcula la línea «Ajuste por redondeo» (indicador de facturación 6/7) que lleva el Monto a pagar al múltiplo configurado. Ya emitido o en curso → idempotente: `200` con `enqueued: false` y el estado actual (el jobId de BullMQ deriva del documento, sin doble-encolado). `FAILED` → reintento: resetea a `READY_TO_ISSUE` (mismo use case que el botón «Reintentar» de la app, auditado) y re-encola (`202`). `CANCELLED` → `409`. El resultado es asíncrono: sondear `GET /documents/{id}`. Scope requerido: `documents:issue`.","x-required-scope":"documents:issue","parameters":[{"name":"id","in":"path","required":true,"description":"Identificador del comprobante a emitir.","schema":{"type":"string","pattern":"^[a-zA-Z0-9]{20,32}$"},"example":"cmc4x8p2h0001uy01a9k3f7d2"},{"name":"Idempotency-Key","in":"header","description":"Clave de idempotencia (8-128 caracteres, única por empresa y operación). Un reintento con la misma clave y el mismo body devuelve la respuesta original (header `Idempotency-Replayed: true`) durante 24 h; la misma clave con otro body responde `409 IDEMPOTENCY_CONFLICT`; si el primer intento sigue en curso, `409` con `Retry-After`.","schema":{"type":"string","minLength":8,"maxLength":128},"example":"erp-op-2026-000123"}],"responses":{"200":{"description":"No-op idempotente: el documento ya fue emitido o está en curso.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssueResult"},"example":{"id":"cmc4x8p2h0001uy01a9k3f7d2","status":"ACCEPTED","enqueued":false}}}},"202":{"description":"Pipeline de emisión encolado.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssueResult"},"example":{"id":"cmc4x8p2h0001uy01a9k3f7d2","status":"READY_TO_ISSUE","enqueued":true}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`documents:issue`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"409":{"description":"Estado incompatible (p.ej. documento anulado).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"El documento está anulado y no puede emitirse"}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"El documento requiere receptor para emitirse"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/documents/{id}/correction-note":{"post":{"operationId":"createCorrectionNote","tags":["Documentos"],"summary":"Crear NC/ND (nota de corrección)","description":"Crea una nota de crédito o débito a partir de un comprobante `ACCEPTED`/`OBSERVED`, pre-llenada desde el origen (cabecera, líneas, pagos, retenciones) y con la referencia Zona F al documento fuente puesta automáticamente (monto acotado al total a pagar del origen). La NC resta o anula el comprobante original (devoluciones, descuentos, corrección de errores — es la forma fiscalmente correcta de anular un CFE en Uruguay: no existe el \"delete\"); la ND suma o aumenta su valor (gastos extra, diferencias de precio, intereses). `correctionTypeCode` debe ser la NC o ND que corresponde al tipo del documento fuente (p.ej. 112 NC de e-Factura). Scope requerido: `documents:write` (más `documents:issue` cuando `autoIssue: true`).","x-required-scope":"documents:write","parameters":[{"name":"id","in":"path","required":true,"description":"Identificador del comprobante origen (ACCEPTED u OBSERVED).","schema":{"type":"string","pattern":"^[a-zA-Z0-9]{20,32}$"},"example":"cmc4x8p2h0001uy01a9k3f7d2"},{"name":"Idempotency-Key","in":"header","description":"Clave de idempotencia (8-128 caracteres, única por empresa y operación). Un reintento con la misma clave y el mismo body devuelve la respuesta original (header `Idempotency-Replayed: true`) durante 24 h; la misma clave con otro body responde `409 IDEMPOTENCY_CONFLICT`; si el primer intento sigue en curso, `409` con `Retry-After`.","schema":{"type":"string","minLength":8,"maxLength":128},"example":"erp-op-2026-000123"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["correctionTypeCode"],"properties":{"correctionTypeCode":{"type":"integer","minimum":100,"maximum":299,"description":"Código de la NC/ND correspondiente al tipo del documento fuente (p.ej. 112 NC de e-Factura, 113 ND de e-Factura)."},"autoIssue":{"type":"boolean","default":false,"description":"true → emite la nota inmediatamente (requiere además `documents:issue`)."}}},"example":{"correctionTypeCode":112,"autoIssue":false}}}},"responses":{"201":{"description":"Nota de corrección creada como borrador (o encolada si `autoIssue`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CorrectionNoteResult"},"example":{"document":{"id":"cmc4x8p2h0001uy01a9k3f7d2","status":"DRAFT","direction":"ISSUED","documentType":{"code":112,"name":"Nota de Crédito de e-Factura","kind":"CFE"},"series":null,"number":null,"issueDate":"2026-07-01","currency":"UYU","total":"12200","receiver":{"legalName":"Estudio Méndez SRL","code":"CLI-001"},"receiverDeliveryStatus":"PENDING","createdAt":"2026-07-01T14:30:00.000Z","updatedAt":"2026-07-01T14:35:12.000Z"},"issue":null}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`documents:write`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"409":{"description":"El documento origen no está en un estado corregible (se requiere ACCEPTED u OBSERVED).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"El documento origen no admite notas de corrección en su estado actual"}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"correctionTypeCode no corresponde al tipo del documento origen"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/documents/{id}/collection-receipt":{"post":{"operationId":"createCollectionReceipt","tags":["Documentos"],"summary":"Crear recibo electrónico","description":"Crea un recibo de cobranza a partir de una e-Factura (111) o e-Ticket (101) en estado ACCEPTED u OBSERVED, según la FAQ 4.10 de DGI (Res. Nº 303/2019): un CFE del mismo tipo con indicador de cobranza propia (A-C20 = 1), una línea «COBRANZA DE …» por cada ítem del original como monto no facturable (B-C4 = 6, IVA incluido en el importe) y la referencia en Zona F al comprobante cobrado. El borrador puede ajustarse (p.ej. cobranza parcial) antes de emitir, o emitirse directo con `autoIssue`. Scope requerido: `documents:write` (+ `documents:issue` si `autoIssue`).","x-required-scope":"documents:write","parameters":[{"name":"id","in":"path","required":true,"description":"Identificador del comprobante origen (e-Factura/e-Ticket ACCEPTED u OBSERVED).","schema":{"type":"string","pattern":"^[a-zA-Z0-9]{20,32}$"},"example":"cmc4x8p2h0001uy01a9k3f7d2"},{"name":"Idempotency-Key","in":"header","description":"Clave de idempotencia (8-128 caracteres, única por empresa y operación). Un reintento con la misma clave y el mismo body devuelve la respuesta original (header `Idempotency-Replayed: true`) durante 24 h; la misma clave con otro body responde `409 IDEMPOTENCY_CONFLICT`; si el primer intento sigue en curso, `409` con `Retry-After`.","schema":{"type":"string","minLength":8,"maxLength":128},"example":"erp-op-2026-000123"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"autoIssue":{"type":"boolean","default":false,"description":"true → emite el recibo inmediatamente (requiere además `documents:issue`)."}}},"example":{"autoIssue":false}}}},"responses":{"201":{"description":"Recibo de cobranza creado como borrador (o encolado si `autoIssue`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionReceiptResult"},"example":{"document":{"id":"cmc4x8p2h0001uy01a9k3f7d2","status":"DRAFT","direction":"ISSUED","documentType":{"code":111,"name":"e-Factura","kind":"CFE"},"series":null,"number":null,"issueDate":"2026-07-01","currency":"UYU","total":"12200","receiver":{"legalName":"Estudio Méndez SRL","code":"CLI-001"},"receiverDeliveryStatus":"PENDING","createdAt":"2026-07-01T14:30:00.000Z","updatedAt":"2026-07-01T14:35:12.000Z"},"issue":null}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`documents:write`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"409":{"description":"El documento origen no está en un estado cobrable (se requiere ACCEPTED u OBSERVED).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"El documento origen no admite recibo de cobranza en su estado actual"}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"El recibo de cobranza solo puede crearse desde una e-Factura (111) o un e-Ticket (101)"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/documents/{id}/pdf":{"get":{"operationId":"getDocumentPdf","tags":["Documentos"],"summary":"Descargar PDF","description":"Representación imprimible del comprobante, renderizada on-demand (nunca se persiste; el artefacto fiscal autoritativo es el XML firmado). Scope requerido: `documents:read`.","x-required-scope":"documents:read","parameters":[{"name":"id","in":"path","required":true,"description":"Identificador del comprobante.","schema":{"type":"string","pattern":"^[a-zA-Z0-9]{20,32}$"},"example":"cmc4x8p2h0001uy01a9k3f7d2"}],"responses":{"200":{"description":"PDF del comprobante.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/pdf":{"schema":{"type":"string","contentMediaType":"application/pdf","contentEncoding":"binary"}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`documents:read`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/documents/{id}/xml":{"get":{"operationId":"getDocumentXml","tags":["Documentos"],"summary":"Descargar XML firmado","description":"El XML firmado del comprobante — el artefacto fiscal autoritativo. Devuelve `409 CONFLICT` mientras el documento exista pero aún no haya sido firmado por el pipeline. Scope requerido: `documents:read`.","x-required-scope":"documents:read","parameters":[{"name":"id","in":"path","required":true,"description":"Identificador del comprobante.","schema":{"type":"string","pattern":"^[a-zA-Z0-9]{20,32}$"},"example":"cmc4x8p2h0001uy01a9k3f7d2"}],"responses":{"200":{"description":"XML firmado del comprobante.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/xml":{"schema":{"type":"string"}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`documents:read`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"409":{"description":"El documento existe pero su XML aún no fue firmado por el pipeline.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"El documento aún no tiene XML firmado"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/branches":{"get":{"operationId":"listBranches","tags":["Sucursales"],"summary":"Listar sucursales","description":"Listado de sucursales (casa central y locales) de la empresa, la de por defecto primero. Es la forma de descubrir los `branchCode` que se usan al crear documentos. Scope requerido: `documents:read`.","x-required-scope":"documents:read","parameters":[{"name":"search","in":"query","description":"Búsqueda por código o nombre de sucursal.","schema":{"type":"string","minLength":3,"maxLength":100}},{"name":"includeInactive","in":"query","description":"Incluye sucursales desactivadas.","schema":{"type":"boolean","default":false}},{"name":"limit","in":"query","description":"Cantidad máxima de resultados (1-100).","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"example":50},{"name":"offset","in":"query","description":"Desplazamiento para paginar (0-10000). Para recorrer más allá usá paginación por `cursor` donde esté disponible.","schema":{"type":"integer","minimum":0,"maximum":10000,"default":0},"example":0}],"responses":{"200":{"description":"Página de sucursales.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/BranchSummary"},"description":"Página de sucursales."},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"example":{"data":[{"code":"1","name":"Casa central","isDefault":true,"isActive":true,"createdAt":"2026-05-12T13:00:00.000Z"}],"pagination":{"total":2,"limit":50,"offset":0}}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`documents:read`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"Parámetros inválidos"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/contacts":{"get":{"operationId":"listContacts","tags":["Contactos"],"summary":"Listar contactos","description":"Listado de contactos (clientes y proveedores) de la empresa. Scope requerido: `contacts:read`.","x-required-scope":"contacts:read","parameters":[{"name":"type","in":"query","description":"Filtra por tipo de contacto.","schema":{"type":"string","enum":["CUSTOMER","SUPPLIER","BOTH"]}},{"name":"search","in":"query","description":"Búsqueda por nombre, documento o código.","schema":{"type":"string","minLength":3,"maxLength":100}},{"name":"includeInactive","in":"query","description":"Incluye contactos desactivados.","schema":{"type":"boolean","default":false}},{"name":"limit","in":"query","description":"Cantidad máxima de resultados (1-100).","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"example":50},{"name":"offset","in":"query","description":"Desplazamiento para paginar (0-10000). Para recorrer más allá usá paginación por `cursor` donde esté disponible.","schema":{"type":"integer","minimum":0,"maximum":10000,"default":0},"example":0}],"responses":{"200":{"description":"Página de contactos.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ContactSummary"},"description":"Página de contactos."},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"example":{"data":[{"id":"cmc2rec9k0002uy01b7m1x4q8","type":"CUSTOMER","code":"CLI-001","documentType":"RUC","countryCode":"UY","documentNumber":"211234567890","legalName":"Estudio Méndez SRL","tradeName":"Méndez & Asociados","email":"administracion@mendez.uy","phone":"+598 99 123 456","city":"Montevideo","department":"Montevideo","isActive":true,"createdAt":"2026-05-12T13:00:00.000Z"}],"pagination":{"total":12,"limit":50,"offset":0}}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`contacts:read`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"Parámetros inválidos"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}},"post":{"operationId":"createContact","tags":["Contactos"],"summary":"Crear un contacto","description":"Crea un contacto con el mismo schema y validaciones que el formulario de la app: dígito verificador de RUC (módulo 11), unicidad por empresa de documento y código interno, reglas de receptor. `code` y `legalName` son requeridos. Scope requerido: `contacts:write`.","x-required-scope":"contacts:write","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateContactRequest"},"example":{"type":"CUSTOMER","code":"CLI-001","legalName":"Estudio Méndez SRL","documentType":"RUC","countryCode":"UY","documentNumber":"211234567890","email":"administracion@mendez.uy","city":"Montevideo","department":"Montevideo"}}}},"responses":{"201":{"description":"Contacto creado (detalle completo).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactSummary"},"example":{"id":"cmc2rec9k0002uy01b7m1x4q8","type":"CUSTOMER","code":"CLI-001","documentType":"RUC","countryCode":"UY","documentNumber":"211234567890","legalName":"Estudio Méndez SRL","tradeName":"Méndez & Asociados","email":"administracion@mendez.uy","phone":"+598 99 123 456","city":"Montevideo","department":"Montevideo","isActive":true,"createdAt":"2026-05-12T13:00:00.000Z"}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`contacts:write`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"409":{"description":"Estado incompatible o `Idempotency-Key` reutilizada con otro payload / aún en curso (`IDEMPOTENCY_CONFLICT`, con `Retry-After` en el segundo caso).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"IDEMPOTENCY_CONFLICT","message":"La Idempotency-Key 'erp-op-2026-000123' ya se usó con un payload distinto."}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"RUC inválido: dígito verificador incorrecto","details":{"documentNumber":["RUC inválido: dígito verificador incorrecto"]}}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}},"parameters":[{"name":"Idempotency-Key","in":"header","description":"Clave de idempotencia (8-128 caracteres, única por empresa y operación). Un reintento con la misma clave y el mismo body devuelve la respuesta original (header `Idempotency-Replayed: true`) durante 24 h; la misma clave con otro body responde `409 IDEMPOTENCY_CONFLICT`; si el primer intento sigue en curso, `409` con `Retry-After`.","schema":{"type":"string","minLength":8,"maxLength":128},"example":"erp-op-2026-000123"}]}},"/api/v1/contacts/{id}":{"get":{"operationId":"getContact","tags":["Contactos"],"summary":"Detalle de un contacto","description":"Detalle completo del contacto, incluyendo dirección, datos de receptor y metadatos. Scope requerido: `contacts:read`.","x-required-scope":"contacts:read","parameters":[{"name":"id","in":"path","required":true,"description":"Identificador del contacto.","schema":{"type":"string","pattern":"^[a-zA-Z0-9]{20,32}$"},"example":"cmc4x8p2h0001uy01a9k3f7d2"}],"responses":{"200":{"description":"Detalle del contacto.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactSummary"},"example":{"id":"cmc2rec9k0002uy01b7m1x4q8","type":"CUSTOMER","code":"CLI-001","documentType":"RUC","countryCode":"UY","documentNumber":"211234567890","legalName":"Estudio Méndez SRL","tradeName":"Méndez & Asociados","email":"administracion@mendez.uy","phone":"+598 99 123 456","city":"Montevideo","department":"Montevideo","isActive":true,"createdAt":"2026-05-12T13:00:00.000Z"}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`contacts:read`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}},"patch":{"operationId":"updateContact","tags":["Contactos"],"summary":"Actualizar un contacto","description":"Actualización COMPLETA (semántica PUT sobre el verbo PATCH): el body debe incluir el objeto entero — `type`, `code` y `legalName` son obligatorios y un campo omitido se limpia. Recomendado: GET del contacto, modificar los campos deseados y reenviar todo. Acepta además `isActive` para activar/desactivar. Scope requerido: `contacts:write`.","x-required-scope":"contacts:write","parameters":[{"name":"id","in":"path","required":true,"description":"Identificador del contacto.","schema":{"type":"string","pattern":"^[a-zA-Z0-9]{20,32}$"},"example":"cmc4x8p2h0001uy01a9k3f7d2"},{"name":"Idempotency-Key","in":"header","description":"Clave de idempotencia (8-128 caracteres, única por empresa y operación). Un reintento con la misma clave y el mismo body devuelve la respuesta original (header `Idempotency-Replayed: true`) durante 24 h; la misma clave con otro body responde `409 IDEMPOTENCY_CONFLICT`; si el primer intento sigue en curso, `409` con `Retry-After`.","schema":{"type":"string","minLength":8,"maxLength":128},"example":"erp-op-2026-000123"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateContactRequest"},"example":{"type":"COMPANY","code":"CLI-0042","legalName":"Méndez y Asociados S.A.","email":"facturacion@mendez.uy","phone":"+598 99 654 321"}}}},"responses":{"200":{"description":"Contacto actualizado (detalle completo).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactSummary"},"example":{"id":"cmc2rec9k0002uy01b7m1x4q8","type":"CUSTOMER","code":"CLI-001","documentType":"RUC","countryCode":"UY","documentNumber":"211234567890","legalName":"Estudio Méndez SRL","tradeName":"Méndez & Asociados","email":"administracion@mendez.uy","phone":"+598 99 123 456","city":"Montevideo","department":"Montevideo","isActive":true,"createdAt":"2026-05-12T13:00:00.000Z"}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`contacts:write`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"409":{"description":"Estado incompatible o `Idempotency-Key` reutilizada con otro payload / aún en curso (`IDEMPOTENCY_CONFLICT`, con `Retry-After` en el segundo caso).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"IDEMPOTENCY_CONFLICT","message":"La Idempotency-Key 'erp-op-2026-000123' ya se usó con un payload distinto."}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"Email inválido","details":{"email":["Email inválido"]}}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/products":{"get":{"operationId":"listProducts","tags":["Productos"],"summary":"Listar productos","description":"Listado de productos y servicios del catálogo de la empresa. Scope requerido: `products:read`.","x-required-scope":"products:read","parameters":[{"name":"type","in":"query","description":"Filtra por tipo.","schema":{"type":"string","enum":["PRODUCT","SERVICE"]}},{"name":"search","in":"query","description":"Búsqueda por nombre o SKU.","schema":{"type":"string","minLength":3,"maxLength":100}},{"name":"includeInactive","in":"query","description":"Incluye productos desactivados.","schema":{"type":"boolean","default":false}},{"name":"limit","in":"query","description":"Cantidad máxima de resultados (1-100).","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"example":50},{"name":"offset","in":"query","description":"Desplazamiento para paginar (0-10000). Para recorrer más allá usá paginación por `cursor` donde esté disponible.","schema":{"type":"integer","minimum":0,"maximum":10000,"default":0},"example":0}],"responses":{"200":{"description":"Página de productos.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ProductSummary"},"description":"Página de productos."},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"example":{"data":[{"id":"cmc3pro5d0003uy01d4p6v2r9","type":"SERVICE","sku":"SRV-CONSULT","name":"Servicio de consultoría","unit":"N/A","currency":"UYU","price":"10000","isActive":true,"createdAt":"2026-05-12T13:00:00.000Z"}],"pagination":{"total":34,"limit":50,"offset":0}}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`products:read`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"Parámetros inválidos"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}},"post":{"operationId":"createProduct","tags":["Productos"],"summary":"Crear un producto","description":"Crea un producto o servicio. `sku`, `name` y `price` son requeridos; los flags de stock solo aplican a `type: \"PRODUCT\"`. Mismas validaciones que la app (SKU único por empresa). Scope requerido: `products:write`.","x-required-scope":"products:write","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductRequest"},"example":{"type":"SERVICE","sku":"SRV-CONSULT","name":"Servicio de consultoría","unit":"N/A","currency":"UYU","price":"10000"}}}},"responses":{"201":{"description":"Producto creado (detalle completo).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductSummary"},"example":{"id":"cmc3pro5d0003uy01d4p6v2r9","type":"SERVICE","sku":"SRV-CONSULT","name":"Servicio de consultoría","unit":"N/A","currency":"UYU","price":"10000","isActive":true,"createdAt":"2026-05-12T13:00:00.000Z"}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`products:write`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"409":{"description":"Estado incompatible o `Idempotency-Key` reutilizada con otro payload / aún en curso (`IDEMPOTENCY_CONFLICT`, con `Retry-After` en el segundo caso).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"IDEMPOTENCY_CONFLICT","message":"La Idempotency-Key 'erp-op-2026-000123' ya se usó con un payload distinto."}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"Ya existe un producto con ese SKU","details":{"sku":["Ya existe un producto con ese SKU"]}}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}},"parameters":[{"name":"Idempotency-Key","in":"header","description":"Clave de idempotencia (8-128 caracteres, única por empresa y operación). Un reintento con la misma clave y el mismo body devuelve la respuesta original (header `Idempotency-Replayed: true`) durante 24 h; la misma clave con otro body responde `409 IDEMPOTENCY_CONFLICT`; si el primer intento sigue en curso, `409` con `Retry-After`.","schema":{"type":"string","minLength":8,"maxLength":128},"example":"erp-op-2026-000123"}]}},"/api/v1/products/{id}":{"get":{"operationId":"getProduct","tags":["Productos"],"summary":"Detalle de un producto","description":"Detalle completo del producto, incluyendo defaults DGI y configuración de stock. Scope requerido: `products:read`.","x-required-scope":"products:read","parameters":[{"name":"id","in":"path","required":true,"description":"Identificador del producto.","schema":{"type":"string","pattern":"^[a-zA-Z0-9]{20,32}$"},"example":"cmc4x8p2h0001uy01a9k3f7d2"}],"responses":{"200":{"description":"Detalle del producto.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductSummary"},"example":{"id":"cmc3pro5d0003uy01d4p6v2r9","type":"SERVICE","sku":"SRV-CONSULT","name":"Servicio de consultoría","unit":"N/A","currency":"UYU","price":"10000","isActive":true,"createdAt":"2026-05-12T13:00:00.000Z"}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`products:read`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}},"patch":{"operationId":"updateProduct","tags":["Productos"],"summary":"Actualizar un producto","description":"Actualización COMPLETA (semántica PUT sobre el verbo PATCH): el body debe incluir el objeto entero — `type`, `sku`, `name` y `price` son obligatorios y un campo omitido se limpia. Recomendado: GET del producto, modificar y reenviar todo. Acepta además `isActive`. Scope requerido: `products:write`.","x-required-scope":"products:write","parameters":[{"name":"id","in":"path","required":true,"description":"Identificador del producto.","schema":{"type":"string","pattern":"^[a-zA-Z0-9]{20,32}$"},"example":"cmc4x8p2h0001uy01a9k3f7d2"},{"name":"Idempotency-Key","in":"header","description":"Clave de idempotencia (8-128 caracteres, única por empresa y operación). Un reintento con la misma clave y el mismo body devuelve la respuesta original (header `Idempotency-Replayed: true`) durante 24 h; la misma clave con otro body responde `409 IDEMPOTENCY_CONFLICT`; si el primer intento sigue en curso, `409` con `Retry-After`.","schema":{"type":"string","minLength":8,"maxLength":128},"example":"erp-op-2026-000123"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProductRequest"},"example":{"type":"PRODUCT","sku":"SKU-001","name":"Consultoría mensual","price":"11500"}}}},"responses":{"200":{"description":"Producto actualizado (detalle completo).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductSummary"},"example":{"id":"cmc3pro5d0003uy01d4p6v2r9","type":"SERVICE","sku":"SRV-CONSULT","name":"Servicio de consultoría","unit":"N/A","currency":"UYU","price":"10000","isActive":true,"createdAt":"2026-05-12T13:00:00.000Z"}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`products:write`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"409":{"description":"Estado incompatible o `Idempotency-Key` reutilizada con otro payload / aún en curso (`IDEMPOTENCY_CONFLICT`, con `Retry-After` en el segundo caso).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"IDEMPOTENCY_CONFLICT","message":"La Idempotency-Key 'erp-op-2026-000123' ya se usó con un payload distinto."}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"Precio inválido","details":{"price":["Precio inválido"]}}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/inventory":{"get":{"operationId":"listInventory","tags":["Inventario"],"summary":"Stock por producto y sucursal","description":"Stock actual por producto y sucursal con estado derivado (`normal`, `low`, `out`, `negative`). Requiere el módulo `inventory` en el plan de la empresa (si no, `403`). Scope requerido: `inventory:read`.","x-required-scope":"inventory:read","parameters":[{"name":"search","in":"query","description":"Búsqueda por producto o SKU.","schema":{"type":"string","minLength":3,"maxLength":100}},{"name":"branchId","in":"query","description":"Filtra por sucursal.","schema":{"type":"string"}},{"name":"status","in":"query","description":"Filtra por estado de stock.","schema":{"type":"string","enum":["all","normal","low","out","negative"],"default":"all"}},{"name":"limit","in":"query","description":"Cantidad máxima de resultados (1-100).","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"example":50},{"name":"offset","in":"query","description":"Desplazamiento para paginar (0-10000). Para recorrer más allá usá paginación por `cursor` donde esté disponible.","schema":{"type":"integer","minimum":0,"maximum":10000,"default":0},"example":0}],"responses":{"200":{"description":"Página de stock con contadores por estado.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"type":"object","required":["data","statusCounts","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InventoryRow"},"description":"Página de stock por producto y sucursal."},"statusCounts":{"type":"object","description":"Contadores por estado de stock.","properties":{"all":{"type":"integer","description":"Total de filas producto×sucursal."},"normal":{"type":"integer","description":"Con stock normal."},"low":{"type":"integer","description":"Por debajo del stock mínimo."},"out":{"type":"integer","description":"En cero."},"negative":{"type":"integer","description":"Por debajo de cero."}}},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"example":{"data":[{"productId":"cmc3pro5d0003uy01d4p6v2r9","productName":"Notebook 14\"","sku":"NB-14","unit":"UN","branchId":"cmc1suc7f0001uy01c2n8w5t3","branchCode":"1","branchName":"Casa central","quantity":"12","minimumStock":"5","allowsNegativeStock":false,"deductsStockOnSale":true,"status":"normal"}],"statusCounts":{"all":18,"normal":14,"low":2,"out":1,"negative":1},"pagination":{"total":18,"limit":50,"offset":0}}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"El plan de la empresa no incluye el módulo de inventario, o la key no tiene el scope `inventory:read`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"El plan no incluye el módulo de inventario"}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"Parámetros inválidos"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/inventory/movements":{"get":{"operationId":"listInventoryMovements","tags":["Inventario"],"summary":"Kardex (movimientos de inventario)","description":"Historial de movimientos de stock (kardex): manuales, ajustes y automáticos por venta/NC. Requiere el módulo `inventory` en el plan. Scope requerido: `inventory:read`.","x-required-scope":"inventory:read","parameters":[{"name":"productId","in":"query","description":"Filtra por producto.","schema":{"type":"string"}},{"name":"branchId","in":"query","description":"Filtra por sucursal.","schema":{"type":"string"}},{"name":"type","in":"query","description":"Filtra por tipo de movimiento.","schema":{"type":"string","enum":["MANUAL_IN","MANUAL_OUT","ADJUSTMENT","SALE","SALE_REVERSAL","CREDIT_NOTE","CREDIT_NOTE_REVERSAL","TRANSFER_IN","TRANSFER_OUT"]}},{"name":"limit","in":"query","description":"Cantidad máxima de resultados (1-100).","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"example":50},{"name":"offset","in":"query","description":"Desplazamiento para paginar (0-10000). Para recorrer más allá usá paginación por `cursor` donde esté disponible.","schema":{"type":"integer","minimum":0,"maximum":10000,"default":0},"example":0}],"responses":{"200":{"description":"Página de movimientos.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InventoryMovement"},"description":"Página del kardex (movimientos más recientes primero)."},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"example":{"data":[{"id":"cmc5mov8n0004uy01e6q9z1s7","type":"MANUAL_IN","quantityDelta":"5","previousQuantity":"7","newQuantity":"12","reason":"Reposición de proveedor","referenceType":null,"referenceId":null,"referenceItemId":null,"productId":"cmc3pro5d0003uy01d4p6v2r9","productName":"Notebook 14\"","branchId":"cmc1suc7f0001uy01c2n8w5t3","branchCode":"1","createdByUserId":"cmc6usr2j0005uy01f8r3y6u4","createdByUserName":"Ana Rodríguez","createdAt":"2026-07-02T10:15:00.000Z"}],"pagination":{"total":240,"limit":50,"offset":0}}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"El plan de la empresa no incluye el módulo de inventario, o la key no tiene el scope `inventory:read`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"El plan no incluye el módulo de inventario"}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"Parámetros inválidos"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/inventory/adjustments":{"post":{"operationId":"createInventoryAdjustment","tags":["Inventario"],"summary":"Movimiento manual de stock","description":"Crea un movimiento manual auditado. `MANUAL_IN`/`MANUAL_OUT` llevan la cantidad movida (`quantity`); `ADJUSTMENT` es un recuento físico: lleva `countedQuantity` (el delta se calcula server-side) y `reason` obligatorio. Mismas garantías que la app: `SELECT FOR UPDATE` sobre el balance, sin stock negativo salvo que el producto lo permita. Requiere el módulo `inventory` en el plan. Scope requerido: `inventory:write`.","x-required-scope":"inventory:write","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InventoryAdjustmentRequest"},"example":{"type":"MANUAL_IN","productId":"cmc3pro5d0003uy01d4p6v2r9","branchId":"cmc1suc7f0001uy01c2n8w5t3","quantity":"5","reason":"Reposición de proveedor"}}}},"responses":{"201":{"description":"Movimiento registrado; devuelve el balance resultante.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"type":"object","required":["type"],"description":"Resultado del movimiento con el balance actualizado.","properties":{"type":{"type":"string","enum":["MANUAL_IN","MANUAL_OUT","ADJUSTMENT"],"description":"Tipo del movimiento registrado (eco del request)."}},"additionalProperties":true},"example":{"type":"MANUAL_IN","movementId":"cmc5mov8n0004uy01e6q9z1s7","productId":"cmc3pro5d0003uy01d4p6v2r9","branchId":"cmc1suc7f0001uy01c2n8w5t3","previousQuantity":"7","newQuantity":"12"}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"El plan de la empresa no incluye el módulo de inventario, o la key no tiene el scope `inventory:write`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"El plan no incluye el módulo de inventario"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"409":{"description":"Estado incompatible o `Idempotency-Key` reutilizada con otro payload / aún en curso (`IDEMPOTENCY_CONFLICT`, con `Retry-After` en el segundo caso).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"IDEMPOTENCY_CONFLICT","message":"La Idempotency-Key 'erp-op-2026-000123' ya se usó con un payload distinto."}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"El motivo del ajuste es requerido","details":{"reason":["El motivo del ajuste es requerido"]}}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}},"parameters":[{"name":"Idempotency-Key","in":"header","description":"Clave de idempotencia (8-128 caracteres, única por empresa y operación). Un reintento con la misma clave y el mismo body devuelve la respuesta original (header `Idempotency-Replayed: true`) durante 24 h; la misma clave con otro body responde `409 IDEMPOTENCY_CONFLICT`; si el primer intento sigue en curso, `409` con `Retry-After`.","schema":{"type":"string","minLength":8,"maxLength":128},"example":"erp-op-2026-000123"}]}},"/api/v1/payments":{"post":{"operationId":"registerPayment","tags":["Cobros"],"summary":"Registrar un cobro","description":"Registra un cobro de cliente (total, parcial o a cuenta), opcionalmente imputado a comprobantes. `contactId: null` = consumidor final (exige al menos una imputación). Reglas: los documentos imputados deben ser emitidos ACCEPTED/OBSERVED, del mismo receptor y moneda, y ninguna imputación puede superar el saldo pendiente. Idempotente vía `Idempotency-Key` (header) o `idempotencyKey` (body): un reintento con la misma clave devuelve el cobro ya creado. El registro NUNCA modifica el comprobante fiscal. Scope requerido: `payments:write`.","x-required-scope":"payments:write","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["contactId","paidOn","amount","currency","method","allocations"],"properties":{"contactId":{"type":["string","null"],"description":"Contacto receptor, o `null` para consumidor final."},"paidOn":{"type":"string","description":"Fecha del cobro (YYYY-MM-DD, zona de la app)."},"amount":{"type":"string","description":"Monto total del cobro (decimal como string)."},"currency":{"type":"string","description":"ISO-4217 de 3 letras."},"method":{"type":"string","enum":["TRANSFER","CASH","CHECK","CARD","OTHER"],"description":"Medio de cobro: TRANSFER (transferencia), CASH (efectivo), CHECK (cheque), CARD (tarjeta), OTHER."},"reference":{"type":"string","description":"Referencia externa del cobro (nº de transferencia, cheque, etc.)."},"note":{"type":"string","description":"Nota interna (opcional)."},"idempotencyKey":{"type":"string","minLength":8,"maxLength":128,"description":"Idempotencia opcional (o header `Idempotency-Key`, que gana). Un reintento con la misma clave devuelve el cobro ya creado."},"allocations":{"type":"array","description":"Imputaciones a comprobantes. Vacío = pago a cuenta.","items":{"type":"object","required":["fiscalDocumentId","amount"],"properties":{"fiscalDocumentId":{"type":"string","description":"Id del comprobante al que se imputa."},"amount":{"type":"string","description":"Monto imputado (string decimal, ≤ saldo pendiente del comprobante)."}}}}},"additionalProperties":false},"example":{"contactId":"cmc2rec9k0002uy01b7m1x4q8","paidOn":"2026-06-15","amount":"5000.00","currency":"UYU","method":"TRANSFER","reference":"TRX-9931","allocations":[{"fiscalDocumentId":"cmc4x8p2h0001uy01a9k3f7d2","amount":"5000.00"}]}}}},"responses":{"201":{"description":"Cobro registrado.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"Id del cobro registrado."}}},"example":{"id":"cmc6pay1a0001uy01f7r2k8m4"}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`payments:write`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"409":{"description":"Algún documento no es cobrable o la imputación supera el saldo pendiente.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"La imputación supera el saldo pendiente del documento"}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"Un cobro a consumidor final debe imputarse a al menos un comprobante.","details":{"allocations":["Un cobro a consumidor final debe imputarse a al menos un comprobante."]}}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}},"parameters":[{"name":"Idempotency-Key","in":"header","description":"Clave de idempotencia (8-128 caracteres, única por empresa y operación). Un reintento con la misma clave y el mismo body devuelve la respuesta original (header `Idempotency-Replayed: true`) durante 24 h; la misma clave con otro body responde `409 IDEMPOTENCY_CONFLICT`; si el primer intento sigue en curso, `409` con `Retry-After`.","schema":{"type":"string","minLength":8,"maxLength":128},"example":"erp-op-2026-000123"}]}},"/api/v1/payments/{id}/allocations":{"post":{"operationId":"applyPaymentAllocations","tags":["Cobros"],"summary":"Aplicar crédito de un cobro a cuenta","description":"Imputa el crédito disponible de un cobro a cuenta existente (monto − imputaciones activas) a comprobantes del mismo receptor y moneda. No crea un cobro nuevo ni modifica el registro fiscal. Scope requerido: `payments:write`.","x-required-scope":"payments:write","parameters":[{"name":"id","in":"path","required":true,"description":"Identificador del cobro a cuenta.","schema":{"type":"string","pattern":"^[a-zA-Z0-9]{20,32}$"},"example":"cmc4x8p2h0001uy01a9k3f7d2"},{"name":"Idempotency-Key","in":"header","description":"Clave de idempotencia (8-128 caracteres, única por empresa y operación). Un reintento con la misma clave y el mismo body devuelve la respuesta original (header `Idempotency-Replayed: true`) durante 24 h; la misma clave con otro body responde `409 IDEMPOTENCY_CONFLICT`; si el primer intento sigue en curso, `409` con `Retry-After`.","schema":{"type":"string","minLength":8,"maxLength":128},"example":"erp-op-2026-000123"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["allocations"],"properties":{"allocations":{"type":"array","minItems":1,"description":"Imputaciones a comprobantes del mismo receptor y moneda.","items":{"type":"object","required":["fiscalDocumentId","amount"],"properties":{"fiscalDocumentId":{"type":"string","description":"Id del comprobante al que se imputa."},"amount":{"type":"string","description":"Monto imputado (string decimal, ≤ crédito disponible y ≤ saldo del comprobante)."}}}}},"additionalProperties":false},"example":{"allocations":[{"fiscalDocumentId":"cmc4x8p2h0001uy01a9k3f7d2","amount":"2500.00"}]}}}},"responses":{"200":{"description":"Crédito aplicado; devuelve lo imputado y el crédito restante.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"type":"object","required":["applied","remainingCredit"],"properties":{"applied":{"type":"string","description":"Total imputado en esta llamada (string decimal)."},"remainingCredit":{"type":"string","description":"Crédito a cuenta restante del cobro (string decimal)."}}},"example":{"applied":"2500.00","remainingCredit":"500.00"}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`payments:write`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"409":{"description":"El cobro está anulado, o las imputaciones superan el crédito disponible o el saldo del documento.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"Las imputaciones superan el crédito disponible del cobro"}}}}},"422":{"description":"Query o body inválidos (validación zod) o requisito de negocio incumplido.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION_ERROR","message":"Se requiere al menos una imputación.","details":{"allocations":["Se requiere al menos una imputación."]}}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/payments/{id}":{"delete":{"operationId":"cancelPayment","tags":["Cobros"],"summary":"Anular un cobro","description":"Anulación lógica: el cobro se conserva con AuditLog y deja de contar en todos los saldos. Un cobro inexistente o ya anulado devuelve `404`. Scope requerido: `payments:write`.","x-required-scope":"payments:write","parameters":[{"name":"id","in":"path","required":true,"description":"Identificador del cobro a anular.","schema":{"type":"string","pattern":"^[a-zA-Z0-9]{20,32}$"},"example":"cmc4x8p2h0001uy01a9k3f7d2"},{"name":"Idempotency-Key","in":"header","description":"Clave de idempotencia (8-128 caracteres, única por empresa y operación). Un reintento con la misma clave y el mismo body devuelve la respuesta original (header `Idempotency-Replayed: true`) durante 24 h; la misma clave con otro body responde `409 IDEMPOTENCY_CONFLICT`; si el primer intento sigue en curso, `409` con `Retry-After`.","schema":{"type":"string","minLength":8,"maxLength":128},"example":"erp-op-2026-000123"}],"responses":{"200":{"description":"Cobro anulado.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"type":"object","required":["id","cancelled"],"properties":{"id":{"type":"string","description":"Id del cobro anulado."},"cancelled":{"type":"boolean","description":"true = el cobro quedó anulado (deja de contar en saldos)."}}},"example":{"id":"cmc6pay1a0001uy01f7r2k8m4","cancelled":true}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`payments:write`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"409":{"description":"Estado incompatible o `Idempotency-Key` reutilizada con otro payload / aún en curso (`IDEMPOTENCY_CONFLICT`, con `Retry-After` en el segundo caso).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"IDEMPOTENCY_CONFLICT","message":"La Idempotency-Key 'erp-op-2026-000123' ya se usó con un payload distinto."}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/payments/receivables":{"get":{"operationId":"listReceivables","tags":["Cobros"],"summary":"Cuentas por cobrar","description":"Saldos pendientes de cobro de comprobantes emitidos ACCEPTED/OBSERVED, agrupados por moneda y con antigüedad (aging): a vencer / vencido 1–30 / 31–60 / +60 días / sin vencimiento. Las notas de crédito quedan excluidas. Scope requerido: `payments:read`.","x-required-scope":"payments:read","responses":{"200":{"description":"Cuentas por cobrar por moneda.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"type":"object","required":["truncated","blocks"],"properties":{"truncated":{"type":"boolean","description":"true si la lista de filas fue recortada por tamaño (consultar con filtros más finos)."},"blocks":{"type":"array","description":"Un bloque por moneda con saldos pendientes.","items":{"type":"object","additionalProperties":true,"properties":{"currency":{"type":"string","description":"Moneda del bloque (ISO 4217)."},"totalOutstanding":{"type":"string","description":"Saldo pendiente total del bloque (string decimal)."},"bucketTotals":{"type":"object","description":"Totales por antigüedad: NOT_DUE (a vencer), D1_30, D31_60, D60_PLUS (días vencidos), NO_DUE_DATE (sin vencimiento).","properties":{"NOT_DUE":{"type":"string","description":"A vencer."},"D1_30":{"type":"string","description":"Vencido 1-30 días."},"D31_60":{"type":"string","description":"Vencido 31-60 días."},"D60_PLUS":{"type":"string","description":"Vencido más de 60 días."},"NO_DUE_DATE":{"type":"string","description":"Sin fecha de vencimiento."}}},"rows":{"type":"array","description":"Comprobantes con saldo pendiente, más vencidos primero.","items":{"type":"object","additionalProperties":true,"properties":{"documentId":{"type":"string","description":"Id del comprobante."},"label":{"type":"string","description":"Etiqueta legible (tipo + serie-número)."},"receiverId":{"type":["string","null"],"description":"Id del contacto receptor."},"receiverName":{"type":["string","null"],"description":"Nombre del receptor."},"issueDate":{"type":["string","null"],"description":"Fecha de emisión (ISO 8601)."},"dueDate":{"type":["string","null"],"description":"Fecha de vencimiento (ISO 8601); null si no tiene."},"daysOverdue":{"type":"integer","description":"Días vencidos (0 si aún no vence)."},"bucket":{"type":"string","enum":["NOT_DUE","D1_30","D31_60","D60_PLUS","NO_DUE_DATE"],"description":"Bucket de antigüedad del saldo."},"total":{"type":"string","description":"Total del comprobante (string decimal)."},"allocated":{"type":"string","description":"Cobros imputados (string decimal)."},"outstanding":{"type":"string","description":"Saldo pendiente (string decimal)."}}}}}}}}},"example":{"truncated":false,"blocks":[{"currency":"UYU","totalOutstanding":"12500.00","bucketTotals":{"NOT_DUE":"2500.00","D1_30":"10000.00","D31_60":"0.00","D60_PLUS":"0.00","NO_DUE_DATE":"0.00"},"rows":[{"documentId":"cmc4x8p2h0001uy01a9k3f7d2","label":"e-Factura A-102","receiverId":"cmc2rec9k0002uy01b7m1x4q8","receiverName":"Estudio Méndez SRL","issueDate":"2026-06-01T12:00:00.000Z","dueDate":"2026-06-30T00:00:00.000Z","daysOverdue":6,"bucket":"D1_30","total":"12500.00","allocated":"0.00","outstanding":"12500.00"}]}]}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`payments:read`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}},"/api/v1/documents/{id}/collection":{"get":{"operationId":"getDocumentCollection","tags":["Cobros"],"summary":"Estado de cobro de un comprobante","description":"Estado de cobro derivado de un comprobante: `PENDING`/`PARTIAL`/`PAID`, total, imputado, saldo y los cobros aplicados. Scope requerido: `payments:read`.","x-required-scope":"payments:read","parameters":[{"name":"id","in":"path","required":true,"description":"Identificador del comprobante.","schema":{"type":"string","pattern":"^[a-zA-Z0-9]{20,32}$"},"example":"cmc4x8p2h0001uy01a9k3f7d2"}],"responses":{"200":{"description":"Estado de cobro del comprobante.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"type":"object","required":["status","outstanding"],"additionalProperties":true,"properties":{"collectible":{"type":"boolean","description":"Si admite cobros: emitido, aceptado u observado por DGI, no recibo de cobranza y con la cobranza gestionada en facturar.uy."},"collectionTracking":{"type":"string","enum":["TRACKED","NOT_TRACKED"],"description":"Si la cobranza se gestiona en facturar.uy. NOT_TRACKED: cobrado o gestionado fuera (p. ej. histórico importado de otro proveedor); no es cuenta a cobrar ni admite cobros."},"status":{"type":"string","enum":["PENDING","PARTIAL","PAID"],"description":"Estado de cobro: PENDING (sin cobros), PARTIAL (cobrado en parte), PAID (saldado)."},"outstanding":{"type":"string","description":"Saldo pendiente de cobro (string decimal)."}}},"example":{"collectible":true,"collectionTracking":"TRACKED","status":"PARTIAL","currency":"UYU","total":"12500.00","allocated":"5000.00","outstanding":"7500.00","contactId":"cmc2rec9k0002uy01b7m1x4q8","payments":[{"paymentId":"cmc6pay1a0001uy01f7r2k8m4","allocationId":"cmc6all2b0002uy01g8s3l9n5","paidOn":"2026-06-15T00:00:00.000Z","amount":"5000.00","method":"TRANSFER","reference":"TRX-9931","note":null}]}}}},"401":{"description":"API key faltante, malformada, revocada o vencida. Mismo mensaje para key inexistente (sin oráculo de enumeración).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"API key inválida"}}}}},"403":{"description":"La API key no tiene el scope requerido (`payments:read`).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"La API key no tiene permisos para esta operación"}}}}},"404":{"description":"Recurso inexistente o perteneciente a otra empresa (misma respuesta en ambos casos: la key nunca ve datos de otro tenant).","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Recurso no encontrado"}}}}},"429":{"description":"Límite de solicitudes superado (240 req/min por IP o 120 req/min por API key). Incluye header `Retry-After`.","headers":{"X-RateLimit-Limit":{"description":"Límite de solicitudes por minuto de la capa aplicada (120/min por API key; 240/min por IP).","schema":{"type":"integer","example":120}},"X-RateLimit-Remaining":{"description":"Solicitudes restantes en la ventana actual.","schema":{"type":"integer","example":118}},"X-RateLimit-Reset":{"description":"Segundos hasta el reinicio de la ventana.","schema":{"type":"integer","example":32}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Demasiadas solicitudes, reintentá en unos segundos"}}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"afk_live_…","description":"API key por empresa: `Authorization: Bearer afk_live_<secreto>`. Se administra en `/app/configuracion/api-keys` (roles OWNER/ADMIN). El secreto se muestra una única vez al crear la key; solo se persiste su hash SHA-256. Keys revocadas o vencidas devuelven `401` con el mismo mensaje que una key inexistente."}},"schemas":{"Error":{"type":"object","title":"Envelope de error","description":"Envelope estable de error. `code` es máquina-legible y estable; `message` es humano-legible y puede cambiar; `details` es opcional (p.ej. errores de campo de zod o `documentId` en 422 de emisión).","required":["error"],"properties":{"error":{"type":"object","description":"Detalle del error (código estable + mensaje humano-legible).","required":["code","message"],"properties":{"code":{"type":"string","description":"Código de error estable.","enum":["UNAUTHORIZED","FORBIDDEN","NOT_FOUND","CONFLICT","VALIDATION_ERROR","RATE_LIMITED","INTERNAL_ERROR"]},"message":{"type":"string","description":"Descripción humano-legible del error."},"details":{"description":"Detalle opcional: errores de campo (zod) o metadatos como `documentId` cuando un borrador quedó creado pero no pudo emitirse.","type":"object","additionalProperties":true}}}}},"Pagination":{"type":"object","description":"Metadatos de paginación de los listados.","required":["limit","offset"],"properties":{"total":{"type":"integer","description":"Total de resultados que matchean los filtros. Presente salvo que el listado acepte `includeTotal` y se haya enviado `false`."},"limit":{"type":"integer","description":"Límite aplicado (1-100)."},"offset":{"type":"integer","description":"Desplazamiento aplicado."}}},"DocumentSummary":{"type":"object","title":"Comprobante (resumen)","description":"Resumen de un comprobante fiscal (emitido o recibido). `total` es un string decimal; `issueDate` es fecha calendario `YYYY-MM-DD`.","required":["id","status","direction","documentType","currency","total","createdAt","updatedAt"],"properties":{"id":{"type":"string","description":"Id del comprobante (usarlo en GET /documents/{id}, /pdf, /xml, /issue)."},"status":{"type":"string","enum":["DRAFT","READY_TO_ISSUE","ISSUING","SIGNED","SUBMITTED","ACCEPTED","REJECTED","OBSERVED","CANCELLED","FAILED","CONTINGENCY"],"description":"Estado del ciclo de vida: DRAFT (borrador) → READY_TO_ISSUE → ISSUING → SIGNED → SUBMITTED → ACCEPTED/REJECTED/OBSERVED por DGI; CANCELLED (anulado antes de emitir), FAILED (falló tras reintentos), CONTINGENCY (emitido como CFC)."},"direction":{"type":"string","enum":["ISSUED","RECEIVED"],"description":"ISSUED = emitido por tu empresa; RECEIVED = recibido de otro emisor electrónico (WS Intercambio)."},"documentType":{"type":"object","description":"Tipo de comprobante DGI (p.ej. 101 e-Ticket, 111 e-Factura).","properties":{"code":{"type":"integer","example":111,"description":"Código DGI del tipo, p.ej. 101, 111, 112, 181, 182."},"name":{"type":"string","example":"e-Factura","description":"Nombre del tipo de comprobante (snapshot informativo)."},"kind":{"type":"string","enum":["CFE","CFC"],"example":"CFE","description":"CFE = comprobante electrónico normal (códigos 1xx); CFC = de contingencia (códigos 2xx)."}}},"series":{"type":["string","null"],"description":"Serie fiscal (null en borradores)."},"number":{"type":["integer","null"],"description":"Número fiscal (null hasta la reserva CAE)."},"issueDate":{"type":["string","null"],"format":"date","description":"Fecha de emisión (YYYY-MM-DD); null en borradores sin fecha."},"currency":{"type":"string","example":"UYU","description":"Moneda ISO 4217 (UYU, USD, EUR, …)."},"total":{"type":"string","description":"Total del comprobante. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"receiver":{"type":["object","null"],"description":"Contacto receptor; null para consumidor final o borradores sin receptor.","properties":{"legalName":{"type":"string","description":"Razón social o nombre del receptor."},"code":{"type":["string","null"],"description":"Código interno del contacto (Contact.code, único por empresa); null si el contacto no tiene código."}}},"receiverDeliveryStatus":{"type":"string","enum":["NOT_REQUIRED","PENDING","SENT","FAILED","SKIPPED"],"description":"Estado de entrega al receptor (email / WS Intercambio): NOT_REQUIRED (no aplica — sin receptor a quien entregar), PENDING (en cola), SENT (entregado), FAILED (falló tras reintentos), SKIPPED (existe receptor pero la entrega se omitió deliberadamente, p. ej. email deshabilitado)."},"createdAt":{"type":"string","format":"date-time","description":"Momento de creación del registro (ISO 8601)."},"updatedAt":{"type":"string","format":"date-time","description":"Última modificación del registro (ISO 8601)."}}},"DocumentDetail":{"title":"Comprobante (detalle)","description":"Detalle completo: identidad fiscal, totales (strings decimales), líneas (con sus retenciones/percepciones B-C20..B-C23), pagos, ajustes, referencias, CAE, tracking DGI, receptor y sucursal.","allOf":[{"$ref":"#/components/schemas/DocumentSummary"},{"type":"object","properties":{"receiver":{"type":["object","null"],"description":"Receptor identificado por su código interno; null para consumidor final.","properties":{"legalName":{"type":"string","description":"Razón social o nombre del receptor."},"code":{"type":"string","description":"Código interno del contacto (Contact.code, único por empresa; obligatorio)."}}},"exchangeRate":{"type":["string","null"],"description":"Tipo de cambio (string decimal) cuando la moneda no es UYU."},"operationType":{"type":["string","null"],"description":"Tipo de operación: SERVICE (servicios) o PRODUCT (bienes); null si no se cargó."},"paymentType":{"type":["string","null"],"description":"Condición de pago: CASH (contado), CREDIT (crédito) o NOT_APPLICABLE; null si no se cargó."},"dueDate":{"type":["string","null"],"format":"date","description":"Fecha de vencimiento (YYYY-MM-DD); null si no aplica."},"periodFrom":{"type":["string","null"],"format":"date","description":"Inicio del período facturado (servicios); null si no aplica."},"periodTo":{"type":["string","null"],"format":"date","description":"Fin del período facturado (servicios); null si no aplica."},"createdVia":{"type":"string","enum":["WIZARD","API","RECURRING","INTEGRATION"],"description":"Origen de creación: WIZARD (app), API (esta API), RECURRING (regla recurrente), INTEGRATION (orden de canal de venta)."},"subtotal":{"type":"string","description":"Subtotal sin impuestos. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"taxTotal":{"type":"string","description":"Total de impuestos. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"totalAmount":{"type":["string","null"],"description":"Monto total del comprobante (string decimal, precisión DGI)."},"totalRetentionPerceptionAmount":{"type":["string","null"],"description":"Total retenciones/percepciones de encabezado."},"totalFiscalCreditAmount":{"type":["string","null"],"description":"Total créditos fiscales de encabezado."},"payableAmount":{"type":["string","null"],"description":"Monto a pagar."},"receiverDocument":{"type":["object","null"],"description":"Identidad fiscal del receptor; null para consumidor final.","properties":{"legalName":{"type":"string","description":"Razón social del receptor."},"documentNumber":{"type":["string","null"],"description":"Número de documento fiscal del receptor (RUC, CI, etc.) como string — puede tener ceros a la izquierda."}}},"branch":{"type":["object","null"],"description":"Sucursal emisora; null en borradores sin sucursal.","properties":{"code":{"type":"string","description":"Código de sucursal DGI (único por empresa)."},"name":{"type":["string","null"],"description":"Nombre de la sucursal."}}},"cae":{"type":["object","null"],"description":"Constancia de Autorización de Emisión asignada al comprobante; null si aún no hay reserva de numeración.","properties":{"number":{"type":"string","description":"Número de autorización CAE (11 dígitos DGI)."},"series":{"type":"string","description":"Serie autorizada por el CAE."},"fromNumber":{"type":"integer","description":"Inicio del rango de numeración autorizado."},"toNumber":{"type":"integer","description":"Fin del rango de numeración autorizado."},"expirationDate":{"type":"string","format":"date","description":"Vencimiento del CAE (YYYY-MM-DD)."}}},"dgi":{"type":"object","description":"Tracking del envío a DGI.","properties":{"trackingId":{"type":["string","null"],"description":"Id de seguimiento asignado por DGI al sobre; null hasta el envío."},"submittedAt":{"type":["string","null"],"format":"date-time","description":"Momento del envío a DGI (ISO 8601); null hasta el envío."},"ackSobreCode":{"type":["string","null"],"description":"Estado del ACKSobre DGI: \"AS\" (sobre aceptado), \"BS\" (sobre rechazado) o \"BA\" (archivo rechazado); null hasta la respuesta."}}},"lines":{"type":"array","items":{"$ref":"#/components/schemas/DocumentLine"},"description":"Líneas del comprobante (Zona B), en orden."},"payments":{"type":"array","description":"Zona E — medios de pago.","items":{"type":"object","properties":{"lineNumber":{"type":"integer","description":"Secuencia 1-based del medio de pago."},"paymentMethodCode":{"type":["integer","null"],"description":"Código del medio de pago (catálogo DGI); null si no se informó."},"paymentMethodLabel":{"type":["string","null"],"description":"Glosa del medio de pago."},"amount":{"type":"string","description":"Monto del pago. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"}}}},"adjustments":{"type":"array","description":"Zona D — descuentos/recargos globales, con el monto resuelto server-side.","items":{"type":"object","properties":{"lineNumber":{"type":"integer","description":"Secuencia 1-based del ajuste."},"movementType":{"type":"string","enum":["DISCOUNT","SURCHARGE"],"description":"DISCOUNT = descuento global; SURCHARGE = recargo global."},"adjustmentType":{"type":["integer","null"],"description":"1 = monto, 2 = porcentaje."},"description":{"type":["string","null"],"description":"Glosa del descuento/recargo."},"code":{"type":["integer","null"],"description":"Código del ajuste (catálogo DGI); null si no se informó."},"percent":{"type":["string","null"],"description":"Porcentaje (string decimal) cuando el tipo es 2; null en ajustes por monto."},"amount":{"type":["string","null"],"description":"Monto monetario resuelto (también para ajustes porcentuales)."},"billingIndicator":{"type":["integer","null"],"description":"Indicador de facturación del ajuste (1-17)."}}}},"references":{"type":"array","description":"Zona F — referencias a otros comprobantes (NC/ND).","items":{"type":"object","properties":{"lineNumber":{"type":"integer","description":"Secuencia 1-based de la referencia."},"isGlobalReference":{"type":"boolean","description":"true = referencia global (sin comprobante específico; requiere referenceReason)."},"referencedDocumentTypeCode":{"type":["integer","null"],"description":"Código DGI del tipo del comprobante referenciado."},"referencedSeries":{"type":["string","null"],"description":"Serie del comprobante referenciado."},"referencedNumber":{"type":["integer","null"],"description":"Número del comprobante referenciado."},"referencedIssueDate":{"type":["string","null"],"format":"date","description":"Fecha del comprobante referenciado (YYYY-MM-DD)."},"referenceReason":{"type":["string","null"],"description":"Razón de la referencia (obligatoria si es global)."},"referencedAmount":{"type":["string","null"],"description":"Monto referenciado (string decimal); null si no se informó."}}}}}}]},"DocumentLine":{"type":"object","title":"Línea de comprobante","properties":{"lineNumber":{"type":["integer","null"],"description":"Número de línea 1-based."},"description":{"type":"string","description":"Nombre del ítem."},"quantity":{"type":"string","description":"Cantidad. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"unit":{"type":["string","null"],"description":"Unidad de medida (\"N/A\" cuando no aplica)."},"unitPrice":{"type":"string","description":"Precio unitario. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"taxRate":{"type":["string","null"],"description":"Tasa de IVA aplicada como fracción (p.ej. \"0.22\"), null si no aplica."},"billingIndicator":{"type":["integer","null"],"description":"Indicador de facturación."},"productCode":{"type":["string","null"],"description":"SKU del producto referenciado (Product.sku, código único por empresa); null si la línea no referencia un producto del catálogo."},"subtotal":{"type":"string","description":"Subtotal de la línea. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"tax":{"type":"string","description":"Impuesto de la línea. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"total":{"type":"string","description":"Total de la línea. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"retentionPerceptions":{"type":"array","description":"B-C20..B-C23 — retenciones/percepciones/crédito fiscal de la línea (máx. 5). A-C125/A-C125.1 y la tabla Totales se derivan de estas por código (los códigos 2181xxx son crédito fiscal).","items":{"type":"object","properties":{"code":{"type":"string","description":"B-C20: código DGI de retención/percepción/crédito fiscal."},"rate":{"type":["string","null"],"description":"B-C21: tasa como porcentaje («5.000» = 5%). null cuando el CFE no la informa (opcional en el XSD)."},"subjectAmount":{"type":"string","description":"B-C22: monto sujeto a retención/percepción/crédito fiscal. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"additionalInfo":{"type":["string","null"],"description":"B-C22.1: información adicional."},"retentionValue":{"type":"string","description":"B-C23: valor de la retención/percepción/crédito fiscal. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"}}}}}},"LineInput":{"type":"object","title":"Línea (creación)","description":"Campos principales de una línea (Zona B del CFE). El resto de los campos DGI (descripción adicional, NCM, retenciones/percepciones, indicador agente/responsable, etc.) se acepta con los mismos nombres y validaciones que la app — ver docs/api/public-api.md.","required":["unitPrice"],"properties":{"description":{"type":"string","maxLength":80,"description":"Nombre del ítem. Requerido para emitir, salvo e-Resguardo (182/282)."},"quantity":{"type":"string","description":"Cantidad — string decimal, máx. 14 enteros y 3 decimales. Requerido para emitir, salvo e-Resguardo (182/282).","example":"1"},"unit":{"type":["string","null"],"maxLength":4,"description":"Unidad de medida. Requerido para emitir, salvo e-Resguardo (182/282).","example":"N/A"},"unitPrice":{"type":"string","description":"Precio unitario — string decimal, máx. 11 enteros y 6 decimales.","example":"10000"},"billingIndicator":{"type":"integer","minimum":1,"maximum":17,"description":"Indicador de facturación (1-17; p.ej. 1 exento, 2 tasa mínima, 3 tasa básica). Requerido para emitir, salvo remitos y e-Resguardo. El rango válido depende del tipo de CFE. Para 2/3 el IVA se calcula automáticamente con las tasas vigentes si no se envía `taxRate`."},"productCode":{"type":"string","maxLength":50,"description":"SKU del producto (Product.sku, único por empresa) para pre-cargar la línea y descontar stock si corresponde.","example":"SRV-CONSULT"},"itemCodes":{"type":"array","maxItems":5,"description":"Códigos del ítem (hasta 5 pares). Los GTIN se validan con dígito verificador GS1.","items":{"type":"object","required":["codeType","code"],"properties":{"codeType":{"type":"string","maxLength":10,"example":"INT1","description":"Tipo de código: EAN/GTIN8/GTIN12/GTIN13/GTIN14 (con dígito verificador GS1), INT1/INT2 (interno), PLU, DUN14, etc."},"code":{"type":"string","maxLength":35,"description":"Valor del código del ítem."}}}},"discountPercent":{"type":"string","description":"Descuento en porcentaje (string decimal)."},"discountAmount":{"type":"string","description":"Descuento en monto (string decimal)."},"surchargePercent":{"type":"string","description":"Recargo en porcentaje (string decimal)."},"surchargeAmount":{"type":"string","description":"Recargo en monto (string decimal)."},"taxRate":{"type":"string","description":"Tasa de IVA como fracción (p.ej. \"0.22\"). Solo requerida cuando el indicador de facturación es 4 (otra tasa); para 2/3 se deriva automáticamente de las tasas vigentes."},"responsibleIndicator":{"type":"string","maxLength":1,"description":"B-C6: indicador Agente/Responsable (\"R\" o \"A\"). Obligatorio si la línea informa retenciones/percepciones, salvo en e-Resguardo (182/282), donde no corresponde."},"retentionPerceptions":{"type":"array","maxItems":5,"description":"B-C20..B-C23: retenciones/percepciones/crédito fiscal de la línea (máx. 5). A-C125/A-C125.1 y la tabla Totales/RetencPercep se derivan de estas por código (2181xxx = crédito fiscal).","items":{"type":"object","required":["retentionCode","rate","subjectAmount","retentionValue"],"properties":{"retentionCode":{"type":"string","maxLength":8,"description":"B-C20: código DGI (numérico; 9999001-9999999 de libre uso).","example":"2183114"},"rate":{"type":"string","description":"B-C21: tasa (NUM 6, máx. 3 enteros y 3 decimales).","example":"15.000"},"subjectAmount":{"type":"string","description":"B-C22: monto sujeto a retención/percepción (> 0).","example":"1000.00"},"additionalInfo":{"type":"string","maxLength":150,"description":"B-C22.1: información adicional (opcional)."},"retentionValue":{"type":"string","description":"B-C23: valor de la retención/percepción/crédito fiscal (> 0).","example":"150.00"}}}}}},"PaymentInput":{"type":"object","title":"Medio de pago (Zona E)","required":["lineNumber","paymentMethodLabel","amount"],"properties":{"lineNumber":{"type":"integer","minimum":1,"description":"Secuencia 1-based del medio de pago (máx. 40 por comprobante)."},"paymentMethodCode":{"type":"integer","description":"Código del medio de pago (catálogo DGI)."},"paymentMethodLabel":{"type":"string","maxLength":150,"description":"Glosa del medio de pago."},"printOrder":{"type":"integer","description":"Orden de impresión del medio de pago en el PDF (opcional)."},"amount":{"type":"string","description":"Monto del pago. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"}}},"ReferenceInput":{"type":"object","title":"Referencia (Zona F)","description":"Referencia a otro documento. Obligatoria para NC/ND. Si `isGlobalReference` es true, `referenceReason` es obligatoria y los campos de documento específico deben omitirse.","required":["lineNumber"],"properties":{"lineNumber":{"type":"integer","minimum":1,"description":"Secuencia 1-based de la referencia."},"isGlobalReference":{"type":"boolean","default":false,"description":"true = referencia global (requiere referenceReason y prohíbe los campos del comprobante específico)."},"referencedDocumentTypeCode":{"type":"integer","minimum":100,"maximum":299,"description":"Código DGI del tipo del comprobante referenciado (p.ej. 111)."},"referencedSeries":{"type":"string","description":"Serie del comprobante referenciado."},"referencedNumber":{"type":"integer","minimum":1,"maximum":9999999,"description":"Número del comprobante referenciado."},"referencedIssueDate":{"type":"string","format":"date-time","description":"Fecha del comprobante referenciado (ISO 8601 con offset)."},"referenceReason":{"type":"string","description":"Razón de la referencia — obligatoria cuando isGlobalReference es true."},"referencedAmount":{"type":"string","description":"Monto referenciado. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"referenceType":{"type":"integer","description":"F-C2.1: tipo de referencia (código DGI). Opcional."},"referencedCurrency":{"type":"string","description":"Moneda del comprobante referenciado (ISO 4217, p.ej. UYU)."},"referencedExchangeRate":{"type":"string","description":"Tipo de cambio del comprobante referenciado. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"}}},"AdjustmentInput":{"type":"object","title":"Ajuste global (Zona D)","description":"Descuento o recargo global del comprobante (Zona D del CFE). Por monto (`adjustmentType: 1` + `amount`) o por porcentaje (`adjustmentType: 2` + `percent`). Los totales se recalculan server-side igual que en la app.","required":["lineNumber","movementType","adjustmentType","description","billingIndicator"],"properties":{"lineNumber":{"type":"integer","minimum":1,"description":"Secuencia 1-based del ajuste."},"movementType":{"type":"string","enum":["DISCOUNT","SURCHARGE"],"description":"Descuento o recargo global."},"adjustmentType":{"type":"integer","enum":[1,2],"description":"Tipo: 1 = monto ($), 2 = porcentaje (%)."},"description":{"type":"string","maxLength":100,"description":"Glosa del descuento/recargo.","example":"Descuento por pronto pago"},"code":{"type":"integer","description":"Código del ajuste (catálogo DGI, opcional)."},"percent":{"type":"string","description":"Porcentaje (string decimal) cuando `adjustmentType: 2`.","example":"10,00"},"amount":{"type":"string","description":"Monto (string decimal, > 0) cuando `adjustmentType: 1`. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"billingIndicator":{"type":"integer","minimum":1,"maximum":17,"description":"Indicador de facturación del ajuste (1-17)."}}},"CreateDocumentRequest":{"type":"object","title":"Creación de comprobante","description":"Compone en una llamada los mismos pasos del wizard de la app (borrador → datos generales → líneas → datos adicionales; las retenciones/percepciones van POR LÍNEA en `lines[].retentionPerceptions`). `series`/`number` se ignoran: la numeración la gestiona la reserva de CAE. Los CFC (2xx) se rechazan y, si la empresa tiene whitelist de tipos autorizados por DGI, un tipo fuera de ella responde 422. El receptor debe existir como contacto o usarse `receiverMode: \"FINAL_CONSUMER\"`. Sucursal, receptor y productos se referencian por su código único por empresa (`branchCode`/`receiverCode`/`productCode`); los ids internos NO son parte del contrato público.","required":["documentTypeCode","lines"],"properties":{"documentTypeCode":{"type":"integer","minimum":100,"maximum":299,"description":"Código DGI del tipo de comprobante (p.ej. 101 e-Ticket, 111 e-Factura)."},"branchCode":{"type":"string","description":"Código de sucursal (único por empresa). Si no se envía, se usa la sucursal por defecto de la empresa. Descubrí los códigos con GET /branches.","example":"1"},"receiverCode":{"type":"string","description":"Código interno del contacto receptor (Contact.code, único por empresa); crealo antes con POST /contacts. Requerido para emitir, salvo consumidor final (`receiverMode: \"FINAL_CONSUMER\"`).","example":"CLI-001"},"receiverMode":{"type":"string","enum":["CONTACT","FINAL_CONSUMER"],"default":"CONTACT","description":"CONTACT = receptor identificado (requiere receiverCode); FINAL_CONSUMER = consumidor final (solo tipos e-Ticket)."},"currency":{"type":"string","default":"UYU","description":"Moneda ISO 4217 (UYU, USD, EUR, …). Si no es UYU se requiere tipo de cambio al emitir."},"exchangeRate":{"type":"string","description":"Tipo de cambio (string decimal) si la moneda no es UYU."},"issueDate":{"type":"string","format":"date","description":"Fecha de emisión. Si se omite, se usa la fecha de hoy (zona horaria de la app) — igual que el wizard."},"valueDate":{"type":"string","format":"date","description":"Fecha valor — solo e-Resguardo (182/282)."},"dueDate":{"type":"string","format":"date","description":"Fecha de vencimiento (típicamente con `paymentType: \"CREDIT\"`)."},"periodFrom":{"type":"string","format":"date","description":"Período desde — operaciones de servicios."},"periodTo":{"type":"string","format":"date","description":"Período hasta — operaciones de servicios."},"paymentType":{"type":"string","enum":["CASH","CREDIT","NOT_APPLICABLE"],"description":"Condición de pago: CASH (contado), CREDIT (crédito) o NOT_APPLICABLE. Requerido para emitir en la mayoría de los tipos."},"operationType":{"type":"string","enum":["SERVICE","PRODUCT"],"description":"Tipo de operación: SERVICE (servicios) o PRODUCT (bienes). Requerido para emitir cuando hay receptor identificado."},"ivaStatusIndicator":{"type":"integer","enum":[1,2,3],"description":"Indicador IVA al día (2/3 solo para tipos VCA)."},"professionalSecretIndicator":{"type":"integer","enum":[1,2],"description":"Indicador secreto profesional."},"purchaseIdentificationNumber":{"type":"string","maxLength":50,"description":"Nº identificación compra."},"notes":{"type":"string","description":"Notas internas — NO se envían a DGI."},"adenda":{"type":"string","description":"Zona J Adenda — máx. 4 líneas × 96 caracteres."},"additionalDocumentInfo":{"type":"string","maxLength":150,"description":"Información adicional del comprobante."},"clause":{"type":"string","maxLength":3,"description":"Cláusula de venta / Incoterm (exportación), p.ej. \"FOB\"."},"saleModality":{"type":"integer","description":"Modalidad de venta (exportación): 1, 2, 3, 4, 80, 90, 91, 99."},"transportRoute":{"type":"integer","description":"Vía de transporte (exportación): 1 marítimo, 2 aéreo, 3 terrestre, 8 N/A, 9 otro."},"grossAmountIndicator":{"type":"integer","enum":[1,2,3],"description":"Indicador montos brutos: 1 = líneas con IVA incluido, 2 = IMEBA/adicionales incluidos, 3 = IVA mínimo/Monotributo. No corresponde (se guarda vacío) en e-Remito, e-Resguardo y e-Remito de Exportación (181/182/124)."},"thirdPartyPaymentsIndicator":{"type":"integer","enum":[1],"description":"Indicador pagos por cuenta de terceros."},"ownCollectionIndicator":{"type":"integer","enum":[1],"description":"Indicador cobranza propia."},"foreignCurrencyResaleIndicator":{"type":"integer","enum":[1,2],"description":"Indicador compra M/E para reventa (solo e-Boleta 151-153)."},"applyRoundingAdjustment":{"type":"boolean","description":"Ajuste por redondeo para ESTE comprobante (override de la configuración de la empresa; ausente = heredar). Con true, al validar/emitir se agrega la línea «Ajuste por redondeo» (indicador 6/7) que lleva el Monto a pagar al múltiplo; con false explícito no se redondea aunque la empresa lo tenga activado."},"roundingMultiple":{"type":"string","enum":["1.00","0.50","0.10"],"description":"Múltiplo objetivo del Monto a pagar para el ajuste por redondeo de ESTE comprobante (ausente = heredar el de la empresa)."},"transferTypeIndicator":{"type":"integer","description":"Tipo de traslado de bienes (remitos): 1 venta, 2 traslados internos."},"goodsOwnershipIndicator":{"type":"integer","description":"Indicador propiedad de la mercadería transportada (1 = cuenta ajena; requiere los campos goodsOwner*)."},"goodsOwnerDocumentType":{"type":"integer","description":"Tipo de documento del propietario."},"goodsOwnerCountryCode":{"type":"string","description":"País del propietario (ISO 3166-1 alpha-2 o \"99\")."},"goodsOwnerDocumentNumberUy":{"type":"string","maxLength":12,"description":"Documento UY del propietario."},"goodsOwnerDocumentNumberForeign":{"type":"string","maxLength":20,"description":"Documento extranjero del propietario."},"goodsOwnerName":{"type":"string","maxLength":150,"description":"Nombre/razón social del propietario."},"complement":{"type":"object","description":"Zona K — Complemento fiscal (mandante). Tipos de venta por cuenta ajena (131-143 / 231-243) y e-Boleta de entrada con compra de M/E para reventa por cuenta ajena (foreignCurrencyResaleIndicator = 2; mandante con NIE, RUT o NIFE). En otra e-Boleta la emisión se rechaza; en el resto de los tipos se ignora.","properties":{"mandanteDocumentType":{"type":"string","maxLength":2,"description":"Tipo de documento del mandante (numérico)."},"mandanteCountryCode":{"type":"string","description":"País del mandante (ISO 3166-1 alpha-2 o \"99\")."},"mandanteDocumentNumber":{"type":"string","maxLength":20,"description":"Número de documento del mandante."},"mandanteName":{"type":"string","maxLength":150,"description":"Nombre o denominación del mandante."}}},"lines":{"type":"array","minItems":1,"maxItems":700,"items":{"$ref":"#/components/schemas/LineInput"},"description":"Líneas del comprobante (Zona B). Límite real según tipo de CFE (700 e-Ticket; 200 el resto)."},"payments":{"type":"array","maxItems":40,"items":{"$ref":"#/components/schemas/PaymentInput"},"description":"Zona E (opcional)."},"references":{"type":"array","items":{"$ref":"#/components/schemas/ReferenceInput"},"description":"Zona F (obligatoria para NC/ND)."},"adjustments":{"type":"array","items":{"$ref":"#/components/schemas/AdjustmentInput"},"description":"Zona D — descuentos/recargos globales (opcional)."},"idempotencyKey":{"type":"string","minLength":8,"maxLength":128,"description":"Clave de idempotencia (el header `Idempotency-Key` gana sobre este campo)."},"autoIssue":{"type":"boolean","default":false,"description":"true → tras crear, corre el gate de validación DGI y encola la emisión (incluye el ajuste por redondeo automático si la empresa lo tiene activado). Requiere además el scope `documents:issue`."}}},"IssueResult":{"type":"object","title":"Resultado de emisión","required":["id","status","enqueued"],"properties":{"id":{"type":"string","description":"Id del comprobante emitido/encolado."},"status":{"type":"string","enum":["DRAFT","READY_TO_ISSUE","ISSUING","SIGNED","SUBMITTED","ACCEPTED","REJECTED","OBSERVED","CANCELLED","FAILED","CONTINGENCY"],"description":"Estado del comprobante al responder (READY_TO_ISSUE recién encolado; sondear GET /documents/{id})."},"enqueued":{"type":"boolean","description":"true si esta llamada encoló el pipeline; false si ya estaba emitido o en curso (no-op idempotente)."}}},"CreateDocumentResult":{"type":"object","title":"Resultado de creación de comprobante","required":["document","reused"],"properties":{"document":{"$ref":"#/components/schemas/DocumentDetail"},"reused":{"type":"boolean","description":"true si un retry con la misma clave de idempotencia devolvió el documento ya creado (HTTP 200 en vez de 201)."},"issue":{"oneOf":[{"$ref":"#/components/schemas/IssueResult"},{"type":"null"}],"description":"Resultado de la emisión cuando `autoIssue: true`; null en caso contrario."}}},"CorrectionNoteResult":{"type":"object","title":"Resultado de nota de corrección","required":["document"],"properties":{"document":{"$ref":"#/components/schemas/DocumentDetail"},"issue":{"oneOf":[{"$ref":"#/components/schemas/IssueResult"},{"type":"null"}],"description":"Resultado de la emisión cuando `autoIssue: true`; null en caso contrario."}}},"CollectionReceiptResult":{"type":"object","title":"Resultado de recibo electrónico","required":["document"],"properties":{"document":{"$ref":"#/components/schemas/DocumentDetail"},"issue":{"oneOf":[{"$ref":"#/components/schemas/IssueResult"},{"type":"null"}],"description":"Resultado de la emisión cuando `autoIssue: true`; null en caso contrario."}}},"ContactSummary":{"type":"object","title":"Contacto (resumen)","properties":{"id":{"type":"string","description":"Id interno del contacto (identificador del recurso en /contacts/{id}). Para referenciarlo al crear documentos se usa su código (receiverCode)."},"type":{"type":"string","enum":["CUSTOMER","SUPPLIER","BOTH"],"description":"CUSTOMER = cliente, SUPPLIER = proveedor, BOTH = ambos."},"code":{"type":"string","description":"Código interno único por empresa, obligatorio (usable como receiverCode al crear documentos)."},"documentType":{"type":["string","null"],"enum":["NIE","RUC","CI","PASSPORT","DNI","NIFE","OTHER"],"example":"RUC","description":"Tipo de documento fiscal: NIE (1, id extranjero UY), RUC (2), CI (3), OTHER (4), PASSPORT (5), DNI (6), NIFE (7, id fiscal extranjero)."},"countryCode":{"type":["string","null"],"example":"UY","description":"País del documento — ISO 3166-1 alpha-2 (UY, AR, BR, …)."},"documentNumber":{"type":["string","null"],"example":"211234567890","description":"Número de documento como string (puede tener ceros a la izquierda)."},"legalName":{"type":"string","description":"Razón social o nombre completo."},"tradeName":{"type":["string","null"],"description":"Nombre comercial (opcional)."},"email":{"type":["string","null"],"description":"Email de contacto (usado para el envío del CFE)."},"phone":{"type":["string","null"],"description":"Teléfono de contacto."},"city":{"type":["string","null"],"description":"Ciudad."},"department":{"type":["string","null"],"description":"Departamento."},"isActive":{"type":"boolean","description":"false = desactivado (no aparece en listados por defecto)."},"createdAt":{"type":"string","format":"date-time","description":"Momento de creación (ISO 8601)."}}},"BranchSummary":{"type":"object","title":"Sucursal (resumen)","description":"Sucursal (casa central o local) de la empresa. Se referencia por su código al crear documentos (`branchCode`).","required":["code","isDefault","isActive","createdAt"],"properties":{"code":{"type":"string","description":"Código de sucursal (único por empresa). Es el `branchCode` que usás al crear documentos."},"name":{"type":["string","null"],"description":"Nombre de la sucursal; null si no tiene."},"isDefault":{"type":"boolean","description":"true = sucursal por defecto (la que se usa al crear un documento sin `branchCode`)."},"isActive":{"type":"boolean","description":"false = sucursal desactivada (no aparece en el listado por defecto)."},"createdAt":{"type":"string","format":"date-time","description":"Momento de creación (ISO 8601)."}}},"CreateContactRequest":{"type":"object","title":"Creación de contacto","description":"Mismo schema que el formulario de la app: RUC con dígito verificador (módulo 11), unicidad por empresa de documento y código interno, reglas de receptor. Campos adicionales (dirección, notas, sitio web, etc.) se aceptan con los mismos nombres que la app — ver docs/api/public-api.md.","required":["type","code","legalName"],"properties":{"type":{"type":"string","enum":["CUSTOMER","SUPPLIER","BOTH"],"description":"CUSTOMER = cliente, SUPPLIER = proveedor, BOTH = ambos."},"code":{"type":"string","maxLength":50,"description":"Código interno (único por empresa). Es el `receiverCode` que después usás al crear documentos."},"legalName":{"type":"string","description":"Razón social."},"tradeName":{"type":["string","null"],"description":"Nombre comercial (opcional)."},"documentType":{"type":"string","enum":["NIE","RUC","CI","PASSPORT","DNI","NIFE","OTHER"],"description":"Tipo de documento fiscal: NIE, RUC (con dígito verificador módulo 11), CI, PASSPORT, DNI, NIFE, OTHER.","example":"RUC"},"countryCode":{"type":"string","example":"UY","description":"País del documento — ISO 3166-1 alpha-2. RUC/CI/NIE exigen UY."},"documentNumber":{"type":["string","null"],"example":"211234567890","description":"Número de documento como string (respetar ceros a la izquierda). Único por empresa junto a tipo y país."},"email":{"type":["string","null"],"description":"Email de contacto (usado para el envío del CFE emitido)."},"phone":{"type":["string","null"],"description":"Teléfono de contacto."},"address":{"type":["string","null"],"description":"Domicilio fiscal (Área Receptor del CFE)."},"city":{"type":["string","null"],"description":"Ciudad."},"department":{"type":["string","null"],"description":"Departamento."}}},"UpdateContactRequest":{"type":"object","title":"Actualización de contacto","description":"Actualización COMPLETA (semántica PUT): enviá el objeto entero — usa las mismas validaciones que la creación, por lo que `type`, `code` y `legalName` son obligatorios y los campos omitidos se limpian. Incluye además `isActive` para activar/desactivar. Recomendado: leer el contacto (GET), modificar y reenviar.","properties":{"legalName":{"type":"string","description":"Razón social."},"tradeName":{"type":["string","null"],"description":"Nombre comercial."},"email":{"type":["string","null"],"description":"Email de contacto."},"phone":{"type":["string","null"],"description":"Teléfono de contacto."},"isActive":{"type":"boolean","description":"false desactiva el contacto (deja de aparecer en listados por defecto)."}},"additionalProperties":true},"ProductSummary":{"type":"object","title":"Producto (resumen)","properties":{"id":{"type":"string","description":"Id interno del producto (identificador del recurso en /products/{id}). Para referenciarlo en las líneas de documentos se usa su SKU (productCode)."},"type":{"type":"string","enum":["PRODUCT","SERVICE"],"description":"PRODUCT = bien físico (puede manejar stock); SERVICE = servicio."},"sku":{"type":"string","description":"SKU único por empresa (usable como productCode en las líneas de documentos)."},"name":{"type":"string","description":"Nombre del producto o servicio."},"unit":{"type":["string","null"],"description":"Unidad de medida por defecto, p.ej. \"UN\", \"kg\", \"N/A\"."},"currency":{"type":"string","example":"UYU","description":"Moneda del precio de lista (ISO 4217)."},"price":{"type":"string","description":"Precio de venta. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"isActive":{"type":"boolean","description":"false = desactivado (no aparece en listados por defecto)."},"createdAt":{"type":"string","format":"date-time","description":"Momento de creación (ISO 8601)."}}},"CreateProductRequest":{"type":"object","title":"Creación de producto","description":"Mismas validaciones que la app: SKU único por empresa; flags de stock (`tracksStock`, `deductsStockOnSale`, `allowsNegativeStock`, `minimumStock`) solo para `type: \"PRODUCT\"`. Campos DGI por defecto (`defaultBillingIndicator`, `defaultItemCodeType`, `ncm`, etc.) — ver docs/api/public-api.md.","required":["type","sku","name","price"],"properties":{"type":{"type":"string","enum":["PRODUCT","SERVICE"],"description":"PRODUCT = bien físico (habilita flags de stock); SERVICE = servicio."},"sku":{"type":"string","maxLength":50,"description":"SKU único por empresa. Es el `productCode` que después usás en las líneas de documentos."},"name":{"type":"string","description":"Nombre del producto o servicio."},"description":{"type":["string","null"],"description":"Descripción interna (opcional)."},"unit":{"type":["string","null"],"maxLength":4,"description":"Unidad de medida por defecto (máx. 4 caracteres), p.ej. \"UN\", \"kg\", \"N/A\"."},"currency":{"type":"string","default":"UYU","description":"Moneda del precio de lista (ISO 4217)."},"price":{"type":"string","description":"Precio de venta. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"defaultBillingIndicator":{"type":"integer","minimum":1,"maximum":17,"description":"Indicador de facturación por defecto al usar el producto en una línea (1-17; p.ej. 1 exento, 2 tasa mínima, 3 tasa básica)."},"tracksStock":{"type":"boolean","default":false,"description":"Solo para PRODUCT."},"deductsStockOnSale":{"type":"boolean","default":false,"description":"Solo para PRODUCT con `tracksStock`."},"allowsNegativeStock":{"type":"boolean","default":false,"description":"Solo para PRODUCT con `tracksStock`."},"restocksOnCreditNote":{"type":"boolean","default":false,"description":"Repone stock ante notas de crédito (NC) que referencian un comprobante que descontó stock. Las notas de débito (ND) no mueven stock. Solo para PRODUCT con `tracksStock`."},"minimumStock":{"type":"string","description":"Stock mínimo (string decimal). Solo con `tracksStock`."}}},"UpdateProductRequest":{"type":"object","title":"Actualización de producto","description":"Actualización COMPLETA (semántica PUT): enviá el objeto entero — mismas validaciones que la creación (`type`, `sku`, `name` y `price` obligatorios); los campos omitidos se limpian. Incluye además `isActive`. Recomendado: leer el producto (GET), modificar y reenviar.","properties":{"name":{"type":"string","description":"Nombre del producto o servicio."},"price":{"type":"string","description":"Precio de venta. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"isActive":{"type":"boolean","description":"false desactiva el producto (deja de aparecer en listados por defecto)."}},"additionalProperties":true},"InventoryRow":{"type":"object","title":"Stock por producto y sucursal","properties":{"productId":{"type":"string","description":"Id del producto."},"productName":{"type":"string","description":"Nombre del producto."},"sku":{"type":["string","null"],"description":"SKU del producto (código único por empresa)."},"unit":{"type":["string","null"],"description":"Unidad de medida del producto."},"branchId":{"type":"string","description":"Id de la sucursal."},"branchCode":{"type":"string","description":"Código de sucursal DGI."},"branchName":{"type":["string","null"],"description":"Nombre de la sucursal."},"quantity":{"type":"string","description":"Stock actual. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"minimumStock":{"type":["string","null"],"description":"Stock mínimo configurado (string decimal) o null."},"allowsNegativeStock":{"type":"boolean","description":"true si el producto admite stock por debajo de cero."},"deductsStockOnSale":{"type":"boolean","description":"true si emitir una venta descuenta stock automáticamente."},"status":{"type":"string","enum":["normal","low","out","negative"],"description":"Estado derivado del stock: normal, low (bajo el mínimo), out (en cero), negative (por debajo de cero)."}}},"InventoryMovement":{"type":"object","title":"Movimiento de inventario (kardex)","properties":{"id":{"type":"string","description":"Id del movimiento."},"referenceItemId":{"type":["string","null"],"description":"Id de la línea del comprobante que originó el movimiento (null en movimientos manuales)."},"type":{"type":"string","enum":["MANUAL_IN","MANUAL_OUT","ADJUSTMENT","SALE","SALE_REVERSAL","CREDIT_NOTE","CREDIT_NOTE_REVERSAL","TRANSFER_IN","TRANSFER_OUT"],"description":"Origen del movimiento: MANUAL_IN/MANUAL_OUT (manual), ADJUSTMENT (recuento físico), SALE/SALE_REVERSAL (venta emitida/revertida), CREDIT_NOTE/CREDIT_NOTE_REVERSAL (nota de crédito — las notas de débito no mueven stock), TRANSFER_IN/TRANSFER_OUT (transferencia entre sucursales)."},"quantityDelta":{"type":"string","description":"Variación de stock (positiva o negativa). Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"previousQuantity":{"type":["string","null"],"description":"Stock antes del movimiento (string decimal)."},"newQuantity":{"type":["string","null"],"description":"Stock después del movimiento (string decimal)."},"reason":{"type":["string","null"],"description":"Motivo declarado (movimientos manuales/ajustes)."},"referenceType":{"type":["string","null"],"description":"Tipo de referencia (p.ej. documento fiscal) cuando el movimiento es automático."},"referenceId":{"type":["string","null"],"description":"Id del registro referenciado (p.ej. el documento fiscal que originó el movimiento)."},"productId":{"type":"string","description":"Id del producto."},"productName":{"type":"string","description":"Nombre del producto."},"branchId":{"type":"string","description":"Id de la sucursal."},"branchCode":{"type":"string","description":"Código de sucursal DGI."},"createdByUserId":{"type":["string","null"],"description":"Id del usuario que registró el movimiento; null en movimientos automáticos."},"createdByUserName":{"type":["string","null"],"description":"Nombre del usuario que registró el movimiento."},"createdAt":{"type":"string","format":"date-time","description":"Momento del movimiento (ISO 8601)."}}},"InventoryAdjustmentRequest":{"title":"Movimiento manual de inventario","description":"Unión discriminada por `type`. MANUAL_IN/MANUAL_OUT llevan la cantidad movida (`quantity`); ADJUSTMENT lleva la cantidad contada físicamente (`countedQuantity`, el delta se calcula server-side) y `reason` obligatorio. Mismas garantías que la app: `SELECT FOR UPDATE` sobre el balance, sin stock negativo salvo que el producto lo permita.","oneOf":[{"type":"object","title":"Entrada manual","required":["type","productId","branchId","quantity"],"properties":{"type":{"const":"MANUAL_IN","description":"Entrada manual de stock."},"productId":{"type":"string","description":"Id del producto (debe tener tracksStock)."},"branchId":{"type":"string","description":"Id de la sucursal donde ingresa el stock."},"quantity":{"type":"string","description":"Cantidad que ingresa. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"reason":{"type":"string","maxLength":500,"description":"Motivo del ingreso (opcional)."}}},{"type":"object","title":"Salida manual","required":["type","productId","branchId","quantity"],"properties":{"type":{"const":"MANUAL_OUT","description":"Salida manual de stock."},"productId":{"type":"string","description":"Id del producto (debe tener tracksStock)."},"branchId":{"type":"string","description":"Id de la sucursal de la que egresa el stock."},"quantity":{"type":"string","description":"Cantidad que egresa. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"reason":{"type":"string","maxLength":500,"description":"Motivo del egreso (opcional)."}}},{"type":"object","title":"Ajuste por recuento físico","required":["type","productId","branchId","countedQuantity","reason"],"properties":{"type":{"const":"ADJUSTMENT","description":"Ajuste por recuento físico (el delta se calcula server-side)."},"productId":{"type":"string","description":"Id del producto (debe tener tracksStock)."},"branchId":{"type":"string","description":"Id de la sucursal recontada."},"countedQuantity":{"type":"string","description":"Cantidad contada físicamente. Decimal serializado como string; salida con coma decimal es-UY («12200,50»), entrada acepta coma o punto.","example":"12200,50"},"reason":{"type":"string","minLength":1,"maxLength":500,"description":"Motivo del ajuste (obligatorio)."}}}],"discriminator":{"propertyName":"type"}}}}}