# API REST do Sistema Loja

Referência completa (1238 endpoints). Documentação: https://developers.nivesistemas.com.br

- URL base: `https://api.nivesistemas.com.br`
- Autenticação: `Authorization: Bearer sl_live_...` e `X-Store-Slug: sua-loja` em toda requisição

## URL base

Em produção use `https://api.nivesistemas.com.br`. A loja é identificada pelo header `X-Store-Slug` (ou `?loja=`), não por um domínio próprio. O catálogo cobre as rotas protegidas por chave de API e as públicas relevantes. Provisionamento da plataforma, chat de suporte Nive, migração de dados (`/data-migration`), assistente de implantação (`/store-onboarding`) e warmup interno (`POST /health/warm`) ficam fora do escopo de integração externa.

## Autenticação

Gere uma chave em Sistema → Integração API no ERP e envie em cada requisição via `Authorization: Bearer sl_live_...` ou `X-API-Key: sl_live_...`, junto com `X-Store-Slug: sua-loja` (ou `?loja=`). Chaves revogadas deixam de funcionar imediatamente. A gestão das próprias chaves (`/api-keys`) exige sessão de usuário (JWT), não API key. Veja o guia de autenticação.

## Escopo da chave

Toda chave tem um escopo, e as permissões efetivas são a interseção do escopo com as permissões do usuário que a criou. `integration` (padrão — "Vendas, estoque e cadastros"): vendas, clientes, produtos, estoque, contas a receber, relatórios e catálogo online, com leitura e escrita. `read` ("Somente leitura"): todas as permissões de consulta (`*.view`) mais o uso do PDV, sem escrita e sem o cofre. `inherit` ("Igual ao meu usuário", legado): as mesmas permissões do usuário. Rotas fora do escopo respondem 403.

## Planos

O acesso à API (criação de chaves) está disponível nos planos Expansão e Rede.

## Formato

Requisições e respostas usam JSON (`Content-Type: application/json`), salvo download de arquivos.

## Limites

A API autenticada aplica rate limit padrão de cerca de 300 requisições por minuto por IP (configurável no servidor). Em 429, aguarde e tente novamente. Detalhes no guia de limites.

## NFS-e e ordens de serviço

Para emitir nota de serviço de outro sistema: use `POST /nfse/emit-direct` (payload completo sem OS) ou `POST /nfse/emit-with-order` (cria cliente/OS e emite). Consulte `GET /nfse/emit-prep?serviceOrderId=` antes de emitir a partir de uma OS. Para faturar OS no PDV: `POST /service-orders/:id/to-sale` e depois finalize a venda (NFS-e via `saleId` / `nfseIntent`). Também é possível gerenciar OS, projetos, contratos, visitas de campo e assinaturas nas rotas `/service-*`. A chave precisa das permissões `nfse.view` / `nfse.manage` (e as de serviços, quando aplicável).

## Documento fiscal de mercadoria (NFC-e / NF-e)

A Sale usa um único `FiscalDocument` (modelo 65 NFC-e ou 55 NF-e de venda). O caixa escolhe o modelo na finalização; o cadastro do cliente pode fixar NFC-e ou NF-e. Emita com `POST /fiscal/documents/emit/:saleId`. Cancelamento e DANFE usam `/fiscal/documents/:id/*`. CC-e e inutilização 55: `POST /fiscal/documents/:id/cce` e `POST /fiscal/inutilize-nfe-sale`. Contingência offline permanece só para NFC-e. Devoluções 55 continuam em `/sale-returns` e `/purchase-returns`.

## Erros comuns

400 — loja não informada (envie X-Store-Slug ou ?loja=). 401 — credencial ausente ou inválida. 403 — sem permissão ou plano sem API. 404 — recurso não encontrado. 422 — validação (corpo da resposta traz detalhes). 409 — conflito (ex.: GTIN duplicado). 429 — rate limit.

## Autenticação

Login de usuário via sessão (cookies HttpOnly). Integrações externas devem usar chave de API — não este fluxo.

### `POST /auth/login`

Login — define cookies de sessão e retorna o usuário

Corpo: `username`* (string) Usuário; `password`* (string) Senha

Resposta: { user, storeSlug } em sucesso; ou { requiresTwoFactor, pendingToken } / { requiresEnrollment, enrollmentToken } quando 2FA estiver ativo

### `POST /auth/2fa/verify`

Confirmar login com código 2FA

Corpo: `pendingToken`* (string) Token retornado no login; `code`* (string) Código TOTP

### `POST /auth/2fa/recover`

Login com código de recuperação 2FA

Corpo: `pendingToken`* (string) Token retornado no login; `backupCode`* (string) Código de backup

### `POST /auth/2fa/enroll/setup`

Iniciar cadastro de 2FA (quando exigido)

Corpo: `enrollmentToken`* (string) Token retornado no login (requiresEnrollment)

### `POST /auth/2fa/enroll/confirm`

Confirmar cadastro de 2FA

Corpo: `enrollmentToken`* (string) Token de enrollment; `code`* (string) Código TOTP

### `GET /auth/me`

Dados do usuário autenticado (sessão ou API key)

Resposta: { id, username, name, permissions, ... }

### `POST /auth/presence`

Heartbeat de presença (usuário online no ERP agora)

### `PATCH /auth/me/print-target`

Atualizar destino de impressão do usuário

Corpo: `printTarget` (string) Destino de impressão (LOCAL ou AGENT); `printA4ViaAgent` (boolean) Se true, folha A4 também vai direto ao ponto (padrão false)

### `PATCH /auth/me/ui-preferences`

Atualizar preferências de interface do usuário (favoritos da navegação)

Corpo: `navFavorites` (string[]) Rotas fixadas (máx. 5)

### `POST /auth/logout`

Logout (limpa cookies de sessão)

### `GET /auth/permissions/catalog`

Catálogo de permissões

### `POST /auth/refresh`

Renovar cookies de sessão

Resposta: { user, storeSlug }

### `POST /auth/forgot-password`

Solicitar redefinição de senha por e-mail

Corpo: `email`* (string) E-mail do usuário

### `POST /auth/reset-password`

Redefinir senha com o token recebido

Corpo: `token`* (string) Token do e-mail; `password`* (string) Nova senha

### `GET /auth/my-stores`

Lojas às quais o usuário tem acesso (sessão)

### `POST /auth/switch-store`

Trocar de loja na sessão atual

Corpo: `storeSlug`* (string) Slug da loja

## Categorias

### `GET /categories`

Listar categoria

### `POST /categories`

Criar categoria

Corpo: `name`* (string) Nome da categoria; `defaultNcm` (string) NCM padrão (8 dígitos, opcional)

Resposta: 201 Created

### `PATCH /categories/:id`

Atualizar categoria

Corpo: `name`* (string) Nome da categoria; `defaultNcm` (string) NCM padrão (8 dígitos, opcional)

### `DELETE /categories/:id`

Excluir categoria

Resposta: 204 No Content

### `GET /categories/seed/profiles`

Ramos disponíveis para sugestão de categorias

Resposta: { profiles: [{ id, label }], suggested }

### `POST /categories/seed/suggest`

Sugerir categorias do ramo (catálogo fiscal + IA)

Corpo: `profile` (string) Ramo; padrão vem do CNAE da loja; `businessDescription` (string) Descrição livre do negócio (até 500 caracteres); `useAi` (boolean) false devolve só o catálogo do ramo

Resposta: { profile, profileLabel, aiUsed, aiError, items }

### `POST /categories/seed/apply`

Cadastrar em massa as categorias escolhidas

Corpo: `items`* ({ name, defaultNcm? }[]) Até 60 categorias; nome já cadastrado é pulado

Resposta: { created, skipped }

### `GET /categories/:id/products`

Produtos da categoria (paginado)

Query: `q` (string) Busca por nome, SKU ou código; `page` (integer) Página (padrão 1); `take` (integer) Itens por página (1–100, padrão 10)

### `DELETE /categories/:id/products/:productId`

Tirar o produto da categoria (não apaga o produto)

Resposta: 204 No Content

### `POST /categories/:id/apply-ncm`

Aplicar o NCM padrão da categoria aos produtos sem NCM válido (nunca sobrescreve; ignora serviços)

### `POST /categories/bulk-delete`

Excluir categorias em lote (produtos ficam sem categoria)

Corpo: `ids`* (uuid[]) Até 200 categorias

Resposta: Retorna { deleted, unlinkedProducts }

## Marcas

### `GET /brands`

Listar marca

### `POST /brands`

Criar marca

Corpo: `name`* (string) Nome da marca

Resposta: 201 Created

### `PATCH /brands/:id`

Atualizar marca

Corpo: `name`* (string) Nome da marca

### `DELETE /brands/:id`

Excluir marca

Resposta: 204 No Content

### `GET /brands/:id/products`

Produtos da marca (paginado)

Query: `q` (string) Busca por nome, SKU ou código; `page` (integer) Página (padrão 1); `take` (integer) Itens por página (1–100, padrão 10)

### `DELETE /brands/:id/products/:productId`

Tirar o produto da marca (não apaga o produto)

Resposta: 204 No Content

## Tags de produto

### `GET /product-tags`

Listar tag

### `POST /product-tags`

Criar tag

Corpo: `name`* (string) Nome da tag (duplicata seleciona a existente)

Resposta: 201 Created

### `PATCH /product-tags/:id`

Atualizar tag

Corpo: `name`* (string) Nome da tag (duplicata seleciona a existente)

### `DELETE /product-tags/:id`

Excluir tag

Resposta: 204 No Content

### `POST /product-tags/bulk-delete`

Remover tags em lote

Corpo: `ids`* (string[]) IDs das tags

### `GET /product-tags/:id/products`

Produtos com a etiqueta (paginado)

Query: `q` (string) Busca por nome, SKU ou código; `page` (integer) Página (padrão 1); `take` (integer) Itens por página (1–100, padrão 10)

### `DELETE /product-tags/:id/products/:productId`

Tirar a etiqueta do produto

Resposta: 204 No Content

## Produtos

### `GET /products`

Listar produtos

Query: `q` (string) Busca por descrição; `active` (boolean) true = só ativos; false = só inativos; omitido = todos; `productType` (string) MERCHANDISE | SERVICE; `needsCreditPrice` (boolean) true = só mercadorias com priceCredit <= 0; `page` (integer) Página (padrão 1); `take` (integer) Itens por página (1–100, padrão 100)

Resposta: Inclui inactiveCount, missingCreditCount e summary { productCount, variantCount, stockQty } do escopo active/productType (summary independente de busca/página/needsCreditPrice)

### `GET /products/description-match`

Verificar se já existe produto com a mesma descrição (ignora maiúsculas e acentos)

Query: `description` (string) Descrição a comparar; `excludeId` (uuid) ID do produto em edição (não conta como duplicata)

Resposta: { exists: boolean, match?: { id, description } }

### `GET /products/:id`

Detalhe do produto com variantes

### `POST /products`

Criar produto

Corpo: `description`* (string) Descrição; `priceCash`* (number) Preço à vista; `priceCredit`* (number) Preço a prazo; `productType` (string) MERCHANDISE | SERVICE (padrão MERCHANDISE); `isComposite` (boolean) Produto kit/composto; `components` (array) Componentes do kit: [{ componentVariantId, quantity, parentVariantId? }]; `expectedComponentCount` (integer) Peças esperadas no kit; `costPrice` (number) Custo fiscal — NF de entrada (opcional); `netWeightKg` (number | null) NF-e industrial: peso líquido por unidade de venda (kg). Também aceitos: packagingTareKg, volumeSpecies, tribUnit, tribUnitFactor, ipiUnitValue, ipiBaseMode, ipiInStBase, pisUnitValue, cofinsUnitValue. null limpa; omitido não altera. As variantes aceitam netWeightKg, packagingTareKg e tribUnitFactor próprios.; `managerialCost` (number) Custo gerencial — precificação e relatórios (opcional); `profitMargin` (number) Margem % sobre o custo gerencial (padrão 100). Com custo informado, alterar a margem recalcula o preço e vice-versa; o custo não muda.; `categoryId` (uuid) ID da categoria; `brandId` (uuid) ID da marca; `active` (boolean) Ativo (padrão true); `availableForDelivery` (boolean) Disponível no catálogo online; `catalogDescription` (string) Descrição amigável no catálogo (vazio usa description); `isFavorite` (boolean) Favorito no PDV; `stockQty` (integer) Estoque (produto sem grade); `gtin` (string) Código de barras do produto sem grade (opcional; vazio gera automático); `ncm` (string) NCM 8 dígitos; `cest` (string) CEST; `issServiceCode` (string) cTribNac / LC 116 (serviço NFS-e); `nbsCode` (string) Código NBS (serviço); `issRate` (number) Alíquota ISS %; `issRetained` (boolean) ISS retido; `variants` (array) Grades: label, division, subdivision, stockQty, gtin

Resposta: 201 Created

### `PATCH /products/:id`

Atualizar produto

Corpo: `description` (string) Descrição; `priceCash` (number) Preço à vista. Com custo informado, altera a margem; o custo não muda.; `priceCredit` (number) Preço a prazo; `productType` (string) MERCHANDISE | SERVICE; `isComposite` (boolean) Produto kit/composto; `categoryId` (uuid) ID da categoria; `brandId` (uuid) ID da marca; `active` (boolean) Ativo; `availableForDelivery` (boolean) Disponível no catálogo online; `catalogDescription` (string) Descrição amigável no catálogo (vazio usa description); `isFavorite` (boolean) Favorito no PDV; `gtin` (string) Código de barras do produto sem grade; `ncm` (string) NCM; `issServiceCode` (string) Código de serviço NFS-e

### `DELETE /products/:id`

Excluir produto

Resposta: 204 No Content

### `POST /products/:id/variants`

Adicionar variante (grade)

Corpo: `label` (string) Rótulo da grade; `division` (string) Divisão (ex.: P, M); `subdivision` (string) Subdivisão (ex.: Azul); `stockQty`* (integer) Quantidade em estoque; `gtin` (string) Código de barras (opcional)

### `POST /products/:id/variants/bulk`

Adicionar variantes em lote

Corpo: `variants`* (array) Lista de variantes

### `PATCH /products/:id/variants/:variantId`

Atualizar variante

Corpo: `label` (string) Rótulo; `stockQty` (integer) Estoque; `gtin` (string) GTIN

### `DELETE /products/:id/variants/:variantId`

Excluir variante

Resposta: 204 No Content

### `PATCH /products/bulk/active`

Ativar/inativar em lote

Corpo: `ids`* (uuid[]) IDs dos produtos; `active`* (boolean) Novo status

### `PATCH /products/bulk/category`

Alterar categoria em lote

Corpo: `ids`* (uuid[]) IDs dos produtos; `categoryId` (uuid) Nova categoria (null para limpar)

### `PATCH /products/bulk/price`

Alterar preços em lote (%)

Corpo: `ids`* (uuid[]) IDs dos produtos; `percent`* (number) Percentual (-99 a 1000)

### `PATCH /products/bulk/prices`

Definir preços absolutos em lote (à vista / a prazo)

Corpo: `items`* (array) [{ id, priceCash, priceCredit? }] (máx. 500)

### `POST /products/bulk/merge-variants`

Mesclar variantes em um produto com grade

Corpo: `description`* (string) Descrição do produto resultante; `divisionName`* (string) Nome do eixo divisão; `subdivisionName`* (string) Nome do eixo subdivisão; `variants`* (array) [{ variantId, division?, subdivision? }] (mín. 2)

### `GET /products/classification/pending`

Produtos sem categoria e/ou sem marca

Query: `page` (integer) Página (padrão 1); `pageSize` (integer) Itens por página (máx. 100); `filter` (string) no-category | no-brand | any; `search` (string) Descrição ou código interno; `includeInactive` (boolean) Inclui produtos inativos

### `POST /products/classification/suggest`

Sugerir categoria/marca em lote (IA)

Corpo: `productIds`* (uuid[]) IDs dos produtos (máx. 60)

### `POST /products/classification/apply`

Aplicar classificações confirmadas

Corpo: `items`* (array) [{ productId, categoryId?, newCategoryName?, brandId?, newBrandName? }] (máx. 100)

Resposta: Só preenche campo vazio; item com categoria/marca já definida volta em skipped

### `GET /products/composites/incomplete`

Kits/compostos incompletos (faltam componentes)

### `GET /products/composites/incomplete/suggest`

Sugestões para completar kits incompletos

### `GET /products/by-component/:variantId/parents`

Produtos pai que usam a variante como componente

### `POST /products/:id/components`

Adicionar componente ao kit

Corpo: `componentVariantId`* (uuid) Variante componente; `quantity`* (number) Quantidade no kit; `parentVariantId` (uuid) Variante pai (grade do kit), quando aplicável

### `POST /products/:id/composition/mark-ready`

Marcar composição do kit como pronta para venda

### `GET /products/inactivate-out-of-stock-jobs`

Listar jobs de inativação de produtos sem estoque

### `POST /products/inactivate-out-of-stock-jobs`

Enfileirar inativação de todos os produtos ativos sem estoque

Resposta: 202 Accepted

### `DELETE /products/inactivate-out-of-stock-jobs/:jobId`

Cancelar job de inativação de produtos sem estoque

Resposta: 204 No Content

### `GET /products/name-format-jobs`

Listar jobs de correção de nomes de produtos

### `POST /products/name-format-jobs`

Enfileirar correção dos nomes de produtos já cadastrados

Resposta: 202 Accepted

### `DELETE /products/name-format-jobs/:jobId`

Cancelar job de correção de nomes de produtos

Resposta: 204 No Content

### `GET /products/image-optimize-jobs`

Listar jobs de otimização de fotos e quantas fotos ainda não foram otimizadas

Resposta: { jobs, pendingImages }

### `POST /products/image-optimize-jobs`

Enfileirar a otimização das fotos de produto enviadas antes da otimização no upload

Resposta: 202 Accepted

### `DELETE /products/image-optimize-jobs/:jobId`

Cancelar job de otimização de fotos

Resposta: 204 No Content

### `GET /products/price-adjust-jobs`

Listar reajustes de preços em massa (últimos 30)

### `POST /products/price-adjust-jobs/preview`

Prévia de um reajuste de preços em massa: produtos alcançados e amostra antes/depois

Corpo: `operation`* (string) PERCENT, AMOUNT, MARGIN_ON_COST, CREDIT_FROM_CASH ou ROUND; `target` (string) CASH, CREDIT ou BOTH (padrão BOTH); `value` (number) % ou R$ conforme a operação (negativo reduz); `rounding` (string) NONE, INTEGER, END_90 ou END_99; `categoryId` (string) Limita a uma categoria; `brandId` (string) Limita a uma marca; `includeInactive` (boolean) Inclui produtos inativos

Resposta: { matched, willUpdate, skipped, samples }

### `POST /products/price-adjust-jobs`

Enfileirar um reajuste de preços em massa (mesmo corpo da prévia)

Resposta: 202 Accepted

### `POST /products/price-adjust-jobs/:jobId/revert`

Enfileirar a reversão de um reajuste (restaura o preço anterior de cada produto)

Resposta: 202 Accepted

### `DELETE /products/price-adjust-jobs/:jobId`

Cancelar reajuste de preços pendente ou em processamento

Resposta: 204 No Content

### `POST /products/:id/image`

Enviar imagem do produto (até 15 MB; gravada otimizada: até 1600 px, JPEG/PNG, com miniatura de 400 px em thumbnailUrl)

### `DELETE /products/:id/image`

Remover imagem do produto

### `GET /products/:id/sales-history`

Obter histórico de vendas

### `GET /products/:id/last-entries`

Obter última nota de entrada por grade

### `GET /products/:id/purchase-codes`

Listar códigos de compra do produto (fardo, código do fornecedor)

### `PUT /products/:id/purchase-codes`

Gravar os códigos de compra do produto

Corpo: `codes`* (array) [{ id?, variantId?, kind: GTIN | SUPPLIER_CODE, code, supplierId (só SUPPLIER_CODE), label, factor, xmlUnit }]

Resposta: Substitui a lista inteira. Na importação da NF-e o item casa por esses códigos e a quantidade é multiplicada pelo fator (1 FARDO = 12 un)

### `GET /products/:id/variants/:variantId/stock-movements`

Obter movimentações de estoque

### `GET /products/composites/match`

Conjuntos existentes (inclusive prontos) que podem receber novas grades

Query: `q` (string) Busca; `componentVariantIds` (string[]) Variantes componentes

### `POST /products/:id/composites/grades`

Adicionar grades a um conjunto

Corpo: `grades`* (array) Grades: [{ label, division, subdivision, … }]; `expectedComponentCount` (integer) Quantidade de componentes esperada

## GTINs

### `GET /gtins`

Listar GTINs

Query: `q` (string) Busca; `page` (integer) Página; `pageSize` (integer) Itens por página

### `GET /gtins/lookup/:gtin`

Buscar produto/variante por GTIN

### `GET /gtins/by-product/:productId`

GTINs de um produto

### `PATCH /gtins/:variantId`

Alterar GTIN de uma variante

Corpo: `gtin`* (string) Novo GTIN

### `POST /gtins/bulk`

Atualizar GTINs em lote

Corpo: `updates`* (array) Lista [{ variantId, gtin }]

## Vendas / Pedidos

Pedidos são representados como vendas (Sale). Rascunho → itens → finalizar. Uma venda pode vir de OS via `POST /service-orders/:id/to-sale`; nesse caso o detalhe inclui `serviceOrder` e o finalize marca a OS como INVOICED.

### `GET /sales`

Listar vendas

Query: `status` (string) DRAFT | COMPLETED | CANCELLED | DRAFT_CANCELLED. CANCELLED inclui também rascunhos cancelados; `q` (string) Busca (nº pedido, cliente, CPF, vendedor); `customerId` (uuid) Filtrar por cliente; `sellerId` (string) Filtrar por vendedor(es) (UUIDs separados por vírgula); `withoutSeller` (boolean) true = vendas sem vendedor; `productId` (uuid) Filtrar vendas que contenham o produto; `variantId` (uuid) Filtrar por grade/variante específica; `campaignId` (string) UUID da campanha, ou "none" para vendas sem item de campanha; `paymentMethodId` (string) Filtrar por forma(s) da venda (IDs separados por vírgula); `receivablePaymentMethodId` (string) Filtrar por forma(s) das baixas do crediário (IDs separados por vírgula); `page` (integer) Página; `pageSize` (integer) Itens por página (máx. 100); `completedFrom` (date) YYYY-MM-DD — filtro completedAt (finalizadas/canceladas); `completedTo` (date) YYYY-MM-DD — filtro completedAt; `updatedFrom` (date) YYYY-MM-DD — filtro updatedAt (rascunhos); `updatedTo` (date) YYYY-MM-DD — filtro updatedAt (rascunhos); `nfcePending` (boolean) true = COMPLETED com NFC-e EMIT_LATER sem documento; `paidViaPaymentLink` (boolean) true = pagas via link de pagamento; `sortBy` (string) Coluna de ordenação (ex.: updatedAt, completedAt, order); `sortDir` (string) asc | desc

### `GET /sales/summary`

Resumo para dashboard de vendas (rascunhos, hoje, NFC-e pendente)

Query: `q` (string) Mesma busca textual da listagem; `customerId` (uuid) Filtrar por cliente; `sellerId` (string) Filtrar por vendedor(es) (UUIDs separados por vírgula); `withoutSeller` (boolean) true = vendas sem vendedor; `productId` (uuid) Filtrar vendas que contenham o produto; `variantId` (uuid) Filtrar por grade/variante específica; `campaignId` (string) UUID da campanha ou "none"; `paymentMethodId` (string) Filtrar por forma(s) de pagamento (IDs separados por vírgula); `receivablePaymentMethodId` (string) Filtrar por forma(s) das baixas do crediário (IDs separados por vírgula)

Resposta: draftCount, draftTotalCash, completedTodayCount, completedTodayRevenue, nfcePendingCount, paidViaPaymentLinkCount, ecommerceOrdersActive, ecommerceAFaturarCount

### `POST /sales`

Criar venda em rascunho

Corpo: `customerId` (uuid) Cliente (opcional); `sellerId` (uuid) Vendedor legado (opcional); `sellerIds` (uuid[]) Vendedores (comissão rateada); `priceListId` (uuid) Tabela de preços; `pendingInstagramHandle` (string) Instagram pendente (@usuario) quando ainda não há cliente

Resposta: 201 Created — venda com status DRAFT

### `GET /sales/:id`

Detalhe da venda com itens e pagamentos

Resposta: Inclui `serviceOrder` ({ id, orderNumber, status }) quando a venda fatura uma OS.

### `GET /sales/:id/non-fiscal-receipt-html`

Cupom não fiscal 80 mm (somente venda sem NFC-e emitida)

### `GET /sales/:id/order-pdf`

PDF A4 do pedido (somente venda sem NFC-e emitida)

### `POST /sales/:id/notify-receipt`

Enviar conferência da venda por e-mail ao cliente

Corpo: `email`* (string) E-mail de destino

### `POST /sales/:id/send-fiscal-email`

Enviar XML e PDF das notas autorizadas da venda (NFC-e/NF-e e/ou NFS-e)

Corpo: `email`* (string) E-mail de destino

### `PATCH /sales/:id/completed-at`

Alterar data de conclusão de venda importada (LEGACY_IMPORT)

Corpo: `saleDate`* (string) AAAA-MM-DD

### `PATCH /sales/:id`

Atualizar venda (cliente, vendedor, observações, descontos)

Corpo: `customerId` (uuid) Cliente; `sellerId` (uuid) Vendedor; `sellerIds` (uuid[]) Vendedores (comissão rateada); `notes` (string) Observações; `shippingAmount` (number) Valor de entrega (frete financeiro); `priceListId` (uuid) Tabela de preços; `pendingInstagramHandle` (string) Instagram pendente; `discountType` (string) NONE | PERCENT | FIXED; `discountValue` (number) Valor do desconto; `couponId` (uuid) Cupom aplicado

### `POST /sales/:id/payment-link`

Gerar link de pagamento para venda em rascunho

Resposta: 201 — { link, url } com token e expiresAt

### `POST /sales/:id/items`

Adicionar item à venda

Corpo: `variantId`* (uuid) ID da variante do produto; `quantity`* (integer) Quantidade; `unitPriceCash` (number) Preço à vista manual; `unitPriceCredit` (number) Preço a prazo manual; `lineNotes` (string) Observação do item; `discountType` (string) NONE | PERCENT | FIXED; `discountValue` (number) Desconto do item

### `POST /sales/:id/items/bulk-remove`

Remover vários itens da venda (rascunho)

Corpo: `itemIds`* (uuid[]) IDs dos itens do carrinho a remover

### `PATCH /sales/:id/items/:itemId`

Alterar item (quantidade, preços, desconto, notas)

Corpo: `quantity` (integer) Nova quantidade; `unitPriceCash` (number) Preço à vista; `unitPriceCredit` (number) Preço a prazo; `lineNotes` (string) Observação do item; `discountType` (string) NONE | PERCENT | FIXED; `discountValue` (number) Desconto do item

### `POST /sales/:id/items/:itemId/campaign-price`

Aplicar preço de campanha ao item

### `DELETE /sales/:id/items/:itemId/campaign-price`

Remover preço de campanha do item

### `DELETE /sales/:id/items/:itemId`

Remover item da venda

Resposta: 204 No Content

### `DELETE /sales/:id`

Excluir venda em rascunho

Resposta: 204 No Content — apaga o registro. Prefira POST /cancel-draft para manter histórico.

### `POST /sales/:id/cancel-draft`

Cancelar rascunho/condicional sem excluir

Corpo: `reason` (string) Motivo opcional (máx. 500)

Resposta: 200 — { id, status: DRAFT_CANCELLED, cancelledAt, orderNumber, itemCount }. Peças voltam à loja; o pedido permanece na aba Canceladas.

### `POST /sales/:id/finalize`

Finalizar venda (baixa estoque, registra pagamentos). Se a venda veio de OS (to-sale), marca a OS como INVOICED, vincula NFS-e à OS/venda e cancela recebíveis abertos da OS.

