> ## 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.

# Descargar el logo del emisor

> Devuelve el archivo tal como se subió, con su `Content-Type`. **404 si el contribuyente no tiene logo**, que es distinto de no existir.

**Scope de API key:** `taxpayer:read`. 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 get /v1/taxpayers/{id}/logo
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/{id}/logo:
    get:
      tags:
        - Contribuyentes
      summary: Descargar el logo del emisor
      description: >-
        Devuelve el archivo tal como se subió, con su `Content-Type`. **404 si
        el contribuyente no tiene logo**, que es distinto de no existir.


        **Scope de API key:** `taxpayer:read`. 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: LogoController_descargar
      parameters:
        - name: id
          required: true
          in: path
          schema:
            type: string
        - 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
      responses:
        '200':
          description: >-
            El archivo, con su `Content-Type` real. **No es JSON**: son los
            bytes de la imagen, listos para un `<img src>`.
          content:
            image/png:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
          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
        '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, 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`, `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
                          - 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
        '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:
    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
  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.