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

# Certificado digital

> Como fazer upload do e-CNPJ A1, conferir a validade real e ser avisado antes do vencimento.

Todo documento fiscal eletrônico precisa ser **assinado digitalmente** com um certificado e-CNPJ. A engineAPI cuida da assinatura automaticamente, você só precisa fazer o upload do arquivo.

<CardGroup cols={2}>
  <Card title="Tipo A1" icon="file-shield">
    Arquivo `.pfx` ou `.p12`. Armazenado em software. Suportado pela engineAPI.
  </Card>

  <Card title="Tipo A3" icon="hard-drive">
    Token físico ou smartcard. **Não suportado**. Use sempre A1.
  </Card>
</CardGroup>

***

## Upload do Certificado

`POST /v1/companies/{issuerId}/certificate`: **multipart/form-data**, campo do arquivo é
**`file`** (não `certificate`), autenticado por JWT do dashboard.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.engineapi.com.br/v1/companies/ISSUER_ID/certificate \
    -H "Authorization: Bearer SEU_TOKEN" \
    -F "file=@/caminho/certificado.pfx" \
    -F "password=senhaDoCertificado"
  ```

  ```typescript Node.js theme={null}
  import { readFileSync } from 'fs';

  const form = new FormData();
  form.append('file', new Blob([readFileSync('certificado.pfx')]));
  form.append('password', 'senhaDoCertificado');

  const resp = await fetch(
    `https://api.engineapi.com.br/v1/companies/${issuerId}/certificate`,
    {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${token}` },
      body: form,
    }
  );

  if (resp.ok) {
    console.log('Certificado enviado com sucesso');
  }
  ```

  ```python Python theme={null}
  with open('certificado.pfx', 'rb') as f:
      resp = httpx.post(
          f'https://api.engineapi.com.br/v1/companies/{issuer_id}/certificate',
          headers={'Authorization': f'Bearer {token}'},
          files={'file': ('cert.pfx', f, 'application/x-pkcs12')},
          data={'password': 'senhaDoCertificado'},
      )

  if resp.status_code == 200:
      print('Certificado enviado com sucesso')
  ```
</CodeGroup>

O upload:

<Steps>
  <Step title="Valida a senha">
    A senha do `.pfx` é validada antes de gravar. Senha errada retorna erro e nada é
    persistido.
  </Step>

  <Step title="Extrai a validade real">
    A **data real de validade** do certificado é extraída via OpenSSL (`certExpiry`).
  </Step>

  <Step title="Criptografa e substitui">
    A senha é criptografada em repouso (`certPassword`) e o `.pfx` anterior é removido do
    disco.
  </Step>
</Steps>

<Warning>
  A senha do certificado é criptografada e **nunca é retornada** pela API após o upload. Você não consegue recuperá-la. Guarde em local seguro.
</Warning>

***

## Verificando o Status

Após o upload, consulte a empresa para conferir o certificado:

```bash theme={null}
curl -X GET https://api.engineapi.com.br/v1/companies/ISSUER_ID \
  -H "Authorization: Bearer SEU_TOKEN"
```

O retorno é o registro do `Issuer` (com segredos removidos): **não existe um objeto
aninhado `certificate` nem um enum de status** (`VALID`/`EXPIRED`/etc.). Os campos reais são:

```json theme={null}
{
  "data": {
    "id": "ISSUER_ID",
    "cnpj": "11222333000181",
    "certFilename": "/app/uploads/certificates/9f1c2b3a-....pfx",
    "certExpiry": "2027-03-15T00:00:00.000Z"
  },
  "meta": { "requestId": "req_abc123", "timestamp": "2026-07-06T12:00:00.000Z" }
}
```

| Campo          | Significado                                                                                                                              |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `certFilename` | Presente = certificado enviado e validado. `null` = nenhum certificado ainda                                                             |
| `certExpiry`   | Data real de expiração (extraída do `.pfx` via OpenSSL no upload). Compare com `new Date()` no seu código para decidir se está expirando |

O upload também extrai o CNPJ do titular do A1 (OID ICP-Brasil `2.16.76.1.3.3`,
com fallback no CN `RAZAO:CNPJ`). Esse metadado **não volta** na resposta
pública. Use `prontoPara.nfse` para saber se o certificado serve para emitir.

`prontoPara.nfse` distingue três recusas de certificado:

| `faltando.nfse`        | Quando                                         |
| ---------------------- | ---------------------------------------------- |
| `certificado`          | A1 ausente                                     |
| `certificadoVencido`   | `certExpiry` já passou                         |
| `certificadoOutroCnpj` | raiz (8 dígitos) do A1 diferente da do emissor |

Filial com e-CNPJ da matriz (mesma pessoa jurídica) **não** entra em
`certificadoOutroCnpj`: a NFS-e Nacional assina por CNPJ raiz.

Nos 30 dias anteriores ao vencimento, `avisos[]` traz `CERTIFICADO_EXPIRANDO`
(`expiresAt`, `daysUntilExpiry`): é aviso, não reprova o cadastro. Se a
validade do A1 não pôde ser lida, `avisos[]` traz
`CERTIFICADO_VALIDADE_DESCONHECIDA` (também aviso; `prontoPara.nfse` segue
`true`). Até o instante de expirar o emissor continua pronto; se o A1 vencer
durante a ida à SEFIN, a recusa é do Fisco.

<Info>
  Não confie em um campo de status pronto: calcule os dias restantes a partir de
  `certExpiry` no seu lado, ou use os webhooks `certificate.expiring` (ver abaixo) que já
  fazem esse cálculo no servidor. Para o botão de emitir NFS-e, leia
  `prontoPara.nfse`: vencido e de outro CNPJ já vêm nomeados em `faltando`.
</Info>

***

## Onde Obter um Certificado

Para **produção**, você precisa de um e-CNPJ A1 emitido por uma Autoridade Certificadora (AC) credenciada pela ICP-Brasil:

| AC        | Site                       |
| --------- | -------------------------- |
| Certisign | certisign.com.br           |
| Serpro    | serpro.gov.br              |
| Valid     | valid.com                  |
| Serasa    | serasacertificadora.com.br |
| Soluti    | soluti.com.br              |

<Info>
  Para **homologação**, você pode usar um certificado de testes emitido por qualquer AC. O SEFAZ de homologação aceita certificados expirados e de teste. Não precisa comprar um certificado só para testar.
</Info>

***

## Monitorando a Validade

A engineAPI roda uma verificação diária (08h, horário de Brasília) e dispara **um único**
evento de webhook por limiar de dias restantes:

| Evento                 | Quando                                    | Payload                                                                         |
| ---------------------- | ----------------------------------------- | ------------------------------------------------------------------------------- |
| `certificate.expiring` | 30, 15, 7, 3 ou 1 dia(s) antes de expirar | `{ issuerId, issuerName, cnpj, expiresAt, daysUntilExpiry, severity, message }` |

Não existem eventos separados `expiring_soon`/`expiring_critical`/`expired`, é sempre o
mesmo `certificate.expiring`, com `daysUntilExpiry` e `severity` indicando a urgência.
Configure seus webhooks em **Dashboard → Configurações → Webhooks** (ver [guia de
webhooks](/guides/webhooks)).

***

## Renovando o Certificado

Quando você receber um novo certificado A1 (renovação ou substituição), simplesmente refaça o upload:

```bash theme={null}
curl -X POST https://api.engineapi.com.br/v1/companies/ISSUER_ID/certificate \
  -H "Authorization: Bearer SEU_TOKEN" \
  -F "file=@novo_certificado.pfx" \
  -F "password=novaSenha"
```

<Check>
  O certificado anterior é substituído automaticamente (o arquivo antigo é removido do disco). Nenhuma emissão em andamento é afetada.
</Check>

***

## Veja também

* **[Primeira emissão](/guides/first-emission):** com o certificado pronto, emita sua primeira nota.
* **[Webhooks](/guides/webhooks):** configure alertas de validade do certificado.