Corpo: `priceBasis` (string) CASH | CREDIT | MIXED (opcional); `payments`* (array) Lista: paymentMethodId, amount, installments?, amountTendered?, pixChargeId?, cardPaymentId?, storeCreditId?, ...; `customerId` (uuid) Cliente; `sellerId` (uuid) Vendedor legado; `sellerIds` (uuid[]) Vendedores (comissão rateada); `discountType` (string) NONE | PERCENT | FIXED; `discountValue` (number) Valor do desconto; `skipCashTableDiscount` (boolean) Pular desconto da tabela à vista; `useCashPriceOnCredit` (boolean) Crediário usando preço à vista; `notes` (string) Observações; `nfceIntent` (string) Intent do documento de mercadoria (NFC-e 65 ou NF-e 55): EMIT_NOW | EMIT_LATER | NON_TAXABLE | SKIP; `nfseIntent` (string) Intent da NFS-e (serviço): EMIT_NOW | EMIT_LATER | SKIP. Se a OS vinculada já tiver NFS-e autorizada/pendente, não reemite.; `goodsDocumentType` (string) Override do tipo documental de mercadoria: NFCE_65 | NFE_55 | NONE. Sem override, usa a preferência do cliente e em seguida o resolver (canal / contribuinte).; `transactionIntermediary` (object) Intermediador da transação (NT 2020.006). Omitido = sem marketplace (balcão/remessa). indIntermed: 0 | 1; se 1, informe cnpj e registrationId (idCadIntTran).; `nonTaxableReason` (string) Motivo da não tributação; `nonTaxableTaxId` (string) CPF/CNPJ para não tributável; `loyaltyRedeemPoints` (integer) Pontos de fidelidade a resgatar; `settleReceivables` (object) { amount, paymentMethodId, discountAmount?, cardPaymentId?, installments?, notes? } — baixa de crediário junto com a venda; `notifyCustomerWhatsApp` (boolean) Enviar comprovante ao cliente no WhatsApp. Omitido mantém o envio automático.; `customerWhatsAppPhone` (string) WhatsApp informado no PDV quando o cliente ainda não tem número; `nfeExtras` (object | null) NF-e 55: transporte e dados opcionais { freightMode?, vehicle?, trailers?, volumes?, insurance?, otherExpenses?, taxpayerNote?, fiscoNote? }. Só vale com goodsDocumentType NFE_55; ignorado na NFC-e.

### `GET /sales/:id/nfe-checkout`

Prévia do checkout da NF-e 55 (frete, volumes, duplicatas, avisos)

Resposta: { saleId, shippingAmount, carrier, calculatedVolume, effectiveVolume, extras, duplicates[{ number, dueDate, amount }], hazardItems[{ description, text, warnings }], warnings[], locked }. Funciona com a venda em rascunho; locked = NF-e já autorizada.

### `PUT /sales/:id/nfe-extras`

Gravar transporte e dados opcionais da NF-e 55 (venda finalizada, NF-e ainda não autorizada)

Corpo: `nfeExtras`* (object | null) freightMode (0|1|2|3|4|9), vehicle { plate, uf, rntc? }, trailers[] (até 5), volumes { quantity, species, brand, number, netWeight, grossWeight, seals[] }, insurance e otherExpenses (R$, fatias do frete cobrado), taxpayerNote e fiscoNote (até 2000). null limpa tudo.

Resposta: { nfeExtras }. 409/400 com error em português quando a NF-e não pode mais ser alterada ou os dados são inválidos.

### `PATCH /sales/:id/customer`

Alterar cliente da venda

### `PATCH /sales/bulk/seller`

Alterar vendedor

### `GET /sales/bulk/seller/apply-defaults/jobs`

Listar jobs

### `DELETE /sales/bulk/seller/apply-defaults/jobs/:jobId`

Cancelar / remover job

### `POST /sales/bulk/seller/apply-defaults`

Aplicar vendedor padrão em lote

### `GET /sales/bulk/seller/recalc-commissions/jobs`

Listar jobs

### `DELETE /sales/bulk/seller/recalc-commissions/jobs/:jobId`

Cancelar / remover job

### `POST /sales/bulk/seller/recalc-commissions`

Recalcular comissões em lote

### `GET /sales/seller-schedules`

Listar agendas de vendedor

### `POST /sales/seller-schedules`

Criar agenda de vendedor

### `GET /sales/seller-schedules/apply/jobs`

Listar jobs

### `DELETE /sales/seller-schedules/apply/jobs/:jobId`

Cancelar / remover job

### `GET /sales/seller-schedules/:id`

Detalhe da agenda

### `PATCH /sales/seller-schedules/:id`

Atualizar agenda

### `DELETE /sales/seller-schedules/:id`

Excluir agenda

### `GET /sales/seller-schedules/:id/preview`

Prévia da aplicação da agenda

### `POST /sales/seller-schedules/:id/apply`

Aplicar

### `PATCH /sales/:id/seller`

Alterar vendedor

### `POST /sales/:id/recalculate-prices`

Recalcular preços

### `POST /sales/:id/recalculate-card-fees`

Recalcular taxas de cartão da venda

### `POST /sales/:id/credit-term-printed`

Registrar impressão do carnê/termo de crédito

### `POST /sales/:id/cancel`

Cancelar (senha de administrador, usuário liberado ou pedido de aprovação)

Resposta: Mesmas regras do reopen-draft: sem senha de administrador, usuário liberado executa na hora e os demais recebem 202 PENDING_APPROVAL.

### `POST /sales/:id/merge`

Mesclar rascunhos de venda

### `POST /sales/:id/payment-link/whatsapp`

Enviar link de pagamento por WhatsApp

### `POST /sales/:id/finalize-paid-link`

Finalizar venda paga por link

### `GET /sales/:id/promotions/eligibility`

Promoções com brinde que a venda em aberto atinge

### `POST /sales/:id/promotion-gifts`

Adicionar o brinde de uma promoção à venda

Corpo: `promotionId`* (uuid) Promoção; `productId`* (uuid) Produto brinde; `variantId` (uuid) Variante

### `POST /sales/:id/items/:itemId/gift`

Marcar/desmarcar item como brinde (cortesia)

Corpo: `isGift`* (boolean) Brinde

### `POST /sales/:id/reopen-draft`

Reabrir venda finalizada como rascunho (senha de administrador, usuário liberado ou pedido de aprovação)

Corpo: `username` (string) Usuário administrador (opcional: libera na hora); `password` (string) Senha do administrador (opcional); `justification` (string) Justificativa

Resposta: Sem credenciais: usuário liberado executa na hora; os demais recebem 202 { status: PENDING_APPROVAL } e o administrador responde em /aprovacoes ou pelo WhatsApp.

### `GET /sales/draft-customer-counts`

Quantidade de rascunhos abertos por cliente

Query: `allLocations` (boolean) Considera todas as filiais (padrão: só a filial do usuário)

Resposta: { counts } — mapa customerId para número de vendas em rascunho com cliente.

### `GET /sales/:id/order-html`

HTML do pedido (para impressão), servido como text/html

### `GET /sales/:id/nfe-operation`

Prévia da natureza da operação que a NF-e 55 da venda vai declarar

Query: `customerId` (uuid) Cliente a considerar no cálculo (omitido = o da venda; vazio = sem cliente)

Resposta: { operationType (SALE_INTERNAL ou SALE_INTERSTATE), originUf, destUf, customerUfMissing, contributor, cfops, nature }. Usa o mesmo motor tributário da finalização.

### `POST /sales/:id/duplicate-draft`

Copiar a venda para um novo rascunho (sem pagamentos), sem alterar a original

Resposta: 201 Created — { sale, copiedItems, skippedItems, skippedSellers, droppedCoupon }. Copia cliente, vendedores, lista de preço, canal, itens com preço e descontos, frete e observações. Não copia pagamentos, documentos fiscais, cupom nem fidelidade; itens de produtos inativos e vendedores inativos são ignorados. 400 se a venda não tem itens ou nenhum item pode ser copiado.

### `POST /sales/:id/gift-wraps`

Adicionar embalagem para presente à venda em rascunho (a um item ou geral)

Corpo: `saleItemId` (uuid) Item da venda; omitido = embalagem geral da venda; `optionId` (uuid) Modelo cadastrado em /gift-wraps; sem modelo, use customName e customUnitPrice; `customName` (string) Nome livre (até 80; padrão "Embalagem para presente"); `customUnitPrice` (number) Valor unitário livre. Também vale para sobrepor o valor do modelo; exige valor livre habilitado na loja e a permissão sales.gift_wrap_custom_price; `quantity` (integer) Quantidade de 1 a 999 (padrão 1)

Resposta: 201 Created — a venda recalculada. 400 se a venda não é rascunho, o recurso está desligado, o modelo está inativo ou o item não pertence à venda; 403 sem permissão para valor livre.

### `PATCH /sales/:id/gift-wraps/:giftWrapId`

Alterar embalagem da venda em rascunho

Corpo: `optionId` (uuid) Outro modelo cadastrado; `customName` (string) Nome livre (até 80); `customUnitPrice` (number) Valor unitário livre (mesmas regras de permissão do POST); `quantity` (integer) Quantidade de 1 a 999

Resposta: A venda recalculada. 404 se a embalagem não pertence à venda.

### `DELETE /sales/:id/gift-wraps/:giftWrapId`

Remover embalagem da venda em rascunho

Resposta: A venda recalculada. 404 se a embalagem não pertence à venda.

## PDV

### `GET /pos/search`

Busca rápida de produtos para PDV

Query: `q` (string) Termo de busca (descrição ou GTIN); `includeInactive` (boolean) Quando true, inclui produtos inativos (compras / vínculo XML)

### `GET /pos/payment-link/sellers`

Listar vendedores (link de pagamento)

### `PATCH /pos/payment-link/sellers/:id/phone`

Atualizar telefone do vendedor

### `GET /pos/payment-link/sales/:id`

Detalhe da venda para link

### `POST /pos/payment-link/sales/:id`

Gerar link de pagamento (PDV)

### `POST /pos/payment-link/sales/:id/whatsapp`

Enviar link de pagamento por WhatsApp

## Clientes

### `GET /customers`

Listar clientes

Query: `q` (string) Busca por nome, CPF/CNPJ, e-mail; `page` (number) Página (ativa paginação); `pageSize` (number) Itens por página (padrão 20); `sortBy` (string) Coluna: name | taxId | phone | email; `sortDir` (string) asc | desc

### `POST /customers`

Criar cliente

Corpo: `name`* (string) Nome (como no RG/CNH); `nickname` (string) Apelido (opcional); `cpf` (string) CPF (legado); `taxId` (string) CPF ou CNPJ (somente dígitos); `taxIdType` (string) CPF | CNPJ; `email` (string) E-mail; `phone` (string) Telefone; `birthDate` (string) Data nascimento (YYYY-MM-DD); `gender` (string) FEMALE | MALE | OTHER; `zipCode` (string) CEP; `addressLine1` (string) Endereço; `addressNumber` (string) Número; `addressLine2` (string) Complemento; `addressDistrict` (string) Bairro; `city` (string) Cidade; `state` (string) UF; `notes` (string) Observações; `creditLimit` (number) Limite de crédito (padrão 0); `priceListId` (uuid) Tabela de preços; `excludeFromCommission` (boolean) Excluir de comissão; `birthdayGreetingEnabled` (boolean) Receber saudação de aniversário (padrão true); `preferredGoodsDocumentType` (string) NFCE_65 | NFE_55 | null — modelo fiscal de mercadoria nas vendas deste cliente; `socialLinks` (array) [{ network: 'INSTAGRAM', handle, label? }]

### `PATCH /customers/:id`

Atualizar cliente

Corpo: `name` (string) Nome e demais campos do POST

### `PATCH /customers/:id/contact`

Completar contato do cliente (só os campos enviados)

Corpo: `phone` (string) Telefone com DDD; `email` (string) E-mail; `taxId` (string) CPF ou CNPJ

### `DELETE /customers/:id`

Excluir cliente

Resposta: 204 No Content

### `GET /customers/:id`

Detalhe do cliente

### `GET /customers/:id/merge-preview`

Prévia da mesclagem de clientes

### `POST /customers/merge`

Mesclar dois clientes (mantém keepId, absorve mergeId)

Corpo: `keepId`* (uuid) Cliente que permanece; `mergeId`* (uuid) Cliente absorvido

### `PATCH /customers/bulk/values`

Atualizar um campo com valores distintos por cliente

Corpo: `field`* (string) Campo da grade; `updates`* (array) [{ id, value }]

### `PATCH /customers/bulk/field`

Atualizar o mesmo valor de um campo em vários clientes

Corpo: `ids`* (uuid[]) IDs dos clientes; `field`* (string) Campo a atualizar; `value` (string) Novo valor

### `GET /customers/bulk/name-format/jobs`

Listar jobs de correção de nomes

### `DELETE /customers/bulk/name-format/jobs/:jobId`

Cancelar correção de nomes em andamento

### `POST /customers/bulk/name-format`

Enfileirar correção dos nomes existentes com a formatação configurada

## Atualizações de cadastro do cliente

Fila administrativa de alterações pedidas pelo cliente (portal/link). Preferências de comunicação e tokens de acesso.

### `GET /customer-profile-updates`

Listar solicitações de alteração

### `GET /customer-profile-updates/:id`

Detalhe da solicitação

### `POST /customer-profile-updates/:id/approve`

Aprovar alteração de cadastro

### `POST /customer-profile-updates/:id/reject`

Rejeitar alteração de cadastro

Corpo: `reason` (string) Motivo da rejeição

### `GET /customer-profile-updates/by-customer/:customerId/preferences`

Preferências de comunicação do cliente

### `PATCH /customer-profile-updates/by-customer/:customerId/preferences`

Atualizar preferências de comunicação

### `POST /customer-profile-updates/by-customer/:customerId/access-token`

Gerar token de acesso ao portal do cliente

Corpo: `purpose` (string) Finalidade do token; `expiresInDays` (integer) Validade em dias

Resposta: 201 Created

### `POST /customer-profile-updates/by-customer/:customerId/unsubscribe-token`

Gerar token de descadastro de notificações

Corpo: `preferenceKey` (string) Chave da preferência; `expiresInDays` (integer) Validade em dias

Resposta: 201 Created

## Ordens de serviço

OS do módulo de serviços. Status: QUOTE | APPROVED | IN_PROGRESS | WAITING_CUSTOMER | COMPLETED | INVOICED | CANCELLED. Concluir (COMPLETED) registra a execução; o faturamento comercial é via `POST /service-orders/:id/to-sale` (rascunho no PDV) e a OS só vira INVOICED ao finalizar essa venda (`POST /sales/:id/finalize`). Detalhe e lista incluem `saleId` / `sale` quando houver vínculo.

### `GET /service-orders`

Listar ordens de serviço

Query: `q` (string) Busca; `status` (string) Status da OS; `customerId` (uuid) Filtrar por cliente; `page` (integer) Página (padrão 1); `pageSize` (integer) Itens por página (máx. 100)

### `GET /service-orders/agenda`

Agenda de OS por responsável (dia ou período)

Query: `date` (date) Dia (YYYY-MM-DD); ou use from/to; `from` (date) Início do período (YYYY-MM-DD); `to` (date) Fim do período (YYYY-MM-DD); `responsibleId` (uuid) Filtrar por responsável; `includeUnscheduled` (boolean) Inclui OS abertas sem horário em `unscheduled`

### `POST /service-orders`

Criar ordem de serviço

Corpo: `customerId`* (uuid) Cliente; `contactName` (string) Nome do contato; `responsibleId` (uuid) Responsável; `projectId` (uuid) Projeto; `description` (string) Descrição; `notes` (string) Observações; `expectedAt` (string) Previsão (ISO datetime); `priority` (string) Prioridade; `assetId` (uuid) Ativo/equipamento; `checklistTemplateId` (uuid) Template de checklist

Resposta: 201 Created

### `GET /service-orders/:id`

Detalhe da OS

Resposta: Inclui itens, recebíveis, `hasIssuedNfse`, `nfseId`, `saleId` e `sale` ({ id, orderNumber, status, completedAt }) quando vinculada a uma venda do PDV.

### `PATCH /service-orders/:id`

Atualizar OS

Corpo: `customerId` (uuid) Cliente; `description` (string) Descrição; `notes` (string) Observações; `discountAmount` (number) Desconto; `expectedAt` (string) Previsão; `priority` (string) Prioridade

### `POST /service-orders/:id/items`

Adicionar item à OS

Corpo: `variantId`* (uuid) Variante do serviço/produto; `quantity`* (number) Quantidade; `unitPrice` (number) Preço unitário

Resposta: 201 Created

### `PATCH /service-orders/:id/items/:itemId`

Atualizar item da OS

Corpo: `quantity` (number) Quantidade; `unitPrice` (number) Preço unitário; `description` (string) Descrição do item

### `DELETE /service-orders/:id/items/:itemId`

Remover item da OS

### `POST /service-orders/:id/to-sale`

Criar ou reabrir rascunho de venda no PDV a partir da OS

Resposta: 201 se criou rascunho; 200 se já existia. Corpo: { saleId, orderNumber, created, serviceOrderId, serviceOrderNumber, total? }. Exige OS COMPLETED (ou INVOICED sem venda). Idempotente com rascunho DRAFT. 409 se já houver venda COMPLETED. Copia itens/preços; `affectsStock=false` se a OS já baixou estoque. NFS-e autorizada/pendente da OS é vinculada à venda (`saleId`). Abrir PDV em `/pos?id={saleId}`. Ao finalizar a venda, a OS vai para INVOICED.

### `POST /service-orders/:id/status`

Alterar status da OS

Corpo: `status`* (string) Próximo status permitido (transições operacionais). INVOICED não é aceito aqui — use to-sale + finalize da venda. QUOTE | APPROVED | IN_PROGRESS | WAITING_CUSTOMER | COMPLETED | CANCELLED conforme o fluxo.

### `POST /service-orders/:id/cancel`

Cancelar OS

Resposta: Não cancela OS INVOICED. Se houver rascunho DRAFT ligado, o vínculo `saleId` é liberado.

### `POST /service-orders/:id/request-confirmation`

Pedir confirmação de presença ao cliente (WhatsApp)

### `POST /service-orders/:id/confirm-attendance`

Marcar presença confirmada

### `POST /service-orders/:id/send-quote-whatsapp`

Enviar orçamento da OS por WhatsApp

### `POST /service-orders/:id/warranty`

Abrir OS de garantia vinculada

Resposta: 201 Created

### `POST /service-orders/:id/attachments`

Anexar arquivo à OS (multipart, campo file)

### `GET /service-orders/:id/pdf`

PDF do orçamento/OS (application/pdf, inline)

Resposta: 404 se a OS não existe; 403 se a OS é de outra filial ou de outro mecânico (perfil mecânico).

### `POST /service-orders/:id/approve-quote`

Oficina: registrar a aprovação do orçamento pela equipe (cliente aprovou por telefone ou balcão)

Corpo: `approvedByName` (string) Quem aprovou (até 200; padrão: nome do cliente da OS)

Resposta: A OS atualizada. Idempotente: se já estava aprovada no valor atual, devolve a OS sem mudança. 400 se a OS está cancelada, sem itens, sem veículo (placa) ou KM de entrada no orçamento, ou não está numa fase que aceite aprovação. Exige o módulo de oficina.

### `POST /service-orders/:id/deliver`

Oficina: registrar a retirada do veículo (quem e quando); a garantia passa a contar da entrega

Corpo: `pickedUpByName`* (string) Quem está retirando o veículo (até 200)

Resposta: A OS atualizada. 400 se a OS não está concluída ou faturada ou se já foi entregue. Exige o módulo de oficina.

### `GET /service-orders/:id/events`

Trilha de auditoria da OS (últimos 500 eventos, do mais recente ao mais antigo)

Resposta: Lista de { id, type, message, userName, createdAt }. 404 se a OS não existe.

## Serviços (configuração)

Liga/desliga o módulo de serviços e os submódulos (campo, projetos, NFS-e, etc.).

### `GET /service-settings`

Obter configurações de serviços

### `PUT /service-settings`

Atualizar configurações de serviços

Corpo: `servicesEnabled` (boolean) Liga o módulo de serviços na loja; `useProjects` (boolean) Projetos; `useContracts` (boolean) Contratos; `useSubscriptions` (boolean) Assinaturas; `useTimeTracking` (boolean) Apontamento de horas; `useFieldService` (boolean) Atendimento em campo; `autoGenerateReceivable` (boolean) Gerar conta a receber ao concluir a OS. Se a OS for faturada no PDV depois, recebíveis abertos da OS são cancelados na finalização da venda.; `autoGenerateNfse` (boolean) Emitir NFS-e ao concluir a OS. Preferível faturar pelo PDV (to-sale + nfseIntent na finalize); se a nota já existir na OS, o finalize da venda não reemite.; `nfseEnvironment` (string) HOMOLOG | PROD; `nfseSeries` (string) Série numérica da NFS-e

### `POST /service-settings/apply-aesthetic-clinic-preset`

Aplicar modelo pronto para clínica de estética

### `POST /service-settings/apply-mechanic-workshop-preset`

Aplicar modelo pronto para oficina mecânica

### `GET /service-settings/assignable-users`

Usuários que podem receber visita, OS ou recorrência (só dados básicos, sem exigir permissão de usuários)

Resposta: Lista ordenada por nome de { id, name, username, agendaColor, active }. O usuário de suporte da plataforma não aparece.

## Projetos de serviço

### `GET /service-projects`

Listar projeto de serviço

### `POST /service-projects`

Criar projeto de serviço

Corpo: `name`* (string) Nome; `customerId`* (uuid) Cliente; `status` (string) Status do projeto; `contractedValue` (number) Valor contratado; `startDate` (string) Início; `endDate` (string) Fim; `plannedHours` (number) Horas planejadas; `notes` (string) Observações

Resposta: 201 Created

### `PATCH /service-projects/:id`

Atualizar projeto de serviço

Corpo: `name`* (string) Nome; `customerId`* (uuid) Cliente; `status` (string) Status do projeto; `contractedValue` (number) Valor contratado; `startDate` (string) Início; `endDate` (string) Fim; `plannedHours` (number) Horas planejadas; `notes` (string) Observações

### `DELETE /service-projects/:id`

Excluir projeto de serviço

Resposta: 204 No Content

### `GET /service-projects/:id`

Detalhe do projeto

### `PUT /service-projects/:id/members`

Substituir membros do projeto

Corpo: `userIds`* (array) IDs de usuários (máx. 50)

## Contratos de serviço

### `GET /service-contracts`

Listar contrato de serviço

### `POST /service-contracts`

Criar contrato de serviço

Corpo: `customerId`* (uuid) Cliente; `title`* (string) Título; `contractType` (string) Tipo; `value` (number) Valor; `period` (string) Periodicidade de faturamento; `startsAt`* (string) Início da vigência; `endsAt` (string) Fim da vigência; `autoRenew` (boolean) Renovação automática; `status` (string) Status; `notes` (string) Observações

Resposta: 201 Created

### `PATCH /service-contracts/:id`

Atualizar contrato de serviço

Corpo: `customerId`* (uuid) Cliente; `title`* (string) Título; `contractType` (string) Tipo; `value` (number) Valor; `period` (string) Periodicidade de faturamento; `startsAt`* (string) Início da vigência; `endsAt` (string) Fim da vigência; `autoRenew` (boolean) Renovação automática; `status` (string) Status; `notes` (string) Observações

### `DELETE /service-contracts/:id`

Excluir contrato de serviço

Resposta: 204 No Content

### `GET /service-contracts/:id`

Detalhe do contrato

## Assinaturas de serviço

### `GET /service-subscriptions`

Listar assinatura de serviço

### `POST /service-subscriptions`

Criar assinatura de serviço

Corpo: `customerId`* (uuid) Cliente; `contractId` (uuid) Contrato vinculado; `name`* (string) Nome; `description` (string) Descrição; `amount`* (number) Valor recorrente; `period` (string) Periodicidade; `nextDueDate`* (string) Próximo vencimento; `paymentMethodId` (uuid) Forma de pagamento; `status` (string) Status

Resposta: 201 Created

### `PATCH /service-subscriptions/:id`

Atualizar assinatura de serviço

Corpo: `customerId`* (uuid) Cliente; `contractId` (uuid) Contrato vinculado; `name`* (string) Nome; `description` (string) Descrição; `amount`* (number) Valor recorrente; `period` (string) Periodicidade; `nextDueDate`* (string) Próximo vencimento; `paymentMethodId` (uuid) Forma de pagamento; `status` (string) Status

### `DELETE /service-subscriptions/:id`

Excluir assinatura de serviço

Resposta: 204 No Content

### `GET /service-subscriptions/:id`

Detalhe da assinatura

### `POST /service-subscriptions/generate-receivables`

Gerar contas a receber vencidas das assinaturas

## Apontamento de horas

### `GET /time-entries`

Listar apontamentos

Query: `userId` (uuid) Colaborador; `projectId` (uuid) Projeto; `serviceOrderId` (uuid) OS; `from` (string) Data inicial; `to` (string) Data final; `billable` (string) true | false

### `GET /time-entries/report`

Relatório de horas

Query: `from` (string) Data inicial; `to` (string) Data final; `customerId` (uuid) Cliente; `userId` (uuid) Colaborador; `projectId` (uuid) Projeto

### `GET /time-entries/:id`

Detalhe do apontamento

### `POST /time-entries`

Criar apontamento

Corpo: `userId`* (uuid) Colaborador; `projectId` (uuid) Projeto; `serviceOrderId` (uuid) OS; `workDate`* (string) Data do trabalho; `startTime` (string) Início (ISO); `endTime` (string) Fim (ISO); `totalMinutes` (integer) Duração em minutos (se não informar intervalo); `description` (string) Descrição; `billable` (boolean) Faturável

Resposta: 201 Created

### `PATCH /time-entries/:id`

Atualizar apontamento

### `DELETE /time-entries/:id`

Excluir apontamento

Resposta: 204 No Content

## Visitas de campo

Agenda, check-in/out, roteirização e cobrança em atendimento externo.

### `GET /field/visits`

Listar visitas

### `POST /field/visits`

Criar visita

Corpo: `serviceOrderId`* (uuid) OS; `technicianId`* (uuid) Técnico; `scheduledStart` (string) Início agendado; `scheduledEnd` (string) Fim agendado; `notes` (string) Observações

Resposta: 201 Created

### `GET /field/visits/:id`

Detalhe da visita

### `PATCH /field/visits/:id`

Atualizar visita

### `POST /field/visits/:id/check-in`

Check-in no local

Corpo: `lat`* (number) Latitude; `lng`* (number) Longitude

### `POST /field/visits/:id/check-out`

Check-out

Corpo: `lat`* (number) Latitude; `lng`* (number) Longitude

### `POST /field/visits/:id/en-route`

Marcar em deslocamento

### `POST /field/visits/:id/complete`

Concluir visita

### `POST /field/visits/:id/cancel`

Cancelar visita

### `POST /field/visits/:id/attachments`

Anexar arquivo (multipart)

Corpo: `file`* (file) Arquivo (campo form-data file)

### `POST /field/visits/:id/signature`

Salvar assinatura do cliente

Corpo: `pngBase64` (string) PNG em base64; `imageBase64` (string) Imagem em base64 (alternativa); `signerName` (string) Nome de quem assinou

### `POST /field/visits/:id/checklist`

Salvar respostas do checklist da visita

Corpo: `responses`* (array) [{ itemId, valueText?, valueNumber?, valueBool?, attachmentUrl? }]

### `POST /field/visits/:id/charge`

Gerar cobrança vinculada à visita

### `GET /field/visits/route-suggest`

Sugerir ordem de visitas no dia

### `POST /field/visits/route-apply`

Aplicar ordem sugerida de visitas

Corpo: `date`* (string) YYYY-MM-DD; `visitIds`* (array) IDs na ordem desejada

### `GET /field/visits/nearest-technician`

Técnico mais próximo

Query: `lat` (number) Latitude; `lng` (number) Longitude; `maxAgeMinutes` (integer) Idade máxima do ping (padrão 120)

## Localização de técnicos

### `POST /field/location/ping`

Registrar posição do técnico autenticado

Corpo: `lat`* (number) Latitude; `lng`* (number) Longitude; `accuracy` (number) Precisão em metros

Resposta: 201 Created

### `GET /field/location/latest`

Últimas posições dos técnicos

### `GET /field/location/pings`

Histórico de pings do dia

Query: `date` (string) YYYY-MM-DD; `userId` (uuid) Técnico (padrão: usuário autenticado)

## Checklists de serviço

### `GET /service-checklists`

Listar templates de checklist

### `GET /service-checklists/responses`

Listar respostas preenchidas

### `GET /service-checklists/:id`

Detalhe do template

### `POST /service-checklists`

Criar template

