# Contrato de la API pública. `test/apiV1.test.js` valida contra este archivo cada respuesta real que # recibe, y falla si la API responde algo que acá no está o si acá hay algo que la API nunca respondió. openapi: 3.1.0 info: title: ModoFactura API version: 1.0.0 description: | Pedido y consulta de comprobantes electrónicos ante ARCA. ARCA puede tardar o caerse, así que la API no promete la factura en el momento: `POST` acepta el pedido y responde `202`, y `GET` dice cómo sigue. Mientras esté `procesando` hay que volver a consultar; ese estado no tiene un plazo garantizado. Si `en_revision` es `true`, el comprobante espera a una persona: sigue `procesando` hasta que se resuelva, y no hace falta pedirlo de nuevo. Un comprobante que una persona de ModoFactura cierra sin mandarlo a ARCA queda `descartado`: no tiene número ni CAE y no cambia más. Si corresponde, se pide de nuevo con otra `Idempotency-Key`. La API puede sumar campos, tipos de evento y valores de `estado` nuevos sin cambiar de versión: hay que ignorar los campos y eventos que no se conozcan, y tratar un estado desconocido como final y sin CAE. Un webhook de un tipo desconocido se contesta con un `2xx` igual. Sólo factura A y B, y sus notas de crédito. Una nota de crédito se pide sobre la factura que corrige, con `POST /v1/comprobantes/{id}/notas-de-credito`, y sale por la misma cola: se consulta igual que una factura. Los importes los calcula el servidor a partir del precio final de cada renglón. El PDF de un comprobante emitido se descarga de `/v1/comprobantes/{id}/pdf`: sale siempre de lo que autorizó ARCA, y el mismo comprobante da siempre el mismo archivo. En vez de consultar, se puede registrar un webhook con `PUT /v1/webhook`: el resultado de cada comprobante llega firmado a la URL del cliente. La forma de cada evento está en `webhooks`. servers: - url: https://autoinvoice.com.ar description: PROD, sólo acepta claves `mf_live_` - url: https://test.autoinvoice.com.ar description: TEST, sólo acepta claves `mf_test_` security: - claveApi: [] paths: /v1/comprobantes: post: operationId: pedirComprobante summary: Pedir un comprobante description: | Repetir el pedido con el mismo `Idempotency-Key` y el mismo cuerpo devuelve el mismo comprobante, en su estado actual, y no crea otro. La comparación es por contenido: el orden de los campos o escribir `121` en vez de `121.00` no cambia el pedido. La misma clave con otro cuerpo es un `409`. Ante un `500` o un corte, hay que repetir con la misma clave. parameters: - name: Idempotency-Key in: header required: true description: Clave elegida por el cliente, única por pedido. schema: type: string pattern: '^[\x21-\x7e]{1,255}$' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PedidoComprobante' example: tipo: 6 concepto: 2 receptor: doc_tipo: 96 doc_nro: '30111222' condicion_iva: 5 nombre: Ana Pérez domicilio: San Martín 123, Rosario renglones: - descripcion: Abono mensual importe: '121.00' alicuota: 21 servicio_desde: '2026-11-01' servicio_hasta: '2026-11-30' vencimiento_pago: '2026-12-10' condicion_venta: contado responses: '202': description: Pedido aceptado. El cuerpo es el comprobante en su estado actual. headers: Cache-Control: $ref: '#/components/headers/SinCache' Location: required: true description: Dónde consultar el comprobante. schema: type: string content: application/json: schema: $ref: '#/components/schemas/Comprobante' '400': description: Falta el `Idempotency-Key`, es inválido, o el cuerpo no es JSON válido. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '401': $ref: '#/components/responses/NoAutorizado' '409': description: | El `Idempotency-Key` ya se usó con otro cuerpo, o a la cuenta le falta el punto de venta o sus datos fiscales de emisor. `title` dice cuál. No se encoló nada. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '413': description: El cuerpo supera los 256 KB. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '415': description: El cuerpo no es `application/json`, o viene comprimido. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '422': description: El pedido está bien formado pero no se puede emitir. `errores` dice por qué. No se encoló nada. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/ProblemaDeValidacion' '429': $ref: '#/components/responses/DemasiadosPedidos' '500': $ref: '#/components/responses/ErrorInterno' /v1/comprobantes/{id}: get: operationId: consultarComprobante summary: Consultar un comprobante parameters: - name: id in: path required: true schema: type: string responses: '200': description: El comprobante en su estado actual. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/json: schema: $ref: '#/components/schemas/Comprobante' '401': $ref: '#/components/responses/NoAutorizado' '404': description: No hay un comprobante de este cliente con ese id. La respuesta no distingue si existe para otro. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '429': $ref: '#/components/responses/DemasiadosPedidos' '500': $ref: '#/components/responses/ErrorInterno' /v1/comprobantes/{id}/notas-de-credito: post: operationId: pedirNotaDeCredito summary: Pedir una nota de crédito sobre una factura description: | Sólo sobre una factura emitida. La nota toma de la factura la letra (factura A → nota de crédito A, tipo 3; factura B → nota de crédito B, tipo 8), el receptor, el concepto, el período del servicio y la condición de venta, y sale del mismo CUIT, por el punto de venta actual de la cuenta. A ARCA va informada la factura que corrige. Sin renglones es una nota por el total de la factura. Con renglones, por lo que digan: cada uno con una alícuota que la factura tenga. Sumada a las otras notas de la misma factura que no fueron rechazadas, nunca supera su total ni lo facturado en cada alícuota; dos pedidos simultáneos que juntos se pasan no salen los dos. El `Idempotency-Key` funciona igual que al pedir una factura, y es uno por pedido entre facturas y notas. parameters: - name: id in: path required: true description: El id de la factura que se corrige. schema: type: string - name: Idempotency-Key in: header required: true description: Clave elegida por el cliente, única por pedido. schema: type: string pattern: '^[\x21-\x7e]{1,255}$' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PedidoNotaDeCredito' example: renglones: - descripcion: Devolución parcial del abono importe: '60.50' alicuota: 21 responses: '202': description: Pedido aceptado. El cuerpo es la nota de crédito en su estado actual. headers: Cache-Control: $ref: '#/components/headers/SinCache' Location: required: true description: Dónde consultar la nota de crédito. schema: type: string content: application/json: schema: $ref: '#/components/schemas/Comprobante' '400': description: Falta el `Idempotency-Key`, es inválido, o el cuerpo no es JSON válido. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '401': $ref: '#/components/responses/NoAutorizado' '404': description: No hay un comprobante de este cliente con ese id. La respuesta no distingue si existe para otro. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '409': description: | El comprobante no es una factura o todavía no está emitido; se emitió con un CUIT distinto del que tiene hoy la cuenta; la nota superaría a la factura sumada a las otras; el `Idempotency-Key` ya se usó con otro cuerpo; o a la cuenta le falta el punto de venta o sus datos fiscales. `title` dice cuál. No se encoló nada. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '413': description: El cuerpo supera los 256 KB. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '415': description: El cuerpo no es `application/json`, o viene comprimido. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '422': description: El pedido está bien formado pero no se puede emitir. `errores` dice por qué. No se encoló nada. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/ProblemaDeValidacion' '429': $ref: '#/components/responses/DemasiadosPedidos' '500': $ref: '#/components/responses/ErrorInterno' /v1/comprobantes/{id}/pdf: get: operationId: descargarPdf summary: Descargar el PDF de un comprobante emitido description: | Devuelve el archivo directamente, sin URL firmada. El contenido sale de lo que autorizó ARCA y de los datos congelados al crear el comprobante: el mismo comprobante da siempre el mismo archivo. parameters: - name: id in: path required: true schema: type: string responses: '200': description: El PDF. headers: Cache-Control: $ref: '#/components/headers/SinCache' Content-Disposition: required: true description: Sugiere el nombre del archivo. schema: type: string pattern: '^attachment; filename="[A-Za-z0-9_-]+\.pdf"$' content: application/pdf: schema: type: string contentMediaType: application/pdf '401': $ref: '#/components/responses/NoAutorizado' '404': description: No hay un comprobante de este cliente con ese id. La respuesta no distingue si existe para otro. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '409': description: | No hay PDF para servir. `title` dice por qué: el comprobante no está emitido (todavía, o fue rechazado); se creó sin los datos del emisor; o el archivo generado no coincide con el original, caso en el que no se sirve y lo revisa una persona. Reintentar sólo sirve en el primer caso, cuando el comprobante esté emitido. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '429': $ref: '#/components/responses/DemasiadosPedidos' '500': $ref: '#/components/responses/ErrorInterno' /v1/comprobantes/{id}/webhook/reenviar: post: operationId: reenviarWebhook summary: Volver a enviar el resultado de un comprobante description: | Encola otra entrega del último evento de resultado, con el mismo `id`, sin tocar el comprobante. Sirve después de un `webhook_agotado`, o si el receptor perdió el mensaje. Si ese evento ya está esperando entrega no se agrega otra: repetir el pedido es seguro. Cada reenvío tiene otras 24 horas de reintentos. Tiene el mismo tope de pendientes por cuenta que los pedidos: pasado ese tope responde `429`, salvo que el evento ya esté esperando. parameters: - name: id in: path required: true schema: type: string responses: '202': description: Entrega encolada. El resultado llega al webhook, no a esta respuesta. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/json: schema: $ref: '#/components/schemas/ReenvioEncolado' '401': $ref: '#/components/responses/NoAutorizado' '404': description: No hay un comprobante de este cliente con ese id. La respuesta no distingue si existe para otro. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '409': description: | No hay nada que enviar. `title` dice por qué: la cuenta no tiene webhook registrado, o el comprobante todavía no tiene un resultado. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '429': $ref: '#/components/responses/DemasiadosPedidos' '500': $ref: '#/components/responses/ErrorInterno' '503': $ref: '#/components/responses/WebhooksNoDisponibles' /v1/webhook: get: operationId: consultarWebhook summary: Consultar el webhook de la cuenta description: El secreto no se vuelve a mostrar. responses: '200': description: El webhook registrado. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/json: schema: $ref: '#/components/schemas/Webhook' '401': $ref: '#/components/responses/NoAutorizado' '404': $ref: '#/components/responses/SinWebhook' '429': $ref: '#/components/responses/DemasiadosPedidos' '500': $ref: '#/components/responses/ErrorInterno' put: operationId: registrarWebhook summary: Dar de alta el webhook o cambiar su URL description: | Hay un webhook por cuenta. El alta responde `201` con el secreto, que se muestra UNA SOLA VEZ: si se pierde, hay que rotarlo. Con un webhook ya registrado cambia la URL, conserva el secreto y responde `200`; lo que esté pendiente de entrega sale a la URL nueva. La URL tiene que ser `https`, sin credenciales, y resolver sólo a direcciones públicas. Se vuelve a verificar antes de cada entrega. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PedidoWebhook' example: url: https://sistema.ejemplo.com/modofactura/eventos responses: '200': description: La URL cambió. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/json: schema: $ref: '#/components/schemas/Webhook' '201': description: Webhook creado. Es la única vez que se muestra el secreto. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/json: schema: $ref: '#/components/schemas/WebhookConSecreto' '400': description: El cuerpo no es JSON válido. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '401': $ref: '#/components/responses/NoAutorizado' '413': description: El cuerpo supera los 256 KB. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '415': description: El cuerpo no es `application/json`, o viene comprimido. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' '422': description: La URL no se admite. Un destino que no resuelve y uno interno dan el mismo error. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/ProblemaDeValidacion' '429': $ref: '#/components/responses/DemasiadosPedidos' '500': $ref: '#/components/responses/ErrorInterno' '503': $ref: '#/components/responses/WebhooksNoDisponibles' delete: operationId: borrarWebhook summary: Borrar el webhook de la cuenta description: Lo que esté pendiente de entrega se descarta. Una URL nueva necesita un alta, con otro secreto. responses: '204': description: Borrado. headers: Cache-Control: $ref: '#/components/headers/SinCache' '401': $ref: '#/components/responses/NoAutorizado' '404': $ref: '#/components/responses/SinWebhook' '429': $ref: '#/components/responses/DemasiadosPedidos' '500': $ref: '#/components/responses/ErrorInterno' /v1/webhook/secreto: post: operationId: rotarSecretoWebhook summary: Rotar el secreto del webhook description: | Genera un secreto nuevo y lo muestra UNA SOLA VEZ. Vale al instante: lo pendiente sale firmado con el nuevo, y lo que el receptor rechace mientras se cambia se reintenta solo. responses: '200': description: Secreto nuevo. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/json: schema: $ref: '#/components/schemas/WebhookConSecreto' '401': $ref: '#/components/responses/NoAutorizado' '404': $ref: '#/components/responses/SinWebhook' '429': $ref: '#/components/responses/DemasiadosPedidos' '500': $ref: '#/components/responses/ErrorInterno' '503': $ref: '#/components/responses/WebhooksNoDisponibles' webhooks: resultadoComprobante: post: operationId: recibirResultado summary: Resultado de un comprobante description: | Cada `POST` lleva `X-ModoFactura-Firma: t=,v1=`: HMAC-SHA256 en hexadecimal, con el secreto entero como clave, sobre `.`. Hay que verificarlo contra los bytes recibidos, antes de interpretar el JSON, y rechazar un `t` con más de 5 minutos de diferencia. Cada intento firma con su propio `t`. La documentación trae un ejemplo de verificación. La entrega es «al menos una vez» y sin orden garantizado: un mismo evento puede llegar más de una vez, siempre con el mismo `id`, y conviene deduplicar por ese campo. `comprobante.estado` es el estado al momento de entregar. Cualquier `2xx` confirma; todo lo demás, también un `3xx`, se reintenta con espera creciente durante 24 horas. No se siguen redirecciones, y el pedido se corta a los 10 segundos. parameters: - name: X-ModoFactura-Firma in: header required: true schema: type: string pattern: '^t=[0-9]+,v1=[0-9a-f]{64}$' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EventoWebhook' responses: '200': description: Recibido. Vale cualquier `2xx`, y la respuesta se descarta. components: securitySchemes: claveApi: type: http scheme: bearer description: | `Authorization: Bearer `. La clave es `mf_live_` (PROD) o `mf_test_` (TEST) seguida de 43 caracteres base64url. headers: SinCache: required: true description: Siempre `no-store`. schema: type: string const: no-store responses: NoAutorizado: description: La clave falta, es inválida, está revocada o es de otro ambiente. La respuesta no distingue cuál. headers: Cache-Control: $ref: '#/components/headers/SinCache' WWW-Authenticate: required: true schema: type: string content: application/problem+json: schema: $ref: '#/components/schemas/Problema' DemasiadosPedidos: description: | Se superó el límite de pedidos del cliente, su tope de pedidos sin procesar, el de autenticaciones fallidas de la IP, o hay demasiados PDF generándose a la vez. Por defecto, 300 pedidos por minuto por cliente, 1000 sin procesar por cliente y 20 autenticaciones fallidas por minuto por IP. Reintentar con el mismo Idempotency-Key es seguro. headers: Cache-Control: $ref: '#/components/headers/SinCache' Retry-After: required: true description: Segundos hasta poder volver a intentar. schema: type: string pattern: '^[1-9][0-9]*$' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' SinWebhook: description: La cuenta no tiene un webhook registrado. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' WebhooksNoDisponibles: description: La entrega de webhooks no está encendida en este ambiente. No se registró ni se encoló nada. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' ErrorInterno: description: Error interno, sin detalles. Se puede reintentar; en un `POST`, con el mismo `Idempotency-Key`. headers: Cache-Control: $ref: '#/components/headers/SinCache' content: application/problem+json: schema: $ref: '#/components/schemas/Problema' schemas: Fecha: type: string format: date description: AAAA-MM-DD Importe: type: string pattern: '^(0|[1-9][0-9]{0,12})(\.[0-9]{1,2})?$' description: En pesos, como texto para no perder precisión. Receptor: type: object additionalProperties: false required: [doc_tipo, doc_nro, condicion_iva] properties: doc_tipo: type: integer enum: [80, 86, 87, 96, 99] description: 80 CUIT, 86 CUIL, 87 CDI, 96 DNI, 99 sin identificar. doc_nro: type: string pattern: '^[0-9]{1,11}$' description: CUIT, CUIL o CDI con dígito verificador válido, DNI, o `"0"` si no se identifica. condicion_iva: type: integer enum: [1, 4, 5, 6, 7, 8, 9, 10, 13, 15, 16] description: | Condición frente al IVA, código de ARCA (RG 5616). La declara el cliente; ARCA la cruza con su padrón al autorizar. Con DNI o sin identificar, sólo 5 (consumidor final). nombre: type: string minLength: 1 maxLength: 200 description: Nombre o razón social. Va en el PDF, no a ARCA. Obligatorio con CUIT. Sin caracteres de control. domicilio: type: string minLength: 1 maxLength: 300 description: Va en el PDF, no a ARCA. Obligatorio con CUIT. Sin caracteres de control. email: type: string format: email maxLength: 254 description: | Opcional. Si viene, el comprobante emitido se le manda por mail con el PDF adjunto, desde `no-responder@modoagrario.com` y con la razón social del emisor como nombre. Las notas de crédito van al mismo mail que su factura. No va a ARCA ni al PDF. if: type: object properties: doc_tipo: const: 80 required: [doc_tipo] then: required: [nombre, domicilio] CondicionVenta: type: string enum: [contado, cuenta_corriente, tarjeta_debito, tarjeta_credito, transferencia, cheque, otra] description: Se imprime en el PDF. No viaja a ARCA. Renglon: type: object additionalProperties: false required: [descripcion, importe, alicuota] properties: descripcion: type: string minLength: 1 maxLength: 500 importe: allOf: - $ref: '#/components/schemas/Importe' description: Precio final del renglón, IVA incluido. Mayor a cero. alicuota: type: number enum: [21, 10.5, 27] PedidoComprobante: type: object additionalProperties: false required: [tipo, concepto, receptor, renglones] description: | Sin importes calculados ni fecha: un cuerpo que los traiga se rechaza. El punto de venta sale de la configuración de la cuenta. properties: tipo: type: integer enum: [1, 6] description: 1 factura A, 6 factura B. Tiene que corresponder a la condición del receptor. concepto: type: integer enum: [1, 2, 3] description: 1 productos, 2 servicios, 3 ambos. receptor: $ref: '#/components/schemas/Receptor' renglones: type: array minItems: 1 maxItems: 200 items: $ref: '#/components/schemas/Renglon' servicio_desde: $ref: '#/components/schemas/Fecha' servicio_hasta: $ref: '#/components/schemas/Fecha' vencimiento_pago: $ref: '#/components/schemas/Fecha' condicion_venta: allOf: - $ref: '#/components/schemas/CondicionVenta' description: Por defecto, `contado`. if: type: object properties: concepto: enum: [2, 3] required: [concepto] then: required: [servicio_desde, servicio_hasta, vencimiento_pago] else: properties: servicio_desde: false servicio_hasta: false vencimiento_pago: false PedidoNotaDeCredito: type: object additionalProperties: false description: Sin renglones, la nota es por el total de la factura. Sin importes calculados ni fecha. properties: renglones: type: array minItems: 1 maxItems: 200 items: $ref: '#/components/schemas/Renglon' MensajeArca: type: object additionalProperties: false required: [codigo, mensaje] properties: codigo: type: integer mensaje: type: string Rechazo: type: object additionalProperties: false required: [errores, observaciones] description: Lo que contestó ARCA. El comprobante rechazado no se reintenta solo. properties: errores: type: array items: $ref: '#/components/schemas/MensajeArca' observaciones: type: array items: $ref: '#/components/schemas/MensajeArca' Comprobante: type: object additionalProperties: false required: - id - estado - en_revision - cuit_emisor - punto_venta - tipo - numero - concepto - fecha - servicio_desde - servicio_hasta - vencimiento_pago - receptor - renglones - condicion_venta - importe_neto - importe_iva - importe_total - cae - cae_vencimiento - emitido_en - comprobante_asociado_id - rechazo - creado_en properties: id: type: string format: uuid estado: type: string enum: [procesando, emitido, rechazado, descartado] en_revision: type: boolean description: | `true` si está `procesando` y nadie lo está trabajando: espera la revisión de una persona. `false` mientras esté en curso, aunque ARCA tarde, y siempre en un comprobante emitido, rechazado o descartado. cuit_emisor: type: string pattern: '^[0-9]{11}$' punto_venta: type: integer minimum: 1 maximum: 99999 tipo: type: integer enum: [1, 6, 3, 8] description: 1 factura A, 6 factura B, 3 nota de crédito A, 8 nota de crédito B. numero: type: [integer, 'null'] description: Sólo con el comprobante emitido. concepto: type: integer enum: [1, 2, 3] fecha: allOf: - $ref: '#/components/schemas/Fecha' description: | La fecha guardada. Hasta que el comprobante esté emitido es sólo la del pedido, y puede cambiar: la definitiva es la del día de emisión en Argentina. servicio_desde: type: [string, 'null'] format: date servicio_hasta: type: [string, 'null'] format: date vencimiento_pago: type: [string, 'null'] format: date description: Si quedó atrás al emitir, pasa a ser el día de emisión. Hasta entonces puede cambiar. receptor: $ref: '#/components/schemas/Receptor' renglones: type: array minItems: 1 items: $ref: '#/components/schemas/Renglon' condicion_venta: $ref: '#/components/schemas/CondicionVenta' importe_neto: $ref: '#/components/schemas/ImporteExacto' importe_iva: $ref: '#/components/schemas/ImporteExacto' importe_total: $ref: '#/components/schemas/ImporteExacto' cae: type: [string, 'null'] pattern: '^[0-9]{14}$' cae_vencimiento: type: [string, 'null'] format: date emitido_en: type: [string, 'null'] format: date-time comprobante_asociado_id: type: [string, 'null'] format: uuid description: En una nota de crédito, la factura que corrige. `null` en una factura. rechazo: anyOf: - $ref: '#/components/schemas/Rechazo' - type: 'null' creado_en: type: string format: date-time allOf: - if: type: object properties: estado: const: emitido then: properties: numero: type: integer minimum: 1 cae: type: string cae_vencimiento: type: string emitido_en: type: string rechazo: type: 'null' else: properties: numero: type: 'null' cae: type: 'null' cae_vencimiento: type: 'null' emitido_en: type: 'null' - if: type: object properties: estado: const: rechazado then: properties: rechazo: type: object else: properties: rechazo: type: 'null' PedidoWebhook: type: object additionalProperties: false required: [url] properties: url: type: string format: uri maxLength: 2048 description: '`https`, sin credenciales, y con un destino público.' Webhook: type: object additionalProperties: false required: [url, creado_en, actualizado_en] properties: url: type: string format: uri creado_en: type: string format: date-time actualizado_en: type: string format: date-time WebhookConSecreto: type: object additionalProperties: false required: [url, creado_en, actualizado_en, secreto] properties: url: type: string format: uri creado_en: type: string format: date-time actualizado_en: type: string format: date-time secreto: type: string pattern: '^mf_whsec_[A-Za-z0-9_-]{43}$' description: Se muestra sólo en esta respuesta. Es la clave del HMAC, entera, como texto. TipoEventoWebhook: type: string enum: [comprobante.emitido, comprobante.rechazado, comprobante.error_datos, comprobante.en_revision, comprobante.descartado] ReenvioEncolado: type: object additionalProperties: false required: [evento] properties: evento: type: object additionalProperties: false required: [id, tipo] properties: id: type: string format: uuid tipo: $ref: '#/components/schemas/TipoEventoWebhook' ComprobanteDelEvento: type: object additionalProperties: false required: [id, estado, tipo, punto_venta, numero, cae, cae_vencimiento, fecha, comprobante_asociado_id] description: Lo necesario para reconocerlo. El detalle completo sale de `GET /v1/comprobantes/{id}`. properties: id: type: string format: uuid estado: type: string enum: [procesando, emitido, rechazado, descartado] description: El estado público al momento de entregar. tipo: type: integer enum: [1, 6, 3, 8] description: 1 factura A, 6 factura B, 3 nota de crédito A, 8 nota de crédito B. punto_venta: type: integer minimum: 1 maximum: 99999 numero: type: [integer, 'null'] description: Sólo con el comprobante emitido. cae: type: [string, 'null'] pattern: '^[0-9]{14}$' cae_vencimiento: type: [string, 'null'] format: date fecha: $ref: '#/components/schemas/Fecha' comprobante_asociado_id: type: [string, 'null'] format: uuid description: En una nota de crédito, la factura que corrige. EventoWebhook: type: object additionalProperties: false required: [id, tipo, creado_en, comprobante] description: | `comprobante.emitido`: ARCA autorizó, y trae número y CAE. `comprobante.rechazado`: ARCA dijo que no; `motivo` trae lo mismo que `rechazo` en el `GET`. `comprobante.error_datos`: los datos del comprobante no se pueden enviar a ARCA; sigue `procesando` hasta que lo resuelva una persona. `comprobante.en_revision`: espera la revisión de una persona; sigue `procesando` y no hace falta pedirlo de nuevo. Estos dos últimos no se entregan si el comprobante se resolvió mientras esperaban: llega el resultado. `comprobante.descartado`: ModoFactura lo cerró sin mandarlo a ARCA; no tiene número ni CAE y no va a cambiar más. properties: id: type: string format: uuid description: Estable. Se repite en cada reintento y en un reenvío, para deduplicar. tipo: $ref: '#/components/schemas/TipoEventoWebhook' creado_en: type: string format: date-time description: Cuándo pasó, no cuándo se entregó. comprobante: $ref: '#/components/schemas/ComprobanteDelEvento' motivo: type: string minLength: 1 description: Legible. No trae más que lo que muestra el `GET` del comprobante. allOf: - if: type: object properties: tipo: const: comprobante.emitido required: [tipo] then: properties: motivo: false comprobante: type: object properties: estado: const: emitido numero: type: integer minimum: 1 cae: type: string cae_vencimiento: type: string else: required: [motivo] properties: comprobante: type: object properties: numero: type: 'null' cae: type: 'null' cae_vencimiento: type: 'null' - if: type: object properties: tipo: enum: [comprobante.error_datos, comprobante.en_revision] required: [tipo] then: properties: comprobante: type: object properties: estado: const: procesando - if: type: object properties: tipo: const: comprobante.rechazado required: [tipo] then: properties: comprobante: type: object properties: estado: const: rechazado - if: type: object properties: tipo: const: comprobante.descartado required: [tipo] then: properties: comprobante: type: object properties: estado: const: descartado ImporteExacto: type: string pattern: '^[0-9]{1,13}\.[0-9]{2}$' description: Calculado por el servidor, siempre con 2 decimales. Problema: type: object additionalProperties: false required: [type, title, status] description: RFC 9457. properties: type: type: string title: type: string status: type: integer detail: type: string ProblemaDeValidacion: type: object additionalProperties: false required: [type, title, status, errores] description: RFC 9457, con la lista de campos a corregir. properties: type: type: string title: type: string status: type: integer errores: type: array minItems: 1 items: type: object additionalProperties: false required: [campo, mensaje] properties: campo: type: string description: Ruta del campo, por ejemplo `renglones.0.importe`. mensaje: type: string