Pular para o conteúdo

Do ERP à primeira análise

Neste tutorial você escreve o script que um ERP roda para manter o Catálogo de Produtos no Cotejar e conferir as exigências: envia os produtos em lotes, valida, aplica, pede a análise completa e lê todos os achados. Cada resposta mostrada aqui foi capturada de uma execução real do fluxo.

Você vai precisar de: uma credencial com Consultar conta, Consultar catálogo, Enviar produtos, Aplicar lotes e Executar análises (veja o guia rápido), Node.js 20 ou mais recente e o cliente HTTP salvo como cotejar.mjs.

Fluxo completo: do ERP ao resultado1. Seu ERP → API Cotejar: POST /v1/catalogos/ingestoes 2. API Cotejar → Seu ERP: 201 · ingestao_id 3. Seu ERP → API Cotejar: PUT …/lotes/1 … n 4. Seu ERP → API Cotejar: POST …/validar 5. API Cotejar → Seu ERP: 200 · pronta_para_aplicar 6. Seu ERP → API Cotejar: POST …/aplicar 7. API Cotejar → Seu ERP: 202 · Location 8. API Cotejar → Seu receptor: catalogo.aplicado 9. Seu ERP → API Cotejar: POST /v1/analises 10. API Cotejar → Seu ERP: 202 · Location 11. API Cotejar → Seu receptor: analise.concluida 12. Seu ERP → API Cotejar: GET …/resultados?pagina=nSeu ERPAPI CotejarSeu receptorPOST /v1/catalogos/ingestoes201 · ingestao_idPUT …/lotes/1 … naté 200 produtos por lotePOST …/validar200 · pronta_para_aplicarPOST …/aplicar202 · Locationcatalogo.aplicadoPOST /v1/analises202 · Locationanalise.concluidaGET …/resultados?pagina=n
Fluxo completo: do ERP ao resultado
  1. GET/v1/catalogoscatalogo:lerReferência

    Cada estabelecimento tem seus catálogos. Guarde o id do catálogo que o ERP alimenta; ele vai em todas as ingestões.

    200 OK
    {
    "itens": [
    {
    "id": "7c7b354f-07b6-4461-9240-263418c81cdb",
    "id_externo": "CAT-ERP-002",
    "estabelecimento_id": "d905fb0d-2f5e-4a67-ad96-955a90fc4431",
    "cnpj_estabelecimento": "12345678000276",
    "nome": "Catálogo da filial",
    "estado": "ativo",
    "versao": 1,
    "produtos": 0
    }
    ],
    "proximo_id": null
    }
  2. POST/v1/catalogos/ingestoescatalogo:escreverReferência
    const { dados: ingestao } = await chamar('POST', '/v1/catalogos/ingestoes', {
    corpo: { catalogo_id: CATALOGO_ID, operacao: 'mesclar', referencia_externa: 'erp-2026-10-13' },
    idempotencia: randomUUID(),
    })
    Campo Uso
    catalogo_id Catálogo de destino. Obrigatório quando a empresa tem mais de um catálogo ativo.
    operacao mesclar adiciona e atualiza os enviados e preserva os ausentes. substituicao_catalogo substitui os vínculos daquele catálogo.
    referencia_externa Seu identificador da execução, até 200 caracteres. Ajuda a cruzar logs.
    201 Created
    { "ingestao_id": "5306d957-04d7-44dd-9c63-e4d04d2e19fe", "estado": "recebendo_lotes", "repetida": false, "catalogo_id": "7c7b354f-07b6-4461-9240-263418c81cdb", "versao_catalogo": 1 }
  3. PUT/v1/catalogos/ingestoes/{id}/lotes/{sequencia}catalogo:escreverReferência

    Numere as sequências a partir de 1. Cada lote leva até 200 produtos e 256 KiB; a ingestão inteira, até 50 mil produtos.

    Corpo de um lote
    {
    "produtos": [
    {
    "codigo_cliente": "000123",
    "descricao_cliente": "Luva cirúrgica estéril de látex",
    "ncm": "40151900",
    "desativado_por_ato": false,
    "atributos": [{ "codigo": "ATT_123", "valores": ["valor informado"] }]
    }
    ]
    }
    Campo Regra
    codigo_cliente Código do produto no ERP, até 256 caracteres. É a identidade estável: mande sempre o mesmo.
    descricao_cliente Descrição, até 2.000 caracteres.
    ncm Oito dígitos, como texto. Preserve zeros à esquerda.
    desativado_por_ato true quando o produto foi desativado por ato administrativo no Portal Único.
    atributos Até 128 atributos { codigo: "ATT_<n>", valores: [...] }, cada um com até 50 valores.
    201 Created
    { "ingestao_id": "5306d957-04d7-44dd-9c63-e4d04d2e19fe", "sequencia": 1, "produtos_recebidos": 3, "repetido": false }

    Se a resposta se perder, reenvie o mesmo lote com a mesma Idempotency-Key: a API devolve 200 com "repetido": true e não conta o lote duas vezes.

  4. POST/v1/catalogos/ingestoes/{id}/validarcatalogo:escreverReferência

    Informe a última sequência enviada. A API confere que nenhuma falta, valida cada NCM contra a nomenclatura vigente e devolve a prévia com o hash_previa.

    {
    "ingestao_id": "5306d957-04d7-44dd-9c63-e4d04d2e19fe",
    "estado": "pronta_para_aplicar",
    "hash_previa": "4bee6b75d2d85b7a1ed8f4dd9657f131c7d9b1dca79c0eac2c3c912091d131ae",
    "totais": { "lotes": 1, "produtos": 3, "incompletos": 0, "atributos": 0 },
    "guardas": []
    }

    Em precisa_revisao, pare e avise alguém: aplicar devolveria 409 REVISAO_HUMANA_NECESSARIA. Corrija no ERP e comece uma ingestão nova.

  5. POST/v1/catalogos/ingestoes/{id}/aplicarcatalogo:aplicarReferência

    Envie o hash_previa que você validou. Se o catálogo mudar entre a validação e a aplicação, a prévia é invalidada e nada é publicado pela metade.

    202 Accepted · Location: /v1/catalogos/ingestoes/5306d957-…
    { "ingestao_id": "5306d957-04d7-44dd-9c63-e4d04d2e19fe", "estado": "aplicando", "repetida": false }

    202 não é catálogo atualizado. Consulte a ingestão até aplicada ou falhou_aplicacao:

    { "estado": "aplicando", "progresso_aplicacao": { "fase": "preparando", "itens_preparados": 0, "itens_previstos": 3 }, "aplicada_em": null }
  6. POST/v1/analisesanalise:executarReferência

    A análise considera todos os produtos ativos da empresa. O corpo é um objeto vazio.

    202 Accepted · Location: /v1/analises/e0da8bf7-0252-4556-9d52-856cc1349507
    { "id": "e0da8bf7-0252-4556-9d52-856cc1349507", "estado": "pendente" }

    Consulte o Location até concluida ou falhou. Enquanto isso, resultado é null, o que não significa zero achados.

    GET /v1/analises/e0da8bf7-… · concluída
    {
    "id": "e0da8bf7-0252-4556-9d52-856cc1349507",
    "estado": "concluida",
    "terminada_em": "2026-10-13T16:47:03.109Z",
    "codigo_falha": null,
    "resultado": {
    "snapshot_id": "snap-338",
    "versao_portal": "338",
    "versao_avaliador": "2",
    "executada_em": "2026-10-13T16:47:01.581Z",
    "regras_coletadas_em": "2026-10-12T16:00:00.000Z",
    "ultima_coleta_util_em": "2026-10-13T06:32:54.000Z",
    "regulamento_degradado": false,
    "assinante": true,
    "franquia_consumida": false,
    "produtos_avaliados": 2847,
    "divergencias": { "orfao": 4, "bloqueante": 8911, "deriva": 0, "anvisa": 2787 },
    "catalogos": [{ "id": "7c7b354f-07b6-4461-9240-263418c81cdb", "versao": 1 }],
    "paginas_resultado": 59
    }
    }

    regulamento_degradado: true avisa que o Cotejar não conseguiu confirmar que a regra do Portal Único usada está atualizada. Mostre essa marcação a quem usa o resultado: nessa condição, zero achados não comprova que o catálogo está em dia. O evento analise.concluida traz o mesmo analiseId e essa marcação.

  7. GET/v1/analises/{id}/resultadoscatalogo:lerReferência

    Comece em pagina=1 e siga proxima_pagina até null. Cada página tem até 200 achados e também é limitada por bytes: não calcule a última página a partir do total.

    200 OK · pagina=1
    {
    "itens": [
    {
    "produto_id": "0001628c-0c07-48ab-85f7-011db27a16ed",
    "codigo_produto": "0001628c-0c07-48ab-85f7-011db27a16ed",
    "ncm": "30021500",
    "tipo": "bloqueante",
    "codigo_atributo": "ATT_1000",
    "detalhe": "Atributo obrigatório ATT_1000 — Marca comercial não preenchido."
    }
    ],
    "proxima_pagina": 2
    }
    tipo Significado
    orfao Produto desativado por ato ou NCM fora da tabela vigente: não há regra contra a qual comparar.
    bloqueante Atributo obrigatório não preenchido ou valor que o Portal Único não aceita: a DUIMP seria rejeitada.
    deriva A regra mudou: um atributo deixou de ser exigido e o produto ainda o informa, ou passou a constar e o produto não o informa. Não trava a DUIMP hoje.
    anvisa Categoria do produto para a Anvisa ausente ou não aceita: o produto depende de regularização da anuência.

    Os valores de exemplo vêm de um ambiente local com regulamento de teste: NCMs, atributos e contagens ilustram o formato e não descrevem exigências reais.

  8. O mesmo resultado sai em um CSV único, com tamanho e SHA-256 nos cabeçalhos. Use o validador de CSV antes de importar.