Corpo: `name`* (string) Nome; `description` (string) Descrição; `active` (boolean) Ativo; `productId` (uuid) Produto vinculado

Resposta: 201 Created

### `PATCH /service-checklists/:id`

Atualizar template

### `DELETE /service-checklists/:id`

Excluir template

Resposta: 204 No Content

### `POST /service-checklists/:id/items`

Adicionar item ao template

Corpo: `label`* (string) Rótulo; `itemType` (string) TEXT | YES_NO | NUMBER | PHOTO | SELECT; `required` (boolean) Obrigatório; `optionsJson` (array) Opções (SELECT); `position` (integer) Ordem

Resposta: 201 Created

### `PATCH /service-checklists/:id/items/:itemId`

Atualizar item

### `DELETE /service-checklists/:id/items/:itemId`

Excluir item

Resposta: 204 No Content

## Ativos / equipamentos

### `GET /service-assets`

Listar ativo de serviço

### `POST /service-assets`

Criar ativo de serviço

Corpo: `customerId`* (uuid) Cliente; `name`* (string) Nome do equipamento; `serialNumber` (string) Número de série; `model` (string) Modelo; `brand` (string) Marca; `addressLine` (string) Endereço de instalação; `city` (string) Cidade; `state` (string) UF; `notes` (string) Observações; `active` (boolean) Ativo

Resposta: 201 Created

### `PATCH /service-assets/:id`

Atualizar ativo de serviço

Corpo: `customerId`* (uuid) Cliente; `name`* (string) Nome do equipamento; `serialNumber` (string) Número de série; `model` (string) Modelo; `brand` (string) Marca; `addressLine` (string) Endereço de instalação; `city` (string) Cidade; `state` (string) UF; `notes` (string) Observações; `active` (boolean) Ativo

### `DELETE /service-assets/:id`

Excluir ativo de serviço

Resposta: 204 No Content

### `GET /service-assets/:id`

Detalhe do ativo

### `GET /service-assets/qr/:token`

Consultar ativo pelo token do QR

### `GET /service-assets/:id/orders`

Ordens de serviço do equipamento/veículo

### `PUT /service-assets/:id`

Atualizar equipamento/veículo (alias do PATCH)

## Recorrência de serviços

### `GET /service-recurrence`

Listar regra de recorrência

### `POST /service-recurrence`

Criar regra de recorrência

Corpo: `customerId`* (uuid) Cliente; `title`* (string) Título; `nextRunAt`* (string) Próxima execução; `period` (string) Periodicidade; `contractId` (uuid) Contrato; `subscriptionId` (uuid) Assinatura; `assetId` (uuid) Ativo; `checklistTemplateId` (uuid) Checklist; `technicianId` (uuid) Técnico; `status` (string) Status

Resposta: 201 Created

### `PATCH /service-recurrence/:id`

Atualizar regra de recorrência

Corpo: `customerId`* (uuid) Cliente; `title`* (string) Título; `nextRunAt`* (string) Próxima execução; `period` (string) Periodicidade; `contractId` (uuid) Contrato; `subscriptionId` (uuid) Assinatura; `assetId` (uuid) Ativo; `checklistTemplateId` (uuid) Checklist; `technicianId` (uuid) Técnico; `status` (string) Status

### `DELETE /service-recurrence/:id`

Excluir regra de recorrência

Resposta: 204 No Content

### `GET /service-recurrence/:id`

Detalhe da regra

### `POST /service-recurrence/run-due`

Executar regras vencidas agora

## Relatórios de serviços e campo

### `GET /service-reports/overview`

Visão geral de serviços no período

Query: `from` (string) Data inicial; `to` (string) Data final

### `GET /service-field/reports`

Snapshot do dia (indicadores de campo)

### `GET /service-field/reports/metrics`

Métricas de campo no período

Query: `from` (string) Data inicial; `to` (string) Data final

## Fidelidade

Programa de pontos. Requer entitlement loyalty e permissões loyalty.*

### `GET /loyalty/settings`

Configurações do programa

### `PUT /loyalty/settings`

Atualizar configurações do programa

### `GET /loyalty/accounts`

Listar contas de fidelidade

### `GET /loyalty/accounts/by-customer/:customerId`

Saldo/status da conta por cliente

### `GET /loyalty/accounts/:accountId/ledger`

Extrato de pontos

### `POST /loyalty/accounts/:accountId/status`

Ativar/bloquear conta

Corpo: `status`* (string) ACTIVE | BLOCKED

### `POST /loyalty/adjust`

Ajuste manual de pontos

Corpo: `customerId`* (uuid) Cliente; `points`* (integer) Quantidade (positiva); `direction`* (string) credit | debit; `reason`* (string) Motivo (mín. 5 caracteres)

### `POST /loyalty/preview`

Prévia de ganho/resgate em uma venda

Corpo: `customerId` (uuid) Cliente; `channel` (string) POS | CONVENIENCE; `priceBasis` (string) CASH | CREDIT | MIXED; `totalDue`* (number) Total devido; `redeemPoints` (integer) Pontos a resgatar

### `GET /loyalty/campaigns`

Listar campanhas de fidelidade

### `GET /loyalty/campaigns/:id`

Detalhe da campanha

### `POST /loyalty/campaigns`

Criar campanha

Corpo: `name`* (string) Nome; `type`* (string) MULTIPLIER | BONUS_FIXED | BONUS_PERCENT_BASE; `status` (string) DRAFT | ACTIVE | FINISHED | CANCELLED; `multiplier` (number) Multiplicador (tipo MULTIPLIER); `bonusPoints` (integer) Pontos bônus; `bonusPercent` (number) Percentual bônus

### `PATCH /loyalty/campaigns/:id`

Atualizar campanha

### `DELETE /loyalty/campaigns/:id`

Excluir campanha

### `GET /loyalty/report`

Relatório de fidelidade

## Campanhas promocionais

### `GET /promotional-campaigns`

Listar campanhas

### `GET /promotional-campaigns/settings`

Configurações de campanhas

### `PATCH /promotional-campaigns/settings`

Atualizar configurações

### `POST /promotional-campaigns`

Criar campanha

Corpo: `name`* (string) Nome; `type`* (string) LIVE | QUEIMA_ESTOQUE | BLACK_FRIDAY | FEIRAO | OUTLET | PROMOCAO | OUTRO; `description` (string) Descrição; `startsAt` (string) Início; `endsAt` (string) Fim; `commissionPercent` (number | null) Comissão do vendedor na campanha (%). Null = usa o cadastro do vendedor

### `GET /promotional-campaigns/:id`

Detalhe

### `GET /promotional-campaigns/:id/details`

Totais por cliente, faturamento e gráficos

Query: `from` (string) Data inicial (YYYY-MM-DD). Omite para toda a campanha; `to` (string) Data final (YYYY-MM-DD). Omite para toda a campanha

### `PATCH /promotional-campaigns/:id`

Atualizar

### `POST /promotional-campaigns/:id/activate`

Ativar

### `POST /promotional-campaigns/:id/finish`

Encerrar

### `POST /promotional-campaigns/:id/cancel`

Cancelar

### `DELETE /promotional-campaigns/:id`

Excluir

### `GET /promotional-campaigns/:id/prices`

Listar preços da campanha

### `GET /promotional-campaigns/:id/prices/:productId`

Preço de um produto

### `PUT /promotional-campaigns/:id/prices/:productId`

Definir preço de campanha do produto

Corpo: `price`* (number) Preço promocional

### `DELETE /promotional-campaigns/:id/prices/:productId`

Remover preço de campanha do produto

## Cupons

### `GET /coupons`

Listar cupons

### `GET /coupons/eligible/birthday`

Cupons de aniversário elegíveis

### `POST /coupons/eligible/code`

Validar código de cupom

Corpo: `code`* (string) Código do cupom

### `GET /coupons/:id`

Detalhe

### `POST /coupons`

Criar cupom

Corpo: `code`* (string) Código; `name`* (string) Nome; `kind`* (string) BIRTHDAY | GENERAL; `discountType`* (string) PERCENT | FIXED; `discountValue`* (number) Valor do desconto; `status` (string) ACTIVE | INACTIVE; `validFrom` (string) YYYY-MM-DD; `validUntil` (string) YYYY-MM-DD; `maxUsesTotal` (integer) Limite total de usos; `maxUsesPerCustomer` (integer) Limite por cliente

### `PATCH /coupons/:id`

Atualizar cupom

### `DELETE /coupons/:id`

Excluir cupom

## Promoções com brinde

Promoções do tipo valor mínimo com brinde (VALOR_MINIMO_COM_BRINDE): a venda que atinge o valor mínimo nas formas de pagamento escolhidas ganha os produtos brinde.

### `GET /promotions`

Listar promoções

Query: `q` (string) Busca por nome; `active` (boolean) true / false; `type` (string) VALOR_MINIMO_COM_BRINDE; `from` (string) Vigência a partir de (AAAA-MM-DD); `to` (string) Vigência até (AAAA-MM-DD)

### `GET /promotions/:id`

Detalhe da promoção

### `POST /promotions`

Criar promoção

Corpo: `name`* (string) Nome; `description` (string) Descrição; `minValue`* (number) Valor mínimo da venda; `valueCriterion`* (string) PRODUCT_GROSS, NET_AFTER_DISCOUNTS ou CASH_VALUE; `startsAt`* (string) Início (ISO ou AAAA-MM-DD); `endsAt` (string) Fim (opcional); `active` (boolean) Padrão true; `paymentMethodIds`* (string[]) Formas de pagamento válidas; `gifts`* (array) [{ productId, quantityPerSale, campaignLimit }]

Resposta: 201 Created

### `PATCH /promotions/:id`

Atualizar promoção (mesmo corpo da criação)

### `PATCH /promotions/:id/active`

Ativar ou pausar

Corpo: `active`* (boolean) Situação

### `DELETE /promotions/:id`

Excluir promoção

Resposta: 204 No Content

## Taxas de cartão

### `GET /card-fees`

Configuração de taxas

### `PUT /card-fees`

Atualizar configuração de taxas

### `POST /card-fees/accounts`

Criar conta/maquininha

### `PATCH /card-fees/accounts/:accountId`

Atualizar conta

### `POST /card-fees/accounts/:accountId/activate`

Ativar conta

### `POST /card-fees/accounts/:accountId/use-for-payment-link`

Usar conta no link de pagamento

### `DELETE /card-fees/accounts/:accountId`

Excluir conta

### `GET /card-fees/calc-jobs`

Listar jobs de recálculo

### `POST /card-fees/calc-jobs`

Enfileirar recálculo de taxas

### `DELETE /card-fees/calc-jobs/:jobId`

Cancelar job

### `PUT /card-fees/accounts/:accountId/brand-rates`

Substituir as taxas próprias de uma bandeira numa maquininha

Corpo: `cardBrandId`* (string) Bandeira; `rates`* (array) [{ cardType (debit ou credit), installments (1 a 12), feePercent (0 a 100, ou null para usar a taxa geral da maquininha) }]

Resposta: A configuração de taxas completa (mesmo retorno de GET /card-fees).

### `POST /card-fees/brands`

Cadastrar bandeira de cartão

Corpo: `name`* (string) Nome (até 60); `tBand` (string) Código tBand da NFC-e (1 a 2 dígitos); null para não informar

Resposta: 201 Created — a configuração de taxas completa.

### `PATCH /card-fees/brands/:brandId`

Atualizar bandeira de cartão

Corpo: `name` (string) Nome (até 60); `tBand` (string) Código tBand da NFC-e (1 a 2 dígitos); null remove; `active` (boolean) Ativa

Resposta: A configuração de taxas completa.

### `DELETE /card-fees/brands/:brandId`

Remover bandeira (se já foi usada em vendas, é apenas desativada)

Resposta: A configuração de taxas completa.

### `GET /card-fees/auto-adjust`

Configuração do reajuste automático de taxas por faixa de faturamento

Resposta: { enabled, lastMonth, lastAt, lastNote, tiers }. tiers lista as faixas (minGross e a maquininha/conta de cada uma).

### `PUT /card-fees/auto-adjust`

Salvar o reajuste automático: a maquininha ativa passa a ser escolhida pelo volume de cartão do mês anterior

Corpo: `enabled`* (boolean) Reajuste ligado (exige ao menos uma faixa); `tiers`* (array) Até 20 faixas: [{ minGross, accountId }] — minGross é o volume mínimo de cartão do mês anterior (R$); não pode haver duas faixas com o mesmo valor

Resposta: A configuração salva (mesmo formato de GET). 400 para faixas duplicadas ou ligado sem faixas; 404 se alguma maquininha não existe.

### `POST /card-fees/auto-adjust/run`

Aplicar agora a regra do mês corrente, trocando a maquininha ativa conforme o volume do mês anterior

Resposta: { result, payload } — result.status indica o que ocorreu (por exemplo applied, com accountName) e payload é a configuração de taxas completa. 409 se o reajuste está desligado ou sem faixas.

### `POST /card-fees/recalc-month/preview`

Simular o recálculo das taxas de um mês com a maquininha vigente, sem gravar nada

Corpo: `month`* (string) Mês no formato AAAA-MM

Resposta: { month, dryRun, activeAccountName, totalMatched, updated, skipped, failed, grossTotal, feeBefore, feeAfter }. Para aplicar de verdade, enfileire com POST /card-fees/calc-jobs enviando { month }.

## Webhooks de saída (fiscal)

A Nive chama a sua URL quando uma nota muda de status (autorizada, rejeitada, cancelada, em contingência). Cada entrega é um POST JSON com os headers X-Nive-Event, X-Nive-Delivery, X-Nive-Timestamp e X-Nive-Signature (v1=HMAC-SHA256 de "{timestamp}.{corpo}" com o segredo whsec_). Responda 2xx para confirmar; falhas são reenviadas em 1min, 5min, 15min, 1h, 3h, 6h, 12h e 24h. Eventos: fiscal.document.authorized, .rejected, .cancelled, .contingency e .failed. Exige a permissão fiscal na chave.

### `GET /outbound-webhooks`

Listar destinos

### `POST /outbound-webhooks`

Criar destino

Corpo: `url`* (string) URL https que receberá os eventos; `description` (string) Descrição (até 200)

Resposta: 201 Created. Retorna o segredo (whsec_...) uma única vez

### `PATCH /outbound-webhooks/:id`

Alterar URL, descrição ou ativar/desativar

Corpo: `url` (string) Nova URL https; `description` (string) Descrição; `active` (boolean) Ativa ou pausa o envio

### `POST /outbound-webhooks/:id/rotate-secret`

Gerar novo segredo (mostrado uma vez)

### `POST /outbound-webhooks/:id/test`

Enviar evento webhook.test para validar URL e assinatura

### `GET /outbound-webhooks/:id/deliveries`

Últimas 50 entregas e seus status

### `POST /outbound-webhooks/:id/deliveries/:deliveryId/retry`

Reenviar entrega que esgotou as tentativas

### `DELETE /outbound-webhooks/:id`

Remover destino

## Chaves de API

Somente sessão de usuário (JWT). API key não autentica estas rotas.

### `GET /api-keys`

Listar chaves

### `POST /api-keys`

Criar chave

Corpo: `name`* (string) Nome da chave (até 100); `scope` (string) integration (padrão), read ou inherit; `permissions` (string[]) Lista personalizada de permissões (substitui o escopo)

Resposta: 201 Created. Retorna o token completo uma única vez

### `DELETE /api-keys/:id`

Revogar chave

## Logs de atividade

Somente sessão de usuário (JWT). API key não autentica estas rotas.

### `GET /activity-logs`

Listar logs de auditoria da loja

Query: `page` (integer) Página; `limit` (integer) Itens por página; `userId` (uuid) Usuário; `action` (string) Ação (ex.: SETTINGS_UPDATE); `entityType` (string) Tipo de entidade; `search` (string) Busca no resumo; `from` (string) Data inicial; `to` (string) Data final

## Linha do tempo

### `GET /store-timeline`

Acontecimentos da loja em ordem cronológica (pedido, condicional, entrada, pagamento, sangria e demais movimentos)

Query: `cursor` (string) Cursor da próxima página (rolagem infinita); `limit` (integer) Itens por página (máx. 50, padrão 30); `types` (string) Tipos separados por vírgula (ex.: SALE_COMPLETED,CASH_WITHDRAWAL); `from` (string) Data inicial; `to` (string) Data final

Resposta: Retorna { items, nextCursor }. nextCursor nulo indica o fim da lista.

### `GET /store-timeline/summary`

Totais de entradas, saídas e quantidade de acontecimentos do período filtrado

Query: `types` (string) Tipos separados por vírgula; só valem os tipos que a permissão do usuário permite; `from` (string) Data inicial (AAAA-MM-DD ou data/hora ISO); `to` (string) Data final (AAAA-MM-DD ou data/hora ISO); `q` (string) Busca por texto (até 120); `allLocations` (boolean) Considera todas as filiais (padrão: só a filial do usuário)

Resposta: { inflow, outflow, count } — mesmos filtros de GET /store-timeline.

## Cofre de senhas e arquivos

Senhas e arquivos da loja com acesso por usuário. O escopo "Somente leitura" das chaves de API não inclui o cofre; use escopo personalizado ou "Igual ao meu usuário" se a integração realmente precisar. Cada item só é visível para quem o criou e para os usuários da lista de acesso.

### `GET /vault/users`

Usuários que podem receber acesso a itens

### `GET /vault/items`

Listar itens visíveis (sem o segredo)

Query: `q` (string) Busca; `kind` (string) PASSWORD ou FILE

### `GET /vault/items/:id`

Detalhe do item (sem o segredo)

### `POST /vault/items`

Criar senha

Corpo: `title`* (string) Nome; `secret`* (string) Senha (guardada criptografada); `username` (string) Usuário/login; `url` (string) Endereço; `notes` (string) Observações; `access` (array) [{ userId, canEdit }]

Resposta: 201 Created. Exige permissão de gerenciar o cofre.

### `POST /vault/files`

Enviar arquivo (multipart/form-data, campo file, até 10 MB)

Corpo: `file`* (file) Arquivo; `title` (string) Nome; `notes` (string) Observações; `access` (string) JSON [{ userId, canEdit }]

Resposta: 201 Created. Exige permissão de gerenciar o cofre.

### `POST /vault/items/:id/reveal`

Revelar a senha — retorna { secret } e registra no log de atividade

### `GET /vault/items/:id/file`

Baixar o arquivo (sempre como download)

### `PUT /vault/items/:id/file`

Substituir o arquivo (multipart, campo file)

### `PATCH /vault/items/:id`

Atualizar item (título, usuário, URL, senha, observações, acesso)

### `DELETE /vault/items/:id`

Excluir item

Resposta: 204 No Content

## Contas a receber

### `GET /receivables/summary`

Resumo (em aberto, vencido, recebido)

Query: `from` (string) ISO date — filtro de baixas; `to` (string) ISO date

### `GET /receivables/installments/summary`

Resumo das parcelas (saldo, vencidas, quantidades)

### `GET /receivables/installments`

Listar todas as parcelas (todos os clientes)

Query: `page` (number) Página (padrão 1); `pageSize` (number) Itens por página (padrão 20); `q` (string) Cliente, CPF, pedido ou nº da parcela; `status` (string) OPEN | PARTIAL | PAID; `customerId` (string) UUID do cliente; `overdue` (boolean) true — só vencidas; `from` (string) ISO date — vencimento inicial; `to` (string) ISO date — vencimento final; `sortBy` (string) customer | origin | sequence | amount | balance | dueDate | status; `sortDir` (string) asc | desc (padrão asc)

### `GET /receivables`

Listar contas

Query: `page` (number) Página (padrão 1); `pageSize` (number) Itens por página (padrão 20); `q` (string) Cliente, pedido ou descrição; `status` (string) OPEN | PARTIAL | PAID | CANCELLED; `customerId` (string) UUID do cliente; `paymentMethodId` (string) Forma de pagamento da venda de origem (ex.: boleto, crediário); `overdue` (boolean) true — só vencidas; `excludePaid` (boolean) true — oculta contas quitadas; `excludeCancelled` (boolean) true — oculta contas canceladas; `sortBy` (string) customer | origin | originalAmount | balance | dueDate | status; `sortDir` (string) asc | desc (padrão asc)

### `GET /receivables/:id`

Detalhe com parcelas e baixas

### `POST /receivables`

Cadastro manual

Corpo: `customerId` (string) UUID do cliente; `originalAmount`* (number) Valor da dívida; `dueDate` (string) Data de vencimento; `installmentCount` (number) 1–48 parcelas; `description` (string) Observação opcional

### `POST /receivables/historical-sales`

Importar venda antiga sem movimentar estoque, caixa ou fiscal

Corpo: `sourceSystem`* (string) Sistema de origem; `externalId`* (string) Número da nota antiga; `saleDate`* (string) Data histórica da venda; `customerId`* (string) UUID do cliente; `items`* (array) Variantes, quantidades e preços históricos; `totalAmount`* (number) Total declarado da nota; `paidAmount` (number) Valor já recebido; `firstDueDate`* (string) Primeiro vencimento

### `PATCH /receivables/:id`

Atualizar conta e parcelamento (não quitada/cancelada)

Corpo: `customerId` (string) UUID do cliente; `description` (string) Observação; `installmentCount` (number) Regerar parcelas (sem baixas); `firstDueDate` (string) 1º vencimento ao regerar; `installments` (array) Lista { id?, sequence, amount, dueDate } — soma = valor original

### `POST /receivables/:id/payments`

Registrar baixa

Corpo: `amount`* (number) Valor recebido; `installmentId` (string) Parcela específica; `paymentMethodId`* (string) Forma de recebimento; `notes` (string) Observação opcional

### `PATCH /receivables/:id/payments/:paymentId`

Editar baixa e recalcular saldos

Corpo: `amount`* (number) Valor recebido; `paidAt` (string) Data/hora da baixa (ISO); `installmentId` (string) Parcela específica (null = ordem de vencimento); `paymentMethodId`* (string) Forma de recebimento; `discountAmount` (number) Desconto concedido; `lateFeeAmount` (number) Multa cobrada; `interestAmount` (number) Juros cobrados; `notes` (string) Observação opcional

### `POST /receivables/:id/cancel`

Cancelar conta — baixa por perda (zera saldo sem entrada no caixa; mantém baixas)

Corpo: `reason`* (string) Motivo (mín. 10 caracteres)

### `POST /receivables/:id/reopen`

Reverter cancelamento (somente administrador; saldo recalculado pelas baixas)

Corpo: `reason`* (string) Motivo (mín. 10 caracteres)

### `DELETE /receivables/:id/payments/:paymentId`

Excluir baixa e recalcular saldos

### `DELETE /receivables/:id`

Excluir conta, parcelas e baixas

### `POST /receivables/historical-sales/parse-text`

Interpretar venda histórica (texto/IA)

### `POST /receivables/historical-sales/parse-image`

Interpretar venda histórica (imagem/IA)

### `GET /receivables/payments`

Obter pagamentos / baixas

### `POST /receivables/customers/:customerId/card-payments`

Criar pagamento com cartão

### `POST /receivables/customers/:customerId/payments`

Registrar pagamento / baixa

### `DELETE /receivables/customers/:customerId/settlements/:settlementId`

Excluir baixa do cliente (lote) e recalcular saldos

### `GET /receivables/customers/:customerId/credit-status`

Obter status de crédito

### `GET /receivables/customers/:customerId/statement`

Listar / obter extrato

### `POST /receivables/customers/:customerId/statement/notify`

Notificar cliente

### `POST /receivables/renegotiate`

Renegociar parcelas

### `POST /receivables/:id/notify`

Notificar cliente

### `GET /receivables/:id/charges`

Obter cobranças

### `POST /receivables/:id/card-payments`

Criar pagamento com cartão

### `GET /receivables/customers/:customerId/charges`

Prévia de encargos (multa/juros) para baixa do cliente

Query: `amount` (number) Valor principal; `paidAt` (string) Data da baixa (ISO)

### `GET /receivables/collection-desk`

Mesa de cobrança: clientes com parcelas vencidas

Query: `page` (integer) Página; `pageSize` (integer) Itens por página (até 100)

### `POST /receivables/collection-desk/notify-batch`

Enviar lembrete de cobrança em lote

Corpo: `receivableIds`* (uuid[]) Até 50 contas

## Cheques

### `GET /cheques/summary`

Resumo dos cheques a depositar

### `GET /cheques`

Listar cheques

Query: `q` (string) Emitente, banco ou número; `status` (string) Situação do cheque; `page` (number) Página; `pageSize` (number) Itens por página

### `POST /cheques`

Cadastrar cheque e calcular o próximo dia útil

Corpo: `issuerName`* (string) Emitente; `bankName`* (string) Banco; `chequeNumber`* (string) Número do cheque; `amount`* (number) Valor; `receivedDate`* (string) Data de recebimento (AAAA-MM-DD); `depositDate`* (string) Data prevista (AAAA-MM-DD)

### `PATCH /cheques/:id`

Atualizar dados do cheque

### `PATCH /cheques/:id/status`

Atualizar situação operacional

### `DELETE /cheques/:id`

Excluir cheque

### `GET /cheques/:id`

Detalhe do cheque

## Vendedores

### `GET /sellers`

Listar vendedores

Query: `q` (string) Busca

### `POST /sellers`

Criar vendedor

Corpo: `name`* (string) Nome; `employeeCode` (string) Código do funcionário; `commissionPercent` (number) Comissão %; `active` (boolean) Ativo

### `PATCH /sellers/:id`

Atualizar vendedor

Corpo: `name` (string) Nome e demais campos

### `DELETE /sellers/:id`

Excluir vendedor

Resposta: 204 No Content

## Fornecedores

### `GET /suppliers`

Listar fornecedores

Query: `q` (string) Busca

### `GET /suppliers/:id`

Detalhe do fornecedor

### `POST /suppliers`

Criar fornecedor

Corpo: `name`* (string) Razão social / nome; `taxId` (string) CNPJ; `email` (string) E-mail; `phone` (string) Telefone

### `PATCH /suppliers/:id`

Atualizar fornecedor

Corpo: `name` (string) Campos do POST

### `DELETE /suppliers/:id`

Excluir fornecedor

Resposta: 204 No Content

### `PUT /suppliers/:id/default-brand`

Definir ou remover a marca padrão do fornecedor, sem alterar o resto do cadastro

Corpo: `brandId`* (string) Marca padrão; null desvincula

Resposta: O fornecedor atualizado. 404 se o fornecedor não existe; 400 se a marca não existe.

## Transportadoras

### `GET /carriers`

Listar transportadoras

Query: `q` (string) Busca por nome ou CNPJ; `active` (boolean) Somente ativas

### `GET /carriers/:id`

Detalhe da transportadora

### `POST /carriers`

Criar transportadora

Corpo: `name`* (string) Razão social / nome; `taxId` (string) CNPJ ou CPF

### `PATCH /carriers/:id`

Atualizar transportadora

Corpo: `name` (string) Campos do POST

### `DELETE /carriers/:id`

Excluir transportadora

Resposta: 204 No Content

## Canais de venda

Origem da venda (loja física, WhatsApp, Instagram, marketplace…), usada em relatórios.

### `GET /sales-channels`

Listar canais

Query: `active` (boolean) Somente ativos (true/1)

### `POST /sales-channels`

Criar canal

Corpo: `name`* (string) Nome; `active` (boolean) Padrão true; `position` (integer) Ordem de exibição

Resposta: 201 Created

### `PATCH /sales-channels/:id`

Atualizar canal (campos parciais)

### `DELETE /sales-channels/:id`

Excluir canal

Resposta: 204 No Content

## Compras

### `GET /purchases`

Listar compras

### `GET /purchases/:id`

Detalhe da compra

### `POST /purchases`

Criar compra em rascunho

Corpo: `supplierId` (uuid) Fornecedor; `invoiceNumber` (string) Número da NF-e de entrada; `invoiceSeries` (string) Série da NF-e de entrada; `accessKey` (string) Chave de acesso (44 dígitos) da NF-e de entrada — necessária para gerar NF-e de devolução ao fornecedor; `notes` (string) Observações; `freightValue` (number) Frete da nota (rateado no custo de entrada); `insuranceValue` (number) Seguro da nota (rateado no custo de entrada); `otherExpenses` (number) Outras despesas da nota (rateadas no custo de entrada); `ipiValue` (number) IPI da nota (entra no custo conforme costIncludesIpi da loja)

### `POST /purchases/import-xml`

Importar NF-e de compra via XML

Corpo: `xml`* (string) Conteúdo XML da NF-e

