> ## Documentation Index
> Fetch the complete documentation index at: https://www.emissa.com.ar/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Alta o edición de un contribuyente

> Con `id` edita; sin `id` da de alta.

**Se puede con API key desde la 5.33**, si la key tiene `taxpayer:write` y **no** está restringida a contribuyentes puntuales. Una key restringida recibe 403: si pudiera crear, el contribuyente nuevo quedaría fuera de su lista y podría operarlo igual, escapándose de su propia restricción.

El `organizationId` del cuerpo lo usa sólo una sesión de panel. Con API key **se ignora**: la key opera en su organización y no la elige un parámetro.

La respuesta trae `datosDelPapel.faltantes` con lo que todavía falta para poder imprimir un comprobante completo.

**El alta fija el `producto` del contribuyente**: el de la key, si está atada a uno, o el de `X-Emissa-Producto`. La edición no lo cambia.

**Scope de API key:** `taxpayer:write`. Una key sin ese scope recibe `403 API_KEY_INSUFFICIENT_SCOPE`, aunque el rol de quien la creó lo permita: el permiso efectivo es la **intersección** de los dos.



## OpenAPI

````yaml https://api.emissa.com.ar/openapi.json post /v1/taxpayers
openapi: 3.0.0
info:
  title: Emissa API
  description: >-
    API de facturación electrónica ante ARCA (ex-AFIP) para la República
    Argentina.


    ## Autenticación


    Dos credenciales, **dos headers distintos**:


    | Quién | Header |

    |---|---|

    | Integración de un sistema de terceros | `X-API-Key: emi_test_… /
    emi_live_…` |

    | Panel de Emissa | `Authorization: Bearer <JWT>` |


    Una API key en `Authorization` devuelve **401 `API_KEY_WRONG_HEADER`** con
    la

    indicación exacta, no un "token inválido" genérico.


    La key se crea desde el panel y **pertenece a la organización**, con una

    restricción **opcional** a un subconjunto de contribuyentes: sin restricción

    alcanza a todos los de la organización, incluidos los que se den de alta

    después. Lleva scopes, y no se puede crear una API key con otra API key:
    sería

    una escalada de privilegio.


    ## Identificá tu producto: `X-Emissa-Producto`


    Cabecera **opcional**, sólo con API key: el producto que origina el pedido

    (`inmos`, por ejemplo). Formato `^[a-z0-9-]{1,32}$`; cualquier otro valor

    devuelve `400 INVALID_INPUT`. Ausente o vacía, no pasa nada: queda `null`.


    Queda en el comprobante (`producto`, que no cambia después: un reintento

    idempotente con otra etiqueta devuelve el original), en el consumo

    (`GET /v1/consumo` trae `porProducto` y acepta `?producto=`), en el registro

    de pedidos y en el payload de `invoice.resolved`. `GET /v1/invoices/search`

    acepta `?producto=`. **Es atribución, no autorización**: no cambia lo que la
    key

    puede hacer.


    ## Sandbox: keys `test` y `live`


    **El modo está en el texto de la key**, así que se ve sin ejecutar nada:


    | Prefijo | Opera contra | Qué emite |

    |---|---|---|

    | `emi_test_` | contribuyentes de `HOMOLOGACION` | CAE real de ARCA, **sin
    validez fiscal** |

    | `emi_live_` | contribuyentes de `PRODUCCION` | comprobantes fiscales |


    El modo **no se elige**: se deriva del ambiente del contribuyente al crear
    la

    key, y se verifica en **cada** llamada. Una key `test` contra un
    contribuyente

    de producción devuelve `403 API_KEY_WRONG_MODE` — nunca emite.


    **Toda respuesta autenticada con API key lleva `X-Emissa-Mode: test|live`**,

    y el comprobante trae `environment`. Si tu suite de integración chequea una

    sola cosa, que sea ésta.


    > **En `PRODUCCION` lo que emitís es un comprobante fiscal real.** Entra en
    los

    > registros del contribuyente ante ARCA, pesa en su declaración y no se
    borra: se

    > revierte con una nota de crédito. Para probar está `HOMOLOGACION`, que
    también

    > da CAE real del organismo y no tiene consecuencia fiscal.


    ## Emitir un comprobante


    ```bash

    curl -X POST https://api.ejemplo/v1/invoices/authorize \
      -H 'X-API-Key: emi_...' \
      -H 'Idempotency-Key: pedido-8421' \
      -H 'Content-Type: application/json' \
      -d '{
        "taxpayerId": "...",
        "cbteTipo": 1, "concepto": 1,
        "receiverDocTipo": 80, "receiverDocNro": "30500010912",
        "receiverName": "Cliente SA", "receiverVatConditionId": 1,
        "issueDate": "2026-07-26",
        "items": [{ "description": "Consultoría", "unit": "UNIDADES",
                    "quantity": 1, "unitPrice": 1000,
                    "tratamiento": "GRAVADO", "vatRateId": 5 }]
      }'
    ```


    **`Idempotency-Key` es obligatorio.** Un duplicado fiscal no se deshace, así

    que el default tiene que ser el seguro: reintentar con la misma clave
    devuelve el

    comprobante original y la respuesta lleva `Idempotent-Replay: true`.


    ## Cuota y consumo


    **La unidad es el comprobante autorizado por ARCA.** No la creación, no el

    request. Un rechazo **no consume cuota**; el padrón, la constatación de CAE
    y los

    PDF se miden pero no se cobran.


    Cada emisión responde con:


    | Cabecera | Qué dice |

    |---|---|

    | `X-Cuota-Limite` | incluidos en el período, o `ilimitado` |

    | `X-Cuota-Usada` | comprobantes autorizados en el período, éste incluido |

    | `X-Cuota-Restante` | nunca negativo |

    | `X-Cuota-Reinicio` | ISO 8601 del primer instante del período siguiente |

    | `X-Cuota-Excedente-Unitario` | **sólo si te excediste**: a qué precio se
    factura cada uno |


    > **La cuota es blanda: llegar a cero NO bloquea nada.** Se sigue emitiendo
    y el

    > excedente se factura al unitario publicado. No la confundas con

    > `X-RateLimit-*`, que sí es duro y sí devuelve 429.


    `GET /v1/consumo?periodo=YYYY-MM` da el total de la organización y el
    desglose

    por CUIT. Un **CUIT activo** es el que emitió al menos un comprobante en el

    período: se deriva de los eventos, así que un contribuyente dado de alta que
    no

    facturó no cuenta.


    ## Reintentos


    | Código | Qué hacer |

    |---|---|

    | `503 SERIES_BUSY` | **Reintentá.** Las emisiones de una serie se
    serializan para que los números lleguen a ARCA en orden; el 503 es esa
    espera hecha explícita. |

    | `429 RATE_LIMIT_EXCEEDED` | Reintentá con backoff. |

    | `409 IDEMPOTENCY_IN_PROGRESS` | El pedido original sigue corriendo. Esperá
    su resultado; **no** cambies la clave. |

    | cortes de red sin respuesta | Reintentá **con la misma
    `Idempotency-Key`**. Es exactamente para lo que está. |


    Si un comprobante quedó en `PENDING` o `UNKNOWN`, `POST
    /v1/invoices/{id}/reconcile`

    le pregunta a ARCA qué pasó. Es el camino de salida: sin él la serie queda
    trabada.


    ## Errores


    Todos tienen la misma forma. Lo estable para ramificar es `errorCode`; el

    `message` es texto para mostrar y puede cambiar. `details` **no viaja en

    producción** cuando es material de diagnóstico.


    ## Webhooks


    Para lo que **no podés saber por la respuesta HTTP**. Se configuran desde el

    panel —una vez por organización, no por cliente— y hay cinco eventos:


    | Evento | Cuándo |

    |---|---|

    | `invoice.resolved` | un comprobante que quedó `PENDING` o `UNKNOWN` llegó
    a un estado final |

    | `connection.established` | un contribuyente pasó a `CONECTADO`: ya puede
    facturar |

    | `credential.expiring` | al certificado de ARCA le quedan 30 días o menos |

    | `api_key.expiring` | a una API key le quedan 30 días o menos |

    | `quota.exceeded` | se pasó la cuota del plan: **no bloquea**, se sigue
    emitiendo y el excedente se factura al unitario. Sale una vez por período |


    `connection.established` se emite **sólo en la transición**, no en cada

    verificación: quien está esperando la conexión verifica varias veces
    seguidas, y

    un aviso por cada una sería ruido. Es lo que le permite a una plataforma
    avisarle

    a su cliente sin encuestar `/conexion/verificar`.


    **No hay `invoice.authorized`**: `POST /v1/invoices/authorize` ya devuelve
    el

    comprobante con su CAE. Un evento por eso te obligaría a procesar dos veces
    lo

    mismo y a resolver cuál llegó primero.


    ### Verificar la firma


    ```

    POST /tu-receptor

    X-Emissa-Signature: t=1769472000,v1=5257a869e7...

    X-Emissa-Event-Id: inv_cmft8x2k30001_authorized

    X-Emissa-Event-Type: invoice.resolved

    X-Emissa-Delivery-Attempt: 1

    ```


    `v1` es `HMAC-SHA256(secreto, "${t}.${cuerpo crudo}")` en hexadecimal.

    Compará **con el cuerpo sin parsear**: reserializar cambia los bytes y la
    firma

    deja de dar. Rechazá lo que llegue con más de 5 minutos de diferencia — el
    `t`

    está dentro del HMAC justamente para que una captura no se pueda repetir
    mañana.


    ```js

    const [t, v1] = cabecera.split(',').map((p) => p.split('=')[1]);

    const esperado = crypto.createHmac('sha256', secreto)
      .update(`${t}.${cuerpoCrudo}`).digest('hex');
    const ok = crypto.timingSafeEqual(Buffer.from(v1,'hex'),
    Buffer.from(esperado,'hex'))
      && Math.abs(Date.now()/1000 - Number(t)) <= 300;
    ```


    ### Deduplicá por `id`


    **La entrega es "al menos una vez".** *Exactamente una vez* no existe sobre

    HTTP: si procesás el evento y te morís antes de contestar 200, desde acá es

    indistinguible de que nunca te llegó, así que se reintenta.


    El `id` del evento es estable entre reintentos. Guardalo y descartá lo
    repetido.


    ### Contestá rápido


    Un 2xx cuenta como entregado. Cualquier otra cosa se reintenta a los 30 s, 2
    min,

    10 min, 1 h, 6 h y 24 h; después queda en la cola de dead-letter para
    reenviarla

    a mano desde el panel. El timeout es de **10 segundos**: encolá y contestá,
    no

    proceses adentro del handler.


    ## Trazabilidad


    **Toda** respuesta lleva `X-Request-Id`. Si lo mandás vos y respeta

    `[A-Za-z0-9._:-]{1,128}`, se te devuelve el mismo, para que tu traza y la

    nuestra sean una sola. Es el dato que hay que citar en un ticket.


    ## Ambientes


    `HOMOLOGACION` es el ambiente de pruebas de ARCA: emite comprobantes con CAE

    real pero **sin validez fiscal**. Es donde se integra y se prueba.


    `PRODUCCION` emite **comprobantes fiscales reales**. Requiere certificado

    productivo, punto de venta de tipo "Factura Electrónica – Web Service" y el

    servicio autorizado en ARCA; si falta alguno, contesta ARCA con el motivo.


    Los dos son conjuntos **disjuntos** de claves: una key `test` contra un

    contribuyente de `PRODUCCION` se rechaza con 403, así que no podés emitir un

    comprobante real con la clave con la que estabas probando.


    ## Compatibilidad


    Mientras `/v1` esté vigente se pueden **agregar** campos, parámetros

    opcionales y endpoints. No se quita ni se renombra un campo, no se le cambia
    el

    tipo ni la semántica, no se vuelve obligatorio algo opcional y no se cambia
    un

    código de estado. **Tu cliente tiene que tolerar campos que no conoce.**
  version: 1.0.0
  contact: {}
