API Pública v1

Integre seu sistema externo com o Katalog via REST

Introdução

A API pública v1 permite que sistemas externos integrem com sua loja. Use-a para sincronizar produtos com seu ERP, importar pedidos, automatizar movimentações de estoque ou exportar clientes.

Base URL: https://katalog.com.br/api/v1

Autenticação

Todas as requisições devem incluir um Bearer token no header. Crie tokens no painel em /painel/api-tokens.

Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

O token está vinculado a uma loja específica. Não compartilhe seu token publicamente. Se for comprometido, revogue-o no painel e gere um novo.

Formato de resposta

Todas as respostas são JSON. Em caso de erro:

{
  "erro": "Mensagem descritiva do erro"
}

Códigos HTTP usados:

  • 200 — sucesso
  • 201 — recurso criado
  • 400 — erro de validação
  • 401 — token inválido ou ausente
  • 404 — recurso não encontrado
  • 500 — erro interno

Produtos

GET /produtos — Listar produtos

Query params:

  • busca — filtra por nome, código de barras, SKU ou codIntegracao (quando numérico)
  • categoriaId — filtra por categoria
  • apenasDisponiveis — true para esconder os indisponíveis
  • page, limite — paginação (default 1, 50)

Cada item da lista retorna também codIntegracao (Código ERP — inteiro identificador no sistema externo, ex.: Integreon) e as variações trazem o própriocodIntegracao. O produto também retorna referencia (texto livre até 80 caracteres, tipicamente a referência do fornecedor/fabricante).

URL pública: o campo url traz o link absoluto do produto no storefront (mesma URL do canonical/og:url) — formato https://{slug}.katalog.com.br/produto/{id} ou o domínio personalizado da loja quando configurado. Devolvido em GET /produtos, GET /produtos/[id], POST /produtos e PUT /produtos/[id].

curl
curl https://katalog.com.br/api/v1/produtos?limite=10 \ -H "Authorization: Bearer sk_live_..."

GET /produtos/[id] — Detalhes

curl
curl https://katalog.com.br/api/v1/produtos/PRODUTO_ID \ -H "Authorization: Bearer sk_live_..."

POST /produtos — Criar produto

Aceita codIntegracao (inteiro) para vincular o registro ao ID do produto no sistema externo (Integreon, ERP, etc). Se categoriaId não for enviado, o produto cai automaticamente em uma categoria "Sem categoria" criada na loja (uma única vez). Isso evita ter que fazer GET /categorias antes de cada importação.

Aceita também unidade (máx 10 chars, ex: UN, KG, L, M, PC; normalizado para maiúsculas; default PC) e multiploVenda (decimal > 0, default 1; ex: 10 para fardo de 10). Ambos podem ser editados depois via PUT.

Imagens externas: se você enviar imagens: ["https://outro-dominio/..."], baixamos cada URL e re-hospedamos no nosso S3 antes de gravar. Sem isso, o catálogo público bloqueia o domínio externo no <Image> do Next e mostra um quadrado vazio. URLs que já estão no nosso S3/CloudFront passam intactas.

curl
curl -X POST https://katalog.com.br/api/v1/produtos \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "nome": "Camiseta Branca", "preco": 49.90, "descricao": "100% algodão", "codigoBarras": "7891234567890", "sku": "CAM-BR-M", "referencia": "REF-FAB-A100", "codIntegracao": 1234, "controlaEstoque": true, "quantidadeEstoque": 100 }'

POST /produtos com upsert — Criar ou atualizar pelo codIntegracao

Envie "upsert": true (booleano) para o POST criar ou atualizar o produto pelo codIntegracao numa única chamada, junto com as variações. Sem essa chave o POST continua apenas criando, como descrito acima.

  • codIntegracao passa a ser obrigatório: é a chave do upsert.
  • Produto novo: criado com as mesmas regras do POST (nome e preco obrigatórios; controlaEstoque só vale na criação). Resposta 201 com "operacao": "criado".
  • Produto existente: atualiza só os campos enviados, como o PUT /produtos/[id] — sem imagens as fotos ficam e imagens: [] apaga; sem categoria a atual fica; controlaEstoque é ignorado com aviso. Aceita categoria por nome. Resposta 200 com "operacao": "atualizado" e webhook produto.atualizado.
  • variacoes[]: cada item é identificado por codIntegracao (ou sku). Se já existe no produto, atualiza os campos enviados; senão, cria (valor1 obrigatório). Variações que não vierem na lista não são apagadas. O estoque do produto é recalculado pela soma das variações.
  • Produto e variações são gravados juntos: se algum item falhar, nada é salvo.
  • Havendo mais de um produto com o mesmo codIntegracao, o alterado mais recentemente é atualizado e a resposta traz avisos.
curl
curl -X POST https://katalog.com.br/api/v1/produtos \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "upsert": true, "codIntegracao": 31, "nome": "Camiseta Básica", "preco": 49.90, "categoria": "Camisetas", "controlaEstoque": true, "variacoes": [ { "codIntegracao": 21136, "valor1": "AZUL", "valor2": "P", "quantidadeEstoque": 3 }, { "codIntegracao": 21138, "valor1": "AZUL", "valor2": "M", "quantidadeEstoque": 5 } ] }'