Resposta: Quantidades fracionadas (kg, L) são preservadas, o rastro (nLote/qLote/dFab/dVal) vira os lotes das linhas e vFrete/vSeg/vOutro/vIPI preenchem as despesas da compra. 409 Conflict quando a nota já existe — pela chave de acesso ou, em XML sem chave, por fornecedor + número + série. Traz code PURCHASE_XML_DUPLICATE, purchaseId e purchaseReference da compra existente

### `POST /purchases/import-xml-by-key/lookup`

Consultar NF-e de entrada na SEFAZ pela chave (prévia, sem criar compra)

Corpo: `accessKey`* (string) Chave de acesso (44 dígitos, DV válido, modelo 55). Consulta NFeDistribuicaoDFe/consChNFe no Ambiente Nacional com o certificado A1 da empresa logada

Resposta: status FULL traz preview (fornecedor, itens, impostos e totais); status SUMMARY traz o resumo (resNFe) e a Ciência já registrada, se houver. Resultados ficam em cache por empresa/chave; não há novas tentativas automáticas. Erros trazem code (INBOUND_NFE_*), retryAt e manualXmlFallback: 404 não localizada; 403 sem permissão; 410 cancelada, denegada ou fora do prazo; 429 consumo indevido (656, bloqueio de 1 h) ou limite local; 409 PURCHASE_XML_DUPLICATE se já importada

### `POST /purchases/import-xml-by-key/ciencia`

Registrar Ciência da Operação (evento 210210) para NF-e que só retornou resumo

Corpo: `accessKey`* (string) Chave de acesso consultada antes; `confirm`* (boolean) Deve ser true — confirmação expressa do usuário. O evento nunca é enviado automaticamente

Resposta: A Ciência não substitui a manifestação conclusiva (Confirmação, Desconhecimento ou Operação não Realizada). Idempotente: se já registrada, não reenvia. nextQueryAllowedAt indica quando consultar de novo para obter o XML completo

### `GET /purchases/inbound-nfe`

Listar NF-e de entrada consultadas pela chave, com situação da manifestação e prazo

Query: `page` (number) Página (padrão 1); `pageSize` (number) Itens por página (1 a 50, padrão 20); `manifest` (string) ALL | PENDING (sem conclusiva) | CIENCIA (ciência sem conclusiva) | CONFIRMATION | UNKNOWN | NOT_PERFORMED; `deadline` (string) ALL | VENCENDO (30 dias ou menos) | VENCIDO; `purchase` (string) ALL | TO_IMPORT (XML completo guardado e ainda sem compra) | IMPORTED (já virou compra); `search` (string) Emitente (resumo) ou trecho da chave de acesso

Resposta: Só lê o banco (sem chamar a SEFAZ); exige purchases.manage. Cada item traz manifest (null enquanto não houver conclusiva), ciencia, deadline { level NO_PRAZO | VENCENDO | VENCIDO | SEM_DATA, deadline, daysRemaining, windowDays } e canCiencia/canManifest/canImport (canImport = XML completo guardado e sem compra: dá para importar direto). O prazo parte da emissão da NF-e (padrão 180 dias, configurável em FISCAL_MANIFESTACAO_PRAZO_DIAS) e é só um aviso — confirmar o prazo legal da UF com o contador. counts resume pendentes, vencendo, vencidas e prontas para importar (toImport)

### `GET /purchases/inbound-nfe/sync-status`

Estado da busca em lote na SEFAZ: último NSU, espera liberada, bloqueio e busca em andamento

Resposta: Só lê o banco e valida o certificado (não chama a SEFAZ); exige purchases.manage. ready=false traz reason (CNPJ/certificado A1 ausente ou vencido). nextSyncAllowedAt é a espera de 1 h que a SEFAZ exige depois de 'sem documentos novos'; blockedUntil aparece após consumo indevido (656); hasMore indica notas ainda não trazidas (use a busca de novo para continuar)

### `POST /purchases/inbound-nfe/sync`

Buscar na SEFAZ todas as NF-e emitidas contra o CNPJ da empresa (NFeDistribuicaoDFe / distNSU)

Resposta: Só por ação do usuário, com o A1 da empresa no Ambiente Nacional; exige purchases.manage. Continua do último NSU guardado (cursor por CNPJ/ambiente) e lê até 10 páginas ou 40 s por chamada: outcome UP_TO_DATE (tudo em dia; nova busca só após 1 h), HAS_MORE (chame de novo para continuar) ou COOLDOWN (dentro da espera, nada foi consultado). Resumos (resNFe) e XMLs (procNFe) entram na lista de NF-e de entrada; canceladas/denegadas e notas emitidas pela própria empresa não aparecem; nada vira compra sem importar. Falha no meio devolve o que já foi salvo com partialError. 429 em consumo indevido (656), que bloqueia o CNPJ por 1 h; 409 se já houver busca em andamento. Auditado em FiscalAuditLog (INBOUND_NFE_DISTDFE_SYNC)

### `POST /purchases/inbound-nfe/manifest`

Manifestação conclusiva do destinatário: Confirmação (210200), Desconhecimento (210220) ou Operação não Realizada (210240)

Corpo: `accessKey`* (string) Chave de acesso da NF-e (44 dígitos); `kind`* (string) CONFIRMATION | UNKNOWN | NOT_PERFORMED; `justification` (string) Obrigatória em NOT_PERFORMED: 15 a 255 caracteres (alfabeto latino, sem emojis); `confirm`* (boolean) Deve ser true — confirmação expressa. O evento é fiscal e irreversível, e nunca é enviado automaticamente

Resposta: Não exige Ciência prévia, mas só existe UMA conclusiva por chave: se já registrada, devolve o estado atual (alreadyRegistered: true; differentFromRequested: true quando o tipo pedido era outro) sem reenviar à SEFAZ. Eventos no Ambiente Nacional (cOrgao 91) com o A1 da empresa; 429 em consumo indevido (656), 400 em rejeição da SEFAZ. Auditado em FiscalAuditLog (INBOUND_NFE_MANIFEST)

### `POST /purchases/import-xml-by-key`

Importar como compra a NF-e de entrada obtida pela chave de acesso

Corpo: `accessKey`* (string) Chave de acesso (44 dígitos) da NF-e; `fromPreview` (boolean) true: importa somente o XML já conferido em /lookup (fluxo da tela). false/omitido: consulta respeitando cache e limites e importa se vier o XML completo

Resposta: O XML segue o mesmo parser da importação por arquivo e a compra nasce como rascunho. 409 INBOUND_NFE_PREVIEW_REQUIRED sem prévia (fromPreview=true); 422 INBOUND_NFE_SUMMARY_ONLY se só houver resumo; 409 PURCHASE_XML_DUPLICATE se a nota já foi importada

### `PATCH /purchases/:id`

Atualizar compra

Corpo: `supplierId` (uuid) Fornecedor; `freightValue` (number) Frete da nota; `insuranceValue` (number) Seguro da nota; `otherExpenses` (number) Outras despesas da nota; `ipiValue` (number) IPI da nota

Resposta: As despesas são rateadas entre os itens pelo valor de cada um e compõem o custo de entrada ao receber (cada item traz ancillaryCostPerUnit e landedUnitCost). O total da compra não muda

### `PATCH /purchases/:id/invoice-data`

Atualizar dados da nota (fornecedor, número, série e chave)

Corpo: `supplierId` (uuid) Fornecedor; `invoiceNumber` (string) Número da NF-e de entrada; `invoiceSeries` (string) Série da NF-e de entrada; `accessKey` (string) Chave de acesso (44 dígitos)

Resposta: Continua disponível depois do recebimento (não mexe em estoque). Bloqueado em compra cancelada e na chave de compra importada por XML

### `DELETE /purchases/:id`

Excluir compra em rascunho

Resposta: 204 No Content

### `POST /purchases/:id/lines`

Adicionar linha manual

Corpo: `description`* (string) Descrição; `quantity`* (number) Quantidade; `unitCost`* (number) Custo unitário

### `PUT /purchases/:id/lines/:lineId`

Atualizar linha

Corpo: `quantity` (number) Quantidade (até 4 casas em kg/L/m; inteira em UN); `unitCost` (number) Custo unitário; `lots` (object[]) Lotes da linha: [{ lotCode, quantity, expiresAt (AAAA-MM-DD), manufacturedAt }]. Substitui os atuais; [] remove. A soma não pode passar da quantidade; o restante entra como saldo sem lote

### `POST /purchases/:id/lines/:lineId/link`

Vincular linha a variante do catálogo

Corpo: `variantId`* (uuid) ID da variante; `pack` (object) { factor, label }: quantas unidades de estoque vale cada unidade da nota (1 FARDO = 12). Omitido = usa o código de compra do produto ou a conversão do produto; `remember` (boolean) Lembra o código do fornecedor/GTIN desta linha para vincular sozinho nas próximas notas

### `DELETE /purchases/:id/lines/:lineId`

Remover linha

Resposta: 204 No Content

### `POST /purchases/:id/receive`

Receber itens vinculados no estoque (parcial ou total)

Corpo: `payable` (object) { create, paymentMethodId, installments[{dueDate, amount}], paidNow{financialAccountId, methodInstallmentCount} }. Omitido = conta automática para 30 dias.

Resposta: Produtos com controle de lote/validade criam StockLot com os lotes da linha. O custo segue o método da loja (último preço ou média ponderada) já com frete/seguro/outras despesas rateados

### `GET /purchases/:id/payable-draft`

Sugestão de conta a pagar do recebimento (duplicatas da NF-e)

### `POST /purchases/:id/cancel`

Cancelar compra

### `POST /purchases/:id/lines/:lineId/unlink`

Desvincular linha do catálogo

### `POST /purchases/:id/lines/:lineId/correct-link`

Corrigir vínculo da linha

### `POST /purchases/:id/lines/:lineId/distribute`

Distribuir quantidade da linha

### `POST /purchases/:id/compare-xml`

Comparar compra com XML

### `POST /purchases/:id/lines/link-batch`

Vincular várias linhas do XML ao catálogo

Corpo: `links`* (array) [{ lineId, variantId, pack?: { factor, label }, remember? }]

### `POST /purchases/from-suggestion`

Criar compra a partir da sugestão de compra

Corpo: `items`* (array) [{ variantId, quantity, unitCost }] (até 500); `supplierId` (uuid) Fornecedor; `notes` (string) Observações

Resposta: 201 Created

### `GET /purchases/item-search`

Achar as compras que trouxeram um produto (código de barras, código ou descrição)

Query: `q` (string) Texto com 2 ou mais caracteres (até 120). Só dígitos com 8+ caracteres é tratado como código de barras e compara o código inteiro; `allLocations` (boolean) Busca em todas as filiais (padrão: só a filial do usuário)

Resposta: Lista de { purchaseId, items: [{ description, gtin, supplierCode, quantity }] }. Menos de 2 caracteres retorna lista vazia; até 2000 itens.

### `GET /purchases/ncm-divergences`

Produtos cujo NCM do cadastro difere do NCM da nota de entrada mais recente

Query: `allLocations` (boolean) Considera todas as filiais (padrão: só a filial do usuário)

Resposta: { items } — cada item: productId, productDescription, currentNcm, xmlNcm, official (NCM existe na tabela oficial), conflictingNcms (preenchido se a nota trouxe NCMs diferentes para o mesmo produto) e a compra de origem. Compras canceladas não entram.

### `POST /purchases/ncm-divergences/apply`

Copiar o NCM da nota de entrada para o cadastro dos produtos escolhidos (exige gerenciar produtos)

Corpo: `productIds`* (uuid[]) Produtos a corrigir (1 a 2000)

Resposta: { updated, skipped }. Só aplica NCM oficial e sem conflito; os demais contam em skipped.

### `POST /purchases/pricing-hints`

Margens praticadas pela loja para sugerir preço de venda ao cadastrar item de compra

Corpo: `categoryId` (uuid) Categoria do produto; `brandId` (uuid) Marca do produto; `title` (string) Descrição (a partir de 4 caracteres procura produto já cadastrado que comece com ela)

Resposta: { categoryMargin, brandMargin, storeMargin, categoryName, brandName, sampleSize, reference }. As margens são medianas em % (null sem amostra); reference traz { description, priceCash, managerialCost } do produto de mesma descrição, se houver.

### `PUT /purchases/:id/tags`

Definir as tags da compra (substitui a lista atual)

Corpo: `tagIds`* (uuid[]) Tags da compra (até 200; no máximo 20 por compra; lista vazia remove todas)

Resposta: O detalhe da compra. Tags novas também são aplicadas aos produtos dos itens já recebidos. 400 em compra cancelada ou tag inexistente.

### `GET /purchases/:id/ncm-diff`

Produtos da compra cujo NCM do cadastro difere do NCM da NF-e

Resposta: { items } no mesmo formato de /purchases/ncm-divergences. 404 se a compra não existe.

### `POST /purchases/:id/apply-ncm`

Copiar o NCM da NF-e desta compra para o cadastro dos produtos escolhidos (exige gerenciar produtos)

Corpo: `productIds`* (uuid[]) Produtos a corrigir (1 a 500)

Resposta: { updated, skipped }. Só aplica NCM oficial e sem conflito.

### `POST /purchases/:id/insights`

Conferência inteligente das linhas da compra: produtos de referência e alertas de custo/quantidade

Corpo: `groups`* (array) Até 500 grupos de linhas da mesma referência: [{ key, title, lines: [{ lineId, size, color }] }] (1 a 200 linhas por grupo; size e color são a grade lida da nota)

Resposta: { matches, alerts }. matches: por grupo, o produto já cadastrado que corresponde (source: gtin, history ou description), preços e o mapeamento de cada linha para uma variante (ou a grade que seria criada). alerts: por linha, avisos como COST_UP, COST_DOWN, QTY_HIGH, GTIN_DUP e GTIN_IN_USE.

## Conferência de estoque

### `GET /inventory-counts`

Listar conferências

### `GET /inventory-counts/:id`

Detalhe da conferência

### `POST /inventory-counts`

Criar conferência

Corpo: `name` (string) Nome/descrição

### `PATCH /inventory-counts/:id`

Atualizar conferência

Corpo: `name` (string) Nome

### `DELETE /inventory-counts/:id`

Excluir conferência em rascunho

Resposta: 204 No Content

### `POST /inventory-counts/:id/scan`

Registrar leitura (GTIN ou variante)

Corpo: `gtin` (string) Código de barras; `variantId` (uuid) ID da variante; `quantity` (integer) Quantidade contada

### `PUT /inventory-counts/:id/lines`

Substituir linhas da conferência

Corpo: `lines`* (array) Lista de { variantId, countedQty }

### `DELETE /inventory-counts/:id/lines/:lineId`

Remover linha

Resposta: 204 No Content

### `POST /inventory-counts/:id/load-all`

Carregar todos os produtos

### `POST /inventory-counts/:id/load-variants`

Carregar variantes específicas na conferência

Corpo: `variantIds`* (string[]) IDs das variantes

### `POST /inventory-counts/:id/apply`

Aplicar contagem ao estoque

### `POST /inventory-counts/:id/cancel`

Cancelar conferência

## Locais (filiais e depósitos)

Locais de estoque da rede: lojas (STORE) e depósitos (WAREHOUSE). Escrita exige o recurso de multi-loja no plano e a permissão de configurações do sistema. Usuários sem acesso a todos os locais só enxergam os locais liberados para eles.

### `GET /locations`

Listar locais

Query: `includeInactive` (boolean) Incluir locais inativos (1/true)

### `GET /locations/transfer-destinations`

Locais que podem receber transferência de estoque

### `GET /locations/:id`

Detalhe do local

### `POST /locations`

Criar local

Corpo: `name`* (string) Nome (até 200); `code` (string) Código interno (até 40); `type` (string) STORE ou WAREHOUSE; `active` (boolean) Ativo; `addressLine1` (string) Endereço; `addressLine2` (string) Complemento; `city` (string) Cidade; `state` (string) UF (2 letras); `zipCode` (string) CEP

Resposta: 201 Created

### `PATCH /locations/:id`

Atualizar local (campos da criação, todos opcionais)

Corpo: `isDefault` (boolean) Tornar o local padrão

### `PUT /locations/:id/fiscal`

Perfil fiscal da filial (CNPJ próprio, IE, série NFC-e, CSC)

Corpo: `fiscalCnpj` (string) CNPJ da filial; `fiscalStateRegistration` (string) Inscrição estadual; `fiscalMunicipalRegistration` (string) Inscrição municipal; `fiscalLegalName` (string) Razão social; `fiscalTradeName` (string) Nome fantasia; `fiscalAddressNumber` (string) Número do endereço; `fiscalAddressDistrict` (string) Bairro; `fiscalIbgeCityCode` (string) Código IBGE do município; `fiscalPhone` (string) Telefone; `fiscalNfceSeries` (integer) Série da NFC-e (1–889); `fiscalNfceNextNumber` (integer) Próximo número da NFC-e; `fiscalCscId` (string) ID do CSC (produção); `fiscalCscToken` (string) Token do CSC (produção); `fiscalCscIdHomolog` (string) ID do CSC (homologação); `fiscalCscTokenHomolog` (string) Token do CSC (homologação)

## Transferências de estoque

Movimenta estoque entre locais. Fluxo: criar (DRAFT) → enviar (IN_TRANSIT) → receber (COMPLETED), ou concluir direto. Entre CNPJs diferentes informe a chave da NF-e de transferência. Escrita exige multi-loja no plano.

### `GET /stock-transfers`

Listar transferências

Query: `status` (string) DRAFT, IN_TRANSIT, COMPLETED ou CANCELLED

### `GET /stock-transfers/:id`

Detalhe da transferência com itens

### `POST /stock-transfers`

Criar transferência (rascunho)

Corpo: `fromLocationId`* (uuid) Local de origem; `toLocationId`* (uuid) Local de destino; `notes` (string) Observações; `lines`* (array) Itens: [{ variantId, quantity }]

Resposta: 201 Created

### `POST /stock-transfers/:id/send`

Enviar (baixa na origem, fica em trânsito)

Corpo: `transferNfeAccessKey` (string) Chave da NF-e de transferência (obrigatória só entre CNPJs diferentes)

### `POST /stock-transfers/:id/receive`

Receber no destino

### `POST /stock-transfers/:id/complete`

Concluir em uma etapa (baixa na origem e entrada no destino)

Corpo: `transferNfeAccessKey` (string) Chave da NF-e de transferência (obrigatória só entre CNPJs diferentes)

### `POST /stock-transfers/:id/cancel`

Cancelar transferência

### `GET /stock-transfers/:id/nfe`

Status da NF-e de transferência (operação fiscal ligada à transferência)

Resposta: { operation } ou { operation: null } enquanto não houver NF-e. Traz status, chave de acesso e o motivo da rejeição.

### `POST /stock-transfers/:id/nfe`

Emitir a NF-e de transferência (CFOP 5151/5152/6151/6152)

Resposta: Só para transferência em rascunho entre CNPJs diferentes; a nota sai pelo CNPJ da filial de origem. Ao autorizar, a chave é gravada em transferNfeAccessKey e o envio/conclusão deixa de pedir a chave. Se a SEFAZ não responder, a operação fica na fila (PENDING). Rejeição definitiva devolve 400 com o motivo.

## Operações fiscais (NF-e além da venda)

Bonificação, amostra, transferência, remessas (industrialização, conserto, demonstração, consignação, armazém, simples remessa), exportação, NF-e complementar/ajuste e baixa de estoque por perda. Não gera contas a receber. Leitura exige fiscal.view e escrita fiscal.manage. Tributação padrão por tipo sujeita à validação do contador.

### `GET /fiscal-operations`

Listar operações fiscais (paginado)

Query: `q` (string) Destinatário, CPF/CNPJ, número da operação ou do documento, ou chave; `kind` (string) Tipo de operação (ex.: BONUS, TRANSFER, EXPORT, LOSS); `status` (string) DRAFT, PENDING, PROCESSING, AUTHORIZED, REJECTED, CANCELLED, CONTINGENCY ou COMPLETED; `from` (string) Criadas a partir de (ISO 8601); `to` (string) Criadas até (ISO 8601); `page` (number) Página (padrão 1); `pageSize` (number) Itens por página (1 a 100, padrão 25)

Resposta: { items, total, page, pageSize }

### `POST /fiscal-operations`

Criar operação em rascunho

Corpo: `kind`* (string) Tipo de operação; `locationId` (uuid) Filial de origem (estoque e emitente); `destType` (string) CUSTOMER, SUPPLIER, LOCATION, MANUAL ou SELF; `destCustomerId / destSupplierId / destLocationId` (uuid) Destinatário cadastrado; `dest` (object) Dados digitados do destinatário (nome, taxId, endereço, país no exterior); `natOp` (string) Natureza da operação (até 60 caracteres; vazio usa a do tipo); `reasonCode / reason` (string) Motivo da baixa (PERDA, QUEBRA, VENCIMENTO, DETERIORACAO, ROUBO, OUTRO) e descrição; `refAccessKeys` (string[]) Chaves de 44 dígitos das NF-e referenciadas (obrigatório em alguns tipos); `exportData` (object) Exportação: { ufSaida, locExporta, locDespacho }; `carrierId / nfeExtras` (object) Transportadora, frete, veículo e volumes; `moveStock / emitNfe` (boolean) Simples remessa movimenta estoque; baixa emite NF-e 5927 só se true; `items`* (array) Itens: variantId ou descrição/NCM/unidade, quantity, unitValue, totalValue, taxOverride, manualTax, lots

Resposta: 201 Created com a operação

### `GET /fiscal-operations/:id`

Detalhe da operação (itens, documento fiscal, avisos)

### `PUT /fiscal-operations/:id`

Editar operação em rascunho ou rejeitada (mesmo corpo do POST; o tipo não muda)

### `DELETE /fiscal-operations/:id`

Excluir rascunho

Resposta: 204 No Content

### `POST /fiscal-operations/:id/preview`

Prévia: valida e mostra CFOP/CST por item, totais, avisos e faltas de estoque

Resposta: { ok: true, scope, natOp, vProd, vNF, items, warnings, stockShortages } ou { ok: false, errors }

### `POST /fiscal-operations/:id/issue`

Emitir: valida, baixa o estoque e transmite a NF-e (baixa sem NF-e é só registrada)

Resposta: 200 { queued: false, operation }; 202 { queued: true, message, operation } quando a SEFAZ não respondeu e a nota ficou na fila; 400 com o motivo.

### `POST /fiscal-operations/:id/cancel`

Cancelar operação emitida (devolve o estoque)

Corpo: `justification`* (string) Justificativa de 15 a 255 caracteres

### `GET /fiscal-operations/:id/xml`

Baixar o XML autorizado

### `GET /fiscal-operations/:id/pdf`

DANFE em PDF

### `GET /fiscal-operations/tax-options`

Tributação padrão por tipo de operação e regime (CRT) da loja

Resposta: { crt, options }. CRT 1, 2 e 4 usam CSOSN; CRT 3 usa CST.

### `PUT /fiscal-operations/tax-options`

Sobrescrever a tributação padrão de cada tipo (confirmar com o contador)

Corpo: `options`* (object) { [tipo]: { icmsMode, icmsCst, icmsCsosn, ipiMode, ipiCst, ipiAlways, pisCofinsMode, pisCofinsCst, pisCofinsCstSimples } }

### `GET /fiscal-operations/referenced-nfe`

Dados da NF-e referenciada (destinatário e itens) para preencher complementar, ajuste e retornos

Query: `accessKey` (string) Chave de 44 dígitos

Resposta: { found: false } quando a nota não foi emitida nem recebida por este sistema.

### `GET /fiscal-operations/:id/danfe-html`

DANFE da operação em HTML (para visualizar ou imprimir no navegador)

Resposta: text/html; charset=utf-8, sem cache. Para o PDF use /fiscal-operations/:id/pdf.

## Produção (receitas e ordens)

Ficha técnica e ordens de produção (padaria, confeitaria, cozinha): a ordem baixa os insumos e dá entrada no produto acabado.

### `GET /bakery-production/status`

Módulos ligados na loja (produção, código de balança, encomendas)

### `GET /bakery-production/recipes`

Listar receitas

Query: `active` (boolean) Somente ativas (1/true)

### `POST /bakery-production/recipes`

Criar receita

Corpo: `name`* (string) Nome; `finishedVariantId`* (uuid) Produto acabado (variante); `yieldQty` (number) Rendimento por lote (padrão 1); `notes` (string) Observações; `items`* (array) Insumos: [{ componentVariantId, quantity }]; `densityKgPerL` (number) Densidade do acabado (kg/L); `expectedLossPercent` (number) Perda esperada do processo, de 0 a 99,99 (%); `stages` (array) Etapas-modelo: ["Pesagem", "Dispersão", ...]

Resposta: 201 Created

### `PATCH /bakery-production/recipes/:id`

Editar receita (items e stages substituem a lista inteira)

### `GET /bakery-production/recipes/:id/preview`

Necessidade de insumos, saldo, custo estimado e lotes máximos

Query: `batches` (number) Lotes a produzir; `locationId` (uuid) Local

### `GET /bakery-production/orders`

Listar ordens de produção

Query: `limit` (integer) Padrão 50

### `POST /bakery-production/orders`

Registrar produção

Corpo: `recipeId`* (uuid) Receita; `batches`* (number) Quantidade de lotes; `outputLotCode` (string) Lote do produto acabado; `outputExpiresAt` (string) Validade (ISO ou AAAA-MM-DD); `locationId` (uuid) Depósito de onde os insumos saem (omitido = padrão da produção); `outputLocationId` (uuid) Depósito onde o acabado entra (omitido = padrão da produção ou o mesmo dos insumos); `allowNegativeStock` (boolean) Permitir insumo com estoque negativo; `notes` (string) Observações; `actualOutputQty` (number) Quantidade realmente obtida (apura a perda)

Resposta: 201 Created. Devolve também nominalQty, lossQty, materialCost e unitCost.

### `POST /bakery-production/orders/plan`

Planejar ordem (PLANNED) com data prevista; copia as etapas da receita

### `GET /bakery-production/orders/:id`

Detalhe: etapas, consumo por lote, custo e perda

### `POST /bakery-production/orders/:id/start`

Iniciar ordem planejada

### `POST /bakery-production/orders/:id/stages/:stageId`

Marcar ou desmarcar etapa

Corpo: `done` (boolean) Concluída; `notes` (string) Observações

### `POST /bakery-production/orders/:id/complete`

Concluir ordem: baixa insumos (FIFO por lote), dá entrada no acabado e apura custo

Corpo: `actualOutputQty` (number) Quantidade obtida; `outputLotCode` (string) Lote do acabado; `outputExpiresAt` (string) Validade; `allowNegativeStock` (boolean) Permitir estoque negativo

### `POST /bakery-production/orders/:id/cancel`

Cancelar ordem não concluída

### `GET /bakery-production/traceability`

Rastreio por lote

Query: `lotCode` (string) Lote; `direction` (string) backward (acabado → insumos) | forward (insumo → acabados); `page` (integer) Página (forward; ordens paginadas, com total real na resposta); `pageSize` (integer) Ordens por página (padrão 50, máximo 200)

### `GET /bakery-production/recall`

Recall: quem recebeu um lote e em quais notas (exige permissão de relatórios)

Query: `lotCode` (string) Lote de produto acabado ou de insumo; `page` (integer) Página; `pageSize` (integer) Registros por página (padrão 50, máximo 500); `format` (string) json (padrão) | csv (todas as páginas, separador ; e BOM para Excel)

Resposta: Lote de insumo alcança também as vendas dos lotes de acabado produzidos com ele. Devolve resumo (vendas, clientes, notas, quantidade líquida), estoque ainda existente dos lotes e as vendas com cliente e documento fiscal (NFC-e/NF-e). Só vendas concluídas feitas depois que o produto passou a controlar lote

### `GET /bakery-production/planning/suggestions`

Sugestão de lotes pela cobertura de estoque e venda média

Query: `days` (integer) Dias de cobertura (padrão 15)

### `PUT /bakery-production/recipes/:id`

Editar receita (mesmo comportamento do PATCH; todos os campos são opcionais)