servers:
  - url: https://api.emissa.com.ar
    description: Producción
  - url: http://localhost:3001
    description: Local
security: []
tags:
  - name: Comprobantes
    description: Emitir, consultar, anular y exportar
  - name: Contribuyentes
    description: Los CUIT desde los que se factura
  - name: Clientes
    description: Receptores y validación contra el padrón de ARCA
  - name: Credenciales ARCA
    description: Certificado y clave privada del emisor
  - name: Consumo
    description: Cuánto se emitió en el período, y con qué cuota
  - name: Panel
    description: 'Operaciones internas del panel: sesión, 2FA, keys'
  - name: Salud
    description: Sondas de vida y de disponibilidad
paths:
  /v1/taxpayers:
    post:
      tags:
        - Contribuyentes
      summary: Alta o edición de un contribuyente
      description: >-
        Con `id` edita; sin `id` da de alta.


        **Se puede con API key desde la 5.33**, si la key tiene `taxpayer:write`
        y **no** está restringida a contribuyentes puntuales. Una key
        restringida recibe 403: si pudiera crear, el contribuyente nuevo
        quedaría fuera de su lista y podría operarlo igual, escapándose de su
        propia restricción.


        El `organizationId` del cuerpo lo usa sólo una sesión de panel. Con API
        key **se ignora**: la key opera en su organización y no la elige un
        parámetro.


        La respuesta trae `datosDelPapel.faltantes` con lo que todavía falta
        para poder imprimir un comprobante completo.


        **El alta fija el `producto` del contribuyente**: el de la key, si está
        atada a uno, o el de `X-Emissa-Producto`. La edición no lo cambia.


        **Scope de API key:** `taxpayer:write`. Una key sin ese scope recibe
        `403 API_KEY_INSUFFICIENT_SCOPE`, aunque el rol de quien la creó lo
        permita: el permiso efectivo es la **intersección** de los dos.
      operationId: TaxpayersController_upsert
      parameters:
        - name: X-Emissa-Producto
          in: header
          description: >-
            El producto que origina el pedido (`inmos`, por ejemplo). Opcional,
            sólo con API key. Formato `^[a-z0-9-]{1,32}$`; otro valor da `400
            INVALID_INPUT`. Queda en el comprobante, en el consumo y en el
            registro de pedidos.


            Con una key **sin** producto es sólo atribución: no autoriza ni
            restringe nada. Con una key **atada a un producto** el pedido se
            etiqueta con el de la key aunque no mandes la cabecera, y si mandás
            otro valor la respuesta es `403 API_KEY_PRODUCTO_DISTINTO`.
          required: false
          schema:
            type: string
            pattern: ^[a-z0-9-]{1,32}$
            example: inmos
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertTaxpayerDto'
      responses:
        '201':
          description: El contribuyente, con el estado de sus datos del papel.
          headers:
            X-Request-Id:
              description: >-
                Identificador del pedido. Va en TODA respuesta. Si lo mandás en
                el pedido y respeta `[A-Za-z0-9._:-]{1,128}`, se te devuelve el
                mismo; si no, se genera uno. Es lo que hay que citar en un
                ticket.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContribuyenteDto'
              example:
                id: cmt42x6jb000curi4sz1gyeij
                organizationId: cmt42x67e0003uri4u2ii1i7o
                createdByUserId: cmt42x67e0001uri4dpdgmvu0
                cuit: '20000000001'
                legalName: Estudio Contable SRL
                vatStatus: Responsable Inscripto
                salePoint: 1
                environment: HOMOLOGACION
                productos: []
                producto: null
                status: PENDING
                hasCredential: false
                commercialAddress: Av. Corrientes 1234, CABA
                grossIncomeTax: 901-123456-01
                activityStartDate: '2020-03-01'
                datosDelPapel:
                  completo: true
                  faltantes: []
        '400':
          description: |-
            Pedido mal formado o inválido.

            `errorCode`: `INVALID_INPUT`
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorDto'
                  - type: object
                    properties:
                      errorCode:
                        type: string
                        enum:
                          - INVALID_INPUT
          headers:
            X-Request-Id:
              description: >-
                Identificador del pedido. Va en TODA respuesta. Si lo mandás en
                el pedido y respeta `[A-Za-z0-9._:-]{1,128}`, se te devuelve el
                mismo; si no, se genera uno. Es lo que hay que citar en un
                ticket.
              schema:
                type: string
            X-Emissa-Mode:
              description: >-
                `test` o `live`. **Va en toda respuesta autenticada con API
                key**, y es la forma más barata de verificar que no estás
                emitiendo contra el ambiente equivocado. No aparece cuando se
                entra con sesión de panel: ahí el ambiente lo decide cada
                contribuyente, no la credencial.
              schema:
                type: string
                enum:
                  - test
                  - live
        '401':
          description: >-
            Falta la credencial o no es válida.


            `errorCode`: `UNAUTHORIZED`, `INVALID_API_KEY`,
            `API_KEY_WRONG_HEADER`
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorDto'
                  - type: object
                    properties:
                      errorCode:
                        type: string
                        enum:
                          - UNAUTHORIZED
                          - INVALID_API_KEY
                          - API_KEY_WRONG_HEADER
          headers:
            X-Request-Id:
              description: >-
                Identificador del pedido. Va en TODA respuesta. Si lo mandás en
                el pedido y respeta `[A-Za-z0-9._:-]{1,128}`, se te devuelve el
                mismo; si no, se genera uno. Es lo que hay que citar en un
                ticket.
              schema:
                type: string
            X-Emissa-Mode:
              description: >-
                `test` o `live`. **Va en toda respuesta autenticada con API
                key**, y es la forma más barata de verificar que no estás
                emitiendo contra el ambiente equivocado. No aparece cuando se
                entra con sesión de panel: ahí el ambiente lo decide cada
                contribuyente, no la credencial.
              schema:
                type: string
                enum:
                  - test
                  - live
        '403':
          description: >-
            La credencial es válida pero no alcanza. Con API key hay tres
            motivos, y cada uno trae su código:


            - `API_KEY_INSUFFICIENT_SCOPE` — falta `taxpayer:write`.
            `details.scopeRequerido` lo nombra.

            - `API_KEY_INSUFFICIENT_PERMISSIONS` — la key está **restringida a
            contribuyentes puntuales**. No puede crear otros: el nuevo quedaría
            fuera de su lista y podría operarlo igual.

            - `INSUFFICIENT_ROLE` — con sesión de panel, el rol no tiene
            `taxpayer:write`. Con API key, que no esté asociada a una
            organización.

            - `API_KEY_PRODUCTO_DISTINTO` — la key está atada a un producto y
            `X-Emissa-Producto` dice otro. Con API key, también: el **modo de la
            key no coincide con el ambiente del contribuyente**. Una `emi_test_`
            contra un contribuyente de `PRODUCCION` nunca emite.


            `errorCode`: `FORBIDDEN`, `API_KEY_INSUFFICIENT_PERMISSIONS`,
            `API_KEY_INSUFFICIENT_SCOPE`, `INSUFFICIENT_ROLE`,
            `API_KEY_PRODUCTO_DISTINTO`, `API_KEY_WRONG_MODE`
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorDto'
                  - type: object
                    properties:
                      errorCode:
                        type: string
                        enum:
                          - FORBIDDEN
                          - API_KEY_INSUFFICIENT_PERMISSIONS
                          - API_KEY_INSUFFICIENT_SCOPE
                          - INSUFFICIENT_ROLE
                          - API_KEY_PRODUCTO_DISTINTO
                          - API_KEY_WRONG_MODE
          headers:
            X-Request-Id:
              description: >-
                Identificador del pedido. Va en TODA respuesta. Si lo mandás en
                el pedido y respeta `[A-Za-z0-9._:-]{1,128}`, se te devuelve el
                mismo; si no, se genera uno. Es lo que hay que citar en un
                ticket.
              schema:
                type: string
            X-Emissa-Mode:
              description: >-
                `test` o `live`. **Va en toda respuesta autenticada con API
                key**, y es la forma más barata de verificar que no estás
                emitiendo contra el ambiente equivocado. No aparece cuando se
                entra con sesión de panel: ahí el ambiente lo decide cada
                contribuyente, no la credencial.
              schema:
                type: string
                enum:
                  - test
                  - live
        '409':
          description: >-
            Ya existe un contribuyente con ese CUIT en ese ambiente. Para
            editarlo, mandá su `id`: sin `id` esto siempre es un alta.


            **Si el contribuyente es de tu producto, la respuesta trae su `id`
            en `details.taxpayerId`** —también en producción—, así no hace falta
            buscarlo. Si es de otro producto o no tiene producto, el 409 viene
            **sin id**: el CUIT está tomado, pero esta credencial no alcanza ese
            contribuyente.


            `errorCode`: `RESOURCE_ALREADY_EXISTS`
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorDto'
                  - type: object
                    properties:
                      errorCode:
                        type: string
                        enum:
                          - RESOURCE_ALREADY_EXISTS
          headers:
            X-Request-Id:
              description: >-
                Identificador del pedido. Va en TODA respuesta. Si lo mandás en
                el pedido y respeta `[A-Za-z0-9._:-]{1,128}`, se te devuelve el
                mismo; si no, se genera uno. Es lo que hay que citar en un
                ticket.
              schema:
                type: string
            X-Emissa-Mode:
              description: >-
                `test` o `live`. **Va en toda respuesta autenticada con API
                key**, y es la forma más barata de verificar que no estás
                emitiendo contra el ambiente equivocado. No aparece cuando se
                entra con sesión de panel: ahí el ambiente lo decide cada
                contribuyente, no la credencial.
              schema:
                type: string
                enum:
                  - test
                  - live
        '429':
          description: |-
            Se excedió el límite de peticiones. **Reintentá con backoff.**

            `errorCode`: `RATE_LIMIT_EXCEEDED`
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorDto'
                  - type: object
                    properties:
                      errorCode:
                        type: string
                        enum:
                          - RATE_LIMIT_EXCEEDED
          headers:
            X-Request-Id:
              description: >-
                Identificador del pedido. Va en TODA respuesta. Si lo mandás en
                el pedido y respeta `[A-Za-z0-9._:-]{1,128}`, se te devuelve el
                mismo; si no, se genera uno. Es lo que hay que citar en un
                ticket.
              schema:
                type: string
            X-Emissa-Mode:
              description: >-
                `test` o `live`. **Va en toda respuesta autenticada con API
                key**, y es la forma más barata de verificar que no estás
                emitiendo contra el ambiente equivocado. No aparece cuando se
                entra con sesión de panel: ahí el ambiente lo decide cada
                contribuyente, no la credencial.
              schema:
                type: string
                enum:
                  - test
                  - live
        '500':
          description: |-
            Error interno. Citá el `requestId` al reportarlo.

            `errorCode`: `INTERNAL_ERROR`
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorDto'
                  - type: object
                    properties:
                      errorCode:
                        type: string
                        enum:
                          - INTERNAL_ERROR
          headers:
            X-Request-Id:
              description: >-
                Identificador del pedido. Va en TODA respuesta. Si lo mandás en
                el pedido y respeta `[A-Za-z0-9._:-]{1,128}`, se te devuelve el
                mismo; si no, se genera uno. Es lo que hay que citar en un
                ticket.
              schema:
                type: string
            X-Emissa-Mode:
              description: >-
                `test` o `live`. **Va en toda respuesta autenticada con API
                key**, y es la forma más barata de verificar que no estás
                emitiendo contra el ambiente equivocado. No aparece cuando se
                entra con sesión de panel: ahí el ambiente lo decide cada
                contribuyente, no la credencial.
              schema:
                type: string
                enum:
                  - test
                  - live
      security:
        - api-key: []
        - panel-jwt: []