PUT /produtos/[id] — Atualizar

Atualiza campos parciais (envie só os que mudaram). Dispara o webhook produto.atualizado. Aceita:

  • nome, descricao, preco, precoPromocional
  • categoriaId (validado: precisa pertencer à loja)
  • imagens (array de URLs), sku, codigoBarras
  • referencia — texto livre até 80 caracteres, referência do fornecedor/fabricante. Envie null para limpar
  • codIntegracao — Código ERP (inteiro). Envie null para limpar
  • disponivel, destaque, ordem
  • quantidadeEstoque, estoqueMinimo
  • peso (kg), altura, largura, comprimento (cm) — para frete em marketplaces

controlaEstoque não é aceito aqui. A flag pertence ao lojista: produto de preparo (hambúrguer, pizza, serviço) fica sem controle de propósito e vende infinito. Um ERP que reenviava controlaEstoque: true a cada sync religava o controle sozinho e o catálogo passava a barrar a venda com "Estoque insuficiente". Defina o valor na criação (POST /produtos); no PUT ele é ignorado e a resposta traz um texto em avisos[]. Pra espelhar saldo do ERP continue usando quantidadeEstoque ou POST /estoque.

curl
curl -X PUT https://katalog.com.br/api/v1/produtos/PRODUTO_ID \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "preco": 59.90, "quantidadeEstoque": 80, "peso": 0.250, "altura": 5, "largura": 30, "comprimento": 40 }'

Resposta 200: produto completo (mesmo formato do GET).

DELETE /produtos/[id] — Remover

Soft-delete: marca o produto como removido (deletadoEm = now() e disponivel = false). O produto somem dos GET da API mas permanece no histórico de pedidos. Idempotente — retorna 404 se já foi removido. Dispara o webhook produto.removido.

curl
curl -X DELETE https://katalog.com.br/api/v1/produtos/PRODUTO_ID \ -H "Authorization: Bearer sk_live_..."
{ "id": "PRODUTO_ID", "removido": true }

Variações

Variações são as combinações de atributos de um produto (ex.: tamanho/cor). Cada variação tem seu próprio preço, estoque, SKU, código de barras e codIntegracao.

GET /produtos/[id]/variacoes — Listar variações do produto

curl
curl https://katalog.com.br/api/v1/produtos/PRODUTO_ID/variacoes \ -H "Authorization: Bearer sk_live_..."

POST /produtos/[id]/variacoes — Criar variação

curl
curl -X POST https://katalog.com.br/api/v1/produtos/PRODUTO_ID/variacoes \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "nome": "Tamanho M", "valor1": "M", "valor2": "Branco", "preco": 49.90, "sku": "CAM-BR-M", "codigoBarras": "7891234567891", "codIntegracao": 5678, "quantidadeEstoque": 30 }'

GET /variacoes/[id] — Detalhes

PUT /variacoes/[id] — Atualizar

Aceita os mesmos campos do POST (todos opcionais). Envie codIntegracao: nullpara limpar.

curl
curl -X PUT https://katalog.com.br/api/v1/variacoes/VARIACAO_ID \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "preco": 54.90, "codIntegracao": 9999 }'

DELETE /variacoes/[id] — Remover

Remoção definitiva (hard delete). Não há soft-delete para variações.

Complementos (Grupos)

Grupos de complemento são reutilizáveis (um mesmo grupo "Sabores" pode ser vinculado a vários produtos). Os itens do grupo são armazenados como JSON e cada item aceita nome, preco, estoque e codIntegracao.

GET /grupos-complemento — Listar grupos

curl
curl https://katalog.com.br/api/v1/grupos-complemento \ -H "Authorization: Bearer sk_live_..."

POST /grupos-complemento — Criar grupo

curl
curl -X POST https://katalog.com.br/api/v1/grupos-complemento \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "nome": "Sabores", "tipo": "SELECAO", "obrigatoria": true, "minimo": 1, "maximo": 1, "itens": [ { "nome": "Chocolate", "preco": 0, "codIntegracao": 11 }, { "nome": "Morango", "preco": 0, "codIntegracao": 12 }, { "nome": "Baunilha", "preco": 0, "codIntegracao": 13 } ] }'

Campos do body:

  • tipo — SELECAO (1 opção) ou MULTIPLA (várias)
  • obrigatoria, minimo, maximo — regras de seleção
  • itens — array de objetos { nome, preco, estoque?, codIntegracao? }. O codIntegracao dentro do item é o Código ERP daquele complemento.

GET /grupos-complemento/[id]

PUT /grupos-complemento/[id] — Atualizar

Se itens estiver presente, substitui completamente a lista anterior (não faz merge item-a-item). Reenvie a lista completa, com oscodIntegracao atualizados.

DELETE /grupos-complemento/[id] — Remover

Categorias

GET /categorias — Listar