Corpo: `name` (string) Nome (até 200 caracteres); `finishedVariantId` (uuid) Produto acabado (variante); `yieldQty` (number) Rendimento por lote; `notes` (string) Observações (até 2000 caracteres); `densityKgPerL` (number) Densidade do acabado (kg/L); `expectedLossPercent` (number) Perda esperada do processo, de 0 a 99,99 (%); `formulaMode` (string) ABSOLUTE (quantidades) | PERCENT (percentual de cada insumo); `percentBasis` (string) MASS | VOLUME (base do percentual); `standardUnitCost` (number) Custo padrão unitário; `stages` (array) Etapas: texto ou { name, required, workCenter, standardMinutes, laborRatePerHour }; substitui a lista inteira; `byproducts` (array) Subprodutos: [{ variantId, quantityPerBatch, costSharePercent }]; substitui a lista inteira; `items` (array) Insumos: [{ componentVariantId, quantity, percent, densityKgPerL, lossPercent, costKind (MATERIAL | PACKAGING) }]; substitui a lista inteira; `active` (boolean) Receita ativa; `changeReason` (string) Motivo da alteração (até 500 caracteres; obrigatório no segmento industrial quando a fórmula muda); `saveAsDraft` (boolean) Salva como rascunho: a receita atual continua valendo até ativar a versão

Resposta: A receita atualizada. Com saveAsDraft a nova versão fica em rascunho (ver /versions e /versions/:version/activate).

### `GET /bakery-production/recipes/:id/versions`

Histórico de versões da receita (ativa, obsoletas e rascunhos), com as diferenças entre versões

Resposta: { currentVersion, versions: [{ version, status, snapshot, changeReason, ..., changes }] } da mais nova para a mais antiga. 404 se a receita não existe.

### `POST /bakery-production/recipes/:id/versions/:version/activate`

Ativar um rascunho de versão: a ativa atual vira obsoleta e o rascunho passa a valer (ganha novo número de versão)

Corpo: `changeReason` (string) Motivo (até 500 caracteres; vazio usa o do rascunho)

Resposta: A receita atualizada. 404 se a versão não existe; 409 se a versão não é um rascunho ou está corrompida.

### `DELETE /bakery-production/recipes/:id/versions/:version`

Descartar um rascunho de versão (só rascunhos; versões ativas e obsoletas não podem ser removidas)

Resposta: 204 No Content. 404 se o rascunho não existe.

### `POST /bakery-production/recipes/:id/standard-cost`

Recalcular o custo padrão da receita a partir do custo estimado de 1 lote e gravá-lo (exige a permissão de custos de produção)

Resposta: { recipeId, standardUnitCost }

### `GET /bakery-production/recipes/:id/tributary-suggestion`

Sugestão do fator da unidade tributável (uTrib/uCom) do acabado para a NF-e, a partir da densidade da receita e do volume da embalagem

Resposta: { recipeId, variantId, productDescription, saleUnit, tributaryUnit, densityKgPerL, unitVolumeLiters, currentFactor, litersPerUnit, kgPerUnit, ... }. Só sugere: a unidade tributável correta é decisão fiscal (NCM/TIPI); confirme com o contador.

### `POST /bakery-production/recipes/:id/apply-tributary-factor`

Gravar na variante do acabado o fator tributável sugerido (exige a permissão de gerenciar produtos)

Corpo: `basis`* (string) L (litros por unidade) | KG (quilos por unidade)

Resposta: A sugestão com currentFactor já aplicado e appliedBasis. 409 se não há como calcular (falta densidade da receita ou volume da embalagem).

### `POST /bakery-production/orders/:id/refresh-recipe`

Atualizar a fórmula de uma ordem planejada para a versão atual da receita (recria as etapas a partir da receita)

Resposta: A ordem atualizada. 409 se a ordem não está PLANNED; 404 se a receita não existe ou está inativa.

### `GET /bakery-production/orders/:id/reversal-preview`

Prévia do estorno de uma ordem concluída: quanto do acabado ainda existe em estoque e o efeito no custo médio (exige a permissão de estornar produção)

Resposta: { orderId, outputLotCode, obtainedQty, remainingQty, availableQty, soldOrConsumedQty, reversibleFraction, full, costReversal }. Nada é gravado. 409 se a ordem não está concluída.

### `POST /bakery-production/orders/:id/reverse`

Estornar ordem concluída: retira o acabado do lote, devolve os insumos aos mesmos lotes e bloqueia o lote estornado (exige a permissão de estornar produção)

Corpo: `reason`* (string) Motivo do estorno (5 a 500 caracteres); `partial` (boolean) Estorno parcial, só do que ainda está em estoque (obrigatório quando o acabado já foi vendido ou consumido em parte)

Resposta: A ordem, mais costReversal e coproductCostReversals (se o custo médio foi refeito ou o motivo de não ter sido). 409 se a ordem não está concluída, já foi totalmente estornada ou o estorno total está bloqueado (code REVERSAL_BLOCKED, com partialPossible).

## Tintométrico (fábrica de tintas)

Cores, fórmulas por base e dosagem de corantes com baixa de estoque. Disponível apenas no segmento Fábrica de tintas.

### `GET /paint/variants`

Buscar base ou corante (produto/variante)

Query: `q` (string) Mínimo 2 caracteres

### `GET /paint/colors`

Listar cores

Query: `q` (string) Código ou nome; `active` (string) 1 | 0; `page` (integer) Página

### `POST /paint/colors`

Criar cor

Corpo: `code`* (string) Código único; `name`* (string) Nome; `hex` (string) #RRGGBB

### `PATCH /paint/colors/:id`

Editar ou inativar cor

### `DELETE /paint/colors/:id`

Excluir cor (409 se já houve dosagem)

### `GET /paint/colors/:id/formulas`

Fórmulas da cor

### `POST /paint/colors/:id/formulas`

Criar fórmula para uma base

Corpo: `baseVariantId`* (uuid) Base; `referenceLiters`* (number) Litros de base a que a fórmula se refere; `items`* (array) [{ colorantVariantId, quantity }]

### `PATCH /paint/formulas/:id`

Editar fórmula (items substituem os atuais)

### `DELETE /paint/formulas/:id`

Excluir fórmula

### `POST /paint/formulas/:id/duplicate`

Copiar fórmula para outra base

### `GET /paint/formulas/:id/preview`

Corantes escalados para a litragem, saldo e custo

Query: `liters` (number) Litros

### `POST /paint/jobs`

Dosar: baixa os corantes e registra a dosagem

Corpo: `colorId`* (uuid) Cor; `baseVariantId`* (uuid) Base; `liters`* (number) Litros; `saleId` (uuid) Venda relacionada; `allowNegativeStock` (boolean) Permitir estoque negativo; `consumeBaseQty` (number) Baixar também a base

Resposta: 201 Created

### `GET /paint/jobs`

Histórico de dosagens

### `GET /paint/jobs/:id`

Detalhe da dosagem

### `GET /paint/jobs/:id/label`

Dados da etiqueta da lata

### `POST /paint/import`

Importar cores e fórmulas (CSV)

Corpo: `csv`* (string) codigo;nome;hex;base_gtin;litros_ref;corante_gtin;quantidade

### `GET /paint/colors/:id/compare`

Comparar a mesma cor entre as bases: corante por litro de base e diferença percentual entre bases

Resposta: { color: { id, code, name, hex }, ... } com uma linha por fórmula/base. 404 se a cor não existe.

### `GET /paint/formulas/:id/versions`

Histórico de versões da fórmula (mais nova primeiro, até 200), com os corantes de cada versão

Resposta: { formulaId, currentVersion, versions: [{ version, reason, createdById, createdAt, referenceLiters, items: [{ colorantVariantId, quantity, description, label, unit }] }] }

### `GET /paint/formulas/preview`

Prévia da dosagem para cor + base (sem precisar do id da fórmula): corantes escalados, saldo e custo

Query: `colorId` (uuid) Cor (informar junto com baseVariantId); `baseVariantId` (uuid) Base (informar junto com colorId); `liters` (number) Litros (informe liters ou packages); `packages` (string) Embalagens "tamanho x quantidade" separadas por vírgula, ex.: 3.6x2,18x1

Resposta: 400 se faltar colorId/baseVariantId juntos ou a litragem/embalagens. Usa o ajuste de doses por mL da loja, quando configurado.

### `POST /paint/jobs/:id/cancel`

Cancelar dosagem: devolve corantes (e a base, se a dosagem a baixou) ao estoque, nos mesmos lotes consumidos

Corpo: `reason`* (string) Motivo (3 a 300 caracteres)

Resposta: A dosagem cancelada. Idempotente: repetir devolve o cancelamento original (com idempotent: true), sem novo movimento de estoque. 409 se a dosagem está ligada a uma venda (cancele ou estorne a venda).

### `POST /paint/jobs/:id/label/reprint`

Reimprimir a etiqueta da lata: devolve os dados da etiqueta e conta a reimpressão

Resposta: Dados da etiqueta, com labelPrintCount (nº da impressão). 409 se a dosagem foi cancelada; 404 se não existe.

## Segurança química (FISPQ)

FISPQ/FDS, classificação de risco e produtos controlados. Disponível apenas no segmento Fábrica de tintas.

### `GET /chemical-safety`

Listar produtos com dados de segurança

Query: `controlled` (boolean) Somente controlados; `missingFispq` (boolean) Sem FISPQ; `expiringInDays` (integer) FISPQ vencendo em N dias; `q` (string) Nome ou número ONU

### `GET /chemical-safety/alerts`

FISPQ vencidas, vencendo, ausentes e produtos controlados

### `GET /chemical-safety/controlled-sales`

Vendas de produtos controlados no período

Query: `from` (string) AAAA-MM-DD; `to` (string) AAAA-MM-DD; `format` (string) csv

### `GET /chemical-safety/products/:productId`

Ficha de segurança do produto

### `PUT /chemical-safety/products/:productId`

Salvar ficha (ONU, classe de risco, grupo de embalagem, validade da FISPQ, controlado)

### `POST /chemical-safety/products/:productId/fispq`

Enviar PDF da FISPQ (campo file, até 10 MB)

### `GET /chemical-safety/products/:productId/fispq`

Baixar a FISPQ

### `DELETE /chemical-safety/products/:productId/fispq`

Remover o arquivo da FISPQ

### `GET /chemical-safety/licenses`

Listar licenças da própria fábrica (Polícia Federal, Exército, ANVISA, IBAMA, Bombeiros etc.) com situação de validade

Resposta: Até 500 itens, ativas primeiro e por validade. Cada item traz status VALID, EXPIRING ou EXPIRED.

### `POST /chemical-safety/licenses`

Cadastrar licença (exige a permissão de gerenciar produtos)

Corpo: `kind`* (string) PF, EXERCITO, ANVISA, IBAMA, BOMBEIROS ou OUTRA; `number`* (string) Número da licença (até 80 caracteres); `validUntil`* (string) Validade (AAAA-MM-DD); `issuer` (string) Órgão emissor (até 120 caracteres); `issuedAt` (string) Data de emissão (AAAA-MM-DD); a validade não pode ser anterior; `notes` (string) Observações (até 1000 caracteres); `active` (boolean) Licença ativa

Resposta: 201 Created com a licença

### `PATCH /chemical-safety/licenses/:id`

Editar licença (mesmos campos do cadastro, todos opcionais; exige a permissão de gerenciar produtos)

Resposta: A licença atualizada. 404 se não existe.

### `DELETE /chemical-safety/licenses/:id`

Excluir licença (exige a permissão de gerenciar produtos)

Resposta: 204 No Content. 404 se não existe.

### `GET /chemical-safety/products/:productId/emergency-sheet`

Ficha de emergência do produto em PDF (uma página)

Resposta: application/pdf (inline), sem cache

### `POST /chemical-safety/sale-check`

Consulta de segurança química antes de finalizar a venda (avisos e bloqueios conforme as regras da loja); não grava nada

Corpo: `items` (array) Itens avulsos: [{ variantId, productId }] (até 500); alternativa a saleId; `saleId` (uuid) Rascunho de venda: os itens são lidos da própria venda; `customerId` (uuid) Cliente (para validar licença/cadastro do comprador); `nature` (string) SALE | OWN_CONSUMPTION (consumo próprio não é verificado)

Resposta: { applicable, blocks: string[], warnings: string[] }. A venda só é bloqueada na finalização se houver blocks.

### `GET /chemical-safety/sales/:saleId/emergency-sheets`

Fichas de emergência da venda em PDF (uma página por produto químico)

Resposta: application/pdf (inline), sem cache

## Representantes comerciais

Região, meta mensal e comissão por representante. Disponível apenas no segmento Fábrica de tintas.

### `GET /representatives`

Representantes ativos com região e comissão

### `GET /representatives/performance`

Desempenho do mês: vendas, meta, % da meta, ranking e totais por região

Query: `year` (integer) Ano; `month` (integer) Mês (1-12)

### `GET /representatives/goals`

Metas do ano (12 meses por representante)

Query: `year` (integer) Ano

### `PUT /representatives/goals`

Definir meta do mês (0 remove)

Corpo: `sellerId`* (uuid) Representante; `year`* (integer) Ano; `month`* (integer) Mês; `targetAmount`* (number) Meta em R$

### `DELETE /representatives/goals`

Remover meta

### `POST /representatives/goals/copy`

Copiar metas de um mês para outro mês ou para o ano todo

## Formas de pagamento

### `GET /payment-methods`

Listar forma de pagamento

### `POST /payment-methods`

Criar forma de pagamento

Corpo: `name`* (string) Nome (ex.: Dinheiro, PIX); `priceBasis` (string) CASH | CREDIT; `allowsChange` (boolean) Permite troco; `active` (boolean) Ativa; `fiscalCode` (string) Código tPag NFC-e (2 dígitos)

Resposta: 201 Created

### `PATCH /payment-methods/:id`

Atualizar forma de pagamento

Corpo: `name`* (string) Nome (ex.: Dinheiro, PIX); `priceBasis` (string) CASH | CREDIT; `allowsChange` (boolean) Permite troco; `active` (boolean) Ativa; `fiscalCode` (string) Código tPag NFC-e (2 dígitos)

### `DELETE /payment-methods/:id`

Excluir forma de pagamento

Resposta: 204 No Content

## Divisões de grade

### `GET /grid-divisions`

Listar divisão

### `POST /grid-divisions`

Criar divisão

Corpo: `name`* (string) Valor do eixo principal (ex.: P, M, G)

Resposta: 201 Created

### `PATCH /grid-divisions/:id`

Atualizar divisão

Corpo: `name`* (string) Valor do eixo principal (ex.: P, M, G)

### `DELETE /grid-divisions/:id`

Excluir divisão

Resposta: 204 No Content

### `PUT /grid-divisions/reorder`

Reordenar divisões

Corpo: `ids`* (string[]) IDs na ordem desejada

### `POST /grid-divisions/:id/merge`

Mesclar divisão em outra

Corpo: `targetId`* (string) ID do item que permanece

## Subdivisões de grade

### `GET /grid-subdivisions`

Listar subdivisão

### `POST /grid-subdivisions`

Criar subdivisão

Corpo: `name`* (string) Valor do eixo secundário (ex.: PRETO, BEGE)

Resposta: 201 Created

### `PATCH /grid-subdivisions/:id`

Atualizar subdivisão

Corpo: `name`* (string) Valor do eixo secundário (ex.: PRETO, BEGE)

### `DELETE /grid-subdivisions/:id`

Excluir subdivisão

Resposta: 204 No Content

### `PUT /grid-subdivisions/reorder`

Reordenar subdivisões

Corpo: `ids`* (string[]) IDs na ordem desejada

### `POST /grid-subdivisions/:id/merge`

Mesclar subdivisão em outra

Corpo: `targetId`* (string) ID do item que permanece

## Grade (eixos e sugestão)

### `GET /grid-catalog/seed/profiles`

Ramos disponíveis para sugestão de grade

Resposta: { profiles: [{ id, label }], suggested }

### `POST /grid-catalog/seed/suggest`

Sugerir rótulos e valores de divisão/subdivisão (catálogo do ramo + IA)

Corpo: `profile` (string) Ramo; padrão vem do CNAE da loja; `businessDescription` (string) Tipo de negócio em texto livre (até 500 caracteres); `useAi` (boolean) false devolve só o catálogo do ramo

Resposta: { divisionAxisLabel, subdivisionAxisLabel, divisions, subdivisions, aiUsed, aiError }

### `POST /grid-catalog/seed/apply`

Gravar rótulos da loja e cadastrar os valores escolhidos

Corpo: `divisionAxisLabel`* (string) Nome do eixo principal; `subdivisionAxisLabel`* (string) Nome do eixo secundário; `divisions` (string[]) Valores da divisão; `subdivisions` (string[]) Valores da subdivisão

### `PUT /grid-catalog/axis-labels`

Atualizar só os nomes dos eixos (padrão legado: Tamanho e Cor)

Corpo: `divisionAxisLabel`* (string) Nome do eixo principal; `subdivisionAxisLabel`* (string) Nome do eixo secundário

## Relatórios

### `GET /reports/sales-summary`

Resumo de vendas

Query: `from` (string) Data inicial (YYYY-MM-DD); `to` (string) Data final; `sellerId` (string) Filtrar por vendedor (UUID); `paymentMethodId` (string) Filtrar por forma de pagamento; `salesChannelId` (string) Filtrar por canal de venda (UUID) ou `none` para vendas sem canal; `includeItems` (boolean) Inclui os produtos de cada venda (relatório detalhado)

### `GET /reports/monthly-sales`

Faturamento comercial agrupado por mês

Query: `from` (string) Data inicial (YYYY-MM-DD); `to` (string) Data final

### `GET /reports/top-products`

Produtos mais vendidos

Query: `from` (string) Data inicial; `to` (string) Data final; `limit` (integer) Quantidade

### `GET /reports/sales-by-category`

Vendas por categoria

Query: `from` (string) Data inicial; `to` (string) Data final

### `GET /reports/sales-by-tag`

Vendas e catálogo por tag de produto

Query: `from` (string) Data inicial; `to` (string) Data final

### `GET /reports/catalog-groups`

Vendas, produtos e estoque por marca, categoria ou tag

Query: `dimension` (string) brand, category ou tag; `from` (string) Data inicial; `to` (string) Data final

### `GET /reports/catalog-groups/products`

Produtos de uma marca, categoria ou tag com vendas do período e estoque atual

Query: `dimension` (string) brand, category ou tag; `groupId` (string) Id do grupo ou none (sem marca/categoria/tag); `from` (string) Data inicial; `to` (string) Data final

### `GET /reports/stock`

Posição de estoque

Query: `all` (boolean) Retorna todos os SKUs (sem paginação); `page` (integer) Página (padrão 1); `pageSize` (integer) Itens por página (padrão 500, máx. 2000)

### `GET /reports/draft-stock`

Peças em rascunhos de venda (para conferência)

### `GET /reports/dashboard-alerts`

Alertas do dashboard

### `GET /reports/top-customers`

Melhores clientes

Query: `from` (string) Data inicial; `to` (string) Data final

### `GET /reports/credit-sales`

Vendas a prazo

Query: `from` (string) Data inicial; `to` (string) Data final

### `GET /reports/card-fees`

Relatório de taxas de cartão (MDR) no período

Query: `from` (string) Data inicial; `to` (string) Data final

### `GET /reports/commissions-by-seller`

Comissões por vendedor

Query: `from` (string) Data inicial; `to` (string) Data final

### `GET /reports/dashboard-overview`

Visão geral do dashboard

### `GET /reports/dashboard-metrics`

Métricas do dashboard

### `GET /reports/dashboard-charts`

Gráficos do dashboard

### `GET /reports/dashboard-insights`

Insights do dashboard

### `GET /reports/product-abc`

Curva ABC de produtos por faturamento

### `GET /reports/sale-margins`

Margem bruta por pedido

### `GET /reports/customer-dashboard`

Indicadores de clientes no período

### `GET /reports/sales-goals`

Meta mensal de faturamento

### `PUT /reports/sales-goals`

Criar ou atualizar meta mensal

### `GET /reports/sales-goals/progress`

Progresso da meta no mês

### `GET /reports/sales-goals/history`

Histórico de metas e realizado

### `GET /reports/customer-birthdays`

Aniversários de clientes

### `GET /reports/seller-birthdays`

Aniversários de vendedores

### `GET /reports/cash-flow`

Fluxo de caixa

### `GET /reports/dre`

DRE gerencial ou contábil (receita, CMV, despesas, impostos e resultado do período)

Query: `from` (string) Data inicial (YYYY-MM-DD); `to` (string) Data final; `view` (string) managerial (padrão) ou accounting

### `GET /reports/cash-inflow-by-payment`

Entradas de caixa por forma de pagamento (à vista, crediário e cheques compensados)

Query: `from` (string) Data inicial; `to` (string) Data final

### `GET /reports/receivables-aging`

Aging de contas a receber

### `GET /reports/receivables-write-offs`

Contas canceladas por perda (perdão de dívida) no período

Query: `from` (string) Início (YYYY-MM-DD); `to` (string) Fim (YYYY-MM-DD)

### `GET /reports/receivables-detailed`

Relatório detalhado de contas a receber (títulos em aberto por cliente)

Query: `status` (string) open (padrão) | overdue | current

### `GET /reports/receivables-summary`

Relatório resumido de contas a receber (saldo total por cliente)

Query: `status` (string) open (padrão) | overdue | current

### `GET /reports/payables-detailed`

Relatório detalhado de contas a pagar (títulos em aberto por fornecedor)

Query: `status` (string) open (padrão) | overdue | current; `supplierId` (uuid) Fornecedor; `categoryId` (string) Categoria (uuid) ou `none` para sem categoria; `costCenterId` (uuid) Centro de custo; `kind` (string) manual | purchase | recurring | card_bill | credit_card; `approvalStatus` (string) PENDING | APPROVED | REJECTED; `q` (string) Busca (descrição, NF, fornecedor, documento); `dueFrom` (string) Vencimento inicial (YYYY-MM-DD); `dueTo` (string) Vencimento final (YYYY-MM-DD)

### `GET /reports/payables-paid`

Contas pagas no período (principal, juros, multa, desconto, forma e conta de saída)

Query: `from` (string) Início (YYYY-MM-DD); `to` (string) Fim (YYYY-MM-DD); `supplierId` (uuid) Fornecedor; `categoryId` (string) Categoria (uuid) ou `none` para sem categoria; `costCenterId` (uuid) Centro de custo; `kind` (string) manual | purchase | recurring | card_bill | credit_card; `q` (string) Busca (descrição, NF, fornecedor, documento); `financialAccountId` (string) Conta de saída (uuid) ou `none`; `paymentMethodId` (string) Forma de pagamento (uuid) ou `none`

### `GET /reports/payables-summary`

Contas a pagar em aberto e pagas no período, agrupadas

Query: `from` (string) Início (YYYY-MM-DD); `to` (string) Fim (YYYY-MM-DD); `dimension` (string) category (padrão) | dreGroup | costCenter | supplier; `supplierId` (uuid) Fornecedor; `categoryId` (string) Categoria (uuid) ou `none` para sem categoria; `costCenterId` (uuid) Centro de custo; `kind` (string) manual | purchase | recurring | card_bill | credit_card; `q` (string) Busca (descrição, NF, fornecedor, documento)

### `GET /reports/payables-forecast`

Projeção de saídas (parcelas em aberto e recorrências a gerar), entradas previstas e saldo projetado

Query: `from` (string) Início (YYYY-MM-DD, padrão hoje); `to` (string) Fim (YYYY-MM-DD, padrão hoje + 90 dias); `groupBy` (string) day | week (padrão) | month; `supplierId` (uuid) Fornecedor; `categoryId` (string) Categoria (uuid) ou `none` para sem categoria; `costCenterId` (uuid) Centro de custo; `kind` (string) manual | purchase | recurring | card_bill | credit_card; `q` (string) Busca (descrição, NF, fornecedor, documento)

### `GET /reports/payables-cancelled`

Contas a pagar canceladas no período (valor cancelado, motivo e usuário)

Query: `from` (string) Início (YYYY-MM-DD); `to` (string) Fim (YYYY-MM-DD); `supplierId` (uuid) Fornecedor; `categoryId` (string) Categoria (uuid) ou `none` para sem categoria; `costCenterId` (uuid) Centro de custo; `kind` (string) manual | purchase | recurring | card_bill | credit_card; `q` (string) Busca (descrição, NF, fornecedor, documento)

### `GET /reports/services-dashboard`

Dashboard de serviços

Query: `from` (string) Data inicial; `to` (string) Data final

### `GET /reports/promotional-campaigns`

Relatório de campanhas promocionais

Query: `from` (string) Data inicial (YYYY-MM-DD); `to` (string) Data final; `campaignId` (uuid) Campanha; `sellerId` (uuid) Vendedor; `productId` (uuid) Produto; `operatorUserId` (uuid) Operador; `sortBy` (string) quantity | revenue | discount | units

### `GET /reports/promotional-campaigns/:campaignId/sales`

Vendas de uma campanha promocional

### `GET /reports/stock-summary`

Contagens de estoque (sem custo) para o painel inicial

### `GET /reports/own-consumption`

Consumo próprio / uso interno

Query: `from` (string) Data inicial; `to` (string) Data final; `status` (string) COMPLETED ou CANCELLED; `locationId` (uuid) Local; `productId` (uuid) Produto; `categoryId` (uuid) Categoria; `userId` (uuid) Usuário

### `GET /reports/suggested-purchase`

Sugestão de compra (relatórios avançados)

Query: `filter` (string) out (zerados), low (abaixo do mínimo) ou all; `limit` (integer) Padrão 200, máx. 500

### `GET /reports/dead-stock`

Estoque parado (relatórios avançados)

Query: `minDays` (integer) Dias sem venda (padrão 60); `limit` (integer) Padrão 500, máx. 1000

### `POST /reports/dashboard-inbox/emit-pending-nfce`

Emitir NFC-e pendentes em lote (relatórios avançados)

Query: `limit` (integer) Máx. 500

## Avaliações

### `GET /reviews`

Listar avaliações

Query: `status` (string) PENDING | SUBMITTED | EXPIRED; `page` (integer) Página; `pageSize` (integer) Itens por página (máx. 100)

### `GET /reviews/:id`

Detalhe da avaliação

### `DELETE /reviews/:id`

Excluir avaliação

Resposta: 204 No Content

## Usuários

### `GET /users`

Listar usuários

### `POST /users`

Criar usuário

Corpo: `username`* (string) Login; `password`* (string) Senha (mín. 6 caracteres); `name` (string) Nome exibido; `active` (boolean) Ativo

### `PATCH /users/:id`

Atualizar usuário

Corpo: `username` (string) Login; `password` (string) Nova senha; `active` (boolean) Ativo

### `DELETE /users/:id`

Excluir usuário

Resposta: 204 No Content

### `GET /users/:id/login-history`

Histórico de logins do usuário (data/hora, IP e navegador)

### `DELETE /users/:id/two-factor`

Remover 2FA do usuário

## Configurações da loja

### `GET /store-settings`

Obter dados da loja

### `PUT /store-settings`

Atualizar dados da loja

Corpo: `tradeName` (string) Nome fantasia; `legalName` (string) Razão social; `cnpj` (string) CNPJ; `phone` (string) Telefone; `email` (string) E-mail; `addressLine1` (string) Endereço; `city` (string) Cidade; `state` (string) UF; `zipCode` (string) CEP; `customerNameFormat` (string) NONE | TITLE | UPPER — formatação do nome do cliente ao salvar; `productNameFormat` (string) NONE | TITLE | UPPER — formatação da descrição do produto ao salvar; `businessSegment` (string) GENERAL | BAKERY | CLOTHING | SPORTING_GOODS | AUTO_PARTS | FOOTWEAR | OPTICS | CLEANING_SUPPLIES | STATIONERY | PET_SHOP | GIFTS_DECOR | VARIETY_STORE | BUILDING_MATERIALS | FLORIST | CURTAINS | ELECTRONICS | PHONE_ACCESSORIES | COMPUTER_STORE | BAR | SERVICES | AESTHETIC_CLINIC | MECHANIC_WORKSHOP | PAINT_MANUFACTURER | MANUFACTURER — ramo do negócio, usado para pré-selecionar os módulos do menu

## Configurações do PDV

### `GET /pos-settings`

Obter configurações da tela de vendas

### `PATCH /pos-settings`

Atualizar configurações da tela de vendas

