buscador-e · notas de entrada

Buscador de notas

Busca NF-e modelo 55, NFS-e (ADN) e CT-e em que a empresa é destinatária ou tomadora, com o certificado A1 dela, e entrega por API. Você envia o certificado uma vez e consulta as notas quando quiser.

1. Acesso

Endereço: https://buscador-e.khontra.com. Só HTTPS.

Peça à Khontra uma chave para o seu produto. Toda rota de /v1 leva o cabeçalho X-API-Key. A chave aparece uma vez, fica só no seu backend e nunca vai para o navegador. Cada produto só enxerga as empresas cujo certificado ele mesmo enviou: o resto responde 404.

curl https://buscador-e.khontra.com/v1/certificates -H "X-API-Key: $BUSCADOR_API_KEY"

2. O fluxo

  1. Seu backend busca a chave pública: GET /v1/certificates/public-key.
  2. O navegador do usuário cifra PFX e senha com essa chave (código abaixo). O PFX nunca anda aberto.
  3. Seu backend repassa o envelope em POST /v1/certificates e guarda o serial_number.
  4. De hora em hora, POST /v1/documents/search com os seriais e o next_cursor de cada um.

3. Enviar o certificado

RSA-OAEP (SHA-256) protege uma chave AES-256-GCM, que cifra {"pfx_base64", "password"}, com o kid como dado associado. A API recusa PFX aberto.

async function encryptCertificate(pfxBase64, password, state, sources, { kid, public_key_pem }) {
  const b64 = (b) => btoa(String.fromCharCode(...new Uint8Array(b)));
  const der = Uint8Array.from(
    atob(public_key_pem.replace(/-----[^-]+-----|\s/g, "")), (c) => c.charCodeAt(0));
  const rsa = await crypto.subtle.importKey(
    "spki", der, { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
  const aes = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt"]);
  const nonce = crypto.getRandomValues(new Uint8Array(12));
  const ciphertext = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv: nonce, additionalData: new TextEncoder().encode(kid) }, aes,
    new TextEncoder().encode(JSON.stringify({ pfx_base64: pfxBase64, password })));
  const raw = await crypto.subtle.exportKey("raw", aes);
  const encryptedKey = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, rsa, raw);
  return { kid, state, sources,
    encrypted_key: b64(encryptedKey), nonce: b64(nonce), ciphertext: b64(ciphertext) };
}

O corpo do POST /v1/certificates é o objeto devolvido. state é a UF (GO). Resposta 201: {"serial_number", "cnpj", "valid_until", "active"}. Reenviar o mesmo certificado não duplica nada.

Quais fontes ligar (sources, obrigatório)

Antes de ligar nfe ou cte, confirme que nenhum outro sistema (ERP, contabilidade) já consulta esse CNPJ na SEFAZ. Dois consultores no mesmo CNPJ fazem a SEFAZ bloqueá-lo por 1 hora. Para mudar depois: PUT /v1/certificates/{serial}/sources.

4. Buscar notas

curl -X POST https://buscador-e.khontra.com/v1/documents/search \
  -H "X-API-Key: $BUSCADOR_API_KEY" -H "Content-Type: application/json" \
  -d '{"serial_numbers": ["3a7f91c2d04e5b6a8c1d"], "cursors": {"3a7f91c2d04e5b6a8c1d": "..."}}'
{
  "results": {
    "3a7f91c2d04e5b6a8c1d": {
      "serial_number": "3a7f91c2d04e5b6a8c1d",
      "cnpj": "11222333000181",
      "company_name": "...",
      "certificate_active": true,
      "last_success": {"cte": null, "nfe": null, "nfse": "2026-11-04T11:55:10Z"},
      "documents": [{"doc_type": "nfse", "access_key": "...", "status": "complete", "...": "..."}],
      "next_cursor": "...",
      "has_more": false
    }
  },
  "not_found": []
}

5. Rotas

RotaPara quê
GET /v1/certificates/public-keyChave pública do envelope
POST /v1/certificatesEnvia o certificado (envelope)
GET /v1/certificatesCertificados do seu produto
PUT /v1/certificates/{serial}/sourcesTroca as fontes ligadas
DELETE /v1/certificates/{serial}Desliga o certificado do seu produto
POST /v1/documents/searchNotas por serial, com cursor
GET /v1/documents/{id} e /xmlUm documento e o XML dele
GET /v1/statusCursores, bloqueios e erros por empresa
GET /v1/runsO que cada ciclo de consulta fez
GET /healthSaúde, sem chave

Esquemas completos e "Try it out": Swagger.

6. Erros e limites

Erros vêm em application/problem+json com code e request_id. Mande o request_id (ou o cabeçalho X-Request-ID da resposta) quando pedir ajuda.

StatusQuando
401Chave ausente ou inválida
404Não existe ou é de outro produto
413Corpo acima de 1 MB
422Entrada inválida (a resposta não repete o que foi enviado)
503Serviço sem configuração para aquela operação