Lista todas as categorias da loja.

POST /categorias — Criar

Cria categoria. nome é único por loja (case-insensitive). ordem é opcional — quando omitido, vai pro fim. Hierarquia Categoria → Subcategoria existe no schema mas é tratada em endpoint separado; categoriaPaiId no body é ignorado.

curl
curl -X POST https://katalog.com.br/api/v1/categorias \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "nome": "Hambúrgueres", "ordem": 1, "ativa": true }'

GET /categorias/[id] — Detalhes

PUT /categorias/[id] — Atualizar

Body (todos opcionais): nome, descricao, ordem, ativa. Renomear pra nome já usado por outra categoria retorna 400.

DELETE /categorias/[id] — Remover

Bloqueia (409) se a categoria tiver produtos ou subcategorias vinculados — sem isso, a cascata onDelete: Cascade apagaria os produtos junto. Realoque os produtos via PUT /produtos/[id] com novo categoriaId antes.

curl
curl -X DELETE https://katalog.com.br/api/v1/categorias/CAT_ID \ -H "Authorization: Bearer sk_live_..."
# 409 Conflict quando tem dependencia:
{ "erro": "Categoria tem 12 produto(s) vinculado(s). Realoque-os antes de remover.",
  "produtosVinculados": 12 }

Upsert de categoria no POST /produtos

Pra evitar 2 chamadas (criar categoria + criar produto), o POST /produtos aceita categoria (string) em vez de categoriaId. Se a categoria já existe (match case-insensitive), reusa; se não, cria. Ordem de prioridade no resolve:

  1. categoriaId explícito (valida pertencimento à loja)
  2. categoria (string) → upsert por nome
  3. Nenhum → cai em "Sem categoria" automática
curl
curl -X POST https://katalog.com.br/api/v1/produtos \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "nome": "X-Bacon", "preco": 28.90, "categoria": "Hambúrgueres" }'

Clientes

GET /clientes

Query params: busca, page, limite.

POST /clientes

Cria ou atualiza (upsert por documento). Campos:

{
  "nome": "João da Silva",
  "documento": "12345678900",
  "tipoDocumento": "CPF",
  "email": "joao@example.com",
  "celular": "11999999999",
  "cep": "01310100",
  "endereco": "Av. Paulista",
  "numero": "1000",
  "bairro": "Bela Vista",
  "cidade": "São Paulo",
  "uf": "SP"
}

Pedidos

GET /pedidos

Query params:

  • status — PENDENTE, CONFIRMADO, PREPARANDO, PRONTO, ENTREGUE, CANCELADO
  • desde — ISO date (filtro de criação >=)
  • ate — ISO date (filtro de criação <=)
  • page, limite
curl
curl "https://katalog.com.br/api/v1/pedidos?status=ENTREGUE&desde=2026-01-01" \ -H "Authorization: Bearer sk_live_..."

Cada item da lista retorna também pagamentos (array — o schema suporta split) e ajustePagamento (valor em R$ do ajuste % da forma). Estrutura de cada pagamento:
{ formaPagamentoId, formaPagamento: "PIX" | "Dinheiro" | "Cartão de Crédito" | ..., valorPago }. Os mesmos campos aparecem em GET /pedidos/[id] e no payload do webhook pedido.criado.

Cada item já traz os dados do produto e da variação que um integrador precisa para mapear no ERP — não é preciso chamar GET /produtos/[id] para cada item:

"itens": [{
  "id": "...",
  "quantidade": 1,
  "precoUnit": 28.00,
  "produto":  { "id": "...", "nome": "CAMISETA VERAO", "sku": "31", "codIntegracao": 31,
                "codigoBarras": "789...", "imagens": ["https://..."], "unidade": "UN",
                "quantidadeEstoque": 12 },
  "variacao": { "id": "...", "nome": "AZUL / M", "valor1": "AZUL", "valor2": "M",
                "sku": "25918", "codIntegracao": 25918, "codigoBarras": null,
                "quantidadeEstoque": 3 }
}]

Buscar o detalhe do produto item a item numa sincronização de pedidos multiplica as chamadas e estoura o limite por minuto. Use os campos acima; consulte GET /produtos/[id] só quando precisar de algo que o item não traz.

Lista e detalhe também trazem notasFiscais — array com o resumo das notas enviadas para o pedido (vazio se não houver). Veja Notas fiscais do pedido.

POST /pedidos — Criar pedido

Cria um pedido na loja. Preço é recalculado server-side (variação > promo > preço base) — o valor enviado no body é ignorado por segurança. Estoque é baixado em transação atômica: se um item não tem estoque, retorna 400 e nenhum item é baixado.

  • tipoEntrega — DELIVERY, BALCAO (retirada no balcão; RETIRADA também é aceito como alias) ou MESA
  • cliente — objeto com snapshot. Se cliente.id for passado, valida que pertence à loja e usa o cadastro como fallback nos campos vazios
  • itens[] — cada um com produtoId, quantidade, opcional variacaoId, observacao e opcoes (JSON de complementos)
  • taxaEntrega, desconto, observacoes — opcionais