Corpo: `requireSellerForPos` (boolean) Exigir vendedor no PDV; `posDefaultSellerIds` (array) IDs dos vendedores padrão (máx. 10); `allowNegativeStock` (boolean) Permitir estoque negativo; `posAllowServices` (boolean) Permitir serviços no PDV; `posAllowShippingAmount` (boolean) Permitir valor de frete; `posAllowSaleCarrier` (boolean) Exibir transportadora na venda e incluir na NF-e 55 (padrão desligado); `posShowManagementIndicators` (boolean) Indicadores gerenciais; `posShowProductCode` (boolean) Exibir código interno no PDV, cadastro e demais telas; no PDV também busca por ele (padrão desligado); `posShowCommissionOnFinalize` (boolean) Mostrar comissão na finalização; `posFinalizeAsModal` (boolean) Finalizar venda em modal central; `posLayout` (string) Layout do PDV: STACKED ou SPLIT; `posInstagramSearchEnabled` (boolean) Busca por Instagram; `posAutoPrintReceipt` (boolean) Imprimir cupom não fiscal automaticamente ao finalizar a venda; `posPrintOrderPdf` (boolean) Gerar PDF A4 do pedido ao finalizar venda sem documento fiscal; `posPrefillPaymentAmount` (boolean) Pré-preencher o valor recebido ao escolher a forma de pagamento (padrão: true); `creditPricingEnabled` (boolean) Preço a prazo; `requireCashSessionForPos` (boolean) Exigir sessão de caixa; `birthdayDiscountEnabled` (boolean) Desconto de aniversário; `birthdayDiscountPercent` (number) Percentual (0–100)

## Configurações de e-mail

### `GET /email-settings`

Obter configurações SMTP

### `PUT /email-settings`

Atualizar configurações SMTP

Corpo: `enabled` (boolean) Habilitar envio; `smtpHost` (string) Servidor SMTP; `smtpPort` (integer) Porta; `smtpUser` (string) Usuário SMTP; `smtpPassword` (string) Senha SMTP; `fromEmail` (string) Remetente; `reviewRequestCooldownDays` (integer) Dias sem novo pedido após o cliente avaliar (padrão 30); `reviewRequestRetryDays` (integer) Dias sem insistir quando o pedido não foi respondido (padrão 14); `reviewRequestExpireDays` (integer) Dias em aguardando até expirar o pedido (padrão 14; 0 = não expirar)

### `POST /email-settings/test`

Enviar e-mail de teste

Corpo: `to`* (string) Destinatário

## Backups

Acesso restrito a sessão de suporte. Não faz parte do escopo de integração via API key.

### `GET /database-backups`

Listar backups

### `POST /database-backups`

Criar backup

Corpo: `name` (string) Nome do backup

### `PATCH /database-backups/:id`

Renomear backup

Corpo: `name`* (string) Novo nome

### `GET /database-backups/:id/download`

Baixar arquivo do backup (stream)

### `POST /database-backups/restore-from-file`

Enfileirar restauração a partir de arquivo enviado

Corpo: `(corpo binário)` (application/octet-stream) Arquivo .sql, .dump, .db, .sqlite ou .enc. Cabeçalho X-Backup-Filename.

Resposta: Responde 202 e restaura em segundo plano. Cria backup automático do estado anterior. Acompanhe em GET /restore-jobs.

### `POST /database-backups/:id/restore`

Enfileirar restauração de um backup da lista

Resposta: Responde 202 e restaura em segundo plano. Cria backup automático do estado anterior. Acompanhe em GET /restore-jobs.

### `GET /database-backups/jobs`

Listar jobs de backup

### `GET /database-backups/restore-jobs`

Listar restaurações em andamento e recentes

### `GET /database-backups/retention-policy`

Política de retenção de backups

### `POST /database-backups/bulk-delete`

Excluir backups em lote

### `POST /database-backups/prune`

Aplicar retenção e remover backups antigos

### `DELETE /database-backups/:id`

Excluir backup

Resposta: 204 No Content

## Fiscal (NFC-e / NF-e)

Documento fiscal de mercadoria da Sale (NFC-e modelo 65 ou NF-e modelo 55), configuração, contingência (NFC-e 65 e NF-e 55) e relatórios. NFS-e permanece em /nfse; devoluções 55 em /sale-returns e /purchase-returns.

### `GET /fiscal/settings`

Configurações fiscais

Resposta: Inclui numbering: série, último nNF gravado e próximo número de NFC-e 65, NF-e 55 de venda e NF-e 55 de devolução no ambiente atual.

### `PUT /fiscal/settings`

Atualizar configurações fiscais

Corpo: `nfceEnabled` (boolean) Habilitar NFC-e (modelo 65) no PDV; `nfeSaleEnabled` (boolean) Habilitar NF-e modelo 55 de venda (série dedicada; opt-in); `nfeSaleSeries` (integer) Série da NF-e de venda (independente da série de devolução); `nfeSaleNextNumber` (integer) Próximo nNF da série de venda 55; `defaultGoodsDocumentType` (string) NFCE_65 | NFE_55 | NONE — tipo documental padrão de mercadoria; `nfseEnabled` (boolean) Habilitar NFS-e no PDV (serviço); `environment` (string) HOMOLOG | PROD; `cscId` (string) ID CSC de produção (NFC-e); `cscToken` (string) Token CSC de produção (NFC-e); `cscIdHomolog` (string) ID CSC de homologação; `cscTokenHomolog` (string) Token CSC de homologação

### `POST /fiscal/certificate/test`

Testar certificado digital

### `GET /fiscal/products-incomplete`

Produtos com cadastro fiscal incompleto

### `GET /fiscal/health`

Saúde fiscal do sistema

### `GET /fiscal/health/emissions`

Indicadores de emissões fiscais

### `GET /fiscal/exports`

Listar todas as notas fiscais para exportação (NFC-e, NF-e, NFS-e, devoluções)

Query: `type` (string) ALL | NFCE_65 | NFE_55 | NFSE | RETURN_SALE | RETURN_PURCHASE | NFE_OPERATION; `from` (string) Data inicial (AAAA-MM-DD); `to` (string) Data final (AAAA-MM-DD); `page` (number) Página (padrão 1); `pageSize` (number) Itens por página (padrão 50, máx. 100)

### `GET /accounting-xml/download`

ZIP com todos os XMLs fiscais do período (NFC-e, NF-e, NFS-e, devoluções e cancelamentos)

Query: `from` (string) Data inicial (AAAA-MM-DD); `to` (string) Data final (AAAA-MM-DD)

Resposta: application/zip. Intervalo máximo de 24 meses.

### `GET /fiscal/documents`

Listar documentos fiscais da Sale (model 65 ou 55)

Query: `status` (string) Status do documento; `from` (string) Data inicial; `to` (string) Data final

### `GET /fiscal/documents/:id`

Detalhe do documento

### `GET /fiscal/documents/:id/pdf`

PDF do documento (cupom/DANFE conforme modelo)

### `GET /fiscal/documents/by-sale/:saleId`

Documento de uma venda

Resposta: Inclui emissionWarnings[{ code, message, item? }]: avisos que não bloquearam a emissão da NF-e.

### `GET /fiscal/nfe-emission`

Opções de emissão da NF-e 55 da loja

Resposta: { crt, isSimples, nfeIpiInIcmsBaseMode (NEVER|ALWAYS|FINAL_CONSUMER), nfeIpiInStBase, nfeTaxesOnTop, nfeCfopFromIcms, difalForSimples, simplesCreditAutoSuggest, simplesIcmsSharePercent, nfeInfCplIpiText, nfeInfCplStText, simplesCreditPreview }.

### `PUT /fiscal/nfe-emission`

Atualizar opções de emissão da NF-e 55 (parcial: envie só o que mudou)

Corpo: `nfeIpiInIcmsBaseMode` (string) NEVER | ALWAYS | FINAL_CONSUMER; `nfeIpiInStBase` (boolean) IPI integra a base do ICMS-ST; `nfeTaxesOnTop` (boolean) IPI e ST por fora do preço da venda; `nfeCfopFromIcms` (boolean) CFOP derivado do ICMS declarado; `difalForSimples` (boolean) Calcular DIFAL também no Simples; `simplesCreditAutoSuggest` (boolean) Sugerir crédito do Simples (pCredSN); `simplesIcmsSharePercent` (number | null) Participação do ICMS na alíquota efetiva (%); `nfeInfCplIpiText` (string | null) Texto fixo do infCpl com IPI destacado; `nfeInfCplStText` (string | null) Texto fixo do infCpl com ICMS-ST

Resposta: Devolve o mesmo formato do GET.

### `POST /fiscal/documents/emit/:saleId`

Emitir documento de mercadoria (NFC-e 65 ou NF-e 55 conforme FiscalDocument.model / resolver)

### `POST /fiscal/documents/:id/reconcile`

Reconciliar documento pendente

### `POST /fiscal/documents/:id/cancel`

Cancelar documento autorizado (NFC-e: cancelMaxMinutes; NF-e 55: até 24h)

Corpo: `justification`* (string) Justificativa (mín. 15 caracteres)

### `POST /fiscal/documents/:id/cce`

Carta de Correção (CC-e) — somente NF-e modelo 55 autorizada

Corpo: `correctionText`* (string) Texto da correção (15–1000 caracteres)

### `POST /fiscal/inutilize`

Inutilizar numeração NFC-e (modelo 65)

Corpo: `series`* (integer) Série; `numberFrom`* (integer) Número inicial; `numberTo`* (integer) Número final; `year` (integer) Ano (opcional); `justification`* (string) Justificativa (mín. 15 caracteres)

### `POST /fiscal/inutilize-nfe-sale`

Inutilizar numeração NF-e de venda (modelo 55, série nfeSale*)

Corpo: `series`* (integer) Série de venda 55; `numberFrom`* (integer) Número inicial; `numberTo`* (integer) Número final; `year` (integer) Ano (opcional); `justification`* (string) Justificativa (mín. 15 caracteres)

### `GET /fiscal/inutilize`

Listar inutilizações (65 e 55)

### `GET /fiscal/contingency/pending`

Documentos em contingência pendentes (somente NFC-e / tpEmis 9)

### `POST /fiscal/contingency/emit-by-order`

Emitir contingência pelo nº do pedido

### `POST /fiscal/contingency/:id/transmit`

Transmitir documento em contingência

### `GET /fiscal/clothing-preset`

Preset fiscal para vestuário

### `POST /fiscal/apply-clothing-preset`

Aplicar preset vestuário

### `GET /fiscal/beauty-preset`

Preset fiscal para estética e beleza

### `POST /fiscal/apply-beauty-preset`

Aplicar preset estética e beleza

### `GET /fiscal/convenience-preset`

Preset fiscal para bebidas e conveniência

### `POST /fiscal/apply-convenience-preset`

Aplicar preset bebidas e conveniência

### `GET /fiscal/bakery-preset`

Preset fiscal para panificadora e confeitaria

### `POST /fiscal/apply-bakery-preset`

Aplicar preset panificadora e confeitaria

### `GET /fiscal/electronics-preset`

Preset fiscal para eletrônicos

### `POST /fiscal/apply-electronics-preset`

Aplicar preset eletrônicos

### `GET /fiscal/supplements-preset`

Preset fiscal para loja de suplementos

### `POST /fiscal/apply-supplements-preset`

Aplicar preset loja de suplementos

### `GET /fiscal/footwear-preset`

Preset fiscal para calçados e bolsas

### `POST /fiscal/apply-footwear-preset`

Aplicar preset calçados e bolsas

### `GET /fiscal/cleaning-supplies-preset`

Preset fiscal para produtos de limpeza

### `POST /fiscal/apply-cleaning-supplies-preset`

Aplicar preset produtos de limpeza

### `GET /fiscal/stationery-preset`

Preset fiscal para papelaria e material escolar

### `POST /fiscal/apply-stationery-preset`

Aplicar preset papelaria e material escolar

### `GET /fiscal/phone-accessories-preset`

Preset fiscal para celulares e acessórios

### `POST /fiscal/apply-phone-accessories-preset`

Aplicar preset celulares e acessórios

### `GET /fiscal/pet-shop-preset`

Preset fiscal para pet shop

### `POST /fiscal/apply-pet-shop-preset`

Aplicar preset pet shop

### `GET /fiscal/gifts-decor-preset`

Preset fiscal para presentes e decoração

### `POST /fiscal/apply-gifts-decor-preset`

Aplicar preset presentes e decoração

### `GET /fiscal/variety-preset`

Preset fiscal para variedades e bazar

### `POST /fiscal/apply-variety-preset`

Aplicar preset variedades e bazar

### `GET /fiscal/florist-preset`

Preset fiscal para floricultura

### `POST /fiscal/apply-florist-preset`

Aplicar preset floricultura

### `GET /fiscal/curtains-preset`

Preset fiscal para cortinas e persianas

### `POST /fiscal/apply-curtains-preset`

Aplicar preset cortinas e persianas

### `GET /fiscal/paint-manufacturer/preset`

Modelo fiscal da fábrica de tintas (categorias e NCM a validar)

### `GET /fiscal/paint-manufacturer/status`

Fábrica de tintas: o que há para preencher no cadastro fiscal

### `POST /fiscal/paint-manufacturer/preset/preview`

Fábrica de tintas: prévia (diff) do modelo fiscal, sem gravar (industrial, suggestStoreOptions opcionais)

### `POST /fiscal/paint-manufacturer/preset/apply`

Fábrica de tintas: aplicar/reaplicar o modelo fiscal (confirm=true; preencher vazios ou sobrescrever; industrial e acceptStoreOptions são opt-in)

### `GET /fiscal/paint-manufacturer/category-profiles`

Fábrica de tintas: perfil tributário por categoria

### `PUT /fiscal/paint-manufacturer/category-profiles/:id`

Fábrica de tintas: salvar/limpar o perfil tributário de uma categoria

### `GET /fiscal/paint-manufacturer/review`

Fábrica de tintas: revisar classificação fiscal dos produtos

### `POST /fiscal/paint-manufacturer/review/fix`

Fábrica de tintas: corrigir pendências em massa (confirm=true)

### `GET /fiscal/computer-preset`

Preset fiscal para informática

### `POST /fiscal/apply-computer-preset`

Aplicar preset informática

### `GET /fiscal/bar-preset`

Preset fiscal para bar e chopperia

### `POST /fiscal/apply-bar-preset`

Aplicar preset bar e chopperia

### `GET /fiscal/auto-parts-preset`

Preset fiscal para autopeças e moto peças

### `POST /fiscal/apply-auto-parts-preset`

Aplicar preset autopeças e moto peças

### `GET /fiscal/optics-preset`

Preset fiscal para ótica

### `POST /fiscal/apply-optics-preset`

Aplicar preset ótica

### `GET /fiscal/natural-grocery-preset`

Preset fiscal para produtos naturais e empório

### `POST /fiscal/apply-natural-grocery-preset`

Aplicar preset produtos naturais e empório

### `GET /fiscal/building-materials-preset`

Preset fiscal para material de construção

### `POST /fiscal/apply-building-materials-preset`

Aplicar preset material de construção

### `GET /fiscal/sporting-goods-preset`

Preset fiscal para surf, praia e artigos esportivos

### `POST /fiscal/apply-sporting-goods-preset`

Aplicar preset surf e artigos esportivos

### `GET /fiscal/tax-estimate`

Previsão gerencial de impostos (documentos fiscais autorizados)

Query: `month` (string) Competência AAAA-MM; `from` (string) Data inicial (AAAA-MM-DD); `to` (string) Data final (AAAA-MM-DD); `includeDocuments` (boolean) Incluir documentos da base

### `PUT /fiscal/tax-estimate/rate`

Definir alíquota estimada e recalcular o histórico de documentos

### `GET /fiscal/simples-nacional`

Status do cálculo automático da alíquota efetiva do Simples (RBT12)

### `PUT /fiscal/simples-nacional/mode`

Definir modo manual/automático, anexo e início de atividade

### `GET /fiscal/simples-nacional/history`

Histórico mensal de faturamento (SISTEMA/MANUAL)

### `PUT /fiscal/simples-nacional/history`

Lançar faturamento anterior (somente competências sem valor de sistema)

### `POST /fiscal/simples-nacional/history/:competence/adjust`

Ajuste explícito de competência gerada pelo sistema (com auditoria)

### `GET /fiscal/simples-nacional/memory`

Memória de cálculo da alíquota efetiva de uma competência

Query: `competence` (string) Competência AAAA-MM

### `GET /fiscal/reports/monthly-revenue`

Faturamento mensal

Query: `year` (integer) Ano

### `GET /fiscal/reports/sales-book`

Livro de vendas

Query: `from` (string) Data inicial; `to` (string) Data final

### `GET /fiscal/reports/sales-without-nfce`

Vendas sem documento fiscal de mercadoria

Query: `from` (string) Data inicial; `to` (string) Data final

### `GET /fiscal/reports/purchases`

Relatório de compras fiscais

Query: `from` (string) Data inicial; `to` (string) Data final

### `GET /fiscal/reports/xml-zip`

ZIP com XMLs de FiscalDocument da Sale (NFC-e/NF-e) e NF-e de devolução do período

Query: `from` (string) Data inicial; `to` (string) Data final

### `GET /fiscal/reports/numbering-gaps`

Lacunas de numeração NFC-e

Query: `series` (integer) Série

### `GET /fiscal/reports/efd`

Exportar EFD (até 31 dias)

### `POST /fiscal/reports/efd/jobs`

Enfileirar geração do EFD (ICMS/IPI ou Contribuições; perfil, inventário e saldos)

### `GET /fiscal/reports/efd/jobs`

Gerações recentes do EFD

### `GET /fiscal/reports/efd/jobs/:id`

Status da geração do EFD

### `GET /fiscal/reports/efd/jobs/:id/download`

Baixar arquivo EFD gerado

### `GET /fiscal/reports/efd/jobs/:id/validation`

Relatório da validação interna do EFD

### `GET /fiscal/reports/efd/jobs/:id/validation.csv`

Inconsistências do EFD em CSV

### `GET /fiscal/sped/prerequisites`

Checklist de pré-requisitos do SPED do período

### `GET /fiscal/sped/config`

Configuração do SPED (perfil, contador, receitas)

### `PUT /fiscal/sped/config`

Salvar configuração do SPED

### `GET /fiscal/sped/adjustments`

Ajustes de apuração do SPED

### `POST /fiscal/sped/adjustments`

Lançar ajuste de apuração (E111/E220/E530/C197)

### `DELETE /fiscal/sped/adjustments/:id`

Remover ajuste de apuração

### `POST /fiscal/certificate/validate`

Validar certificado digital

### `GET /fiscal/certificate/history`

Histórico do certificado

### `GET /fiscal/certificate/secret`

Senha do certificado (somente suporte Nive)

### `GET /fiscal/certificate/download`

Download do certificado A1 (somente suporte Nive)

### `GET /fiscal/homologation-readiness`

Prontidão para homologação

### `GET /fiscal/observability`

Observabilidade fiscal

### `POST /fiscal/homologation/xml-test`

Teste de XML (homologação)

### `POST /fiscal/homologation/emission-test`

Teste de emissão (homologação)

### `POST /fiscal/health/audit`

Analisar tributação do produto (IA, com confirmação)

### `POST /fiscal/health/audit-batch`

Auditar fiscal em lote (IA)

### `POST /fiscal/health/audit-all`

Auditar todo o cadastro fiscal (IA)

### `POST /fiscal/health/bulk-ncm-unknown/preview`

Prévia: NCM desconhecido em lote

### `POST /fiscal/health/bulk-ncm-unknown/apply`

Aplicar NCM desconhecido em lote

### `POST /fiscal/health/bulk-no-ibpt/preview`

Prévia: sem IBPT em lote

### `POST /fiscal/health/bulk-no-ibpt/apply`

Aplicar correção sem IBPT em lote

### `GET /fiscal/health/ncm-review`

Revisão de NCM

### `GET /fiscal/health/entry-ncm-alerts`

NCMs recusados em nota de entrada

### `POST /fiscal/health/ncm-review/confirm`

Confirmar revisão de NCM

### `POST /fiscal/health/ncm-review/confirm-recognized`

Confirmar NCMs reconhecidos

### `POST /fiscal/health/ncm-review/suggest`

Sugerir NCM (IA)

### `POST /fiscal/health/ncm-review/describe`

Descrever NCMs (IA)

### `POST /fiscal/health/ncm-review/apply`

Aplicar

### `GET /fiscal/gtin-health`

Saúde de GTINs

### `GET /fiscal/documents/:id/receipt-html`

Cupom HTML 80 mm (NFC-e modelo 65)

### `GET /fiscal/documents/:id/danfe-html`

DANFE HTML A4 (NFC-e 65 simplificado ou NF-e 55 completo conforme model)

### `GET /fiscal/documents/:id/xml`

Baixar XML

### `POST /fiscal/documents/:id/send-xml`

Enviar XML e PDF por e-mail

### `POST /fiscal/documents/emit-batch`

Enfileirar emissão de documentos de mercadoria em lote

### `GET /fiscal/documents/retroactive-readiness/:saleId`

Prontidão para emissão retroativa

### `POST /fiscal/documents/emit-retroactive/:saleId`

Emitir documento fiscal retroativo (NFC-e; contingência quando aplicável)

### `GET /fiscal/audit/verify`

Verificar auditoria fiscal

### `GET /fiscal/audit/logs`

Logs de auditoria fiscal

### `POST /fiscal/products/:id/confirm-st-suggestion`

Confirmar sugestão de ICMS-ST no produto

### `GET /fiscal/documents/:id/numbering-recovery`

Opções para reemitir nota rejeitada por numeração já usada (539/206)

### `POST /fiscal/documents/:id/numbering-recovery`

Reemitir em outra série/número

Corpo: `series`* (integer) Série (1–889); `number` (integer) Número (vazio = próximo livre)

### `POST /fiscal/documents/:id/numbering-recovery/adopt`

Rejeição 539 de tentativa desta venda: consultar na SEFAZ e vincular a nota autorizada

### `POST /fiscal/nfce-accreditation/request`

Solicitar à Nive o credenciamento para NFC-e (exige certificado A1 e senha salvos)

### `POST /fiscal/sync-nfe-mod55-counters`

Realinhar a numeração da NF-e modelo 55 ao maior número já gravado

### `GET /accounting-xml/settings`

Envio automático de XMLs para a contabilidade — configuração

### `PUT /accounting-xml/settings`

Configurar envio automático mensal

Corpo: `enabled` (boolean) Ligado; `accountingEmail` (string) E-mail da contabilidade; `scheduledDay` (integer) Dia do mês (1–31, padrão 1); `scheduledHour` (integer) Hora (0–23, padrão 6)

### `GET /accounting-xml/jobs`

Histórico de envios para a contabilidade

Query: `limit` (integer) Padrão 50

### `POST /accounting-xml/jobs`

Enfileirar envio manual de uma competência

Corpo: `periodMonth` (string) Competência AAAA-MM (vazio = mês anterior); `recipientEmail` (string) E-mail (vazio = da configuração)

Resposta: 201 Created

### `GET /fiscal/contingency/nfe`

NF-e modelo 55 (venda e operações fiscais) em contingência EPEC/SVC aguardando transmissão

Resposta: Lista de { kind (SALE | OPERATION), id, series, number, tpEmis, modeLabel (EPEC | SVC-AN | SVC-RS), accessKey, contingencyReason, contingencyAt, epecProtocol, epecAt, deadlineAt, hoursLeft, deadlineExpired, lastError, title, subtitle }. O prazo legal de 168 h vale só para EPEC. A NFC-e (modelo 65) fica em /contingency/pending.

### `POST /fiscal/contingency/nfe/:kind/:id/transmit`

Transmitir agora uma NF-e 55 em contingência (kind = SALE para venda ou OPERATION para operação fiscal)

Resposta: { kind, id, status, accessKey, rejectionMsg }. 400 se kind for inválido; 409 se o documento não está mais em contingência ou já está sendo transmitido; 503 se a SEFAZ ainda não aceitou (o documento continua na fila e é reenviado automaticamente).

### `GET /fiscal/documents/:id/share`

Dados para enviar a NFC-e digital ao cliente (quando a impressora não responde): link assinado do PDF e contatos do cliente da venda

Resposta: { documentId, number, storeName, shareUrl, customerName, customerPhone, customerEmail }. O link vale 30 dias e abre sem login. 400 se o documento não está autorizado nem em contingência; 404 se não existe.

### `POST /fiscal/issuer-completion/preview`

Comparar o cadastro do emitente com a Receita Federal e o cadastro estadual do CNPJ do certificado; não grava nada

Resposta: { status: "ready", cnpj, fields (valor atual x oficial de cada campo), stateRegistrations, ieChoices, crt (apenas sugestão), cnae (oficiais e sugestão) }. Sem certificado A1, com certificado vencido ou com CNPJ divergente do cadastro devolve status "blocked" ou "conflict" com message. 502 se a consulta oficial falhar.

### `POST /fiscal/issuer-completion/apply`

Gravar no emitente só os campos aceitos no comparativo (o valor gravado vem da consulta oficial, nunca do corpo da requisição)

Corpo: `fields`* (string[]) Campos aceitos: cnpj, legalName, addressLine1, addressNumber, addressDistrict, zipCode, city, state, ibgeCityCode, stateRegistration; `stateRegistration` (string) Inscrição estadual escolhida quando há mais de uma ativa (até 30 caracteres); `cnae` (string) CNAE a gravar, entre os oficiais do CNPJ (até 10 caracteres)

Resposta: { applied, cnaeApplied, readiness }. O CRT nunca é alterado por este endpoint. 400/409 se o certificado ou o CNPJ não permitem; 502 se a consulta oficial falhar.

### `POST /fiscal/nfce-accreditation/complete`

Marcar (ou remover) a autorização de emissão NFC-e da loja após o credenciamento (somente suporte Nive)

Corpo: `authorized` (boolean) true autoriza (padrão); false remove a autorização

## Fiscal (NFS-e)

Emissão de Nota Fiscal de Serviços (Sistema Nacional). Use API key com permissões nfse.view / nfse.manage e header X-Store-Slug.

### `GET /nfse/readiness`

Prontidão para emissão (certificado, IM, IBGE, etc.)

### `GET /nfse/emit-prep`

Pendências para emitir NFS-e de uma OS

Query: `serviceOrderId` (uuid) ID da ordem de serviço; `tomadorKind` (string) PF | PJ | EXTERIOR | UNIDENTIFIED — UNIDENTIFIED não exige CPF/CNPJ do cliente da OS

Resposta: Checklist estruturado da loja/OS/cliente

### `GET /nfse`

Listar NFS-e

Query: `status` (string) DRAFT | PENDING | AUTHORIZED | REJECTED | CANCELLED | MANUAL; `serviceOrderId` (uuid) Filtrar por OS; `receivableId` (uuid) Filtrar por conta a receber; `page` (number) Página (default 1); `pageSize` (number) Itens por página (max 100)

Resposta: { items, total, page, pageSize }

### `GET /nfse/:id`

Detalhe da NFS-e

### `GET /nfse/:id/xml`

Baixar XML da NFS-e autorizada

### `POST /nfse/emit`

Emitir NFS-e a partir de OS concluída/faturada ou de venda do PDV

Corpo: `serviceOrderId` (uuid) ID da ordem de serviço (ou receivableId / saleId); `saleId` (uuid) ID da venda do PDV com itens de serviço — vincula também a OS se a venda veio de to-sale; `receivableId` (uuid) ID da conta a receber vinculada à OS; `notes` (string) Observações internas; `tomadorKind` (string) PF | PJ | EXTERIOR | UNIDENTIFIED — UNIDENTIFIED omite o tomador na DPS; `dryRun` (boolean) true = gera DPS assinada sem enviar à Receita

Resposta: 201 — documento fiscal (status PENDING/AUTHORIZED/DRAFT)

### `POST /nfse/emit-direct`

Emitir NFS-e autônoma (sem OS) — ideal para outro sistema

Corpo: `tomadorKind` (string) PF | PJ | EXTERIOR | UNIDENTIFIED — com UNIDENTIFIED não envie tomador; `tomador` (object) { name, taxId, taxIdType?, email?, phone?, endereço? } — omitir se tomadorKind=UNIDENTIFIED; `servico`* (object) { cTribNac, descricao, cNBS?, codMunicipioPrestacao? }; `valores`* (object) { vServ, aliqIss?, issRetained? }; `externalReference` (string) Referência do sistema externo (salva em notes); `notes` (string) Observações; `dryRun` (boolean) true = não envia à Receita

Resposta: 201 — documento fiscal sem serviceOrderId

### `POST /nfse/emit-with-order`

Fluxo completo: cria/localiza cliente + OS concluída + emite NFS-e