components:
  schemas:
    UpsertTaxpayerDto:
      type: object
      properties:
        id:
          type: string
          description: Presente = edición, ausente = alta.
        organizationId:
          type: string
          description: >-
            Sólo aplica al alta; si no viene se usa la organización propia. En
            la edición la organización sale del contribuyente y **no** se puede
            mover de tenant por este endpoint.
        cuit:
          type: string
          example: '20437136697'
          description: >-
            CUIT con dígito verificador: se valida por módulo 11, no sólo el
            largo. Un CUIT mal tipeado que pase la validación no se descubre
            hasta que ARCA rechaza el comprobante, con el número ya consumido.
        legalName:
          type: string
          example: Estudio Contable SRL
          description: >-
            Razón social, **como figura en ARCA**. Va impresa en cada
            comprobante, así que una diferencia con el padrón se ve en el papel.


            **Opcional desde la 5.13: si no lo mandás se completa desde el
            padrón** de ARCA. Lo que mandés gana siempre — el padrón sólo llena
            lo que falta. Si no se pudo consultar y hace falta, la respuesta es
            un 400 que dice por qué.
        vatStatus:
          type: string
          enum:
            - Responsable Inscripto
            - Monotributista
            - Exento
            - Consumidor Final
          example: Responsable Inscripto
          description: >-
            Condición del **emisor** frente al IVA. Es lo que decide qué letra
            de comprobante puede emitir. `Consumidor Final` se acepta sólo para
            no dejar sin editar a quien ya lo tiene cargado: al emitir se
            rechaza.


            **Opcional desde la 5.13: si no lo mandás se completa desde el
            padrón** de ARCA. Lo que mandés gana. `Consumidor Final` no se
            completa nunca solo: quien no emite comprobantes no puede deducirse
            del padrón.
        salePoint:
          type: number
          minimum: 1
          maximum: 99999
          example: 1
          description: Punto de venta habilitado en ARCA para este CUIT.
        environment:
          type: string
          enum:
            - HOMOLOGACION
            - PRODUCCION
          example: HOMOLOGACION
          description: >-
            Ambiente de ARCA. **`PRODUCCION` emite comprobantes fiscales reales
            con CAE real** desde la 5.10: entran en los registros del
            contribuyente, pesan en su declaración y no se borran — se revierten
            con una nota de crédito. Para probar está `HOMOLOGACION`, que
            también emite con CAE real del organismo y no tiene consecuencia
            fiscal.


            Los dos ambientes son conjuntos **disjuntos** de claves: una API key
            `test` contra un contribuyente de `PRODUCCION` se rechaza con 403.
        commercialAddress:
          type: string
          maxLength: 255
          example: Av. Corrientes 1234, CABA
          description: >-
            Domicilio comercial. **Obligatorio en el papel**, opcional acá: ver
            `datosDelPapel.faltantes` en la respuesta.
        grossIncomeTax:
          type: string
          maxLength: 64
          example: 901-123456-01
          description: >-
            Ingresos Brutos. Texto libre porque el formato real varía: número
            local, Convenio Multilateral con guiones, o `Exento`.
        activityStartDate:
          type: string
          format: date
          example: '2020-03-01'
          description: >-
            Inicio de actividades. No puede ser futura **según el día
            argentino**, no según el reloj del servidor.
      required:
        - cuit
        - salePoint
        - environment
    ContribuyenteDto:
      type: object
      properties:
        id:
          type: string
          example: cms5182yf00gxurgsa7fzew8p
          description: >-
            Id del contribuyente. Es lo que se manda como `taxpayerId` al
            emitir.
        organizationId:
          type: string
          example: cms5182yf00gxurgsa7fzew8q
          description: >-
            La organización a la que pertenece. Un contribuyente vive en una
            sola, y una organización puede tener varios CUIT.
        createdByUserId:
          type: string
          nullable: true
          description: >-
            Quién lo dio de alta. `null` en los que se crearon antes de que se
            registrara el dato.
        cuit:
          type: string
          example: '20437136697'
          description: >-
            El CUIT bajo el que se emite, sin guiones. **Tiene que coincidir con
            el del certificado de ARCA** o la carga de credenciales se rechaza.
        legalName:
          type: string
          example: Estudio Contable SRL
          description: Razón social, como va impresa en el comprobante.
        vatStatus:
          type: string
          example: Responsable Inscripto
          description: >-
            Condición del emisor frente al IVA. Junto con la del receptor decide
            qué letra de comprobante corresponde.
        salePoint:
          type: number
          example: 1
          description: >-
            El **punto de venta** habilitado en ARCA para esta identidad fiscal.
            Es un número corto que ARCA asigna y que numera los comprobantes de
            forma independiente: el 0001 y el 0002 llevan cada uno su propia
            serie. Tiene que estar dado de alta en ARCA para el ambiente de
            `environment`, y el de homologación no sirve en producción.
        environment:
          type: string
          example: HOMOLOGACION
          enum:
            - HOMOLOGACION
            - PRODUCCION
          description: >-
            Contra qué ambiente de ARCA se emite. **La emisión en `PRODUCCION`
            está deshabilitada en esta versión.**
        productos:
          example:
            - inmos
            - define
          description: >-
            Los productos de define-it a los que pertenece este contribuyente, o
            `[]` si no tiene ninguno. **Un contribuyente puede ser de varios**:
            el mismo CUIT lo factura más de un producto. **Lo crea el alta**
            —con el producto de la key o, si la key no tiene, el de
            `X-Emissa-Producto`— y **no cambia por API**: sumar o quitar
            productos lo hace el dueño de la cuenta desde el panel. Una key
            atada a un producto alcanza a los contribuyentes cuyo `productos`
            **incluye** el suyo.
          type: array
          items:
            type: string
        producto:
          type: string
          nullable: true
          deprecated: true
          example: inmos
          description: >-
            **Deprecado: usá `productos`.** Es el primero de `productos`, o
            `null` si no tiene ninguno. Se mantiene un tiempo para el cliente
            que ya lo leía; con más de un producto, no dice cuáles son los
            demás.
        status:
          type: string
          example: PENDING
          description: >-
            Titularidad del CUIT. Queda en `PENDING` hasta que se cargue un
            certificado de ARCA emitido a ese mismo CUIT.
        hasCredential:
          type: boolean
          example: false
          description: >-
            Si hay una credencial activa **para el ambiente de `environment`**.
            Tener una de homologación no lo pone en `true` si el contribuyente
            está en producción.
        commercialAddress:
          type: string
          nullable: true
          example: Av. Corrientes 1234, CABA
          description: Domicilio comercial. Va impreso en el comprobante.
        grossIncomeTax:
          type: string
          nullable: true
          example: 901-123456-01
          description: Ingresos Brutos. Obligatorio en el papel (RG 1415).
        activityStartDate:
          type: string
          nullable: true
          example: '2020-03-01'
          description: Inicio de actividades, `YYYY-MM-DD`. Va impreso.
        datosDelPapel:
          description: >-
            Qué falta para que el comprobante impreso cumpla la RG 1415. **No
            bloquea la emisión**: sirve para avisar antes de que el contador lo
            note.
          allOf:
            - $ref: '#/components/schemas/DatosDelPapelDto'
      required:
        - id
        - organizationId
        - createdByUserId
        - cuit
        - legalName
        - vatStatus
        - salePoint
        - environment
        - productos
        - producto
        - status
        - hasCredential
        - commercialAddress
        - grossIncomeTax
        - activityStartDate
        - datosDelPapel
    ErrorDto:
      type: object
      properties:
        statusCode:
          type: number
          example: 401
          description: >-
            El mismo código HTTP de la respuesta. Va repetido en el cuerpo para
            que un cliente que sólo loguea el JSON no pierda el dato.
        errorCode:
          type: string
          example: UNAUTHORIZED
          description: >-
            Siempre una entrada del catálogo de errores. Es el campo por el que
            se ramifica.
        message:
          type: string
          example: No autorizado
          description: '**Siempre un string**, nunca un arreglo.'
        details:
          description: >-
            Detalle adicional. Lo contractual viaja siempre; lo de diagnóstico
            **no viaja en producción**. No ramifiques por este campo.
          oneOf:
            - type: string
            - type: object
              additionalProperties: true
        retryable:
          type: boolean
          example: false
          description: >-
            **¿Tiene sentido reintentar?** Si viene, le gana a cualquier
            heurística basada en el status.


            Existe porque el status no alcanza: `ARCA_TICKET_ALREADY_ISSUED` es
            un 409 que **se resuelve esperando**, y
            `ARCA_COMPUTER_NOT_AUTHORIZED` es un 502 que **no se arregla nunca
            esperando** — hay que autorizar el certificado en ARCA. Un cliente
            que ramifique sólo por 4xx/5xx acierta al revés en los dos.


            **Ausente significa "no lo sabemos"**, y es distinto de `false`: ahí
            corresponde la heurística de siempre. Sólo se declara donde la
            respuesta es certera.
        retryAfterSeconds:
          type: number
          example: 43200
          description: >-
            Cuántos segundos esperar antes de reintentar. **Sólo cuando el
            número es real** —el vencimiento de un ticket, la ventana de un
            límite—, nunca una estimación.


            El 429 y el bloqueo del segundo factor ya lo mandaban como
            `details.reintentarEnSegundos`, en castellano y anidado. Sigue ahí
            para no romper a nadie, y ahora **también arriba**, con el nombre
            del resto del sobre.
        requestId:
          type: string
          example: 0f2c1a5e-1d9b-4c7a-9a55-7f1a2b3c4d5e
          description: >-
            El mismo valor de la cabecera `X-Request-Id`. Es lo que hay que
            pegar en un ticket.
        timestamp:
          type: string
          example: '2026-07-26T18:30:00.000Z'
          description: Cuándo falló, en UTC. Sirve para ubicarlo en los logs.
        path:
          type: string
          example: /v1/invoices/authorize
          description: La ruta que se pidió, sin el host ni la query.
      required:
        - statusCode
        - errorCode
        - message
        - timestamp
        - path
    DatosDelPapelDto:
      type: object
      properties:
        completo:
          type: boolean
          example: false
          description: >-
            Si están los tres datos que la RG 1415 exige en el comprobante
            impreso. En `false` se puede emitir igual, pero el PDF sale
            incompleto.
        faltantes:
          example:
            - grossIncomeTax
            - activityStartDate
          description: >-
            Qué falta, con el nombre del campo tal como se manda al darlo de
            alta. Vacío cuando `completo` es `true`.
          type: array
          items:
            type: string
      required:
        - completo
        - faltantes
  securitySchemes:
    api-key:
      type: apiKey
      in: header
      name: X-API-Key
      description: >-
        API key de integración. **El modo va en el prefijo**: `emi_test_` opera
        contra homologación, `emi_live_` contra producción. Después, 64
        hexadecimales.
    panel-jwt:
      scheme: bearer
      bearerFormat: JWT
      type: http
      description: Sesión del panel. **No** es donde va la API key.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.