Dispara webhook pedido.criado ao sucesso. Não suporta cupons, cashback, mesas, múltiplas formas de pagamento, frete dinâmico, tabela de preço — use o painel pra esses fluxos.

Atenção ao múltiplo de venda: produtos com multiploVenda > 1 (ex: parafuso vendido de 10 em 10) têm a quantidade arredondada automaticamente para o próximo múltiplo. Pedir quantidade: 1 de um produto com multiploVenda: 10 grava 10 no pedido. Cheque esse campo no GET /produtos antes de enviar.

curl
curl -X POST https://katalog.com.br/api/v1/pedidos \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "tipoEntrega": "DELIVERY", "cliente": { "nome": "João da Silva", "celular": "11999999999", "email": "joao@example.com", "documento": "12345678900", "endereco": "Av. Paulista", "numero": "1000", "bairro": "Bela Vista", "cidade": "São Paulo", "uf": "SP", "cep": "01310100" }, "itens": [ { "produtoId": "PROD_ABC", "quantidade": 2 }, { "produtoId": "PROD_XYZ", "variacaoId": "VAR_123", "quantidade": 1, "observacao": "sem cebola" } ], "taxaEntrega": 8.50, "observacoes": "Entregar após 18h" }'

GET /pedidos/[id] — Detalhes

Retorna o pedido completo com itens, cliente e dados de rastreio.

PATCH /pedidos/[id] — Atualizar status / rastreio

Body (todos os campos exceto status são opcionais):

  • status — PENDENTE, CONFIRMADO, PREPARANDO, PRONTO, ENVIADO, ENTREGUE ou CANCELADO
  • motivoCancelamento — obrigatório quando status = CANCELADO
  • tempoEntrega — string livre (ex: "30-45 min")
  • codigoRastreio, urlRastreio — para marketplaces / Correios

Comportamento automático:

  • Ao mudar PENDENTE → CONFIRMADO: baixa estoque dos itens
  • Ao cancelar pedido confirmado: restaura estoque
  • Sempre dispara webhook pedido.status
  • Se status = CANCELADO: também dispara pedido.cancelado
curl
curl -X PATCH https://katalog.com.br/api/v1/pedidos/PEDIDO_ID \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "status": "ENVIADO", "codigoRastreio": "BR123456789", "urlRastreio": "https://rastreamento.correios.com.br/...", "tempoEntrega": "3-5 dias úteis" }'

DELETE /pedidos/[id] — Apagar pedido

Apaga o pedido em definitivo (itens e pagamentos vão junto). Irreversível.

Só apaga pedido cancelado. Se o pedido estiver em qualquer outro status, a resposta é 409 — cancele antes (PATCH comstatus: "CANCELADO"). Cancelar mantém o pedido no histórico; apagar o remove.

curl
curl -X DELETE https://katalog.com.br/api/v1/pedidos/PEDIDO_ID -H "Authorization: Bearer sk_live_..."
{
  "ok": true,
  "numero": 774,
  "mensagem": "Pedido #774 apagado permanentemente."
}

Notas fiscais do pedido

O ERP (ou um integrador, como o Integreon) envia a nota fiscal emitida para o pedido. É o mesmo modelo do Mercado Livre: você manda o XML autorizado e o Katalog lê do próprio arquivo a chave de acesso, número, série, modelo, data de emissão, valor, emitente, destinatário e protocolo. A nota aparece no gerenciamento do pedido no painel, com download do XML e do DANFE — e o lojista também pode anexar por lá.

Envie sempre os dois arquivos: o XML e o PDF (DANFE). O XML é o documento fiscal; o PDF é o que o lojista consegue abrir e imprimir. O Katalog guarda e devolve os arquivos que você manda — não gera DANFE nem espelho da nota, porque não é emissor fiscal. Quem produz o DANFE é o seu emissor.

Mandar só o XML não dá erro, mas a resposta vem com o aviso "Nota sem DANFE em PDF" e o lojista fica sem conseguir visualizar a nota no painel. Se o PDF só existir depois, envie-o depois com o campo chave — ele é anexado à mesma nota.

POST /pedidos/[id]/notas-fiscais — Enviar nota

Três formatos, use o mais simples para o seu sistema:

1. XML cru no corpo (como o invoice_data do Mercado Livre):

curl
curl -X POST https://katalog.com.br/api/v1/pedidos/PEDIDO_ID/notas-fiscais \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/xml" \ --data-binary @53260912345678000195550010000012341123456786-procNFe.xml

2. multipart/form-data — XML e DANFE juntos nos campos xml e pdf. O campo fiscal_document do Mercado Livre também é aceito (até um XML e um PDF, tipo detectado pelo conteúdo):

curl
curl -X POST https://katalog.com.br/api/v1/pedidos/PEDIDO_ID/notas-fiscais \ -H "Authorization: Bearer sk_live_..." \ -F "xml=@nota.xml" \ -F "pdf=@danfe.pdf"

