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
- Seu backend busca a chave pública:
GET /v1/certificates/public-key. - O navegador do usuário cifra PFX e senha com essa chave (código abaixo). O PFX nunca anda aberto.
- Seu backend repassa o envelope em
POST /v1/certificatese guarda oserial_number. - De hora em hora,
POST /v1/documents/searchcom os seriais e onext_cursorde 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)
nfse: pode ligar sempre.nfeecte: a SEFAZ tem uma fila só por CNPJ.
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": []
}
- A busca lê o banco do buscador e nunca chama a SEFAZ. Uma nota nova leva até cerca de 1 hora para aparecer.
next_cursoré a sua posição: enviado de volta, só vêm documentos novos ou alterados, inclusive cancelamentos. Um documento pode voltar; grave pordoc_type+access_key.has_more: true: chame de novo na hora com o novo cursor.- Até 100 seriais por chamada (hexadecimal, com
:ou decimal). Serial desconhecido ou de outro produto vem emnot_found. - Filtros:
doc_type(nfe,nfse,cte),status(summary,complete,cancelled,denied),issued_from,issued_to,limit_per_certificate(até 500) einclude_xml. - NF-e em
summaryé só o resumo: o XML completo depende da ciência, que só é ligada com autorização do cliente.
5. Rotas
| Rota | Para quê |
|---|---|
GET /v1/certificates/public-key | Chave pública do envelope |
POST /v1/certificates | Envia o certificado (envelope) |
GET /v1/certificates | Certificados do seu produto |
PUT /v1/certificates/{serial}/sources | Troca as fontes ligadas |
DELETE /v1/certificates/{serial} | Desliga o certificado do seu produto |
POST /v1/documents/search | Notas por serial, com cursor |
GET /v1/documents/{id} e /xml | Um documento e o XML dele |
GET /v1/status | Cursores, bloqueios e erros por empresa |
GET /v1/runs | O que cada ciclo de consulta fez |
GET /health | Saú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.
| Status | Quando |
|---|---|
401 | Chave ausente ou inválida |
404 | Não existe ou é de outro produto |
413 | Corpo acima de 1 MB |
422 | Entrada inválida (a resposta não repete o que foi enviado) |
503 | Serviço sem configuração para aquela operação |