Corpo: `tomadorKind` (string) PF | PJ | EXTERIOR | UNIDENTIFIED — UNIDENTIFIED omite o tomador na DPS; `customer`* (object) { customerId } OU { name, taxId, email?, phone?, zipCode?, addressLine1?, addressNumber?, addressDistrict?, city?, state?, createIfMissing? }; `items`* (array) [{ variantId, quantity, unitPrice? }] — variantes de produtos tipo SERVICE; `description` (string) Descrição da OS; `notes` (string) Observações; `dryRun` (boolean) true = não envia à Receita; `createReceivable` (boolean) Gerar conta a receber (respeita config da loja)

Resposta: 201 — { serviceOrderId, document }

### `POST /nfse/manual`

Registrar NFS-e emitida fora do sistema

Corpo: `serviceOrderId` (uuid) OS vinculada (ou receivableId); `receivableId` (uuid) Conta a receber vinculada; `number` (number) Número da nota; `series` (string) Série; `verificationCode` (string) Código de verificação; `xmlContent` (string) XML da nota; `pdfStorageKey` (string) Chave de armazenamento do PDF; `issuedAt` (string) Data/hora de emissão (ISO); `notes` (string) Observações; `status` (string) MANUAL | AUTHORIZED

### `POST /nfse/:id/retry`

Reenviar NFS-e rejeitada/pendente (somente com OS vinculada)

### `POST /nfse/:id/cancel`

Cancelar NFS-e autorizada

Corpo: `reason` (string) Justificativa; `reasonCode` (string) 1=erro emissão | 2=serviço não prestado | 9=outros

### `GET /nfse/:id/danfse`

PDF do DANFSe (application/pdf)

### `GET /nfse/:id/danfse-preview`

Prévia do DANFSe

### `GET /nfse/:id/consulta-publica`

URL oficial de consulta pública (portal nacional)

### `GET /nfse/:id/logs`

Histórico de eventos da NFS-e

### `POST /nfse/:id/consultar`

Consultar a situação na prefeitura/portal e atualizar

### `POST /nfse/:id/send-email`

Enviar a NFS-e por e-mail

Corpo: `email`* (string) E-mail

### `POST /nfse/:id/substituir`

Emitir NFS-e substituta

Corpo: `reasonCode` (string) 01, 02, 03, 04, 05 ou 99; `reason` (string) Motivo; `tomadorKind` (string) Tipo de tomador; `competenceDate` (string) Competência; `notes` (string) Observações

Resposta: 201 Created

### `GET /nfse/reports/xml-zip`

ZIP com os XMLs das NFS-e do período

Query: `from` (string) Data inicial; `to` (string) Data final

### `POST /nfse/op-simp-nac`

Corrigir a situação da loja no Simples Nacional da NFS-e (erro E0160) e, se pedido, reenviar a nota rejeitada

Corpo: `value`* (string) NAO_OPTANTE | MEI | ME_EPP; `retryId` (string) Id da NFS-e rejeitada a reenviar logo após a correção

Resposta: { ok: true, value, retried } (retried é a NFS-e reenviada ou null). NAO_OPTANTE também ajusta o CRT da loja para 3; os demais valores tiram o CRT 3.

### `GET /nfse/standalone-migration`

Quantas NFS-e da tela antiga (sem venda/OS) ainda aguardam migração para vendas

Resposta: { pending }

### `POST /nfse/standalone-migration`

Converter as NFS-e avulsas em vendas com a nota anexada

Corpo: `dryRun` (boolean) true só simula, sem gravar

Resposta: { dryRun, pending, migrated, skipped, items: [{ documentId, number, status, outcome (migrated | skipped), saleId, reason }] }

## Cadastro NCM

### `GET /fiscal-ncm`

Listar NCM

### `POST /fiscal-ncm`

Criar NCM

Corpo: `code`* (string) Código NCM (8 dígitos); `description` (string) Descrição

Resposta: 201 Created

### `PATCH /fiscal-ncm/:id`

Atualizar NCM

Corpo: `code`* (string) Código NCM (8 dígitos); `description` (string) Descrição

### `DELETE /fiscal-ncm/:id`

Excluir NCM

Resposta: 204 No Content

## Cadastro CEST

### `GET /fiscal-cest`

Listar CEST

### `POST /fiscal-cest`

Criar CEST

Corpo: `code`* (string) Código CEST; `description` (string) Descrição

Resposta: 201 Created

### `PATCH /fiscal-cest/:id`

Atualizar CEST

Corpo: `code`* (string) Código CEST; `description` (string) Descrição

### `DELETE /fiscal-cest/:id`

Excluir CEST

Resposta: 204 No Content

## Tabela IBPT

### `GET /ibpt`

Listar entradas IBPT

Query: `q` (string) Busca por NCM

### `POST /ibpt`

Criar entrada IBPT

Corpo: `ncm`* (string) NCM; `description` (string) Descrição; `federal` (number) Alíquota federal %; `state` (number) Alíquota estadual %

### `PATCH /ibpt/:id`

Atualizar entrada

Corpo: `federal` (number) Alíquotas

### `DELETE /ibpt/:id`

Excluir entrada

Resposta: 204 No Content

### `POST /ibpt/import`

Importar tabela IBPT (CSV)

Corpo: `csv`* (string) Conteúdo CSV

### `POST /ibpt/import-official`

Enfileirar importação da tabela IBPT oficial

Corpo: `uf` (string) UF (opcional; padrão: UF da loja)

Resposta: 202 — job enfileirado (status PENDING). Acompanhe em GET /ibpt/import-jobs

### `GET /ibpt/import-jobs`

Listar jobs de importação IBPT da loja

### `GET /ibpt/import-jobs/:id`

Detalhe de um job de importação IBPT

### `DELETE /ibpt/import-jobs/failed`

Remover todas as importações IBPT com falha

Resposta: 200 — { deleted: number }

### `DELETE /ibpt/import-jobs/:id`

Remover uma importação IBPT com falha

Resposta: 204 — só jobs com status FAILED

## Regras tributárias

### `GET /tax-rules`

Listar regras tributárias (por NCM)

### `POST /tax-rules`

Criar regra

Corpo: `ncm`* (string) NCM (8 dígitos); `description` (string) Descrição; `cfop` (string) CFOP (padrão 5102); `icmsCsosn` (string) CSOSN (padrão 102); `icmsCst` (string) CST ICMS; `pisCst` (string) CST PIS; `cofinsCst` (string) CST COFINS; `origin` (integer) Origem da mercadoria (0–8); `cest` (string) CEST; `active` (boolean) Ativa

### `PATCH /tax-rules/:id`

Atualizar regra (NCM não pode ser alterado)

Corpo: `description` (string) Descrição; `cfop` (string) CFOP; `icmsCsosn` (string) CSOSN; `icmsCst` (string) CST ICMS; `pisCst` (string) CST PIS; `cofinsCst` (string) CST COFINS; `origin` (integer) Origem; `cest` (string) CEST; `active` (boolean) Ativa

### `DELETE /tax-rules/:id`

Excluir regra

Resposta: 204 No Content

### `POST /tax-rules/:id/apply`

Aplicar a regra a todos os produtos com o mesmo NCM

### `GET /tax-rules/seed-profiles`

Ramos disponíveis para importar NCM

### `POST /tax-rules/seed-defaults`

Importar NCM e regras do ramo

## Regras fiscais parametrizadas (ICMS-ST)

Motor de regras por UF, NCM, CEST e tipo de operação. Complementa o cadastro simples em /tax-rules.

### `GET /fiscal-rules`

Listar regras fiscais

### `GET /fiscal-rules/:id`

Detalhe da regra

### `POST /fiscal-rules`

Criar regra fiscal

Corpo: `originUf` (string) UF de origem; `destUf` (string) UF de destino; `ncm` (string) NCM; `cest` (string) CEST; `operationType` (string) SALE_INTERNAL | SALE_INTERSTATE | RETURN | ENTRY | …; `stSituation` (string) NOT_SUBJECT | SUBJECT | RETAINED_PREVIOUSLY | …; `cfop` (string) CFOP; `icmsCsosn` (string) CSOSN; `icmsCst` (string) CST ICMS; `active` (boolean) Ativa

Resposta: 201 Created

### `PATCH /fiscal-rules/:id`

Atualizar regra fiscal

### `DELETE /fiscal-rules/:id`

Desativar regra fiscal

Resposta: 204 No Content

## Alíquotas internas por UF

Alíquota interna de ICMS e FCP por estado, usadas no cálculo de DIFAL e operações interestaduais.

### `GET /fiscal-uf-rates`

Listar alíquotas cadastradas

### `PUT /fiscal-uf-rates/:uf`

Criar ou atualizar a alíquota da UF

Corpo: `internalRate`* (number) Alíquota interna (%); `fcpRate` (number) FCP (%); `notes` (string) Observações

### `DELETE /fiscal-uf-rates/:uf`

Remover alíquota da UF

Resposta: 204 No Content

## Contas a pagar

Despesas e pagamentos a fornecedores.

### `GET /payables/summary`

Resumo

### `GET /payables/installments/summary`

Resumo

### `GET /payables/installments`

Listar parcelas

### `GET /payables`

Listar contas a pagar

### `POST /payables/suppliers/:supplierId/payments`

Registrar pagamento / baixa

### `GET /payables/:id`

Detalhe

### `POST /payables`

Criar conta a pagar

### `PATCH /payables/:id`

Atualizar

### `POST /payables/:id/payments`

Registrar pagamento / baixa

### `POST /payables/:id/cancel`

Cancelar (body opcional: { reason })

### `DELETE /payables/:id/payments/:paymentId`

Excluir baixa/pagamento

### `DELETE /payables/:id`

Excluir

### `GET /payables/alerts`

Alertas de contas a pagar (vencidas, a vencer, aguardando aprovação)

### `POST /payables/:id/approve`

Aprovar conta (exige permissão de aprovar)

### `POST /payables/:id/reject`

Reprovar conta (exige permissão de aprovar)

Corpo: `reason` (string) Motivo

### `POST /payables/batch-payments`

Pagar várias contas de uma vez

Corpo: `items`* (array) [{ payableId, installmentId, amount, lateFeeAmount, interestAmount, discountAmount }]; `paidAt` (string) Data do pagamento; `paymentMethodId` (string) Forma de pagamento; `financialAccountId` (string) Conta financeira; `notes` (string) Observações

### `POST /payables/:id/attachments`

Anexar comprovante/boleto (multipart, campo file, até 10 MB)

Corpo: `file`* (file) Arquivo; `paymentId` (uuid) Pagamento ao qual o anexo pertence

Resposta: 201 Created

### `GET /payables/:id/attachments/:attachmentId/url`

URL temporária para baixar o anexo

### `DELETE /payables/:id/attachments/:attachmentId`

Excluir anexo

Resposta: 204 No Content

### `POST /payables/batch-cancel`

Cancelar várias contas a pagar de uma vez

Corpo: `ids`* (uuid[]) Contas a cancelar (1 a 200); `reason` (string) Motivo (até 500)

Resposta: { requested, succeeded, failed }. Cada conta é processada na própria transação: failed lista { id, description, message } (conta já paga, já cancelada, não encontrada...) sem desfazer as demais.

### `POST /payables/batch-delete`

Excluir várias contas a pagar de uma vez

Corpo: `ids`* (uuid[]) Contas a excluir (1 a 200)

Resposta: { requested, succeeded, failed } — mesmo formato do cancelamento em lote; falhas individuais não derrubam as demais.

## Cartões de crédito da empresa

Cartões corporativos usados para pagar despesas. Cada compra gera parcelas nas faturas conforme o dia de fechamento; a fatura é paga como uma conta a pagar.

### `GET /credit-cards`

Listar cartões

Query: `q` (string) Busca por apelido, titular ou final; `active` (boolean) true / false

### `POST /credit-cards`

Cadastrar cartão

Corpo: `name`* (string) Apelido do cartão; `holderName`* (string) Titular; `institution` (string) Banco / emissor; `brand` (string) Bandeira; `last4` (string) Últimos dígitos; `closingDay`* (integer) Dia de fechamento (1–31); `dueDay`* (integer) Dia de vencimento (1–31); `bestPurchaseDay` (integer) Melhor dia de compra; `closingPolicy` (string) CURRENT_INVOICE ou NEXT_INVOICE (compra no dia do fechamento); `notes` (string) Observações; `active` (boolean) Ativo; `locationId` (string) Filial

Resposta: 201 Created

### `GET /credit-cards/:id`

Detalhe do cartão

### `PATCH /credit-cards/:id`

Atualizar cartão (campos parciais)

### `DELETE /credit-cards/:id`

Excluir cartão

### `GET /credit-cards/:id/purchases`

Compras lançadas no cartão

### `POST /credit-cards/purchases/preview`

Simular parcelas e faturas de uma compra (não grava)

### `POST /credit-cards/purchases`

Lançar compra no cartão

Corpo: `creditCardId`* (uuid) Cartão; `purchasedAt`* (string) Data da compra; `description`* (string) Descrição; `totalAmount`* (number) Valor total; `installmentCount`* (integer) Parcelas (1–48); `supplierId` (uuid) Fornecedor; `documentNumber` (string) Número do documento; `categoryId` (string) Categoria financeira; `costCenterId` (string) Centro de custo

Resposta: 201 Created

### `GET /credit-cards/purchases/:purchaseId`

Detalhe da compra

### `PATCH /credit-cards/purchases/:purchaseId`

Editar compra (mesmo corpo do lançamento)

### `POST /credit-cards/purchases/:purchaseId/cancel`

Cancelar compra

Corpo: `reason` (string) Motivo

### `GET /credit-cards/invoices/:invoiceId`

Detalhe da fatura

### `POST /credit-cards/invoices/:invoiceId/pay`

Pagar fatura

Corpo: `amount`* (number) Valor pago; `…` (—) Demais campos iguais ao pagamento de conta a pagar (data, conta, juros, multa, desconto)

## Devoluções de venda

Trocas/devoluções de clientes e NF-e de devolução.

### `GET /sale-returns`

Listar devoluções de venda

### `GET /sale-returns/:id`

Detalhe

### `POST /sale-returns/:id/cancel`

Cancelar

Corpo: `storeCreditAction` ("cancel" | "keep") Vale-troca da devolução: cancelar junto (padrão) ou manter valendo

### `PATCH /sale-returns/:id`

Atualizar

### `DELETE /sale-returns/:id`

Excluir

Corpo: `username` (string) Usuário administrador; `password` (string) Senha do administrador; `storeCreditAction` ("cancel" | "keep") Vale-troca da devolução: excluir junto (padrão) ou manter valendo

### `POST /sale-returns/:id/emit-nfe`

Emitir NF-e de devolução

Corpo: `taxId` (string) CPF/CNPJ do cliente, quando ainda falta no cadastro

### `POST /sale-returns/:id/return-nfe/cancel`

Cancelar NF-e de devolução na SEFAZ (até 24h após a autorização)

Corpo: `justification` (string) Justificativa com 15 a 255 caracteres

### `GET /sale-returns/:id/return-nfe/xml`

Baixar XML

### `GET /sale-returns/:id/return-nfe/pdf`

Baixar PDF

### `GET /sale-returns/:id/return-nfe/danfe-html`

DANFE em HTML (autorizada ou cancelada)

### `POST /sale-returns`

Criar devolução de venda

### `GET /sale-returns/ref-lookup`

Devolução avulsa: verifica se a chave da nota de venda é de uma venda deste sistema

Query: `accessKey` (string) Chave de acesso da NF-e/NFC-e de venda (44 dígitos; pontuação é ignorada)

Resposta: { accessKey, found, sale, previousReturns }. sale traz id, orderNumber, status, completedAt, customerName, documentModel e documentStatus quando found é true. previousReturns lista as devoluções avulsas já lançadas para a mesma chave (id, returnNumber, totalAmount, processedAt) para evitar devolver duas vezes.

### `POST /sale-returns/standalone`

Devolução avulsa de venda feita em outro sistema, informando só a chave da nota de venda

Corpo: `refAccessKey`* (string) Chave de acesso (44 dígitos válidos) da NF-e (55) ou NFC-e (65) de venda original; `confirmSaleNotFound`* (boolean) Deve ser true: confirma que a venda não está neste sistema; `items`* (array) [{ variantId, quantity, unitPrice }] — de 1 a 200 itens; unitPrice maior que zero; `customerId` (uuid) Cliente (opcional; vira o destinatário da NF-e de devolução e o titular do vale-troca); `locationId` (uuid) Filial da devolução (padrão: filial de operação do usuário); `reason` (string) Motivo (até 500); `restockItems` (boolean) Devolver os itens ao estoque (padrão true); `issueStoreCredit` (boolean) Gerar vale-troca para o cliente (padrão false); `emitReturnNfe` (boolean) Emitir NF-e de devolução referenciando a nota original (padrão true)

Resposta: 201 Created — a devolução registrada (id, returnNumber, totalAmount...).

## Devoluções de compra

Devoluções a fornecedores e NF-e de retorno.

### `GET /purchase-returns`

Listar devoluções de compra

### `POST /purchase-returns/parse-source-xml`

Ler XML da NF-e de entrada na devolução avulsa, sem criar compra

Corpo: `xml` (string) Conteúdo do XML da NF-e

### `GET /purchase-returns/:id`

Detalhe

### `POST /purchase-returns/:id/cancel`

Cancelar

### `POST /purchase-returns/:id/emit-nfe`

Emitir NF-e de devolução

### `POST /purchase-returns/:id/return-nfe/cancel`

Cancelar NF-e de devolução na SEFAZ (até 24h após a autorização)

Corpo: `justification` (string) Justificativa com 15 a 255 caracteres

### `POST /purchase-returns/:id/return-nfe/reconcile`

Consultar na SEFAZ se a NF-e já foi autorizada, sem reenviar

### `GET /purchase-returns/:id/return-nfe/xml`

Baixar XML

### `GET /purchase-returns/:id/return-nfe/pdf`

Baixar PDF

### `GET /purchase-returns/:id/return-nfe/danfe-html`

DANFE em HTML (autorizada ou cancelada)

### `POST /purchase-returns/return-nfe/bulk-download`

Baixar em ZIP o DANFE (PDF) ou o XML de várias devoluções

Corpo: `ids` (string[]) Devoluções selecionadas (máximo 50); `kind` ("pdf" | "xml") O que baixar de cada NF-e

### `POST /purchase-returns/:id/return-nfe/send-email`

Enviar XML e DANFE por e-mail ao fornecedor

Corpo: `email` (string) E-mail de destino

### `POST /purchase-returns`

Criar devolução de compra

Corpo: `purchaseId` (string) Compra recebida de origem; `items` (array) [{ purchaseItemId, quantity }]; `reason` (string) Motivo da devolução (opcional); `emitPurchaseReturnNfe` (boolean) Emite NF-e modelo 55 de devolução referenciando a NF-e de entrada; `adjustPayable` (boolean) Abate o valor devolvido do saldo em aberto da conta a pagar da compra (últimas parcelas primeiro)

### `POST /purchase-returns/standalone`

Criar devolução avulsa (sem compra lançada)

Corpo: `supplierId` (string) Fornecedor que vai receber a mercadoria; `items` (array) [{ variantId, quantity, unitCost }] — unitCost opcional usa o custo do produto; `refAccessKey` (string) Chave (44 dígitos) da NF-e de entrada, quando existir. Sem ela a NF-e sai sem refNFe; `reason` (string) Motivo da devolução (opcional); `emitPurchaseReturnNfe` (boolean) Emite NF-e modelo 55 de devolução para o fornecedor

### `GET /purchase-returns/source-nfe`

Buscar a NF-e de compra de origem pela chave

Query: `accessKey` (string) Chave de acesso (44 dígitos)

Resposta: { found, accessKey, emitCrt, items }

### `GET /purchase-returns/drafts`

Listar rascunhos de devolução

### `POST /purchase-returns/drafts`

Salvar rascunho de devolução (fornecedor, chave de referência, itens)

Resposta: 201 Created

### `PUT /purchase-returns/drafts/:id`

Atualizar rascunho

### `DELETE /purchase-returns/drafts/:id`

Excluir rascunho

Resposta: 204 No Content

### `POST /purchase-returns/drafts/:id/finalize`

Finalizar rascunho em devolução

Corpo: `emitPurchaseReturnNfe` (boolean) Emitir NF-e de devolução (padrão false)

### `GET /purchase-returns/ref-nfe-suggestions`

Sugerir a NF-e de entrada a referenciar numa devolução avulsa, a partir do fornecedor e das variantes devolvidas

Query: `supplierId` (uuid) Fornecedor (omitido = qualquer); `variantIds` (string) Variantes devolvidas, separadas por vírgula (até 200)

Resposta: Lista (até 50) de compras recebidas que trouxeram essas variantes: purchaseId, purchaseReference, accessKey (44 dígitos), invoiceNumber, invoiceSeries, issueDate, receivedAt, totalAmount e supplierName. Sem variantIds retorna lista vazia.

### `GET /purchase-returns/:id/ref-nfe-suggestions`

Sugerir a NF-e de entrada para uma devolução já registrada (usa o fornecedor e os itens dela)

Resposta: Mesmo formato de /purchase-returns/ref-nfe-suggestions. 404 se a devolução não existe.

### `PUT /purchase-returns/:id/ref-access-key`

Referenciar ou corrigir a NF-e de entrada de uma devolução avulsa antes de (re)emitir a NF-e de devolução

Corpo: `refAccessKey`* (string) Chave de acesso da NF-e de entrada (44 dígitos, modelo 55; pontuação é ignorada)

Resposta: O detalhe da devolução. 400 se a chave é inválida, não é modelo 55, não confere com o CNPJ do fornecedor, a devolução é vinculada a uma compra (corrija na compra), não está registrada ou a NF-e de devolução já foi autorizada; 409 se estiver em processamento na SEFAZ.

## Créditos de loja

Vales / créditos de cliente.

### `GET /store-credits`

Listar créditos de loja

Query: `q` (string) Código ou cliente (busca fuzzy / tolerante a erros); `status` (string) ACTIVE | FULLY_USED | EXPIRED | CANCELLED; `sortBy` (string) code | customer | originalAmount | balance | expiresAt | used | status | issuedAt; `sortDir` (string) asc | desc; `page` (number) Página (padrão 1); `pageSize` (number) Itens por página (padrão 20)

### `GET /store-credits/lookup`

Consultar crédito (lookup)

### `GET /store-credits/summary`

Resumo de vales ativos (saldo e a vencer)

### `GET /store-credits/:id`

Detalhe

### `POST /store-credits`

Criar crédito de loja

### `POST /store-credits/:id/cancel`

Cancelar

### `PATCH /store-credits/:id`

Atualizar

### `DELETE /store-credits/:id`

Excluir

## Caixas (cadastro)

### `GET /cash-registers`

Listar caixas

### `GET /cash-registers/:id`

Detalhe

### `POST /cash-registers`

Criar caixa

### `PATCH /cash-registers/:id`

Atualizar

### `DELETE /cash-registers/:id`

Excluir

## Sessões de caixa

Abertura, movimentos e fechamento.

### `GET /cash-sessions/active`

Sessão de caixa ativa

### `GET /cash-sessions/suggested-opening`

Sugestão de saldo inicial (último fechamento)

### `GET /cash-sessions`

Listar sessões de caixa

### `GET /cash-sessions/:id`

Detalhe

### `POST /cash-sessions/open`

Abrir sessão de caixa

### `POST /cash-sessions/:id/movements`

Registrar lançamento de caixa

### `POST /cash-sessions/:id/close`

Fechar sessão de caixa

### `GET /cash-sessions/movements`

Histórico de movimentações de caixa (todas as sessões)

Query: `from` (string) Data inicial; `to` (string) Data final; `registerId` (uuid) Caixa; `userId` (uuid) Operador; `sessionId` (uuid) Sessão; `type` (string) Tipo da movimentação; `status` (string) ACTIVE ou CANCELLED; `page` (integer) Página; `pageSize` (integer) Itens por página (até 100)

### `GET /cash-sessions/:id/movements`

Movimentações da sessão

Query: `status` (string) ACTIVE, CANCELLED ou all; `type` (string) Tipo da movimentação

### `GET /cash-sessions/:id/summary`

Resumo da sessão (totais por forma de pagamento)

### `GET /cash-sessions/:id/report`

Relatório de fechamento

### `POST /cash-sessions/:id/adjustments`

Lançar ajuste de caixa

Corpo: `type`* (string) ADJUSTMENT_IN ou ADJUSTMENT_OUT; `amount`* (number) Valor; `notes`* (string) Motivo

Resposta: 201 Created

### `POST /cash-sessions/:id/reopen`

Reabrir sessão fechada

Corpo: `reason` (string) Motivo

### `POST /cash-sessions/movements/:movementId/cancel`

Cancelar movimentação (sangria, suprimento, ajuste)

Corpo: `reason` (string) Motivo

### `GET /cash-sessions/treasury-accounts`

Contas (cofre, banco...) que podem receber a sangria ou o valor retirado no fechamento

Resposta: Lista de contas ativas: id, name, type e defaultCashDestination.

### `POST /cash-sessions/:id/transfer`

Transferir dinheiro entre dois caixas abertos (sangria na origem + suprimento no destino)

Corpo: `targetSessionId`* (uuid) Sessão de caixa de destino (diferente da origem); `amount`* (number) Valor maior que zero; `notes` (string) Motivo (até 500); `allowOverdraft` (boolean) Permite deixar o caixa de origem negativo; só vale para quem tem a permissão cash.authorize_difference

Resposta: 201 Created — o detalhe da sessão de origem. Exige a permissão cash.manage. 400 se as duas sessões não estão abertas ou reabertas, ou se são de filiais diferentes; saldo insuficiente na origem também é recusado, salvo allowOverdraft autorizado.

## Tabelas de preço

### `GET /price-lists`

Listar tabelas de preço

### `POST /price-lists`

Criar tabela de preço

### `GET /price-lists/:id`

Detalhe

### `PATCH /price-lists/:id`

Atualizar

### `DELETE /price-lists/:id`

Excluir

### `GET /price-lists/:id/items`

Listar itens da tabela

### `PUT /price-lists/:id/items/:productId`

Definir preço do produto

### `DELETE /price-lists/:id/items/:productId`

Remover produto da tabela

### `PATCH /price-lists/:id/bulk-price`

Ajustar preços em lote

## Fila de etiquetas

### `GET /label-print-queue`

Obter fila de etiquetas

### `PUT /label-print-queue`

Atualizar fila de etiquetas

### `GET /dispatch-label-history`

Histórico compartilhado das etiquetas rápidas

### `POST /dispatch-label-history`

Registrar impressão ou remoção de bolsas

### `POST /dispatch-label-history/import`

Importar histórico local se o banco estiver vazio

### `DELETE /dispatch-label-history`

Limpar o histórico da loja

### `GET /dispatch-label-users`

Lista de confiança dos @ das etiquetas rápidas

### `PUT /dispatch-label-users`

Anotar ou atualizar confiança de um @

### `POST /dispatch-label-users/import`

Incluir anotações locais na lista da loja

### `DELETE /dispatch-label-users/:id`

Remover um @ da lista de confiança

## Agente de impressão

### `PATCH /print/settings`

Habilitar/desabilitar agente na loja

### `GET /print/agents/status`

Status do ponto de impressão

### `POST /print/agents/heartbeat`

Heartbeat do agente Windows

### `GET /print/agents`

Listar agentes

### `POST /print/jobs/claim`

Reivindicar próximo job

### `POST /print/jobs/:id/complete`

Marcar job como impresso

### `POST /print/jobs/:id/fail`

Marcar job como falho

### `GET /print/jobs`

Listar jobs recentes

### `GET /print/jobs/:id`

Prévia do job

### `POST /print/jobs`

Enfileirar job (HTML/PDF)

### `POST /print/jobs/:id/cancel`

Cancelar job

### `POST /print/jobs/:id/requeue`

Reenfileirar job

### `DELETE /print/jobs/:id`

Excluir job

### `PATCH /auth/me/print-target`

Preferência de impressão (LOCAL|AGENT e folha A4)

### `PATCH /auth/me/ui-preferences`

Preferências de interface (favoritos da navegação)

## Cobranças PIX

### `POST /pix/charges`

Cobranças

### `GET /pix/charges/:id/status`

Status da cobrança PIX

### `DELETE /pix/charges/:id`

Excluir

## Pagamentos com cartão (online)

### `POST /card-payments`

Criar pagamento com cartão

### `GET /card-payments/:id/status`

Status

### `DELETE /card-payments/:id`

Excluir

## Operações de maquininha

### `GET /maquininha-operacoes/report`

Obter relatório

### `GET /maquininha-operacoes`