3. JSON — quando o XML já está em memória (ex.: ERP em Delphi):

{
  "xml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><nfeProc ...>...</nfeProc>",
  "pdfBase64": "JVBERi0xLjcK..."      // opcional
}

No JSON, xmlBase64 substitui xml. Sem XML nenhum, dá para registrar só os metadados com { "chave", "numero", "serie" } — a chave é validada pelo dígito verificador.

Resposta (201 ao criar, 200 ao atualizar ou cancelar):

{
  "acao": "criada",                  // criada | atualizada | cancelada
  "id": "cmf1x...",
  "chave": "53260912345678000195550010000012341123456786",
  "numero": "1234",
  "serie": "1",
  "modelo": "55",                    // 55 NF-e, 65 NFC-e
  "situacao": "AUTORIZADA",          // AUTORIZADA | CANCELADA
  "dataEmissao": "2026-09-11T20:50:00.000Z",
  "valorTotal": 37.04,
  "cnpjEmitente": "12345678000195",
  "documentoDestinatario": "12345678909",
  "protocolo": "353260000123456",
  "canceladaEm": null,
  "temXml": true,
  "temPdf": true,
  "temXmlCancelamento": false,
  "origem": "API",
  "criadoEm": "2026-09-11T21:02:13.000Z",
  "atualizadoEm": "2026-09-11T21:02:13.000Z",
  "avisos": []
}

Regras:

  • Idempotente pela chave de acesso. Reenviar a mesma nota responde 200 com acao: "atualizada" — não duplica. Só os arquivos enviados são substituídos; para mandar o DANFE depois, envie o pdf com o campo chave.
  • Só nota autorizada (cStat 100 ou 150 no protNFe). Nota denegada ou rejeitada volta 400. XML sem protocolo é aceito, mas com aviso — prefira o nfeProc.
  • Cancelamento: envie ao mesmo endpoint o XML do evento (procEventoNFe, tipo 110111 ou 110112, homologado pela SEFAZ). A nota vira CANCELADA e a resposta traz acao: "cancelada". Carta de correção e outros eventos são recusados.
  • avisos[] lista inconsistências que não bloqueiam: emitente diferente do CNPJ da loja, destinatário diferente do documento do cliente do pedido, valor da nota diferente do total, XML sem protocolo, nota de homologação. Vale registrar no log do integrador.
  • O status do pedido não muda — use PATCH /pedidos/[id] se quiser marcar como enviado.
  • Os arquivos não têm URL pública (o XML traz dados pessoais do cliente): baixe pela API.
  • Limites: XML 2 MB, PDF 5 MB, 20 notas por pedido.
  • Dispara o webhook pedido.nota_fiscal.

Erros:

  • 400 — XML não é NF-e/NFC-e, nota não autorizada, chave inválida ou diferente da do XML, arquivo vazio ou PDF inválido
  • 400 — XML de transporte: lote de envio (enviNFe), retorno do lote, consulta de situação, inutilização ou envelope SOAP. São arquivos que o emissor gera no caminho da autorização, mas não são a nota. A mensagem diz qual é o arquivo e o que enviar no lugar. Atenção aoenviNFe: ele contém a nota, mas sem protocolo — é a versão anterior à autorização, e por isso é recusado
  • 404 — pedido não encontrado, ou cancelamento de uma nota que ainda não foi enviada
  • 409 — a chave já está vinculada a outro pedido da loja, ou o pedido já tem 20 notas
  • 413 — arquivo acima do limite
  • 415 — Content-Type diferente de XML, multipart ou JSON

NFS-e (nota de serviço)

Além de NF-e e NFC-e, o mesmo endpoint aceita NFS-e no padrão ABRASF (o XML que começa com <CompNfse>, gerado pela maioria das prefeituras). Mande do mesmo jeito — XML cru, multipart ou JSON — que o Katalog identifica o layout sozinho.

  • Não existe chave de acesso em NFS-e. A identidade é numero + codigoVerificacao dentro do CNPJ do prestador. O campo chave volta null e a idempotência do reenvio usa essa combinação — continua sem duplicar.
  • modelo vem "56" e serie vem null (o Serie do XML é do RPS que gerou a nota, não da NFS-e).
  • Cancelamento: a NFS-e cancelada traz o bloco <NfseCancelamento> no próprio XML, então basta enviar o arquivo — a nota entra como CANCELADA, com a data do cancelamento. Não existe XML de evento separado como na NF-e.
  • Prestador e tomador entram em cnpjEmitente e documentoDestinatario (o tomador pode ser CPF ou CNPJ), e os mesmos avisos[] de divergência valem.
curl
curl -X POST https://katalog.com.br/api/v1/pedidos/PEDIDO_ID/notas-fiscais \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/xml" \ --data-binary @nfse.xml
{
  "acao": "criada",
  "chave": null,
  "numero": "1445",
  "serie": null,
  "modelo": "56",
  "codigoVerificacao": "21473A803",
  "situacao": "AUTORIZADA",
  "valorTotal": 2658.00,
  "cnpjEmitente": "02887418000198",
  "documentoDestinatario": "26516381000150"
}

