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

# Cargar el certificado y la clave privada

> El par que ARCA emitió para este CUIT y ambiente. **Se guarda cifrado y no se devuelve nunca**, ni completo ni parcial.

**Se puede con API key desde la 5.35**, con el scope `credential:write`. Si la key está restringida a ciertos contribuyentes, el de este pedido tiene que estar entre ellos.

Cargar un certificado propio es **opcional**: un contribuyente que autorizó a Emissa en ARCA emite firmando con el certificado de la plataforma. El propio le gana cuando está, y no depende de la delegación.

**Scope de API key:** `credential: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/arca/credentials
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/arca/credentials:
    post:
      tags:
        - Credenciales ARCA
      summary: Cargar el certificado y la clave privada
      description: >-
        El par que ARCA emitió para este CUIT y ambiente. **Se guarda cifrado y
        no se devuelve nunca**, ni completo ni parcial.


        **Se puede con API key desde la 5.35**, con el scope `credential:write`.
        Si la key está restringida a ciertos contribuyentes, el de este pedido
        tiene que estar entre ellos.


        Cargar un certificado propio es **opcional**: un contribuyente que
        autorizó a Emissa en ARCA emite firmando con el certificado de la
        plataforma. El propio le gana cuando está, y no depende de la
        delegación.


        **Scope de API key:** `credential: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: CredentialsController_save
      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/SaveCredentialDto'
      responses:
        '201':
          description: Metadatos del certificado guardado. Nunca el PEM.
          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/CredencialDto'
              example:
                id: cmt42x6k6000furi4veejuoyl
                taxpayerId: cmt42x6jb000curi4sz1gyeij
                environment: HOMOLOGACION
                status: ACTIVE
                cuit: '20000000001'
                certificateSubject: CN=emissa-e2e, O=Emissa, C=AR, serialNumber=CUIT 20000000001
                certificateIssuer: CN=emissa-e2e, O=Emissa, C=AR, serialNumber=CUIT 20000000001
                certificateSerial: '01'
                certificateFingerprint: >-
                  04790c756e83825857aa2a007df482eef4af7e5b4177b7d28d629f45a803e9ec
                notBefore: '2026-08-21T07:51:04.000Z'
                notAfter: '2027-08-22T07:51:04.000Z'
                diasParaVencer: 364
                createdAt: '2026-08-22T07:51:09.942Z'
                supersededAt: null
                revokedAt: null
        '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:
    SaveCredentialDto:
      type: object
      properties:
        taxpayerId:
          type: string
          example: cmfa1b2c30000
          description: >-
            A qué contribuyente se le carga. **El CUIT del certificado tiene que
            coincidir con el suyo** o la carga se rechaza: es lo que impide
            subir el certificado de otro por error.
        environment:
          type: string
          enum:
            - HOMOLOGACION
            - PRODUCCION
          example: HOMOLOGACION
          description: >-
            Para qué ambiente sirve este certificado. **Son trámites distintos
            en ARCA y certificados distintos**: el de homologación no funciona
            en producción ni al revés.
        certificate:
          type: string
          example: |-
            -----BEGIN CERTIFICATE-----
            MIIB...
            -----END CERTIFICATE-----
          description: >-
            Certificado X.509 en PEM, el que emitió ARCA para este CUIT y
            ambiente.
        privateKey:
          type: string
          example: |-
            -----BEGIN PRIVATE KEY-----
            MIIE...
            -----END PRIVATE KEY-----
          description: >-
            Clave privada en PEM. **Se guarda cifrada y no se devuelve nunca**,
            ni completa ni parcial.
      required:
        - taxpayerId
        - environment
        - certificate
        - privateKey
    CredencialDto:
      type: object
      properties:
        id:
          type: string
          example: cms5182zi00heurgsh49sgxwv
          description: Id de la credencial. Es lo que se usa para revocarla.
        taxpayerId:
          type: string
          example: cms5182yf00gxurgsa7fzew8p
          description: A qué contribuyente pertenece.
        environment:
          type: string
          example: HOMOLOGACION
          enum:
            - HOMOLOGACION
            - PRODUCCION
          description: >-
            Contra qué ambiente de ARCA sirve. **Un certificado de homologación
            no funciona en producción**: son trámites distintos y certificados
            distintos.
        status:
          type: string
          example: ACTIVE
          description: >-
            `ACTIVE` es la que se usa para emitir. Cargar una nueva deja la
            anterior en `SUPERSEDED`; revocarla la deja en `REVOKED`. **El
            historial no se borra**: la trazabilidad de con qué certificado se
            emitió cada comprobante depende de que queden todas.
        cuit:
          type: string
          example: '20437136697'
          description: >-
            El CUIT **leído del certificado**, no el declarado. Si no coincide
            con el del contribuyente, la carga se rechaza.
        certificateSubject:
          type: string
          example: CN=emissa, SERIALNUMBER=CUIT 20437136697
          description: >-
            El *subject* del certificado, tal como lo emitió ARCA. De ahí se lee
            el CUIT.
        certificateIssuer:
          type: string
          example: CN=Computadores Test, O=AFIP
          description: >-
            Quién lo emitió. El de homologación dice `Computadores Test`; el de
            producción, no.
        certificateSerial:
          type: string
          example: 3c9a1f2b4d5e6071
          description: Número de serie del certificado, en hexadecimal.
        certificateFingerprint:
          type: string
          example: a4:1f:9c:2b:...
          description: SHA-256 del certificado. Sirve para identificarlo sin abrirlo.
        notBefore:
          type: string
          example: '2026-01-15T00:00:00.000Z'
          description: >-
            Desde cuándo vale el certificado. Uno cargado antes de esta fecha no
            sirve para emitir todavía.
        notAfter:
          type: string
          example: '2028-01-15T00:00:00.000Z'
          description: >-
            Cuándo vence. **Pasada esta fecha ARCA rechaza todo**, así que hay
            que renovarlo antes: es la causa más común de que un sistema que
            venía facturando deje de hacerlo de un día para el otro.
        diasParaVencer:
          type: number
          example: 540
          description: >-
            Días hasta `notAfter`. **Puede ser negativo** si ya venció: no se
            recorta a cero para que un certificado vencido se distinga de uno
            que vence hoy.
        createdAt:
          type: string
          example: '2026-01-20T14:05:00.000Z'
          description: Cuándo se cargó en Emissa, en UTC.
        supersededAt:
          type: string
          nullable: true
          description: Cuándo la reemplazó otra. `null` mientras sea la vigente.
        revokedAt:
          type: string
          nullable: true
          description: Cuándo se revocó a mano. `null` si no se revocó.
      required:
        - id
        - taxpayerId
        - environment
        - status
        - cuit
        - certificateSubject
        - certificateIssuer
        - certificateSerial
        - certificateFingerprint
        - notBefore
        - notAfter
        - diasParaVencer
        - createdAt
        - supersededAt
        - revokedAt
    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.