Listar operações de maquininha

### `GET /maquininha-operacoes/:id/status`

Status

### `GET /maquininha-operacoes/:id`

Detalhe

### `POST /maquininha-operacoes`

Criar operação de maquininha

### `DELETE /maquininha-operacoes/:id`

Excluir

## Inteligência artificial

Recursos de IA da loja. A chave e o modelo vêm do admin Nive.

### `POST /ai/help/ask`

Perguntar sobre uso do sistema (IA)

### `POST /ai/reviews/analyze-pending`

Classificar avaliações e gerar análise (IA)

### `POST /ai/reviews/:id/analyze`

Analisar avaliação (IA)

### `GET /ai/reviews/summary`

Resumo de sentimento e temas

### `POST /ai/purchase/:purchaseId/suggest-matches`

Sugerir vínculos de compra (IA)

### `POST /ai/fiscal/suggest`

Sugerir dados fiscais (IA)

### `POST /ai/fiscal/suggest-batch`

Sugerir fiscal em lote (IA)

### `POST /ai/fiscal/audit`

Auditar fiscal (IA)

### `POST /ai/fiscal/audit-batch`

Auditar fiscal em lote (IA)

### `POST /ai/fiscal/audit-all`

Auditar todo o cadastro fiscal (IA)

### `POST /ai/reports/ask`

Perguntar ao Assistente Nive (dados da loja e uso)

### `GET /ai/reports/insights`

Insights de relatórios (IA)

### `GET /ai/dashboard/analyze`

Analisar dashboard (IA)

### `POST /ai/dashboard/analyze`

Analisar dashboard (IA)

### `POST /ai/pos/search`

Buscar

### `POST /ai/pos/tts`

Gerar áudio da leitura do produto no PDV

### `GET /ai/stock/predictions`

Previsão de estoque (IA)

### `POST /ai/products/enrich`

Enriquecer produto (IA)

### `POST /ai/products/title-suggest`

Sugerir títulos comerciais do produto (IA)

### `POST /ai/purchases/variant-suggest`

Sugerir tamanho e cor das linhas da compra (IA)

### `POST /ai/products/category-suggest`

Sugerir categoria do produto (IA)

### `POST /ai/products/fiscal-suggest`

Sugerir dados fiscais do produto (IA)

### `POST /ai/collection/suggest`

Sugerir mensagem de cobrança (IA)

### `POST /ai/collection/send-to-seller`

Enviar rascunho de cobrança ao WhatsApp do vendedor

## Configurações de IA

### `GET /ai-settings/features`

Obter features disponíveis

### `GET /ai-settings`

Obter configurações de IA

### `PUT /ai-settings`

Atualizar configurações de IA

### `POST /ai-settings/test`

Testar provedor de IA

## Assistente

### `GET /assistant/status`

Status

### `POST /assistant/ask`

Perguntar como usar o sistema (IA)

## Envios / frete

### `GET /shipments`

Listar envios (status, provedor, busca, filas de rastreio)

### `POST /shipments`

Criar envio

### `POST /shipments/quote`

Cotação de frete

### `POST /shipments/manual`

Criar envio manual

### `GET /shipments/superfrete/account`

Saldo e dados da carteira SuperFrete

### `GET /shipments/superfrete/orders`

Listar etiquetas da conta SuperFrete

### `POST /shipments/superfrete/print`

Gerar PDF de impressão (uma ou várias etiquetas)

### `GET /shipments/:id`

Detalhe

### `PATCH /shipments/:id/tracking-code`

Atualizar tracking-code

### `POST /shipments/:id/purchase`

Comprar etiqueta / frete

### `GET /shipments/:id/label`

Obter / renovar PDF da etiqueta

### `GET /shipments/:id/tracking`

Listar / obter tracking

### `POST /shipments/:id/notify-tracking`

Notificar rastreio

### `POST /shipments/:id/cancel`

Cancelar

## Configurações de frete

### `GET /shipping-settings/status`

Status

### `GET /shipping-settings`

Obter configurações de frete

### `PUT /shipping-settings`

Atualizar configurações de frete

### `GET /shipping-settings/authorize`

Gerar a URL de autorização OAuth do Melhor Envio

### `POST /shipping-settings/test`

Testar conexão com o provedor de frete

### `POST /shipping-settings/disconnect`

Desconectar Melhor Envio

### `GET /shipping-settings/oauth/callback`

Callback OAuth Melhor Envio (público)

## E-commerce (Loja Integrada, Nuvemshop e Mercado Livre)

Configuração e sincronização das lojas virtuais integradas: produtos, preços, estoque e pedidos. Exige a permissão de e-commerce e plano com acesso à API. Troque `:provider` por `loja-integrada` ou `nuvemshop`; o Mercado Livre tem rotas próprias (conexão por OAuth, feita pela tela do ERP).

### `GET /ecommerce`

Listar integrações e situação de cada uma

### `GET /ecommerce/:provider`

Configuração da integração (credenciais nunca retornam, só hasAccessToken/hasApiKey)

### `PUT /ecommerce/:provider`

Salvar configuração

Corpo: `accessToken` (string) Token de acesso da plataforma; `apiKey` (string) Chave de API (Loja Integrada); `appKey` (string) Chave de aplicação (Loja Integrada); `storeId` (string) ID da loja (Nuvemshop); `syncProducts` (boolean) Enviar produtos; `syncPrices` (boolean) Enviar preços; `syncStock` (boolean) Enviar estoque; `syncOrders` (boolean) Importar pedidos; `priceListId` (uuid) Tabela de preço usada no canal; `stockLocationId` (uuid) Filial do estoque publicado e dos pedidos (null = rede toda)

### `POST /ecommerce/:provider/test`

Testar a conexão

### `POST /ecommerce/:provider/disconnect`

Desconectar e apagar credenciais

### `POST /ecommerce/:provider/sync`

Enfileirar sincronização completa

Resposta: 202 Accepted

### `POST /ecommerce/:provider/map-products`

Vincular produtos locais aos da loja virtual (por SKU)

### `GET /ecommerce/:provider/maps`

Vínculos produto/pedido (últimos 100)

Query: `errors` (boolean) Somente vínculos com erro (true)

### `GET /ecommerce/:provider/jobs`

Fila de sincronização (últimos 50 jobs)

### `GET /ecommerce/mercado-livre`

Configuração e conta conectada do Mercado Livre

### `PUT /ecommerce/mercado-livre`

Salvar opções de sincronização

Corpo: `syncProducts` (boolean) Enviar produtos; `syncPrices` (boolean) Enviar preços; `syncStock` (boolean) Enviar estoque; `syncOrders` (boolean) Importar pedidos; `priceListId` (uuid) Tabela de preço do canal; `stockLocationId` (uuid) Filial do estoque (null = rede toda)

### `POST /ecommerce/mercado-livre/oauth/start`

Gerar URL de autorização OAuth — retorna { url }

### `POST /ecommerce/mercado-livre/test`

Testar a conexão

### `POST /ecommerce/mercado-livre/disconnect`

Desconectar a conta

### `POST /ecommerce/mercado-livre/sync`

Enfileirar sincronização completa

Resposta: 202 Accepted

### `POST /ecommerce/mercado-livre/map-products`

Vincular anúncios da conta aos produtos locais (por SKU)

### `GET /ecommerce/mercado-livre/listings`

Listar anúncios

### `POST /ecommerce/mercado-livre/listings`

Criar anúncio (rascunho)

Corpo: `productId`* (uuid) Produto; `variantId` (uuid) Variante; `title` (string) Título do anúncio; `categoryId` (string) Categoria ML (ex.: MLB1234); `listingTypeId` (string) gold_special ou gold_pro; `condition` (string) new (padrão) ou used; `attributesJson` (string) Atributos da categoria em JSON

### `PATCH /ecommerce/mercado-livre/listings/:id`

Editar anúncio (título, categoria, tipo, condição, atributos)

### `POST /ecommerce/mercado-livre/listings/map-existing`

Vincular um anúncio já publicado a um produto

Corpo: `productId`* (uuid) Produto; `variantId` (uuid) Variante; `externalId`* (string) ID do anúncio (MLB…)

### `POST /ecommerce/mercado-livre/listings/:id/publish`

Publicar anúncio

### `POST /ecommerce/mercado-livre/listings/publish-batch`

Publicar vários anúncios

Corpo: `listingIds`* (uuid[]) Até 50 anúncios; `confirm`* (boolean) Deve ser true

### `POST /ecommerce/mercado-livre/listings/:id/pause`

Pausar anúncio

### `POST /ecommerce/mercado-livre/listings/:id/activate`

Reativar anúncio

### `POST /ecommerce/mercado-livre/suggest-category`

Sugerir categoria a partir do título

Corpo: `title`* (string) Título (3–120)

### `GET /ecommerce/mercado-livre/categories/:categoryId/attributes`

Atributos exigidos pela categoria

### `GET /ecommerce/mercado-livre/maps`

Vínculos produto/pedido

Query: `errors` (boolean) Somente com erro (true)

### `GET /ecommerce/mercado-livre/jobs`

Fila de sincronização

## Catálogo online (admin)

Catálogo, zonas de entrega e pedidos do catálogo online — as rotas mantêm o nome interno /convenience.

### `GET /convenience/settings`

Obter configurações

### `PUT /convenience/settings`

Atualizar configurações

### `GET /convenience/cities`

Listar cidades

### `POST /convenience/cities`

Criar cidade

### `PUT /convenience/cities/:id`

Atualizar cidade

### `DELETE /convenience/cities/:id`

Excluir

### `POST /convenience/cities/:cityId/neighborhoods`

Criar bairro

### `PUT /convenience/neighborhoods/:id`

Atualizar bairro

### `DELETE /convenience/neighborhoods/:id`

Excluir bairro

### `GET /convenience/catalog-products`

Listar produtos do catálogo (paginado)

### `PATCH /convenience/catalog-products/:id`

Atualizar

### `GET /convenience/orders`

Listar pedidos

### `GET /convenience/orders/summary`

Resumo

### `GET /convenience/orders/:id`

Detalhe

### `PATCH /convenience/orders/:id/fulfillment`

Atualizar fulfillment do pedido

### `POST /convenience/orders/:id/finalize-cod`

Finalizar pedido COD

### `GET /convenience/catalogs`

Listar catálogos online

### `POST /convenience/catalogs`

Criar catálogo

Corpo: `name`* (string) Nome; `active` (boolean) Ativo; `templateId` (string) Modelo visual; `brandColor` (string) Cor da marca; `whatsappOrderEnabled` (boolean) Pedido via WhatsApp; `whatsappPhone` (string) WhatsApp; `showInactiveProducts` (boolean) Mostra produtos inativos como Indisponível (sem compra); `showOutOfStock` (boolean) Mostra produtos esgotados como Esgotado (sem compra); `carouselLayout` (boolean) Vitrine em carrossel por categoria (padrão: grade); `showWhatsappContact` (boolean) Botão de WhatsApp no topo da vitrine (usa whatsappPhone); `bannerDataUrl` (string) Banner no topo da vitrine (data URL PNG/JPEG/WebP; null remove); `fulfillmentLocationId` (uuid) Local que atende

Resposta: 201 Created

### `PATCH /convenience/catalogs/:id`

Atualizar catálogo

### `DELETE /convenience/catalogs/:id`

Excluir catálogo

Resposta: 204 No Content

### `PATCH /convenience/orders/:id/delivery-fee`

Ajustar a taxa de entrega do pedido

Corpo: `fee`* (number) Taxa

### `POST /convenience/catalog-products/add-all`

Incluir no catálogo online todos os produtos ativos que ainda não estão nele

Corpo: `catalogId` (string) Catálogo (omitido = o catálogo padrão); `q` (string) Só produtos cuja descrição contenha o texto (até 200)

Resposta: { catalog: { id, name }, added }. Não inclui produto composto com composição incompleta. 404 se o catálogo informado não existe.

## Mercado Pago

### `GET /mercadopago-settings/webhook-logs`

Logs de webhook

### `DELETE /mercadopago-settings/webhook-logs`

Limpar logs de webhook

### `GET /mercadopago-settings/status`

Status

### `GET /mercadopago-settings`

Configurações Mercado Pago

### `PUT /mercadopago-settings`

Substituir mercadopago-settings

### `POST /mercadopago-settings/test`

Testar conexão Mercado Pago

## PIX estático

### `GET /static-pix-settings/status`

Status

### `GET /static-pix-settings`

Configurações PIX estático

### `PUT /static-pix-settings`

Atualizar PIX estático

## Terminal Point (MP)

### `GET /point-terminal-settings/status`

Status

### `GET /point-terminal-settings`

Configurações do terminal Point

### `PUT /point-terminal-settings`

Substituir point-terminal-settings

### `POST /point-terminal-settings/test`

Testar conexão do terminal Point

### `POST /point-terminal-settings/test-installment`

Testar parcela no terminal

### `GET /point-terminal-settings/test-installment/:intentId`

Obter :intentId

### `DELETE /point-terminal-settings/test-installment/:intentId`

Excluir :intentId

## WhatsApp (config)

### `GET /whatsapp-settings`

Configurações WhatsApp

### `PUT /whatsapp-settings`

Substituir whatsapp-settings

### `POST /whatsapp-settings/test`

Enviar mensagem de teste pela Cloud API

## WhatsApp (destinatários)

### `GET /whatsapp-recipients`

Listar destinatários WhatsApp

### `POST /whatsapp-recipients`

Criar destinatário WhatsApp

### `PATCH /whatsapp-recipients/:id`

Atualizar

### `DELETE /whatsapp-recipients/:id`

Excluir

## Destinatários de e-mail

### `GET /email-recipients`

Listar destinatários de e-mail

### `POST /email-recipients`

Criar destinatário de e-mail

### `PATCH /email-recipients/:id`

Atualizar

### `DELETE /email-recipients/:id`

Excluir

## Armazenamento (S3)

### `GET /storage-settings`

Configurações de armazenamento

### `PUT /storage-settings`

Atualizar armazenamento

### `POST /storage-settings/test-s3`

Testar conexão S3

## Perfis de usuário

### `GET /user-profiles/catalog`

Catálogo de permissões de perfil

### `GET /user-profiles`

Listar perfis

### `GET /user-profiles/:id`

Detalhe

### `POST /user-profiles`

Criar perfil

### `PATCH /user-profiles/:id`

Atualizar

### `DELETE /user-profiles/:id`

Excluir

## Notificações do sistema

### `GET /system-notifications`

Listar notificações

### `POST /system-notifications/mark-all-read`

Marcar todas como lidas

### `POST /system-notifications/read`

Marcar notificações como lidas

### `POST /system-notifications/:notificationId/read`

Marcar notificação como lida

## PAF / Menu fiscal

### `GET /paf/settings`

Obter configurações

### `PUT /paf/settings`

Atualizar configurações

### `GET /paf/identification`

Listar / obter identification

### `GET /paf/build-md5`

Listar / obter build-md5

### `POST /paf/build-md5/sync`

Sincronizar MD5 PAF

### `GET /paf/export/arquivo-i`

Listar / obter arquivo-i

### `GET /paf/export/arquivo-ii`

Listar / obter arquivo-ii

### `GET /paf/export/arquivo-iii`

Listar / obter arquivo-iii

### `GET /paf/export/arquivo-iv`

Listar / obter arquivo-iv

## Workers / jobs

### `GET /workers`

Listar workers/jobs

### `GET /workers/:id`

Detalhe

### `PATCH /workers/:id`

Atualizar

### `POST /workers/:id/actions`

Executar ação no job

## Assinatura / billing

Planos e cobrança da plataforma (escopo da loja).

### `GET /billing/config`

Configuração de billing

### `GET /billing/plans`

Listar planos

### `GET /billing/status`

Status

### `GET /billing/payments`

Obter pagamentos / baixas

### `GET /billing/cards`

Listar cartões

### `POST /billing/subscribe`

Assinar plano

### `POST /billing/pix/renew`

Renovar via PIX

### `POST /billing/pix/reconcile`

Reconciliar PIX pendente

### `POST /billing/auto-renewal/cancel`

Desligar pagamento automático no cartão (cancelar ou trocar por PIX)

## Assinatura da loja

### `GET /store-subscription`

Assinatura da loja

## Rotas públicas (sem autenticação)

Estas rotas não usam chave de API.

### `GET /health`

Health check — { ok, service }

### `GET /public/reviews/:token`

Obter avaliação pendente (link enviado ao cliente)

### `POST /public/reviews/:token`

Enviar resposta de avaliação

Corpo: `rating`* (integer) Nota de 1 a 5; `npsScore`* (integer) NPS de 0 a 10; `comment` (string) Comentário opcional

### `GET /public/checkout/:token`

Resumo da venda para checkout por link (sem login)

### `GET /public/checkout/:token/config`

Public Key MP e flags do checkout online

### `POST /public/checkout/:token/customer`

Identificar/atualizar cliente no checkout

Corpo: `name`* (string) Nome completo; `cpf`* (string) CPF do cliente; `email`* (string) E-mail; `phone`* (string) Telefone; `confirmUpdate` (boolean) Confirma alteração de cadastro existente

### `GET /public/checkout/:token/payment-conditions`

Formas de pagamento disponíveis (à vista/a prazo)

### `POST /public/checkout/:token/charge`

Criar cobrança PIX ou cartão online

Corpo: `channel`* (string) PIX ou CARD; `paymentMethodId`* (string) ID da forma de pagamento; `priceBasis`* (string) CASH ou CREDIT; `cardToken` (string) Token do Payment Brick (cartão); `installments` (integer) Parcelas (cartão)

### `GET /public/checkout/:token/charge/:chargeId/status`

Status da cobrança (polling UX; confirmação via webhook)

### `GET /public/convenience/settings`

Obter configurações

### `GET /public/convenience/slots`

Horários disponíveis

### `GET /public/convenience/catalog`

Catálogo público online

### `GET /public/convenience/zones`

Zonas de entrega

### `POST /public/convenience/auth/request-code`

Solicitar código de autenticação

### `POST /public/convenience/auth/verify-code`

Validar código de autenticação

### `GET /public/convenience/me`

Cliente autenticado (catálogo online)

### `PATCH /public/convenience/me`

Atualizar perfil (catálogo online)

### `GET /public/convenience/addresses`

Listar endereços

### `POST /public/convenience/addresses`

Criar endereço

### `PUT /public/convenience/addresses/:id`

Atualizar endereço

### `DELETE /public/convenience/addresses/:id`

Excluir endereço

### `POST /public/convenience/orders`

Criar pedido (catálogo online)

### `GET /public/convenience/orders`

Listar pedidos

### `GET /public/convenience/orders/:id`

Detalhe

### `POST /public/checkout/:token/open`

Registrar abertura do checkout

### `POST /public/checkout/:token/preview-total`

Prévia do total do checkout

### `GET /health/ready`

Readiness check

### `GET /health/pools`

Status dos pools

### `GET /openapi.json`

Especificação OpenAPI (parcial)

### `GET /platform-status`

Status de manutenção da plataforma

### `GET /store-lookup`

Localizar loja por CPF/CNPJ

Query: `document` (string) CPF (11) ou CNPJ (14 dígitos); `cnpj` (string) Alias de document

### `GET /public/service-orders/:token`

Portal público da OS (token)

### `POST /public/service-orders/:token/satisfaction`

Enviar satisfação no portal da OS

Corpo: `score`* (integer) Nota de 1 a 5; `comment` (string) Comentário

### `GET /public/customer-profile/store`

Nome e logo da loja (portal do cliente)

### `GET /public/customer-profile/me`

Perfil do cliente (JWT do catálogo online ou token de acesso)

### `GET /public/customer-profile/requests`

Solicitações de alteração do próprio cliente

### `POST /public/customer-profile/requests`

Pedir alteração de cadastro

Corpo: `changes`* (object) Campos alterados; `source` (string) PORTAL | SECURE_LINK | PERIODIC_PROMPT

### `POST /public/customer-profile/requests/pending/cancel`

Cancelar solicitação pendente do próprio cliente

### `GET /public/customer-profile/preferences`

Preferências de comunicação

### `PATCH /public/customer-profile/preferences`

Atualizar preferências de comunicação

### `GET /public/unsubscribe/:token`

Info do descadastro de notificações

### `POST /public/unsubscribe/:token`

Confirmar descadastro de notificações

### `GET /public/notificacoes/desativar/:token`

Alias em português de GET /unsubscribe/:token

### `POST /public/notificacoes/desativar/:token`

Alias em português de POST /unsubscribe/:token

### `GET /public/infinity-tap/:loja/callback`

Callback InfiniteTap (loja no path) — redireciona para a UI

### `GET /public/infinity-tap/callback`

Callback InfiniteTap (loja via header/query)

### `GET /mercadopago/webhook`

Health do webhook Mercado Pago

### `POST /mercadopago/webhook`

Receber notificação Mercado Pago (use ?loja=)

### `GET /webhooks/superfrete`

Health do webhook SuperFrete

### `POST /webhooks/superfrete`

Receber notificação SuperFrete (use ?loja=)

### `POST /public/convenience/orders/guest`

Criar pedido sem cadastro (nome, telefone, endereço, itens, pagamento)

Resposta: 201 Created

### `POST /public/convenience/catalog-access`

Registrar acesso ao catálogo (métrica)

Corpo: `catalogo` (string) Slug do catálogo

Resposta: 204 No Content

### `GET /public/convenience/loyalty`

Pontos de fidelidade do cliente logado no catálogo

### `GET /public/customer-profile/loyalty`

Pontos de fidelidade no portal do cliente (token)

### `POST /public/service-orders/:token/confirm`

Cliente confirma presença no horário

### `POST /public/service-orders/:token/decline`

Cliente avisa que não poderá ir

### `GET /public/nota/:file`

PDF da nota fiscal enviado ao cliente por link assinado (sem login)

Query: `exp` (string) Expiração do link; `sig` (string) Assinatura do link

Resposta: :file é o id do documento fiscal seguido de .pdf. Retorna application/pdf inline; 404 se o link for inválido ou expirado. Os links são gerados pelo vendedor (app), não pela API de integração.

### `GET /public/rastreio/:shipmentId`

Redireciona para a página pública de rastreio da transportadora (destino do botão "Rastrear pedido" do WhatsApp)

Query: `loja` (string) Slug da loja (alternativa ao header X-Store-Slug)

Resposta: 302 para a URL de rastreio da transportadora. 404 se o envio não existe ou ainda não tem rastreio.

### `POST /public/service-orders/:token/quote-decision`

Oficina: o cliente aprova ou recusa o orçamento pelo link do portal da OS

Corpo: `decision`* (string) APPROVE ou DECLINE; `comment` (string) Comentário (até 2000)

Resposta: { ok, decision, status, quoteApprovedAt, quoteDeclinedAt, needsReapproval }. 404 se o link é inválido; 400 se o orçamento venceu ou não está aguardando resposta; 409 se o orçamento já foi aprovado e o cliente tenta recusar.

## Aprovações

Pedidos de aprovação de ações sensíveis (cancelar ou reabrir venda, etc.) feitos por usuários sem liberação e respondidos por quem tem permissão de aprovar. Também podem ser respondidos pelo WhatsApp.

### `GET /approvals/policy`

Como a tela deve confirmar ações sensíveis para o usuário logado

Resposta: { policy }. ADMIN = confirma com a própria senha de administrador; FREE = usuário liberado, executa na hora; APPROVAL = a ação vira um pedido de aprovação.

### `GET /approvals/count`

Contadores de pedidos pendentes (para o selo de notificação)

Resposta: { canDecide, toDecide, mine }. toDecide = pedidos de outros usuários aguardando resposta (0 se o usuário não pode aprovar); mine = pedidos pendentes feitos pelo próprio usuário.

### `GET /approvals`

Listar pedidos de aprovação

Query: `scope` (string) mine (padrão: últimos 50 pedidos do próprio usuário), pending (pendentes de todos, mais antigos primeiro; só para quem aprova) ou history (já respondidos, últimos 100; só para quem aprova)

Resposta: { canDecide, items }. Cada item traz id, type, typeLabel, entityType, entityId, status (PENDING, APPROVED, REJECTED, CANCELLED, EXPIRED ou FAILED), summary, justification, requestedByName, decidedByName, decidedAt, decidedVia (APP ou WHATSAPP), decisionNote, resultMessage, expiresAt e createdAt. Pedidos vencidos viram EXPIRED na leitura.

### `POST /approvals/:id/approve`

Aprovar o pedido — a ação solicitada é executada na hora (exige permissão de aprovar)

Corpo: `note` (string) Observação da decisão (até 300 caracteres)

Resposta: { outcome, message, approval }. outcome: approved (ação executada; message traz o resultado), failed (aprovado mas a execução falhou; message traz o erro e o pedido fica FAILED), already (já respondido) ou expired. 403 se o usuário não pode aprovar ou se foi ele mesmo quem fez o pedido; 404 se não existe.

### `POST /approvals/:id/reject`

Recusar o pedido — a ação solicitada não é executada (exige permissão de aprovar)

Corpo: `note` (string) Motivo da recusa (até 300 caracteres)

Resposta: { outcome, message, approval }. outcome: rejected, already ou expired. 403 se o usuário não pode aprovar ou se foi ele mesmo quem fez o pedido.

### `POST /approvals/:id/cancel`

Desistir de um pedido ainda pendente (quem pediu ou um aprovador)

Resposta: { approval } com status CANCELLED. 403 se não for o solicitante nem aprovador; 409 se o pedido já foi respondido.

## Custos adicionais

Custos extras (valor fixo ou percentual) somados ao custo gerencial dos produtos, opcionalmente limitados a categorias. Exigem a permissão de gerenciar catálogo.

### `GET /additional-costs`

Listar custos adicionais e ver se o recurso está ligado

Resposta: { enabled, items }. Cada item: id, name, kind (FIXED ou PERCENT), value, active, categoryIds (vazio = vale para todas as categorias) e position.

### `PUT /additional-costs/settings`

Ligar ou desligar o uso de custos adicionais na loja

Corpo: `enabled`* (boolean) Recurso ligado

Resposta: Retorna o mesmo payload de GET /additional-costs.

### `POST /additional-costs`

Criar custo adicional

Corpo: `name`* (string) Nome (até 80); `kind`* (string) FIXED (valor em R$) ou PERCENT (percentual, até 100); `value`* (number) Valor ou percentual (0 a 100000; em PERCENT, no máximo 100); `active` (boolean) Ativo (padrão true); `categoryIds` (string[]) Categorias às quais se aplica (até 500; vazio = todas)

Resposta: 201 Created — o custo criado, no fim da ordenação.

### `PATCH /additional-costs/:id`

Atualizar custo adicional (campos parciais)

Corpo: `name` (string) Nome (até 80); `kind` (string) FIXED ou PERCENT; `value` (number) Valor ou percentual; `active` (boolean) Ativo; `categoryIds` (string[]) Categorias às quais se aplica

### `DELETE /additional-costs/:id`

Excluir custo adicional

Resposta: 204 No Content

## Embalagem para presente

Modelos de embalagem cobrados no PDV e as regras do recurso. O lançamento na venda fica em /sales/:id/gift-wraps. Os cadastros exigem a permissão de gerenciar catálogo.

### `GET /gift-wraps`

Listar modelos de embalagem e as configurações do recurso

Resposta: { enabled, allowCustomValue, items }. Cada item: id, name, description, code, value, active e position.

### `PUT /gift-wraps/settings`

Ligar o recurso e permitir valor livre no PDV

Corpo: `enabled` (boolean) Embalagem para presente habilitada na loja; `allowCustomValue` (boolean) Permite informar valor/nome livres no PDV (ainda exige a permissão sales.gift_wrap_custom_price do usuário)

Resposta: Retorna o mesmo payload de GET /gift-wraps.

### `POST /gift-wraps`

Criar modelo de embalagem

Corpo: `name`* (string) Nome (até 80); `value`* (number) Valor cobrado por embalagem (0 a 100000); `description` (string) Descrição (até 200); `code` (string) Código interno (até 30); `active` (boolean) Ativo (padrão true)

Resposta: 201 Created

### `PATCH /gift-wraps/:id`

Atualizar modelo de embalagem (campos parciais)

Corpo: `name` (string) Nome (até 80); `value` (number) Valor cobrado; `description` (string) Descrição (até 200); `code` (string) Código interno (até 30); `active` (boolean) Ativo

Resposta: 404 se o modelo não existe.

### `DELETE /gift-wraps/:id`

Excluir modelo de embalagem

Resposta: 204 No Content. 404 se o modelo não existe.