O arquivo abaixo é o fluxo inteiro, já com o tratamento de recusa da prévia. Ele foi executado do início ao fim contra uma instância local da API.

sincronizar.mjs
// Do ERP à primeira análise: envia o catálogo, aplica, analisa e lê os achados.
import { randomUUID } from 'node:crypto'
import { aguardar, chamar } from './cotejar.mjs'
const CATALOGO_ID = process.env.COTEJAR_CATALOGO_ID
const produtos = [ // na integração real, venha do ERP
{ codigo_cliente: '000123', descricao_cliente: 'Luva cirúrgica estéril de látex', ncm: '40151900', desativado_por_ato: false, atributos: [] },
{ codigo_cliente: '000124', descricao_cliente: 'Agulha hipodérmica 25x7', ncm: '90183210', desativado_por_ato: false, atributos: [] },
{ codigo_cliente: '000125', descricao_cliente: 'Monitor de pressão arterial de braço', ncm: '90189010', desativado_por_ato: false, atributos: [] },
]
// 1. Iniciar a ingestão. Em produção, persista cada chave antes de enviar.
const { dados: ingestao } = await chamar('POST', '/v1/catalogos/ingestoes', {
corpo: { catalogo_id: CATALOGO_ID, operacao: 'mesclar', referencia_externa: `erp-${new Date().toISOString().slice(0, 10)}` },
idempotencia: randomUUID(),
})
const base = `/v1/catalogos/ingestoes/${ingestao.ingestao_id}`
// 2. Enviar lotes de até 200 produtos, numerados a partir de 1.
let sequencia = 0
for (let i = 0; i < produtos.length; i += 200) {
sequencia++
await chamar('PUT', `${base}/lotes/${sequencia}`, { corpo: { produtos: produtos.slice(i, i + 200) }, idempotencia: randomUUID() })
}
// 3. Fechar e validar. Recusas param o fluxo antes de qualquer alteração.
const { dados: previa } = await chamar('POST', `${base}/validar`, { corpo: { ultima_sequencia: sequencia }, idempotencia: randomUUID() })
if (previa.estado !== 'pronta_para_aplicar') {
const { dados } = await chamar('GET', base)
const recusados = dados.itens.filter(item => item.erros.length > 0)
console.error(`Prévia em ${previa.estado}; guardas: ${previa.guardas.join(', ')}`)
for (const item of recusados) console.error(` ${item.codigo_cliente}: ${item.erros.join(' ')}`)
process.exit(1)
}
// 4. Aplicar a prévia validada e acompanhar até o fim.
await chamar('POST', `${base}/aplicar`, { corpo: { hash_previa: previa.hash_previa }, idempotencia: randomUUID() })
const aplicada = await aguardar(base, ['aplicada', 'falhou_aplicacao'])
if (aplicada.estado !== 'aplicada') throw new Error(`Aplicação falhou: ${aplicada.codigo_falha}`)
console.log(`Catálogo aplicado: ${aplicada.totais.produtos} produtos.`)
// 5. Pedir a análise completa e acompanhar pelo Location.
const pedido = await chamar('POST', '/v1/analises', { corpo: {}, idempotencia: randomUUID() })
const analise = await aguardar(pedido.location, ['concluida', 'falhou'])
if (analise.estado !== 'concluida') throw new Error(`Análise falhou: ${analise.codigo_falha}`)
const { produtos_avaliados, divergencias } = analise.resultado
console.log(`Análise ${analise.id}: ${produtos_avaliados} produtos avaliados`, divergencias)
// 6. Percorrer todos os achados até proxima_pagina ser null.
let total = 0
for (let pagina = 1; pagina !== null;) {
const { dados } = await chamar('GET', `/v1/analises/${analise.id}/resultados?pagina=${pagina}`)
total += dados.itens.length // grave cada página no seu sistema aqui
pagina = dados.proxima_pagina
}
console.log(`${total} achados lidos.`)
Terminal
COTEJAR_API_KEY=ctj_api_... COTEJAR_CATALOGO_ID=7c7b354f-07b6-4461-9240-263418c81cdb node sincronizar.mjs
Saída
Catálogo aplicado: 3 produtos.
Análise 8c59328a-3e5d-4af1-a4b4-b7914876644b: 3 produtos avaliados { orfao: 0, bloqueante: 9, deriva: 0, anvisa: 3 }
12 achados lidos.
  • Rode a sincronização num agendamento, não a cada alteração no ERP: o teto de ingestão é de 12 operações por minuto por empresa.
  • Persista ingestao_id, cada chave de idempotência e o hash_previa antes de enviar. Um processo reiniciado retoma do último passo confirmado.
  • Prefira o webhook catalogo.aplicado e analise.concluida à consulta em laço. Mantenha a consulta como plano B, com prazo.
  • Não peça uma análise a cada lote. Uma execução concluída e ainda atual é reaproveitada; análises novas contam na franquia.