# API de Integração Commercio > API pública de integração do ERP Commercio (versão da superfície 1.12). Serve clientes, > ordens de serviço (e receita óptica), vendas com pagamentos, devoluções, financeiro (contas a receber e cobrança) > e as áreas com escrita — e-commerce e dilab (O.S. de laboratório) — para portais e integradores que NÃO acessam > o banco do ERP. ## Links - Este guia: /api/integracao/llms.txt (também em /llms.txt) — público, sem token - OpenAPI (Swagger, público): /api/swagger/integracao/swagger.json — navegável em /api/index.html - Token OAuth2: POST /api/connect/token - Descoberta OAuth: /.well-known/openid-configuration (= /api/.well-known/openid-configuration); chaves: /api/.well-known/jwks - Base v1: /api/integracao/v1 — a antiga /api/integracao/commercio/v1 responde 404 ## Como começar 1. Peça um token (client_id e client_secret fornecidos pela Dataweb): POST /api/connect/token Content-Type: application/x-www-form-urlencoded grant_type=client_credentials&client_id=&client_secret= Resposta: { access_token, token_type: "Bearer", expires_in: 3600 }. O parâmetro `scope` é ignorado. 2. Envie Authorization: Bearer em toda chamada de /api/integracao/v1. 3. GET /api/integracao/v1/empresas → as filiais liberadas; o `cnpj` delas é o `empresaCnpj` das demais rotas. 4. GET /api/integracao/v1/ping?empresaCnpj= → confere token, escopo e empresa. Sem token, a API responde 401 com `WWW-Authenticate: Bearer` e um corpo explicando como obter o token. ## Token - JWS RS256 (typ at+jwt, com `kid`), válido por 1 hora. Claims: sub (o client_id), integrador (a empresa integradora), scope, allowed_cnpjs, allowed_tenants. - allowed_cnpjs: as empresas liberadas para o cliente ("*" = todas). `empresaCnpj` fora dela = 403 EMPRESA_NAO_AUTORIZADA (mesmo que o CNPJ não esteja cadastrado). - Validação offline: baixe o JWKS e guarde por `kid`; ao ver um `kid` desconhecido, baixe de novo (as chaves giram com sobreposição). Nunca fixe um `kid` nem monte a URL do JWKS a partir do `iss`. ## Escopos (por área) - commercio.integracao.clientes:read → /clientes, /clientes/aniversariantes - commercio.integracao.ordens-servico:read → /ordens-servico, /ordens-servico/{numero} - commercio.integracao.ordens-servico.receita:read → /ordens-servico/{numero}/receita (+ imagens) — exigido ALÉM de ordens-servico:read (dado de saúde) - commercio.integracao.vendas:read → /vendas, /vendas/{codVenda}, /devolucoes, /devolucoes/{codDevolucao} - commercio.integracao.financeiro.contas-receber:read → /financeiro/clientes, /financeiro/lancamentos, /financeiro/notas-fiscais (+ XML/PDF), boleto - commercio.integracao.financeiro.cobranca:read → /financeiro/cobranca/lancamentos (+ os binários da cobrança) - commercio.integracao.ecommerce.pedidos:write → POST /ecommerce/pedidos, POST /ecommerce/pedidos/{numeroPedido}/cancelar - commercio.integracao.ecommerce.pedidos:read → GET /ecommerce/pedidos, /ecommerce/pedidos/{numeroPedido} - commercio.integracao.ecommerce.produtos:read → /ecommerce/produtos, /ecommerce/produtos/{id} - commercio.integracao.ecommerce.estoque:read → /ecommerce/estoque - commercio.integracao.dilab.ordens-servico:write → POST /dilab/ordens-servico, POST /dilab/ordens-servico/validar, /dilab/servicos, /dilab/materiais, /dilab/formatos-aro - commercio.integracao.dilab.ordens-servico:read → GET /dilab/ordens-servico, /dilab/ordens-servico/{numero}, /dilab/ordens-servico/{numero}/status, /dilab/servicos, /dilab/materiais, /dilab/formatos-aro - Qualquer escopo de integração → /, /version, /ping, /empresas - commercio.integracao:read e commercio.integracao:write: DESCONTINUADOS (read = clientes, O.S., receita e vendas) Sem o escopo da rota = 403 ESCOPO_INSUFICIENTE. ## Convenções - empresaCnpj: query param obrigatório nas rotas de dados (com ou sem máscara); inválido ou não cadastrado = 400. - Listas: { data, nextCursor }. Próxima página = mesma chamada, MESMOS filtros, + cursor=. Fim da lista = nextCursor AUSENTE (nunca compare o tamanho de data com o limite). Lista vazia = 200 com data: []. - limite: padrão 100, máximo 1000 (acima é reduzido para 1000, não é erro); zero ou negativo = 400 INVALID_LIMIT. - cursor: opaco, não guarde por muito tempo; inválido ou com filtros diferentes = 400 INVALID_CURSOR. O cursor não fica amarrado a `incluir` (que muda a forma da resposta, nunca as linhas). - Limite de uso: 120 requisições por minuto por credencial (janela fixa); acima = 429 RATE_LIMITED + Retry-After (segundos). - JSON em camelCase. Datas: YYYY-MM-DD; data e hora: ISO 8601 sem fuso, no horário da instalação. - Campos vazios são OMITIDOS, nunca null. Exceção: `cliente: null` explícito quando o cliente não é identificado (/vendas, /devolucoes, cobrança). - Vocabulários (situacao, tipo, consentimentoLgpd…) aceitam qualquer caixa na entrada e saem como documentado. - cpf e cnpj juntos no mesmo filtro = 400 INVALID_PARAMS (uma pessoa tem um OU outro). ## Filtros de intervalo (v1.8) - O operador vai entre colchetes no nome do CAMPO DA RESPOSTA: campo[gte] (≥), campo[gt] (>), campo[lte] (≤), campo[lt] (<). Ex.: dataEmissao[gte]=2026-08-01&dataEmissao[lte]=2026-08-31. No curl, use -g (ou %5B %5D). - Datas (YYYY-MM-DD) contam o dia inteiro: gt/lt equivalem ao dia seguinte/anterior. Data e hora (alteradoEm): ISO 8601, com ou sem hora; gt é estritamente depois. - dataEmissao e dataEntrega: período fechado (início E fim), no máximo 92 dias. alteradoEm: só limite inferior (gte ou gt) — é o watermark do polling; a resposta traz o campo alteradoEm: guarde o maior visto. - Nomes anteriores à v1.8 continuam aceitos (DESCONTINUADOS): dataInicial/dataFinal = dataEmissao[gte]/[lte]; entregaDe/entregaAte = dataEntrega[gte]/[lte]; alteradosDesde = alteradoEm[gte]. Não misture os dois estilos no mesmo campo (400). Cursor emitido com um estilo vale no outro. - Campo desconhecido entre colchetes, operador inválido, período incompleto/invertido ou acima da janela = 400 INVALID_PARAMS, com a lista do que é aceito. ## Erros (RFC 9457, application/problem+json, com `code` estável e `traceId`) - 400 INVALID_PARAMS | INVALID_LIMIT | INVALID_CURSOR — parâmetro inválido (erro de formato traz o mapa `errors`) - 400 ORDEM_SERVICO_INVALIDA — a O.S. do dilab não passou na validação: a lista `erros` traz TODOS os problemas ({ campo, olho, tipo, limite, mensagem }); 400 OTICA_NAO_ENCONTRADA | OTICA_NEGATIVADA | LENTE_GENERICA_NAO_CONFIGURADA - 401 NAO_AUTENTICADO — sem token, token inválido ou expirado; 401 CREDENCIAL_REVOGADA — credencial desativada - 403 ESCOPO_INSUFICIENTE — o token não tem o escopo da rota; 403 EMPRESA_NAO_AUTORIZADA — empresa fora da lista do token - 404 NOT_FOUND — recurso inexistente ou fora do escopo (a resposta nunca revela se existe) - 409 CONFLICT | PEDIDO_DUPLICADO | PEDIDO_JA_ACEITO | PEDIDO_EM_PRODUCAO | PEDIDO_CANCELADO | PEDIDO_REJEITADO | ORDEM_SERVICO_DUPLICADA - 429 RATE_LIMITED (+ Retry-After) · 500 INTERNAL_ERROR (+ correlationId) · 502 GATEWAY_INDISPONIVEL · 503 GATEWAY_OCUPADO (+ Retry-After) Informe o `traceId` à Dataweb ao reportar um problema. ## Descoberta e status (qualquer escopo) - GET /api/integracao/v1 → nome, versão, escopos e links (self, version, openapi, llmsTxt, ping) - GET /api/integracao/v1/version → { nome, versao }; versao = maior.menor, dois inteiros comparados parte a parte (1.10 vem depois de 1.9) - GET /api/integracao/v1/ping[?empresaCnpj=] → { status: "ok", integrador, cnpj, time }; com empresaCnpj também confere a empresa - GET /api/integracao/v1/empresas → { nome, cnpj } das filiais liberadas (sem empresaCnpj, sem cursor) ## Clientes (commercio.integracao.clientes:read) - GET /api/integracao/v1/clientes?empresaCnpj=..[&cpf=..|&cnpj=..][&consentimentoLgpd=..][&alteradoEm[gte]=..][&limite=..&cursor=..] Snapshot: só clientes ativos. Busca exata por cpf ou cnpj (com ou sem máscara). Delta: alteradoEm[gte] (ou [gt]; YYYY-MM-DD ou com hora) traz também os desativados (ativo: false); deduplique por cpf/cnpj e use como próximo valor o maior alteradoEm visto MENOS 1 dia. consentimentoLgpd: naoEnviado | naoRespondido | aguardando | aceitou | recusou. celular sai normalizado para WhatsApp (55DDD9XXXXXXXX) ou omitido; telefone1/2 só dígitos. - GET /api/integracao/v1/clientes/aniversariantes?empresaCnpj=..[&mes=1..12][&dia=1..31][&consentimentoLgpd=..] mes padrão = o atual; janela que cruza o mês = duas chamadas. ## Ordens de serviço (commercio.integracao.ordens-servico:read) - GET /api/integracao/v1/ordens-servico?empresaCnpj=.. filtros combináveis: cpf; etapa (código da etapa ATUAL, inteiro ≥ 0; sem registro de etapa = 0); situacao = orcamento | aberta | pronta | entregue (sem ela, orçamentos ficam de fora); alteradoEm[gte] (polling: toda troca de etapa conta; deduplique por numeroOrdemServico); dataEntrega[gte] + dataEntrega[lte] (YYYY-MM-DD, juntos, no máximo 92 dias, só entregues); incluir = itens,ordensCompra (ausente se não pedido; [] se pedido e vazio). Cada O.S.: situacao, datas, etapaAtual, cliente, vendedor { codigo, nome } (o mesmo de /vendas), alteradoEm. - GET /api/integracao/v1/ordens-servico/{numero}?empresaCnpj=..[&cpf=..] → detalhe com histórico, itens e ordens de compra. cpf opcional para conferência: divergente = 404 (a mesma resposta de O.S. inexistente). tipoItem: produto | lente | armacao | servico | outros. temReceita indica se há receita. - GET /api/integracao/v1/ordens-servico/{numero}/receita?empresaCnpj=..[&cpf=..] (commercio.integracao.ordens-servico.receita:read + ordens-servico:read) Graus por olho (longe/perto), medidas, armação, médico, paciente e LINKS das imagens. Sem receita = 404. - GET /api/integracao/v1/ordens-servico/{numero}/receita/imagens/{qual}?empresaCnpj=.. qual = receita | armacao | armacaoAntiga 200 com o binário (image/jpeg ou png) + ETag (If-None-Match → 304); 302 para armazenamento externo (siga o Location); 404 sem imagem. ## Vendas e devoluções (commercio.integracao.vendas:read) - GET /api/integracao/v1/vendas?empresaCnpj=.. ao menos um recorte, combináveis: cpf OU cnpj; dataEmissao[gte] + dataEmissao[lte] (YYYY-MM-DD, juntos, no máximo 92 dias). [&incluir=pagamentos,devolucoes] Só VENDAS encerradas (bonificação/brinde incluída; pedidos, ordens de compra e notas de entrada/devolução NÃO). Cada venda: cliente (null explícito se não identificado), vendedor, naturezaOperacao (separa brinde de receita: 5.910 = bonificação), notaFiscal (situação da NOTA: autorizada | pendente | enviada | rejeitada | denegada | cancelada | inutilizada | emitida), itens. Item (vendas e devoluções): codItem, descricao, quantidade, valorUnitarioOriginal (preço de tabela ou o especial vigente), valorUnitario (líquido), desconto (R$ da linha = (valorUnitarioOriginal - valorUnitario) x quantidade; negativo = acréscimo; desconto que ficou só no cabeçalho da venda não entra), total. - GET /api/integracao/v1/vendas/{codVenda}?empresaCnpj=..[&incluir=..] → a mesma forma. 404 = não existe nesta empresa, não é venda (pedido, devolução), ainda está aberta ou foi CANCELADA (o ERP exclui a venda cancelada — confirme por aqui antes de liberar cashback/comissão). - incluir=pagamentos: fatura { numero, valorTotal, vendas[] } + as parcelas DA FATURA (uma fatura pode cobrir várias vendas: deduplique pelo `id` da parcela, o mesmo de /financeiro/lancamentos). Só as parcelas LANÇADAS na empresa consultada (a fatura pode cobrir venda de outra empresa). Parcela: valor, valorOriginal, valorPago, juros, desconto, dataEmissao, dataVencimento, dataVencimentoOriginal, dataPagamento, situacao, forma. forma.tipo: dinheiro | cheque | cartaoCredito | cartaoDebito | pix | boleto | banco | carne | creditoCliente | outro; cartão: bandeira { codigoNfe (tBand), nome }, numeroParcela, totalParcelas, nsu, codigoAutorizacao; carnê: numero e tipo. Quando o cliente pagou: cartão, PIX, dinheiro e crédito do cliente → dataEmissao; carnê, boleto e cheque → dataPagamento (em cartão/PIX, dataPagamento é o repasse da adquirente). A lista muda depois da venda (baixas; saldo recebido vira parcela nova). convenios[] (com incluir=pagamentos; [] se não há): a parte paga por convênio ou ticket, em PARALELO à fatura do cliente (NÃO entra em pagamentos; uma venda pode ser paga toda pelo convênio, sem fatura do cliente): { id, convenio { codigo, nome }, tipo (convenio | ticket | outro), valor, numeroDocumento, responsavel, fatura (a cobrança do convênio, quando faturado: { numero, valorTotal, vendas[] }), pagamentos (as parcelas dessa cobrança, na mesma forma e com o mesmo id das de cima; dataPagamento = quando o convênio pagou; [] sem fatura) }. - incluir=devolucoes: [{ codDevolucao, dataEmissao, total }]. Outro valor em incluir = 400. - GET /api/integracao/v1/devolucoes?empresaCnpj=.. ao menos um recorte, combináveis: cpf OU cnpj; dataEmissao[gte] + dataEmissao[lte] (emissão DA DEVOLUÇÃO, no máximo 92 dias); codVenda (a venda de origem). Devolução = nota de entrada de devolução do cliente: vendaOrigem, tipo (mercadoria | brinde | outro), itens devolvidos (parcial = só o que voltou), creditoGerado (usado depois aparece como forma creditoCliente). - GET /api/integracao/v1/devolucoes/{codDevolucao}?empresaCnpj=.. → a mesma forma; 404 fora desta empresa. ## Financeiro — contas a receber (commercio.integracao.financeiro.contas-receber:read) Dois eixos: empresaCnpj = a empresa CREDORA (obrigatória, na lista do token); cpf e cnpj = o CLIENTE, parâmetros SEPARADOS e REPETÍVEIS (cpf=..&cpf=..&cnpj=..), ao menos um, até 100 no total (documento válido mas desconhecido é omitido). Recurso fora do escopo = 404. - GET /api/integracao/v1/financeiro/clientes?empresaCnpj=..&cpf=.. (só pessoas com vínculo financeiro com a empresa) - GET /api/integracao/v1/financeiro/lancamentos?empresaCnpj=..&cpf=.. → parcelas: id, numeroFatura, numeroDocumento, dataVencimento, dataPagamento, valorTotal, situacao (pago | pagoEmAtraso | emAberto | vencido, calculada no servidor com tolerância de fim de semana; recebida a liquidar conta como paga) - GET /api/integracao/v1/financeiro/notas-fiscais?empresaCnpj=..&cpf=.. → id opaco, tipo nfe | nfce | nfse, chave, numero, serie - GET /api/integracao/v1/financeiro/notas-fiscais/{id}/xml e GET /api/integracao/v1/financeiro/notas-fiscais/{id}/pdf (id = o da nota) - GET /api/integracao/v1/financeiro/lancamentos/{id}/boleto (id = o da parcela) Binários: XML/PDF = 200 + ETag/304 (Cache-Control: private); boleto = 200 application/pdf sem ETag (no-store); 404 ausente/sem posse; 409 boleto de parcela paga ou sem registro; 502 GATEWAY_INDISPONIVEL; 503 GATEWAY_OCUPADO + Retry-After. ## Financeiro — cobrança (commercio.integracao.financeiro.cobranca:read) - GET /api/integracao/v1/financeiro/cobranca/lancamentos[?empresaCnpj=..(repetível; padrão: todas as do token)][&antecedencia=0..7] Parcelas VENCIDAS (cobráveis) de todos os clientes; antecedencia inclui as que vencem nos próximos dias (1 = hoje, 2 = amanhã…). Cada item traz empresaCnpj e cliente (null se não identificado). - Os binários (xml/pdf/boleto) também aceitam este escopo, SEM cpf/cnpj (com antecedencia opcional): só o que a cobrança lista. Com cpf/cnpj, a credencial de cobrança recebe 403 ESCOPO_INSUFICIENTE. ## E-commerce (integrador de e-commerce) - POST /api/integracao/v1/ecommerce/pedidos?empresaCnpj=.. (commercio.integracao.ecommerce.pedidos:write) Grava o pedido na loja. numeroPedido: 1 a 30 caracteres [A-Za-z0-9._-], chave de idempotência: novo = 201; reenvio pelo mesmo integrador = 200 com o pedido gravado; já recebido por outra integração ou gravado para outra empresa = 409 PEDIDO_DUPLICADO. Itens pelo SKU (o id de /ecommerce/produtos; SKU fora do catálogo = 400 PRODUTO_NAO_ENCONTRADO). pagamentos[].codigo: DINHEIRO | CHEQUE | CARTAO_CREDITO | CARTAO_DEBITO | CREDITO (crediário) | BOLETO | DEP_BANCARIO | PIX | SALDO_RECEBER (pagamento na retirada); a soma = total. - GET /api/integracao/v1/ecommerce/pedidos?empresaCnpj=..[&alteradoEm[gte]=..] (commercio.integracao.ecommerce.pedidos:read) Só os pedidos do próprio integrador (claim integrador: as credenciais dele veem os mesmos pedidos). Polling: aceite, NF-e, despacho e entrega contam como alteração. - GET /api/integracao/v1/ecommerce/pedidos/{numeroPedido}?empresaCnpj=.. situacao: recebido | aceito (a loja gerou a venda) | emitidaNf | despachado | entregue | cancelado (pelo integrador) | rejeitado (pela loja). - POST /api/integracao/v1/ecommerce/pedidos/{numeroPedido}/cancelar?empresaCnpj=.. Só enquanto a loja não aceitou: depois = 409 PEDIDO_JA_ACEITO; com o laboratório já trabalhando na O.S. = 409 PEDIDO_EM_PRODUCAO; repetido = 409 PEDIDO_CANCELADO; rejeitado = 409 PEDIDO_REJEITADO. - GET /api/integracao/v1/ecommerce/produtos?alteradoEm[gte]=..(obrigatório) e GET /api/integracao/v1/ecommerce/produtos/{id} (commercio.integracao.ecommerce.produtos:read) - GET /api/integracao/v1/ecommerce/estoque?empresaCnpj=..[&alteradoEm[gte]=..] (commercio.integracao.ecommerce.estoque:read) Saldo da PRATELEIRA (local 1, o disponível para venda) por produto. Carga inicial (sem alteradoEm): só saldos diferentes de zero (o que não vem está zerado). Com alteradoEm: todo saldo alterado, inclusive os zerados. Saldo negativo sai como está (trate como zero). ## Dilab — O.S. de laboratório (integrador que envia O.S. de óticas para o laboratório) empresaCnpj = o LABORATÓRIO. A lente não é escolhida: cada olho traz só a DESCRIÇÃO da lente; o laboratório grava a O.S. com uma lente genérica e troca pela lente real antes de produzir. A O.S. não é calculada na entrada (sem preços nem previsão). Cada integrador só vê as próprias O.S. - GET /api/integracao/v1/dilab/servicos?empresaCnpj=..[&descricao=..][&limite=..&cursor=..] (qualquer escopo dilab) Os serviços que a O.S. pode levar: { codigo, descricao }. Use o codigo em servicos[].codigo. - GET /api/integracao/v1/dilab/materiais?empresaCnpj=.. (qualquer escopo dilab) Todos os materiais da armação: { codigo, descricao }, sem paginação. Use o codigo em armacao.material. - GET /api/integracao/v1/dilab/formatos-aro?empresaCnpj=.. (qualquer escopo dilab) Todos os formatos do aro ativos: { codigo, descricao, imagem (PNG em base64) }, sem paginação. Use o codigo em armacao.formatoAro. - POST /api/integracao/v1/dilab/ordens-servico?empresaCnpj=.. (commercio.integracao.dilab.ordens-servico:write) Corpo: idPedidoOtica (1 a 30 caracteres, chave de idempotência por ótica), oticaCnpj (cliente ativo do laboratório), receita.olhoDireito/olhoEsquerdo { descricaoLente (obrigatória), grauEsferico, grauCilindro, eixo, dnp, adicao, altura, ... }, armacao, parametros, paciente, medico, vendedor, servicos [{ codigo }], observacao. Nova = 201 com a O.S.; reenvio do mesmo idPedidoOtica para a mesma ótica = 200 com a O.S. já gravada (mesmo recusada: para corrigir, envie outro idPedidoOtica). Inválida = 400 ORDEM_SERVICO_INVALIDA com TODOS os erros na lista `erros`. idPedidoOtica já usado por outra O.S. da ótica = 409 ORDEM_SERVICO_DUPLICADA. - POST /api/integracao/v1/dilab/ordens-servico/validar?empresaCnpj=.. (commercio.integracao.dilab.ordens-servico:write) O mesmo corpo e as mesmas validações do POST, SEM gravar: 204 = seria aceita; senão o mesmo 400/409. - GET /api/integracao/v1/dilab/ordens-servico?empresaCnpj=..[&oticaCnpj=..][&idPedidoOtica=..][&situacao=..][&dataInclusao[gte]=..&dataInclusao[lte]=..] (commercio.integracao.dilab.ordens-servico:read) As O.S. enviadas pelo integrador, inclusive as recusadas. situacao: aberta | aprovada | encerrada | cancelada | recusada (pelo laboratório, com o motivo). - GET /api/integracao/v1/dilab/ordens-servico/{numero}?empresaCnpj=.. A O.S. como está no laboratório (a lente já trocada aparece com a descrição real). Recusada: o que foi enviado, com situacao recusada e recusa { data, motivo }. - GET /api/integracao/v1/dilab/ordens-servico/{numero}/status?empresaCnpj=.. { situacao, recusa: { data, motivo } (só na recusada), etapas: [{ codigo, descricao, data }] } — as etapas da produção. ## LGPD - Todo dado de pessoa traz consentimentoLgpd; contatos e endereço de quem RECUSOU saem omitidos. - Mensagens (WhatsApp, e-mail) só para quem aceitou. - Receita (dado de saúde) não é mascarada por consentimento — uso restrito à finalidade do contrato.