GET /pedidos/[id]/notas-fiscais — Listar notas

{ "notasFiscais": [ { "id": "...", "chave": "...", "numero": "1234", "situacao": "AUTORIZADA", ... } ] }

GET /pedidos/[id]/notas-fiscais/[notaId] — Detalhes ou arquivo

Sem query devolve o JSON da nota. Com ?arquivo=xml, pdf ou xml-cancelamento devolve o arquivo.

curl
curl "https://katalog.com.br/api/v1/pedidos/PEDIDO_ID/notas-fiscais/NOTA_ID?arquivo=xml" \ -H "Authorization: Bearer sk_live_..." -o nota.xml

Acrescente &inline=1 para receber o arquivo com Content-Disposition: inline — o navegador abre no visualizador em vez de baixar. É o que o painel usa no botão Ver / Imprimir do DANFE. Sem o parâmetro, o arquivo vem como anexo.

DELETE /pedidos/[id]/notas-fiscais/[notaId] — Remover nota

Desvincula a nota do pedido e apaga os arquivos. Não cancela nada na SEFAZ — para registrar cancelamento, envie o XML do evento no POST.

Etiqueta de envio do pedido

O Katalog gera a etiqueta do pedido em PDF, no molde das de Nuvemshop, Mercado Livre e Shopee:

  • número do pedido em destaque, com código de barras Code 128 (o valor lido é o numero do pedido — curto, fácil de bipar na expedição);
  • QR Code com o link de acompanhamento do pedido;
  • destinatário com CEP em destaque, telefone e referência;
  • forma de entrega, código de rastreio e a nota fiscal autorizada vinculada, se houver;
  • peso — só quando todos os itens têm peso cadastrado (peso parcial engana quem despacha);
  • volume N/total — uma etiqueta por caixa;
  • conteúdo (itens com a grade) e remetente (dados da loja).

É a etiqueta da loja: serve para entrega própria, motoboy, retirada e para identificar volumes. Envio por transportadora (Correios, Jadlog etc.) continua usando a etiqueta oficial da transportadora.

GET /pedidos/[id]/etiqueta — Etiqueta de um pedido

curl
curl "https://katalog.com.br/api/v1/pedidos/PEDIDO_ID/etiqueta?tamanho=10x15&volumes=2" \ -H "Authorization: Bearer sk_live_..." \ -o etiqueta.pdf
  • tamanho — 10x15 (padrão): uma etiqueta por página, 100×150 mm, para impressora térmica. a4: 4 etiquetas por folha, com linha de corte.
  • volumes — de 1 a 50 (padrão 1). Gera uma etiqueta por volume: 1/2, 2/2.
  • itens=0 — esconde a lista de conteúdo.
  • inline=0 — devolve como anexo. Por padrão o PDF vem inline, para abrir e imprimir.
  • formato=zpl — em vez de PDF, devolve ZPL, a linguagem nativa das impressoras térmicas Zebra (e compatíveis). A impressora desenha código de barras e QR na própria resolução: sai mais nítido e dá para mandar direto para a impressora, sem abrir visualizador. Só com tamanho=10x15; vem como anexo .zpl, um bloco ^XA…^XZ por volume.
  • dpi — resolução da impressora para o ZPL: 203 (padrão, 8 dots/mm) ou 300.
curl
# ZPL direto para a Zebra compartilhada na rede (porta 9100) curl "https://katalog.com.br/api/v1/pedidos/PEDIDO_ID/etiqueta?formato=zpl&volumes=2" \ -H "Authorization: Bearer sk_live_..." -o etiqueta.zpl nc 192.168.0.50 9100 < etiqueta.zpl

Erros: 400 tamanho, formato, dpi ou volumes inválido · 404 pedido não encontrado na loja.

POST /etiquetas/lote — Várias etiquetas num PDF

Para a expedição imprimir tudo de uma vez. As etiquetas saem na ordem enviada.

POST /api/v1/etiquetas/lote
{
  "pedidos": [
    "k7qm2xpa9r4t",
    { "id": "m3tq8wz2kd5p", "volumes": 3 }
  ],
  "tamanho": "10x15",      // 10x15 | a4
  "formato": "pdf",        // pdf | zpl (zpl só 10x15)
  "volumes": 1,            // padrão para quem não informou o seu
  "itens": true
}
  • Resposta: application/pdf (anexo; ?inline=1 abre no navegador) ou, com formato: "zpl", um .zpl com todas as etiquetas.
  • Se algum pedido não existir na loja, o lote inteiro é recusado com 404 e a lista em naoEncontrados — melhor saber agora do que descobrir um volume sem etiqueta na hora de despachar.
  • Limites: 100 pedidos e 200 etiquetas (soma dos volumes) por requisição.

Etiqueta da transportadora

Quando o seu sistema compra o frete (Correios, Jadlog, Loggi, etiqueta de marketplace…), envie a etiqueta oficial para o pedido — igual à nota fiscal: o Katalog não compra frete nem gera etiqueta de transportadora, ele guarda o arquivo que você manda, mostra ao lojista no painel (Ver / Imprimir) e devolve pela API. Aceita PDF ou ZPL, com o tipo detectado pelo conteúdo.

POST /pedidos/[id]/etiquetas — Enviar etiqueta

curl
curl -X POST https://katalog.com.br/api/v1/pedidos/PEDIDO_ID/etiquetas \ -H "Authorization: Bearer sk_live_..." \ -F "arquivo=@etiqueta-correios.pdf" \ -F "transportadora=Correios" \ -F "servico=PAC" \ -F "codigoRastreio=AA123456789BR" \ -F "urlRastreio=https://rastreamento.correios.com.br/app/index.php?objetos=AA123456789BR" \ -F "volume=1"

Também aceita JSON (pdfBase64, zplBase64 ou zpl em texto, mais os mesmos campos) e o arquivo cru no corpo (application/pdf ou text/plain para ZPL), com os campos na query. Resposta (201 criada, 200 substituída):

{
  "acao": "criada",                 // criada | atualizada
  "id": "cmf9x...",
  "transportadora": "Correios",
  "servico": "PAC",
  "codigoRastreio": "AA123456789BR",
  "urlRastreio": "https://rastreamento.correios.com.br/...",
  "volume": 1,
  "formato": "PDF",                 // PDF | ZPL
  "tamanhoBytes": 48211,
  "origem": "API",
  "criadoEm": "2026-09-14T21:30:00.000Z",
  "atualizadoEm": "2026-09-14T21:30:00.000Z",
  "avisos": []
}

Regras:

  • Idempotente pelo codigoRastreio dentro do pedido: reenviar com o mesmo código substitui o arquivo em vez de duplicar. Sem código, cada envio cria uma etiqueta nova — a resposta avisa.
  • Um pedido com vários volumes pode ter uma etiqueta por volume, cada uma com o seu código.
  • Se o pedido ainda não tem código de rastreio, o enviado passa a ser o do pedido (o que o cliente vê ao acompanhar). Código já preenchido não é sobrescrito. O status do pedido não muda.
  • O arquivo não tem URL pública (traz nome e endereço do cliente): baixe pela API.
  • Limites: PDF 5 MB, ZPL 1 MB, 50 etiquetas por pedido. Dispara o webhook pedido.etiqueta.

Erros: 400 sem arquivo, arquivo que não é PDF nem ZPL, volume ou urlRastreio inválido · 404 pedido não encontrado · 409 limite de etiquetas · 413 arquivo grande · 415 Content-Type não suportado.

GET /pedidos/[id]/etiquetas — Listar

Devolve { etiquetas: [...] }. As etiquetas também vêm em etiquetasEnvio no GET de pedidos.

GET /pedidos/[id]/etiquetas/[etiquetaId] — Detalhes ou arquivo

Sem query, o JSON. Com ?arquivo=1, o arquivo (PDF ou ZPL); &inline=1 abre o PDF no navegador.

DELETE /pedidos/[id]/etiquetas/[etiquetaId] — Remover

Apaga a etiqueta e o arquivo. Não cancela o envio na transportadora.

Estoque

POST /estoque — Movimentação

Registra movimentação de estoque (entrada, saída ou ajuste).

{
  "produtoId": "PRODUTO_ID",
  "tipo": "ENTRADA",        // ou "SAIDA" ou "AJUSTE"
  "quantidade": 50,
  "variacaoId": null,        // opcional
  "observacao": "Compra do fornecedor X"
}
  • ENTRADA: soma à quantidade atual
  • SAIDA: subtrai da quantidade atual
  • AJUSTE: define a quantidade absoluta

Exemplo: importar produtos do ERP

javascript
const TOKEN = "sk_live_..."; const BASE = "https://katalog.com.br/api/v1"; async function criarProduto(produto) { const res = await fetch(`${BASE}/produtos`, { method: "POST", headers: { "Authorization": `Bearer ${TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify(produto), }); if (!res.ok) { const err = await res.json(); throw new Error(err.erro); } return res.json(); } // Loop sobre os produtos do ERP for (const item of produtosDoERP) { await criarProduto({ nome: item.descricao, preco: item.precoVenda, codigoBarras: item.ean, sku: item.codigo, controlaEstoque: true, quantidadeEstoque: item.estoqueAtual, }); }

Limites

Limite atual: 180 requisições por minuto por token, das quais até 90 podem ser de escrita (POST, PUT, PATCH, DELETE). A cota é de cada token — duas lojas integradas pelo mesmo sistema não dividem limite. Para volume alto, prefira os endpoints em lote, como POST /estoque/lote. Em caso de excesso, retornamos 429 Too Many Requests com os headers:

  • X-RateLimit-Limit — limite total por minuto (180)
  • X-RateLimit-Remaining — quantas restam na janela
  • X-RateLimit-Reset — segundos até resetar
  • Retry-After — segundos sugeridos antes de tentar de novo

Recursos extras

Webhooks de saída

Receba notificações em uma URL externa quando eventos ocorrerem na sua loja. Pode configurar pelo painel em /painel/webhooks ou via API (abaixo).

Eventos disponíveis

  • pedido.criado — novo pedido recebido
  • pedido.status — status do pedido mudou
  • pedido.pago — pagamento confirmado
  • pedido.cancelado — pedido cancelado
  • pedido.nota_fiscal — nota fiscal do pedido foi criada, atualizada, cancelada ou removida (pela API ou pelo painel). dados = { acao, pedidoId, pedidoNumero, notaFiscal }
  • pedido.etiqueta — etiqueta de transportadora criada, substituída ou removida (pela API ou pelo painel). dados = { acao, pedidoId, pedidoNumero, etiqueta }
  • pedido.editado — a loja alterou itens, quantidades, preços, desconto ou frete do pedido no painel. dados = { id, numero, status, resumo, antes, depois, editadoPor, editadoEm } (antes/depois são snapshots com itens, subtotal, ajusteTabela, desconto, taxaEntrega, ajustePagamento e total). Ao receber, refaça GET /pedidos/{id} para pegar o pedido atualizado (itens e totais).
  • cliente.criado — novo cliente cadastrado
  • produto.atualizado — produto foi alterado (PUT)
  • produto.removido — produto foi removido (DELETE)
  • produto.estoque_baixo — produto atingiu o mínimo

POST /webhooks — Cadastrar webhook

Cria um webhook scoped à loja autenticada. O secret é gerado pelo servidor (formato whsec_<48 hex>) e retornado apenas neste response — guarde-o, não há como recuperá-lo depois.

curl
curl -X POST https://katalog.com.br/api/v1/webhooks \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "nome": "Integreon", "url": "https://hub.integreon.com.br/webhooks/meucatalogo", "eventos": ["pedido.criado", "produto.atualizado", "produto.removido"] }'
{
  "id": "...",
  "nome": "Integreon",
  "url": "https://...",
  "eventos": ["pedido.criado", "produto.atualizado", "produto.removido"],
  "ativo": true,
  "secret": "whsec_abc123...",
  "criadoEm": "2026-04-27T..."
}

GET /webhooks — Listar

Retorna os webhooks da loja (sem expor o secret).

DELETE /webhooks/[id] — Remover

curl
curl -X DELETE https://katalog.com.br/api/v1/webhooks/WEBHOOK_ID \ -H "Authorization: Bearer sk_live_..."

Formato do payload

POST com Content-Type application/json e os headers:

X-MeuCatalogo-Event: pedido.criado
X-MeuCatalogo-Signature: sha256=<hex>
Content-Type: application/json

Body:

{
  "evento": "pedido.criado",
  "criadoEm": "2026-04-22T15:30:00Z",
  "dados": {
    "id": "...",
    "numero": 123,
    "status": "PENDENTE",
    "tipoEntrega": "RETIRADA",
    "subtotal": 100.00,
    "desconto": 0,
    "ajustePagamento": -5.00,
    "taxaEntrega": 0,
    "total": 95.00,
    "observacoes": null,
    "pagamentoStatus": "PENDENTE",
    "pagamentos": [
      { "formaPagamentoId": "...", "formaPagamento": "PIX", "valorPago": 95.00 }
    ],
    "clienteNome": "Fulano",
    "clienteCelular": "5561999999999",
    "clienteEmail": "fulano@example.com",
    "clienteDocumento": "12345678901",
    "clienteEndereco": "...",
    "criadoEm": "2026-04-22T15:30:00Z"
  }
}

pagamentos é um array — o schema suporta split (PIX + Dinheiro, etc). Cada item traz a descrição canônica da forma (Dinheiro, PIX, Cartão de Crédito, etc.) para você mapear pra sua tabela. ajustePagamento em reais reflete o ajuste % da forma (negativo = desconto no PIX, positivo = acréscimo no cartão).

Validar a assinatura HMAC

Use o secret mostrado no momento da criação do webhook. Calcule HMAC SHA-256 do body com o secret e compare com o header X-MeuCatalogo-Signature.

javascript
import crypto from "crypto"; function verificarAssinatura(secret, body, assinatura) { const hmac = crypto.createHmac("sha256", secret); hmac.update(body); const esperada = "sha256=" + hmac.digest("hex"); return crypto.timingSafeEqual( Buffer.from(esperada), Buffer.from(assinatura) ); }

Retentativas e timeout

Cada entrega tem timeout de 10 segundos. Sua URL deve responder com status 2xx para ser considerada sucesso.

Em caso de falha, retentamos com backoff exponencial: +30s, +2min, +10min, +1h (total de 5 tentativas). O header X-MeuCatalogo-Tentativa indica o número da tentativa.

Logs

Visualize histórico de entregas em /painel/webhooks. Cada log mostra: payload enviado, status HTTP recebido, resposta e duração.

Suporte

Encontrou um bug ou precisa de um endpoint que ainda não existe? Fale com nosso time pelo WhatsApp.