# bitERP Developer Docs
Bem-vindo à documentação do **bitERP** para desenvolvedores e parceiros.
O bitERP expõe duas formas de integração:
API REST para sistemas externos. Autenticação via API Keys ou OAuth2, endpoints
CRUD com paginação por cursor e filtros estilo Stripe.
Servidor Model Context Protocol para agentes de IA. Conecte LLMs como Claude ou
GPT ao bitERP via protocolo padronizado.
Primeiros passos [#primeiros-passos]
Se você está começando agora, siga o guia rápido:
Conceitos básicos, ambientes e como obter suas credenciais.
Faça sua primeira chamada à API em menos de 5 minutos.
# Modelos de IA suportados
O catálogo abaixo vale para o seletor de modelos do chat do produto (`app.biterp.ai`).
**Não** se aplica ao [MCP Server](/docs/mcp) (`mcp.biterp.ai`), onde o modelo é
escolhido pelo seu próprio cliente de IA, nem à [API de Integrações](/docs/api), que
não usa LLM.
Status dos modelos [#status-dos-modelos]
| Status | Significado |
| ---------------------------- | --------------------------------------------------------------------------------------------------- |
| **Disponível** | Selecionável no seletor de modelos do chat |
| **Uso interno** | Não selecionável; a plataforma usa em etapas auxiliares do atendimento (consome créditos do tenant) |
| **Fim de suporte anunciado** | Ainda selecionável, com data de saída publicada na coluna "Fim do suporte" |
| **Descontinuado** | Removido do catálogo; quem estava com ele selecionado já foi migrado para o substituto |
Modelos disponíveis [#modelos-disponíveis]
Modelos que você pode escolher no seletor do chat.
| Modelo | Provedor | Status | Custo relativo | Fim do suporte | Substituto |
| --------------- | --------- | ---------- | -------------- | -------------- | ---------- |
| GPT-5.6 Sol | OpenAI | Disponível | 5 | — | — |
| GPT-5.6 Terra | OpenAI | Disponível | 2 | — | — |
| Claude Opus 5 | Anthropic | Disponível | 4 | — | — |
| Claude Sonnet 5 | Anthropic | Disponível | 3 | — | — |
O **custo relativo** vai de 1 (mais econômico) a 5 (mais caro). É a mesma faixa exibida no seletor do chat — não é preço por token.
Como escolher o modelo [#como-escolher-o-modelo]
No chat do `app.biterp.ai`, use o seletor de modelos para definir em qual LLM o agente principal vai rodar. A escolha vale para aquela conversa e consome créditos do tenant conforme o uso.
Em geral:
* Modelos com **custo relativo mais alto** tendem a ter maior capacidade de raciocínio em tarefas complexas.
* Modelos com **custo relativo mais baixo** priorizam custo e latência, com capacidade adequada para operações do dia a dia.
Não há configuração de modelo na API de Integrações nem no MCP — veja [O que esta página não cobre](#o-que-esta-pagina-nao-cobre).
Modelos de uso interno [#modelos-de-uso-interno]
Estes modelos **não aparecem no seletor**. A plataforma os usa em etapas auxiliares do atendimento (por exemplo, especialistas que apoiam o agente principal). Mesmo assim, o consumo **é debitado em créditos do tenant**.
| Modelo | Provedor | Status | Custo relativo | Fim do suporte | Substituto |
| ---------------- | --------- | ----------- | -------------- | -------------- | ---------- |
| GPT-5.6 Luna | OpenAI | Uso interno | 1 | — | — |
| Claude Haiku 4.5 | Anthropic | Uso interno | 2 | — | — |
Política de descontinuação [#política-de-descontinuação]
Quando um provedor lança uma versão nova (ou retira a anterior), o bitERP atualiza o catálogo do chat. A troca **não quebra a sua seleção**:
* A seleção salva migra automaticamente para o **substituto da mesma família** (mesmo provedor e faixa de capacidade).
* Você não precisa reescolher o modelo após a troca.
* Quando houver data de saída, ela aparece na coluna **Fim do suporte** no formato `AAAA-MM-DD`. Sem data anunciada, a coluna mostra `—`.
A data de fim de suporte depende do provedor e do catálogo que consumimos. Esta página descreve **como** avisamos e migramos — não promete um prazo mínimo fixo de aviso.
Modelos descontinuados [#modelos-descontinuados]
Histórico de modelos que já saíram do seletor. Quem os tinha selecionados foi migrado para o substituto abaixo.
| Modelo descontinuado | Substituto atual |
| -------------------- | ---------------- |
| GPT-5.5 | GPT-5.6 Sol |
| GPT-5.4 | GPT-5.6 Terra |
| GPT-5.4 mini | GPT-5.6 Luna |
| GPT-5.2 | GPT-5.6 Sol |
| GPT-5.2 chat | GPT-5.6 Sol |
| GPT-5.1 | GPT-5.6 Terra |
| GPT-5 mini | GPT-5.6 Luna |
| Claude Opus 4.7 | Claude Opus 5 |
| Claude Opus 4.6 Fast | Claude Sonnet 5 |
| Claude Sonnet 4.6 | Claude Sonnet 5 |
| Claude Sonnet 4.5 | Claude Opus 5 |
O que esta página não cobre [#o-que-esta-página-não-cobre]
* **[MCP Server](/docs/mcp)** (`mcp.biterp.ai`): o servidor só expõe tools via protocolo MCP. Quem escolhe o modelo (e paga o token) é o **cliente de IA do usuário** — Claude Desktop, Cursor, ChatGPT e similares.
* **[API de Integrações](/docs/api)**: API REST para sistemas externos. Não envolve LLM.
# Autenticação
A API de Integrações suporta dois métodos de autenticação:
| Método | Uso | Token |
| ----------------------------- | ------------------------------ | -------------------- |
| **API Keys** | Integrações machine-to-machine | `sk__` |
| **OAuth2 Authorization Code** | Automações (Zapier, Make, n8n) | Bearer JWT / `bit_*` |
API Keys [#api-keys]
Formato do token [#formato-do-token]
```
sk__
```
O token é exibido **apenas uma vez** no momento da criação. Armazene-o de forma segura imediatamente.
Como usar [#como-usar]
Envie o token no header `Authorization`:
```bash
# Com prefixo Bearer
curl -H "Authorization: Bearer sk_abc123_secretXYZ" \
https://api.biterp.ai/products
# Ou diretamente
curl -H "Authorization: sk_abc123_secretXYZ" \
https://api.biterp.ai/products
```
Características [#características]
| Propriedade | Descrição |
| -------------- | -------------------------------------------------------- |
| **Escopo** | Cada key pertence a um único tenant |
| **Permissões** | Granulares por recurso e ação (imutáveis após criação) |
| **Expiração** | Opcional — campo `expires_at` configurável |
| **Segurança** | Hash SHA-256 do secret (nunca armazenado em texto) |
| **Revogação** | Soft delete — a key pode ser revogada a qualquer momento |
Criando uma API Key [#criando-uma-api-key]
1. Acesse **Configurações > API Keys** no painel do bitERP
2. Clique em **Criar API Key**
3. Defina nome, descrição e permissões
4. Copie o token exibido
Exemplo de payload de criação:
```json
{
"name": "ERP Integration",
"description": "Integração com sistema ERP externo",
"permissions": [
{ "resource_code": "products", "can_read": true, "can_create": true },
{ "resource_code": "customers", "can_read": true },
{ "resource_code": "invoices", "can_read": true, "can_create": true }
],
"expires_at": "2026-12-31"
}
```
***
OAuth2 Authorization Code [#oauth2-authorization-code]
Para integrações via plataformas como Zapier, Make e n8n, a API suporta o fluxo **OAuth2 Authorization Code com PKCE (S256)**.
Endpoints OAuth [#endpoints-oauth]
| Método | Endpoint | Descrição |
| ------ | ----------------------------------------- | ------------------------------------- |
| `GET` | `/.well-known/oauth-authorization-server` | Metadata do servidor OAuth (RFC 8414) |
| `GET` | `/oauth/authorize` | Inicia o fluxo de autorização |
| `POST` | `/oauth/token` | Troca code por access token |
| `POST` | `/oauth/revoke` | Revoga um token |
| `GET` | `/oauth/userinfo` | Retorna dados do usuário autenticado |
Fluxo de autorização [#fluxo-de-autorização]
```
1. Sua app redireciona o usuário para /oauth/authorize
com client_id, redirect_uri, scope, state, code_challenge
2. Usuário autentica e autoriza no painel bitERP
3. bitERP redireciona de volta com authorization code
4. Sua app troca o code em POST /oauth/token
com client_id + client_secret
5. Usa o access token (bit_*) para chamar a API
```
Formato dos tokens OAuth [#formato-dos-tokens-oauth]
| Token | Prefixo | Descrição |
| ------------------ | -------- | --------------------------------------- |
| Access token | `bit_` | Token de acesso para chamadas à API |
| Refresh token | `bitrf_` | Token para renovar o access token |
| Authorization code | `bitac_` | Código temporário para troca por tokens |
Rate limits OAuth [#rate-limits-oauth]
| Endpoint | Limite |
| ---------------------- | -------------------------- |
| `GET /oauth/authorize` | 20 req/min por IP |
| `POST /oauth/token` | 10 req/min por `client_id` |
Níveis de acesso OAuth [#níveis-de-acesso-oauth]
| Role | Acesso |
| -------- | ---------------------------------------------- |
| `admin` | Acesso completo a todos os recursos |
| `member` | Acesso limitado conforme permissões granulares |
Usuários com role `member` só acessam endpoints para os quais possuem permissão configurada. Recursos marcados como `is_admin_only` são restritos a usuários `admin`.
# Códigos de Erro
Formato de erro [#formato-de-erro]
Todas as respostas de erro seguem o mesmo formato JSON:
```json
{
"status_code": 400,
"message": "Descrição do erro",
"error": "Bad Request"
}
```
Códigos HTTP [#códigos-http]
| Código | Significado | Quando ocorre |
| ------ | --------------------- | ----------------------------------------------------- |
| `200` | OK | Requisição bem-sucedida (GET, PATCH) |
| `201` | Created | Recurso criado com sucesso (POST) |
| `204` | No Content | Recurso removido com sucesso (DELETE) |
| `400` | Bad Request | Dados inválidos, filtro desconhecido, versão inválida |
| `401` | Unauthorized | Token ausente, inválido ou expirado |
| `403` | Forbidden | Sem permissão para a operação |
| `404` | Not Found | Recurso não encontrado |
| `409` | Conflict | Conflito (ex: recurso duplicado) |
| `422` | Unprocessable Entity | Payload bem formado, mas semanticamente inválido |
| `429` | Too Many Requests | Rate limit excedido |
| `500` | Internal Server Error | Erro interno do servidor |
Troubleshooting [#troubleshooting]
401 Unauthorized [#401-unauthorized]
**Causa**: Token de autenticação ausente, inválido ou expirado.
**Soluções**:
* Verifique se o header `Authorization` está presente
* Confirme que o formato é `Bearer sk__` ou `Bearer bit_`
* Verifique se a API Key não foi revogada
* Verifique se a API Key não expirou (`expires_at`)
403 Forbidden [#403-forbidden]
**Causa**: O token é válido, mas não tem permissão para a operação.
**Soluções**:
* Verifique as permissões da API Key (recurso + ação)
* Para OAuth: confirme que o usuário tem a role necessária
* Recursos `admin_only` requerem role `admin`
400 Bad Request [#400-bad-request]
**Causa**: Dados enviados são inválidos.
**Cenários comuns**:
* Campo obrigatório ausente no body
* Tipo de dado incorreto (ex: string onde esperava number)
* Filtro com campo não suportado pelo recurso
* Header `api-version` com versão inválida
* Formato de UUID inválido
422 Unprocessable Entity [#422-unprocessable-entity]
**Causa**: o corpo da requisição está bem formado e passou na validação de tipos, mas a API não conseguiu processar o conteúdo.
**Cenários comuns**:
* **Geografia não resolvida**: `country_code`, `state`/`state_id` ou `city`/`city_id` que a API não conseguiu casar com o catálogo interno com segurança. Não há degradação silenciosa — se você enviou o campo, ele precisa resolver. Ver [Clientes](/docs/api/guides/customers) e [Dados de referência](/docs/api/guides/reference-data).
* **Snapshot financeiro incompatível** em [notas de compra](/docs/api/guides/purchase-invoices), quando o total muda sem o `payables` correspondente.
O corpo do erro traz `details.fields` — um mapa de **campo → lista de motivos** — apontando exatamente o que falhou. As mensagens de `details` vêm em inglês:
```json
{
"status_code": 422,
"message": "Could not resolve provided geography fields",
"error": "Unprocessable Entity",
"details": {
"fields": {
"country_code": [
"Could not resolve country from '1112' (tried ISO2, ISO3, numeric code and names)"
],
"city": ["Could not resolve city from 'CidadeQueNaoExiste' in country 'BR'"]
}
}
}
```
**Solução**: consulte o catálogo em [Dados de referência](/docs/api/guides/reference-data) e envie os IDs (`state_id`, `city_id`) em vez do texto livre quando precisar de determinismo.
429 Too Many Requests [#429-too-many-requests]
**Causa**: Rate limit excedido.
**Endpoints com rate limit**:
* `GET /oauth/authorize`: 20 req/min por IP
* `POST /oauth/token`: 10 req/min por `client_id`
**Solução**: Implemente backoff exponencial e respeite os headers de retry.
404 Not Found [#404-not-found]
**Causa**: Recurso não existe ou pertence a outro tenant.
**Nota**: Por segurança, a API retorna 404 (não 403) quando o recurso existe mas pertence a outro tenant.
Health check [#health-check]
O endpoint `GET /health` responde `200 OK` quando a API está no ar — use-o para monitoramento. Ele não exige autenticação nem contexto de tenant.
# API de Integrações
A **API de Integrações** do bitERP permite que sistemas externos realizem operações CRUD em recursos do ERP via endpoints REST.
Base URL [#base-url]
```
https://api.biterp.ai
```
Características [#características]
* **Autenticação dupla**: API Keys (machine-to-machine) ou OAuth2 Authorization Code (Zapier, Make, n8n)
* **Permissões granulares**: controle por recurso e ação (create, read, update, delete)
* **Paginação por cursor**: navegação eficiente em listas grandes
* **Filtros estilo Stripe**: query params planos com bracket notation para operadores
* **Versionamento por header**: header `api-version` para compatibilidade futura
* **OpenAPI spec**: especificação gerada automaticamente para referência de endpoints
* **Webhooks de saída**: notificações assinadas enviadas ao seu servidor quando um recurso muda, sem *polling*
Recursos disponíveis [#recursos-disponíveis]
Para cada recurso há um **guia narrativo** (o que é, ciclo de vida, fluxos) e a **referência** endpoint a endpoint — veja, por exemplo, o [guia de Produtos](/docs/api/guides/products). Os recursos estão organizados por domínio:
Cadastros [#cadastros]
| Recurso | Referência OpenAPI | Guia |
| ------------ | --------------------------------------------------- | ------------------------------------------ |
| Produtos | [Operações](/docs/api/reference/products/find-all) | [Produtos](/docs/api/guides/products) |
| Clientes | [Operações](/docs/api/reference/customers/find-all) | [Clientes](/docs/api/guides/customers) |
| Fornecedores | [Operações](/docs/api/reference/suppliers/find-all) | [Fornecedores](/docs/api/guides/suppliers) |
Financeiro [#financeiro]
| Recurso | Referência OpenAPI | Guia |
| ---------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- |
| Contas financeiras | [Operações](/docs/api/reference/financial-accounts/find-all) | [Contas financeiras](/docs/api/guides/financial-accounts) |
| Categorias financeiras | [Operações](/docs/api/reference/financial-categories/find-all) | [Categorias financeiras](/docs/api/guides/financial-categories) |
| Transações e extrato | [Operações](/docs/api/reference/financial-transactions/find-all) | [Transações e extrato](/docs/api/guides/financial-transactions) |
| Contas a receber | [Operações](/docs/api/reference/receivables/find-all) | [Contas a receber](/docs/api/guides/receivables) |
| Contas a pagar | [Operações](/docs/api/reference/payables/find-all) | [Contas a pagar](/docs/api/guides/payables) |
Vendas e faturamento [#vendas-e-faturamento]
| Recurso | Referência OpenAPI | Guia |
| ---------------- | ------------------------------------------------------ | ------------------------------------------------- |
| Orçamentos | [Operações](/docs/api/reference/quotes/find-all) | [Orçamentos](/docs/api/guides/quotes) |
| Pedidos de venda | [Operações](/docs/api/reference/sales-orders/find-all) | [Pedidos de venda](/docs/api/guides/sales-orders) |
| Notas / faturas | [Operações](/docs/api/reference/invoices/find-all) | [Notas / faturas](/docs/api/guides/invoices) |
O guia [Fluxo de vendas](/docs/api/guides/sales-flow) costura orçamento → pedido → nota → contas a receber.
Compras [#compras]
| Recurso | Referência OpenAPI | Guia |
| --------------- | ----------------------------------------------------------- | ----------------------------------------------------- |
| Notas de compra | [Operações](/docs/api/reference/purchase-invoices/find-all) | [Notas de compra](/docs/api/guides/purchase-invoices) |
Dados de referência (somente leitura) [#dados-de-referência-somente-leitura]
Bancos (`/banks`), formas de pagamento (`/payment-methods`), unidades de medida (`/units-of-measurement`), países (`/countries`), estados (`/states`) e cidades (`/cities`) são catálogos consultados nos cadastros. Ver o guia [Dados de referência](/docs/api/guides/reference-data).
Integração [#integração]
| Recurso | Referência OpenAPI | Guia |
| ----------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Mapeamento de entidades | [Operações](/docs/api/reference/integration-entity-mappings/find-all) | [Mapeamento de entidades](/docs/api/guides/integration-entity-mappings) |
Utilitários [#utilitários]
Consulte a [referência OpenAPI](/docs/api/reference) para [health](/docs/api/reference/health/get-health), [tenant](/docs/api/reference/tenant/get-current-tenant) e [oauth/token](/docs/api/reference/oauth/token). O fluxo de autenticação está em [Autenticação](/docs/api/authentication).
A maioria dos recursos de negócio expõe operações padrão de listagem, busca por ID, criação, atualização e remoção — veja a referência OpenAPI de cada recurso e os guias narrativos para endpoints específicos.
> A referência completa de endpoints é gerada automaticamente a partir da especificação OpenAPI. Veja a [Referência da API](/docs/api/reference).
`DELETE` é de mão única [#delete-é-de-mão-única]
`DELETE /:id` faz **soft delete**: o registro sai das listagens e das buscas, mas continua no banco para preservar histórico e referências.
**Não existe endpoint de restauração nesta API** — em nenhum recurso. Não é uma lacuna a ser preenchida: desfazer uma exclusão é uma operação de **usuário**, feita no painel do bitERP por alguém autenticado, e não por um sistema externo. Do ponto de vista da sua integração, trate todo `DELETE` como definitivo e confirme antes de chamar.
Webhooks [#webhooks]
Além de consultar a API, você pode ser **notificado** quando algo muda no ERP. Um administrador registra uma URL HTTPS no painel do bitERP, escolhe os eventos e passa a receber um `POST` assinado a cada criação, atualização, exclusão ou restauração de recurso.
O payload identifica o recurso que mudou; os dados completos você busca com o `GET /:id` correspondente. Ver [Webhooks](/docs/api/webhooks).
Formato de resposta [#formato-de-resposta]
Todas as respostas usam JSON. Listagens retornam dados paginados, com os metadados de navegação em `pagination`:
```json
{
"data": [...],
"pagination": {
"limit": 20,
"has_next_page": true,
"has_previous_page": false,
"next_cursor": "eyJmIjoiY3JlYXRlZF9hdCIsInYiOi...",
"previous_cursor": null
},
"meta": {
"sort": { "field": "created_at", "order": "asc" }
}
}
```
Recursos individuais retornam o objeto diretamente:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Notebook Dell",
"sale_price": "4500.000000",
"is_active": true,
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-01-15T10:30:00Z"
}
```
Valores decimais são strings [#valores-decimais-são-strings]
Todo campo **decimal ou monetário** trafega como **string**, no formato `numeric(18,6)` — sempre com **6 casas decimais** —, tanto na **requisição** quanto na **resposta**. O motivo é precisão: o `number` do JSON é um float de ponto flutuante (IEEE 754) e arredonda valores financeiros de forma silenciosa. A string preserva o valor exato.
```json
{
"amount": "1250.000000",
"quantity": "2.000000",
"unit_price": "450.000000"
}
```
Isso vale para campos como `amount`, `total_amount`, `allocated_amount`, `discount_amount`, `surcharge_amount`, `unit_price`, `sale_price`, `cost_price`, `quantity`, `initial_balance`, `subtotal`, `total`, `base_amount`, entre outros. Campos que aceitam **sinal** (como o `amount` de uma transação financeira e o `initial_balance`) usam o `-` na própria string: `"-250.500000"`.
> **Percentuais também são strings.** Alíquotas e taxas (`rate`, `discount_rate`) usam a escala de 0 a 100 e trafegam como decimal `numeric(9,6)` em string — `"18.000000"` é 18%. Isso vale para **todos** os recursos, inclusive [notas](/docs/api/guides/invoices). Já contadores e índices (`installment_count`, `position`, `display_order`, `limit`) são **inteiros** e seguem como `number` — esses, sim, nunca envie como string.
Ao consumir a API, **não** converta os decimais para `float` na sua linguagem: use um tipo decimal (`BigDecimal`, `decimal.Decimal`, `Decimal.js`) ou mantenha a string.
Próximos passos [#próximos-passos]
Configure API Keys ou OAuth2.
Entenda o modelo de permissões granulares.
Navegue e filtre resultados.
Um exemplo de guia narrativo por recurso.
Receba notificações quando recursos mudarem, em vez de consultar a API em laço.
# Paginação e Filtros
Paginação por cursor [#paginação-por-cursor]
Endpoints de listagem usam **cursor-based pagination**. Esse modelo é mais eficiente que offset pagination para datasets grandes.
Como funciona [#como-funciona]
1. Faça a primeira chamada sem o parâmetro `cursor`
2. Se `pagination.has_next_page` for `true`, use o valor de `pagination.next_cursor` na próxima chamada
3. Repita até `pagination.has_next_page` ser `false`
Parâmetros [#parâmetros]
| Parâmetro | Tipo | Descrição |
| --------- | ------ | ------------------------------------------------------------ |
| `cursor` | string | Cursor opaco da página anterior. Omitir na primeira chamada. |
| `limit` | number | Itens por página (padrão: 20, máximo: 100) |
Exemplo [#exemplo]
```bash
# Primeira página
curl -H "Authorization: Bearer sk_xxx_yyy" \
"https://api.biterp.ai/products?limit=10"
# Próxima página
curl -H "Authorization: Bearer sk_xxx_yyy" \
"https://api.biterp.ai/products?limit=10&cursor=eyJmIjoiY3Jl..."
```
Formato da resposta [#formato-da-resposta]
Os dados vêm em `data`; os metadados de navegação, em `pagination`; e o eco de filtros/ordenação aplicados, em `meta`:
```json
{
"data": [...],
"pagination": {
"limit": 20,
"has_next_page": true,
"has_previous_page": false,
"next_cursor": "eyJmIjoiY3JlYXRlZF9hdCIsInYiOi...",
"previous_cursor": null
},
"meta": {
"sort": { "field": "created_at", "order": "asc" }
}
}
```
***
Filtros (Flat Query Params) [#filtros-flat-query-params]
Os endpoints de listagem aceitam **filtros via query params planos**, no estilo Stripe (bracket notation).
Sintaxe [#sintaxe]
```bash
# Igualdade (sem operador)
GET /products?name=notebook&is_active=true
# Operadores via bracket notation
GET /products?sale_price[gte]=1000&sale_price[lte]=5000
# Busca parcial (contains)
GET /customers?legal_name[contains]=acme
# Combinando filtros, ordenação e paginação
GET /quotes?created_at[gte]=2026-01-01&sort=created_at:desc&limit=50
```
Operadores disponíveis [#operadores-disponíveis]
| Sintaxe | Operador | Tipos suportados |
| --------------------------- | ---------------------- | --------------------------------- |
| `field=value` | Igual (`eq`) | string, number, boolean, date |
| `field[neq]=value` | Diferente | string, number, boolean, date |
| `field[gt]=value` | Maior que | number, date |
| `field[gte]=value` | Maior ou igual | number, date |
| `field[lt]=value` | Menor que | number, date |
| `field[lte]=value` | Menor ou igual | number, date |
| `field[contains]=value` | Contém (busca parcial) | string |
| `field[not_contains]=value` | Não contém | string |
| `field[is_null]=true` | É nulo | todos os tipos (campo `nullable`) |
| `field[is_not_null]=true` | Não é nulo | todos os tipos (campo `nullable`) |
No payload JSON de `filters`, use `{ "f": "reference_code", "op": "is_null" }` **sem** `v` (ou `"v": null`) — o operador não carrega valor:
```json
{
"logic": "and",
"conditions": [{ "f": "reference_code", "op": "is_null" }]
}
```
Equivalente flat: `GET /products?reference_code[is_null]=true`
Ordenação [#ordenação]
Use o parâmetro `sort` com o formato `campo:direcao`:
```bash
# Ordenar por preço decrescente
GET /products?sort=sale_price:desc
# Ordenar por data de criação crescente
GET /products?sort=created_at:asc
```
Ordenação padrão por recurso [#ordenação-padrão-por-recurso]
Se você **não** enviar `sort`, cada recurso aplica a ordenação que faz sentido para o seu domínio — não existe um padrão único para toda a API:
| Recurso | Ordenação padrão |
| ------------------------------------------------------------------------------- | -------------------- |
| `/products`, `/customers`, `/suppliers` | `created_at:asc` |
| `/quotes`, `/sales-orders` | `record_number:asc` |
| `/invoices` | `invoice_date:desc` |
| `/purchase-invoices` | `purchase_date:desc` |
| `/receivables`, `/payables` | `due_date:asc` |
| `/financial-transactions` | `posted_at:desc` |
| `/financial-accounts`, `/banks` | `display_order:asc` |
| `/financial-categories` | `position:asc` |
| `/countries`, `/states`, `/cities`, `/payment-methods`, `/units-of-measurement` | `id:desc` |
O bloco `meta.sort` da resposta **sempre ecoa a ordenação efetivamente aplicada**, então você pode confirmar o comportamento sem adivinhar.
> A ordenação padrão é **estável** e faz parte do contrato: paginar por cursor sem `sort` percorre a coleção inteira sem repetir nem pular registros. Se a sua integração depende de uma ordem específica, envie `sort` explicitamente em vez de confiar no padrão.
Regras [#regras]
* Todos os filtros são combinados com **AND**
* `is_null` / `is_not_null` só funcionam em campos marcados como nullable no contrato do recurso (ex.: `reference_code` em cadastros que aceitam código opcional)
* Campos desconhecidos retornam `400 Bad Request`
* Valores inválidos (ex: texto em campo numérico) retornam `400 Bad Request`
* Cada recurso tem seus próprios campos filtráveis — consulte a referência de endpoints
# Permissões
O bitERP usa um modelo de permissões **granular por recurso e ação**. Cada API Key ou usuário OAuth tem permissões específicas que controlam o que pode ser acessado.
Modelo de permissões [#modelo-de-permissões]
Cada permissão é composta por:
| Campo | Descrição | Exemplo |
| --------------- | ------------------------------ | ----------------------- |
| `resource_code` | Código do recurso | `products`, `customers` |
| `can_create` | Permissão para criar | `true` / `false` |
| `can_read` | Permissão para listar e buscar | `true` / `false` |
| `can_update` | Permissão para atualizar | `true` / `false` |
| `can_delete` | Permissão para remover | `true` / `false` |
Como funciona [#como-funciona]
Com API Keys [#com-api-keys]
As permissões são definidas na criação da API Key e são **imutáveis** — não podem ser alteradas depois. Para mudar permissões, crie uma nova key.
Exemplo: uma API Key com permissão `products:read` e `products:create` pode listar e criar produtos, mas não pode atualizar ou deletar.
Com OAuth2 [#com-oauth2]
O comportamento depende da role do usuário:
| Role | Comportamento |
| -------- | ------------------------------------------------------------------ |
| `admin` | Acesso completo (bypass de permissões) |
| `member` | Permissões granulares por recurso, idênticas ao modelo de API Keys |
Respostas de erro [#respostas-de-erro]
Quando uma operação é negada por falta de permissão, a API retorna:
```json
{
"status_code": 403,
"message": "Forbidden",
"error": "You do not have permission to perform this action"
}
```
Recursos disponíveis [#recursos-disponíveis]
Estes são os `resource_code` aceitos na criação de uma API Key, com as ações que cada um expõe na API de Integrações. Conceder uma ação que o recurso não expõe (por exemplo `can_create` em um catálogo de referência) não habilita nada.
> **Os códigos usam hífen, não underscore** — é `financial-accounts`, nunca `financial_accounts`. Um código inexistente não gera erro na criação da key: ele simplesmente não concede permissão nenhuma, e as chamadas passam a responder `403`.
Cadastros [#cadastros]
| Código | Recurso | Ações |
| ----------- | ------------ | ------------------------------------ |
| `products` | Produtos | `create`, `read`, `update`, `delete` |
| `customers` | Clientes | `create`, `read`, `update`, `delete` |
| `suppliers` | Fornecedores | `create`, `read`, `update`, `delete` |
Financeiro [#financeiro]
| Código | Recurso | Ações |
| ------------------------ | ---------------------- | ------------------------------------ |
| `financial-accounts` | Contas financeiras | `create`, `read`, `update`, `delete` |
| `financial-categories` | Categorias financeiras | `create`, `read`, `update`, `delete` |
| `financial-transactions` | Transações financeiras | `read`, `delete` |
| `receivables` | Contas a receber | `create`, `read`, `update`, `delete` |
| `payables` | Contas a pagar | `create`, `read`, `update`, `delete` |
Vendas, faturamento e compras [#vendas-faturamento-e-compras]
| Código | Recurso | Ações |
| ------------------- | ---------------- | ------------------------------------ |
| `quotes` | Orçamentos | `create`, `read`, `update`, `delete` |
| `sales-orders` | Pedidos de venda | `create`, `read`, `update`, `delete` |
| `invoices` | Notas / faturas | `create`, `read`, `update`, `delete` |
| `purchase-invoices` | Notas de compra | `create`, `read`, `update`, `delete` |
Dados de referência (somente leitura) [#dados-de-referência-somente-leitura]
| Código | Recurso | Ações |
| ---------------------- | ------------------- | ------ |
| `banks` | Bancos | `read` |
| `payment-methods` | Formas de pagamento | `read` |
| `units-of-measurement` | Unidades de medida | `read` |
| `countries` | Países | `read` |
| `states` | Estados | `read` |
| `cities` | Cidades | `read` |
Integração [#integração]
| Código | Recurso | Ações |
| ---------------------- | ----------------------- | ------------------------------------ |
| `integration-mappings` | Mapeamento de entidades | `create`, `read`, `update`, `delete` |
> ⚠️ O código de permissão é **`integration-mappings`**, mas o caminho da URL é `/integration-entity-mappings`. É a única divergência entre código e rota na API — ver o [guia do recurso](/docs/api/guides/integration-entity-mappings).
Sem permissão específica [#sem-permissão-específica]
`GET /tenant` e `GET /health` não exigem permissão de recurso: qualquer credencial válida acessa o próprio tenant, e o health check é público.
Boas práticas [#boas-práticas]
1. **Princípio do menor privilégio**: conceda apenas as permissões necessárias para a integração
2. **Keys separadas por integração**: crie uma API Key diferente para cada sistema que integra com o bitERP
3. **Expiração**: configure `expires_at` sempre que possível
4. **Revogação**: revogue keys que não são mais necessárias
# Versionamento da API
A API de Integrações usa versionamento via **header customizado**, garantindo que integrações existentes continuem funcionando quando novas versões forem lançadas.
Como usar [#como-usar]
Envie o header `api-version` em suas requisições:
```bash
curl -H "Authorization: Bearer " \
-H "api-version: v1" \
https://api.biterp.ai/products
```
Comportamento [#comportamento]
| Cenário | Resultado |
| --------------------------------------- | ------------------------------------------------ |
| Header `api-version` presente e válido | Usa a versão solicitada |
| Header `api-version` ausente | Usa a versão atual (`v1`) |
| Header `api-version` com valor inválido | Retorna `400 Bad Request` com as versões aceitas |
Versões disponíveis [#versões-disponíveis]
| Versão | Status |
| ------ | ------------------------- |
| `v1` | **Atual** — versão padrão |
Boas práticas [#boas-práticas]
1. **Sempre envie o header**: mesmo que a versão padrão funcione hoje, enviar explicitamente protege sua integração contra mudanças futuras
2. **Teste antes de migrar**: quando uma nova versão for lançada, teste sua integração antes de atualizar o header
3. **Monitore deprecações**: versões antigas serão depreciadas com antecedência
Exemplo de erro [#exemplo-de-erro]
Quando uma versão inválida é enviada, a resposta é `400 Bad Request` com as versões aceitas:
```json
{
"status_code": 400,
"message": "Invalid api-version. Accepted: v1. Current: v1",
"error": "Bad Request"
}
```
Changelog (breaking changes em `v1`) [#changelog-breaking-changes-em-v1]
Mudanças de contrato na versão atual (`v1`) são documentadas aqui. Integrações que dependem de campos removidos devem buscar o dado no recurso dedicado (ex.: `GET /customers/{id}`).
2026-08 — `customer` embutido em orçamentos (`GET /quotes`, `GET /quotes/{id}`) [#2026-08--customer-embutido-em-orçamentos-get-quotes-get-quotesid]
O objeto `customer` aninhado nas respostas de listagem e detalhe de orçamentos foi **reduzido** para alinhar com [pedidos de venda](/docs/api/guides/sales-orders) e [notas](/docs/api/guides/invoices): apenas identificação mínima do cliente, sem dados bancários, de contato ou de endereço.
**Antes** (\~17 campos):
```json
{
"customer": {
"id": "8f2c1e4a-9b3d-4c7e-a1f2-6d5b8e0c3a71",
"type": "company",
"tax_id": "12345678000195",
"tax_id_type": "cnpj",
"legal_name": "ACME Comércio Ltda",
"trade_name": "ACME",
"website": "https://acme.example",
"address_line1": "Rua das Flores, 100",
"address_line2": "Sala 12",
"city": "São Paulo",
"state": "SP",
"postal_code": "01310100",
"country_code": "BR",
"is_blocked": false,
"metadata": { "erp_code": "C-001" },
"created_at": "2026-01-15T10:00:00.000Z",
"updated_at": "2026-06-01T14:30:00.000Z"
}
}
```
**Depois** (4 campos):
```json
{
"customer": {
"id": "8f2c1e4a-9b3d-4c7e-a1f2-6d5b8e0c3a71",
"legal_name": "ACME Comércio Ltda",
"trade_name": "ACME",
"tax_id": "12345678000195"
}
}
```
**Campos removidos** do objeto embutido: `type`, `tax_id_type`, `website`, `address_line1`, `address_line2`, `city`, `state`, `postal_code`, `country_code`, `is_blocked`, `metadata`, `created_at`, `updated_at`.
O `customer_id` no corpo do orçamento **não muda**. Para o cadastro completo, use [`GET /customers/{id}`](/docs/api/reference/customers/find-by-id). Detalhes no guia de [orçamentos](/docs/api/guides/quotes#cliente-embutido-nas-respostas).
# Introdução
O que é o bitERP? [#o-que-é-o-biterp]
O **bitERP** é um sistema ERP (Enterprise Resource Planning) AI-first, projetado como plataforma SaaS multi-tenant. Ele permite que empresas gerenciem produtos, clientes, fornecedores, pedidos, financeiro e mais — tudo com integração nativa a agentes de IA.
Formas de integração [#formas-de-integração]
| Canal | Protocolo | Público-alvo | Autenticação |
| ---------------------- | --------------------------------- | ------------------------------------------------- | ------------------ |
| **API de Integrações** | REST (JSON) | Sistemas externos, automações (Zapier, Make, n8n) | API Keys ou OAuth2 |
| **MCP Server** | Model Context Protocol (JSON-RPC) | Agentes de IA (Claude, GPT, etc.) | OAuth2 via Clerk |
Ambientes [#ambientes]
| Ambiente | Base URL API | Base URL MCP |
| ------------ | ----------------------- | ----------------------- |
| **Produção** | `https://api.biterp.ai` | `https://mcp.biterp.ai` |
Obtendo credenciais [#obtendo-credenciais]
API Keys (para API de Integrações) [#api-keys-para-api-de-integrações]
1. Acesse o painel do bitERP em [app.biterp.ai](https://app.biterp.ai)
2. Navegue até **Configurações > API Keys**
3. Clique em **Criar API Key**
4. Configure o nome, descrição e permissões por recurso
5. **Copie o token imediatamente** — ele só é exibido uma vez
O token tem o formato `sk__`.
OAuth2 (para automações e MCP) [#oauth2-para-automações-e-mcp]
Para integrações via OAuth2 (Zapier, Make, n8n) ou para conectar agentes de IA via MCP, a autenticação é feita via fluxo OAuth2 Authorization Code. Consulte a seção de autenticação de cada canal para detalhes.
Conceitos básicos [#conceitos-básicos]
Multi-tenancy [#multi-tenancy]
Cada empresa no bitERP é um **tenant** isolado. Todas as operações são executadas no contexto de um tenant específico:
* **API Keys**: cada key pertence a um único tenant — o contexto é automático
* **OAuth2/MCP**: o agente deve chamar `list_tenants` primeiro e passar o `tenant_id` nas operações
Na API de Integrações, o endpoint `GET /tenant` retorna os dados do tenant atual (aquele a que a sua credencial pertence). Você nunca envia `tenant_id` nos payloads — ele é sempre inferido da credencial.
Permissões [#permissões]
O bitERP usa um modelo de permissões granular por recurso e ação (create, read, update, delete). Cada API Key ou usuário OAuth tem permissões específicas configuradas pelo administrador.
Isolamento de dados [#isolamento-de-dados]
Dados de diferentes tenants são completamente isolados via Row-Level Security (RLS) no banco de dados. Não é possível acessar dados de outro tenant, mesmo com credenciais válidas.
# Quickstart
Pré-requisitos [#pré-requisitos]
* Uma conta no bitERP com acesso de administrador
* Uma API Key criada (veja [Introdução](/docs/getting-started))
Sua primeira chamada [#sua-primeira-chamada]
Substitua `sk_xxx_yyy` pela sua API Key real.
curl [#curl]
```bash
curl -X GET https://api.biterp.ai/products \
-H "Authorization: Bearer sk_xxx_yyy" \
-H "Content-Type: application/json"
```
JavaScript / TypeScript [#javascript--typescript]
```typescript
const response = await fetch("https://api.biterp.ai/products", {
headers: {
Authorization: "Bearer sk_xxx_yyy",
"Content-Type": "application/json",
},
})
const data = await response.json()
console.log(data)
```
Python [#python]
```python
import requests
response = requests.get(
'https://api.biterp.ai/products',
headers={
'Authorization': 'Bearer sk_xxx_yyy',
'Content-Type': 'application/json',
},
)
data = response.json()
print(data)
```
Resposta esperada [#resposta-esperada]
```json
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Notebook Dell Inspiron",
"sale_price": "4500.000000",
"is_active": true,
"created_at": "2026-01-15T10:30:00Z"
}
],
"has_next_page": false,
"next_cursor": null
}
```
> Campos decimais e monetários (como `sale_price`) trafegam como **string** no formato `numeric(18,6)`, tanto na resposta quanto na requisição. Ver [formato de resposta](/docs/api).
Próximos passos [#próximos-passos]
Entenda os métodos de autenticação em detalhe.
Aprenda a paginar resultados e filtrar dados.
Conecte um agente de IA ao bitERP.
# Autenticação MCP
O MCP Server usa **OAuth 2.0 via Clerk** para autenticação. Cada request é autenticado independentemente via JWT.
Fluxo de autenticação [#fluxo-de-autenticação]
```
1. Cliente MCP descobre OAuth metadata
GET /.well-known/oauth-authorization-server
2. Redireciona usuário para autenticação Clerk
3. Recebe token OAuth (JWT)
4. POST / sem Mcp-Session-Id (cria sessão)
→ Servidor valida JWT
→ Cria sessão vinculada ao usuário
→ Retorna Mcp-Session-Id
5. Requests seguintes incluem Mcp-Session-Id
→ JWT validado em cada request
→ Sessão verificada contra userId
```
OAuth Discovery [#oauth-discovery]
O servidor expõe endpoints de discovery conforme os RFCs 9728 e 8414:
| Endpoint | RFC | Descrição |
| --------------------------------------------- | -------- | ----------------------------------- |
| `GET /.well-known/oauth-protected-resource` | RFC 9728 | Metadata do recurso protegido |
| `GET /.well-known/oauth-authorization-server` | RFC 8414 | Metadata do servidor de autorização |
Clientes MCP compatíveis (Claude Desktop, Cursor, etc.) usam esses endpoints automaticamente para descobrir como autenticar.
Contexto do usuário [#contexto-do-usuário]
Após autenticação, o servidor resolve o contexto completo:
| Campo | Descrição |
| -------------- | ----------------------------------------------- |
| `userId` | ID do usuário no bitERP |
| `email` | Email do usuário |
| `tenantId` | ID do tenant (resolvido via `list_tenants`) |
| `membershipId` | ID da associação usuário-tenant |
| `role` | Role do usuário no tenant (`admin` ou `member`) |
O token OAuth contém apenas o `userId`. O contexto de tenant é resolvido quando o agente chama `list_tenants` e passa o `tenant_id` nas operações.
Permissões [#permissões]
O sistema de permissões é idêntico ao da API de Integrações:
* Usuários `admin` têm acesso completo
* Usuários `member` têm acesso controlado por permissões granulares por recurso/ação
* Permissões são verificadas antes de cada operação
Quando o agente tenta uma operação sem permissão, recebe um erro que permite informar o usuário sobre a restrição.
Segurança [#segurança]
* Cada request é autenticado via JWT (não depende apenas da sessão)
* Sessões são vinculadas ao `userId` — tentativas de uso por outro usuário são bloqueadas
* Dados são isolados por tenant via Row-Level Security (RLS)
* HTTPS obrigatório em produção
# Tratamento de Erros
O MCP Server distingue dois tipos de erro, conforme a especificação do protocolo:
Protocol Errors [#protocol-errors]
Erros de estrutura JSON-RPC, tratados automaticamente pelo SDK:
```json
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Unknown tool: invalid_tool_name"
},
"id": 1
}
```
Esses erros indicam problemas no formato da mensagem, não na lógica de negócio.
Tool Execution Errors [#tool-execution-errors]
Erros de negócio retornados no resultado da tool com `isError: true`. O agente de IA recebe feedback acionável e pode se auto-corrigir:
```json
{
"content": [
{
"type": "text",
"text": "{\"error\":true,\"type\":\"PermissionError\",\"message\":\"You do not have permission to perform this action\"}"
}
],
"isError": true
}
```
Tipos de erro de negócio [#tipos-de-erro-de-negócio]
| Tipo | Descrição | Ação esperada do agente |
| ----------------- | ----------------------------- | ----------------------------------------- |
| `PermissionError` | Sem permissão para a operação | Informar o usuário sobre a restrição |
| `NotFoundError` | Recurso não encontrado | Tentar com outro ID ou informar o usuário |
| `ConflictError` | Conflito (ex: duplicata) | Verificar dados e ajustar |
| `ValidationError` | Dados inválidos | Corrigir parâmetros e tentar novamente |
| `Error` | Erro interno genérico | Reportar erro ao usuário |
Exemplos de cenários [#exemplos-de-cenários]
Permissão negada [#permissão-negada]
O usuário `member` tenta deletar um produto sem permissão `products:delete`:
```
Agente: Vou excluir o produto "Notebook Dell"
→ delete_product({ tenant_id: "...", id: "..." })
← PermissionError: You do not have permission to perform this action
Agente: Você não tem permissão para excluir produtos.
Peça ao administrador para conceder essa permissão.
```
Recurso não encontrado [#recurso-não-encontrado]
O agente tenta buscar um produto com ID inválido:
```
Agente: Buscando o produto...
→ get_product({ tenant_id: "...", id: "id-invalido" })
← NotFoundError: Product not found
Agente: Não encontrei um produto com esse ID.
Pode verificar o ID correto?
```
Tenant inválido [#tenant-inválido]
O agente passa um `tenant_id` que o usuário não tem acesso:
```
→ list_products({ tenant_id: "tenant-sem-acesso" })
← Error: Membership not found or inactive
Agente: Não foi possível acessar essa empresa.
Vamos verificar suas empresas disponíveis.
→ list_tenants()
```
Boas práticas para integradores [#boas-práticas-para-integradores]
1. **Sempre verifique `isError`**: Se `true`, o conteúdo contém uma mensagem de erro, não dados
2. **Use o campo `type`**: Permite tratar cada tipo de erro de forma específica
3. **Erros são acionáveis**: O agente deve usar a mensagem para se auto-corrigir quando possível
# MCP Server
O **bitERP MCP Server** permite que agentes de IA (como Claude, GPT e outros LLMs) executem operações de negócio no bitERP via o protocolo [Model Context Protocol (MCP)](https://modelcontextprotocol.io/).
O que é MCP? [#o-que-é-mcp]
O Model Context Protocol é um protocolo padronizado que permite a comunicação entre agentes de IA e serviços externos. Ele define como o agente descobre ferramentas (tools) disponíveis, as invoca e recebe resultados — tudo via JSON-RPC sobre HTTP.
Base URL [#base-url]
```
https://mcp.biterp.ai
```
Como conectar [#como-conectar]
1\. Configurar a conexão [#1-configurar-a-conexão]
No seu cliente MCP (Claude Desktop, Cursor, etc.), adicione o servidor:
```json
{
"mcpServers": {
"biterp": {
"url": "https://mcp.biterp.ai"
}
}
}
```
2\. Autenticar via OAuth [#2-autenticar-via-oauth]
O servidor usa OAuth 2.0 via Clerk. O cliente MCP gerencia o fluxo automaticamente:
1. O cliente descobre os endpoints OAuth via `/.well-known/oauth-authorization-server`
2. Redireciona você para autenticar no bitERP
3. Recebe o token e estabelece a sessão
3\. Selecionar o tenant [#3-selecionar-o-tenant]
Após autenticar, o agente deve chamar `list_tenants` para obter os tenants disponíveis. Se houver mais de um, o agente perguntará qual usar.
4\. Usar as ferramentas [#4-usar-as-ferramentas]
O agente de IA pode então usar todas as ferramentas disponíveis para executar operações de negócio no contexto do tenant selecionado.
Endpoints [#endpoints]
| Endpoint | Auth | Descrição |
| --------------------------------------------- | ---- | --------------------------------------------- |
| `HEAD /` | Não | Health check (verificação de disponibilidade) |
| `POST /` | Sim | Mensagens JSON-RPC (cria sessão se nova) |
| `GET /` | Sim | Conexão SSE (Server-Sent Events) |
| `DELETE /` | Sim | Encerramento de sessão |
| `GET /.well-known/oauth-protected-resource` | Não | Metadata do recurso (RFC 9728) |
| `GET /.well-known/oauth-authorization-server` | Não | Metadata OAuth (RFC 8414) |
| `GET /health` | Não | Health check alternativo |
Próximos passos [#próximos-passos]
Detalhes do fluxo OAuth 2.0.
Lista completa de ferramentas que o agente pode usar.
Como funciona o gerenciamento de sessões.
# Sessões
O MCP Server do bitERP usa **sessões stateful** — cada conexão de agente de IA recebe sua própria instância de servidor MCP com transport dedicado.
Ciclo de vida [#ciclo-de-vida]
1\. Criação [#1-criação]
Quando o cliente envia `POST /` sem o header `Mcp-Session-Id`, uma nova sessão é criada:
* O servidor valida o JWT
* Cria uma instância `McpServer` dedicada
* Registra todas as tools disponíveis
* Conecta o transport HTTP
* Retorna o `Mcp-Session-Id` no header da resposta
2\. Uso [#2-uso]
Requests subsequentes incluem o header `Mcp-Session-Id`:
```
POST / HTTP/1.1
Authorization: Bearer
Mcp-Session-Id:
Content-Type: application/json
```
A cada request, o servidor:
* Valida o JWT (autenticação independente por request)
* Verifica que a sessão pertence ao usuário (`userId`)
* Atualiza o timestamp de última atividade
* Delega a mensagem ao transport da sessão
3\. Expiração [#3-expiração]
Sessões inativas são removidas automaticamente:
| Configuração | Valor |
| ------------ | ------------------------- |
| **TTL** | 30 minutos de inatividade |
| **Cleanup** | A cada 5 minutos |
4\. Encerramento [#4-encerramento]
O cliente pode encerrar a sessão explicitamente:
```
DELETE / HTTP/1.1
Authorization: Bearer
Mcp-Session-Id:
```
Server-Sent Events (SSE) [#server-sent-events-sse]
O servidor suporta conexões SSE para streaming de mensagens server-to-client:
```
GET / HTTP/1.1
Authorization: Bearer
Mcp-Session-Id:
Accept: text/event-stream
```
Proteção anti-hijack [#proteção-anti-hijack]
Cada sessão é vinculada ao `userId` que a criou. Se um request com `Mcp-Session-Id` vem de outro usuário:
* O servidor retorna **404 Not Found**
* Não revela se a sessão existe (previne enumeração)
* Registra tentativa de hijack nos logs
Graceful shutdown [#graceful-shutdown]
Quando o servidor é encerrado (deploy, restart), todas as sessões ativas são fechadas de forma controlada — os transports e servers são encerrados antes do processo finalizar.
# Tools Disponíveis
O MCP Server expõe ferramentas (tools) que o agente de IA pode invocar para executar operações de negócio. O agente decide quando usar cada tool com base no contexto da conversa.
Fluxo padrão [#fluxo-padrão]
Toda interação com o MCP Server segue este padrão:
1. **`get_user_data`** — O agente obtém o perfil do usuário
2. **`list_tenants`** — O agente descobre os tenants disponíveis
3. **Operações de negócio** — O agente usa as tools de CRUD passando o `tenant_id`
***
Dados do usuário [#dados-do-usuário]
get_user_data [#get_user_data]
Retorna o perfil do usuário autenticado.
| Campo | Tipo | Descrição |
| ----- | ---- | --------------------------- |
| — | — | Nenhum parâmetro necessário |
**Resposta:**
```json
{
"id": "uuid",
"email": "usuario@empresa.com",
"name": "Nome do Usuário",
"language": "pt-BR",
"timezone": "America/Sao_Paulo"
}
```
***
Tenants [#tenants]
list_tenants [#list_tenants]
Lista os tenants (empresas) que o usuário pode acessar. **Deve ser chamada antes de qualquer operação de negócio.**
| Campo | Tipo | Descrição |
| ----- | ---- | --------------------------- |
| — | — | Nenhum parâmetro necessário |
**Resposta:**
```json
[
{
"tenant": "minha-empresa",
"company_name": "Minha Empresa Ltda",
"role": "admin"
}
]
```
Se o usuário tem acesso a apenas um tenant, o agente o usa automaticamente. Se há múltiplos, o agente pergunta qual usar.
***
Produtos [#produtos]
Todas as tools de produtos requerem o parâmetro `tenant_id` (obtido via `list_tenants`).
list_products [#list_products]
Lista todos os produtos do tenant.
| Parâmetro | Tipo | Obrigatório | Descrição |
| ------------- | ------------- | ----------- | --------------------- |
| `tenant_id` | string (UUID) | Sim | ID do tenant |
| `search` | string | Não | Busca por texto |
| `active_only` | boolean | Não | Filtrar apenas ativos |
**Permissão necessária:** `products:read`
get_product [#get_product]
Busca um produto por ID.
| Parâmetro | Tipo | Obrigatório | Descrição |
| ----------- | ------------- | ----------- | ------------- |
| `tenant_id` | string (UUID) | Sim | ID do tenant |
| `id` | string (UUID) | Sim | ID do produto |
**Permissão necessária:** `products:read`
create_product [#create_product]
Cria um novo produto.
| Parâmetro | Tipo | Obrigatório | Descrição |
| ------------- | ------------- | ----------- | -------------------- |
| `tenant_id` | string (UUID) | Sim | ID do tenant |
| `name` | string | Sim | Nome do produto |
| `sale_price` | number | Sim | Preço de venda |
| `description` | string | Não | Descrição |
| `sku` | string | Não | Código SKU |
| `is_active` | boolean | Não | Ativo (padrão: true) |
**Permissão necessária:** `products:create`
update_product [#update_product]
Atualiza um produto existente (partial update).
| Parâmetro | Tipo | Obrigatório | Descrição |
| ------------- | ------------- | ----------- | ---------------- |
| `tenant_id` | string (UUID) | Sim | ID do tenant |
| `id` | string (UUID) | Sim | ID do produto |
| `name` | string | Não | Novo nome |
| `sale_price` | number | Não | Novo preço |
| `description` | string | Não | Nova descrição |
| `is_active` | boolean | Não | Ativar/desativar |
**Permissão necessária:** `products:update`
delete_product [#delete_product]
Remove um produto (soft delete — o registro é preservado no histórico).
O servidor **não expõe tools de restore** em nenhum recurso: para o agente, `delete_*`
é de mão única. Desfazer uma exclusão é operação de usuário, feita no app web do
bitERP.
| Parâmetro | Tipo | Obrigatório | Descrição |
| ----------- | ------------- | ----------- | ------------- |
| `tenant_id` | string (UUID) | Sim | ID do tenant |
| `id` | string (UUID) | Sim | ID do produto |
**Permissão necessária:** `products:delete`
***
Annotations [#annotations]
Cada tool inclui annotations que descrevem seu comportamento:
| Tool | Read-only | Destrutiva |
| ---------------- | :-------: | :--------: |
| `get_user_data` | Sim | — |
| `list_tenants` | Sim | — |
| `list_products` | Sim | — |
| `get_product` | Sim | — |
| `create_product` | — | Não |
| `update_product` | — | Sim |
| `delete_product` | — | Sim |
O cliente MCP pode usar essas annotations para pedir confirmação do usuário antes de executar operações destrutivas.
***
Paginação por cursor [#paginação-por-cursor]
Tools de listagem que retornam muitos registros usam paginação por cursor:
| Parâmetro | Tipo | Descrição |
| --------- | ------ | ------------------------------------------------------ |
| `cursor` | string | Cursor da página anterior (omitir na primeira chamada) |
| `limit` | number | Itens por página (padrão: 20, máximo: 100) |
O agente itera automaticamente até `has_next_page` ser `false`, agregando os resultados antes de apresentar ao usuário.
# Clientes
O recurso **Clientes** (`/customers`) mantém o cadastro de clientes do tenant — pessoas físicas (`individual`) ou jurídicas (`company`). Assim como produtos, clientes têm campos universais e extensões por país (`br_data`, `us_data`).
Permissão exigida: `customers` (ações `create`, `read`, `update`, `delete`).
Conceitos e campos-chave [#conceitos-e-campos-chave]
| Campo | Tipo | Obrigatório | Observações |
| -------------------- | ------------ | ----------- | --------------------------------------------------------------------------------------- |
| `type` | enum | Sim | `individual` ou `company` |
| `tax_id` | string | Sim | Documento fiscal (CPF/CNPJ no BR, EIN/SSN nos EUA). Único por tenant |
| `legal_name` | string | Sim | Razão social / nome completo |
| `tax_id_type` | enum | Não | `CPF`, `CNPJ`, `EIN` ou `SSN`. Gravado como enviado (não é auto-derivado) |
| `trade_name` | string | Não | Nome fantasia |
| `reference_code` | string | Não | Código externo; auto-gerado (`CUS-…`) se omitido; único por tenant |
| `emails` | string\[] | Não | Lista de e-mails |
| `phones` | string\[] | Não | Lista de telefones |
| `website` | string | Não | |
| `address_line1` | string | Não | Endereço (linha 1) |
| `city` / `city_id` | string / int | Não | Cidade por nome (`city`) ou por ID do catálogo (`city_id`) |
| `state` / `state_id` | string / int | Não | Estado por nome (`state`) ou por ID do catálogo (`state_id`) |
| `postal_code` | string | Não | CEP / ZIP |
| `country_code` | string | Não | ISO2 (`BR`), ISO3 (`BRA`), numérico (`076`) ou nome. Assume o país do tenant se omitido |
| `tags` | string\[] | Não | |
| `is_active` | boolean | Não | Padrão `true` |
| `metadata` | object | Não | Dados livres da sua integração |
| `br_data` | object | Não | Extensão Brasil (`ie`, `ie_exempt`, `im`, `tax_regime`, `pix_key`…) |
| `us_data` | object | Não | Extensão EUA (`w9_on_file`, `naics_code`, `routing_number`…) |
> Na API de Integrações, **`tax_id` é obrigatório** ao criar um cliente. Diferente de outros contextos internos do bitERP, onde ele é opcional.
`tax_id` — CPF/CNPJ e EIN/SSN [#tax_id--cpfcnpj-e-einssn]
O `tax_id` é **normalizado** antes de ser gravado e comparado: tudo que não é letra ou dígito é removido e o valor é convertido para maiúsculas. Ou seja, `12.345.678/0001-95` e `12345678000195` representam o mesmo cliente. A API **não valida dígitos verificadores** de CPF/CNPJ nem o formato de EIN/SSN — ela apenas normaliza e garante unicidade por tenant. Envie o `tax_id_type` que se aplica ao documento; ele é gravado como veio.
Endereço e geografia [#endereço-e-geografia]
Você pode informar cidade e estado por **texto** (`city`, `state`) — que a API resolve para o catálogo interno — ou diretamente pelos **IDs** do catálogo (`city_id`, `state_id`, obtidos via [Dados de referência](/docs/api/guides/reference-data)). Um ID inexistente ou um `country_code` não resolvível retorna `422 Unprocessable Entity`. Na resposta, cidade, estado e país vêm resolvidos como objetos (`city: { id, name }`, `state: { id, state_code, name }`, `country: { id, iso2, iso3, name }`).
Criar um cliente [#criar-um-cliente]
```bash
curl -X POST https://api.biterp.ai/customers \
-H "Authorization: Bearer sk_abc123_secretXYZ" \
-H "Content-Type: application/json" \
-d '{
"type": "company",
"tax_id": "12.345.678/0001-95",
"tax_id_type": "CNPJ",
"legal_name": "Comercial Exemplo LTDA",
"trade_name": "Exemplo Store",
"emails": ["contato@exemplo.com.br"],
"phones": ["+55 11 4002-8922"],
"address_line1": "Av. Paulista, 1000",
"city": "São Paulo",
"state": "SP",
"postal_code": "01310-100",
"country_code": "BR",
"br_data": {
"ie": "123.456.789.012",
"ie_exempt": false,
"tax_regime": "simples_nacional"
}
}'
```
Buscar clientes [#buscar-clientes]
Além da listagem paginada (`GET /customers`, com [filtros e paginação](/docs/api/pagination)):
| Endpoint | Uso |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `GET /customers/search` | Busca full-text por `legal_name`, `trade_name` e `tax_id`. Aceita `q`, `limit` (padrão 50, 1–200) e `activeOnly`. |
| `GET /customers/tax-id/{taxId}` | Localiza um cliente pelo `tax_id` (normalizado). Retorna `404` se não existir. |
| `GET /customers/count` | Retorna a contagem total de clientes do tenant: `{ "count": 128 }`. |
| `GET /customers/{id}` | Busca por UUID. |
```bash
# Localizar por documento (pontuação é ignorada)
curl "https://api.biterp.ai/customers/tax-id/12345678000195" \
-H "Authorization: Bearer sk_abc123_secretXYZ"
```
Campos filtráveis em `GET /customers`: `legal_name`, `trade_name`, `tax_id`, `type`, `city`, `state`, `country_code`, `is_active`, `reference_code`, `tags`, `created_at`, `updated_at`. Ordenação por `id`, `legal_name`, `trade_name`, `reference_code`, `created_at`, `updated_at`.
Remover [#remover]
`DELETE /customers/{id}` faz **soft delete** e responde `204 No Content`.
Referência dos endpoints [#referência-dos-endpoints]
| Método | Endpoint | Referência |
| ------ | --------------------------- | ------------------------------------------------------------------ |
| POST | `/customers` | [Criar cliente](/docs/api/reference/customers/create) |
| GET | `/customers` | [Listar clientes](/docs/api/reference/customers/find-all) |
| GET | `/customers/search` | [Buscar por texto](/docs/api/reference/customers/search) |
| GET | `/customers/tax-id/{taxId}` | [Buscar por tax\_id](/docs/api/reference/customers/find-by-tax-id) |
| GET | `/customers/count` | [Contar clientes](/docs/api/reference/customers/count) |
| GET | `/customers/{id}` | [Buscar por ID](/docs/api/reference/customers/find-by-id) |
| PATCH | `/customers/{id}` | [Atualizar cliente](/docs/api/reference/customers/update) |
| DELETE | `/customers/{id}` | [Remover cliente](/docs/api/reference/customers/delete) |
Veja também [#veja-também]
O cadastro espelho de clientes, para o fluxo de compras.
Onde os clientes aparecem no financeiro.
# Contas financeiras
Uma **conta financeira** (`/financial-accounts`) representa onde o dinheiro do tenant entra e sai: uma conta corrente, poupança, caixa ou carteira digital. É a conta que amarra o financeiro — toda [transação](/docs/api/guides/financial-transactions), [conta a receber](/docs/api/guides/receivables) e [conta a pagar](/docs/api/guides/payables) referencia uma conta financeira.
Permissão exigida: `financial-accounts` (ações `create`, `read`, `update`, `delete`).
Conceitos e campos-chave [#conceitos-e-campos-chave]
| Campo | Tipo | Obrigatório | Observações |
| ----------------- | ------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | string | Sim | Nome da conta |
| `type` | enum | Sim | `checking`, `savings`, `cash`, `digital_wallet` ou `gateway` |
| `bank_id` | UUID | Não | Banco associado (ver [Dados de referência](/docs/api/guides/reference-data)) |
| `initial_balance` | string | Não | Saldo inicial; base do cálculo de saldo. Decimal `numeric(18,6)` como string, **com sinal** (ex.: `"10000.000000"`, `"-250.500000"`); padrão `"0.000000"` |
| `description` | string | Não | |
| `is_active` | boolean | Não | |
| `display_order` | number | Não | Ordem de exibição |
| `metadata` | object | Não | Dados livres da sua integração |
Os tipos `cash` e `digital_wallet` são contas **manuais** — é nelas que a liquidação automática de contas a receber/pagar pode gerar transações (ver `auto_generate_transaction` em [Contas a receber](/docs/api/guides/receivables)).
O tipo `gateway` representa o saldo retido por um gateway ou adquirente (Stripe, por exemplo) antes do repasse para a conta bancária. Ele **não** é uma conta manual: as transações chegam pela própria integração, então `auto_generate_transaction` não tem efeito nele. Alocar uma transação a um título (`POST /payables/{id}/allocate`, `POST /receivables/{id}/allocate`) continua funcionando em qualquer tipo de conta, inclusive `gateway`.
Criar uma conta [#criar-uma-conta]
```bash
curl -X POST https://api.biterp.ai/financial-accounts \
-H "Authorization: Bearer sk_abc123_secretXYZ" \
-H "Content-Type: application/json" \
-d '{
"name": "Conta Corrente - Banco do Brasil",
"type": "checking",
"initial_balance": "10000.000000",
"display_order": 1
}'
```
Calcular o saldo [#calcular-o-saldo]
`GET /financial-accounts/{id}/balance` calcula o saldo atual da conta:
```
saldo = initial_balance + SOMA(amount de todas as transações da conta)
```
Como créditos têm `amount` positivo e débitos `amount` negativo, a soma já é o valor líquido. O saldo é retornado como **string** com 6 casas decimais:
```json
{ "balance": "15234.560000" }
```
Para um **saldo histórico** (saldo em uma data específica), envie o query param `up_to_date` (ISO 8601) — só as transações com `posted_at` até essa data são consideradas:
```bash
curl "https://api.biterp.ai/financial-accounts/{id}/balance?up_to_date=2026-06-30T23:59:59Z" \
-H "Authorization: Bearer sk_abc123_secretXYZ"
```
Listar e manter [#listar-e-manter]
`GET /financial-accounts` retorna a lista paginada (ver [filtros e paginação](/docs/api/pagination)). Campos filtráveis: `name`, `type`, `is_active`, `created_at`, `updated_at`. Ordenação por `id`, `name`, `type`, `initial_balance`, `display_order`, `created_at`.
`DELETE /financial-accounts/{id}` faz **soft delete** e responde `204 No Content`.
Referência dos endpoints [#referência-dos-endpoints]
| Método | Endpoint | Referência |
| ------ | ---------------------------------- | -------------------------------------------------------------------- |
| POST | `/financial-accounts` | [Criar conta](/docs/api/reference/financial-accounts/create) |
| GET | `/financial-accounts` | [Listar contas](/docs/api/reference/financial-accounts/find-all) |
| GET | `/financial-accounts/{id}/balance` | [Calcular saldo](/docs/api/reference/financial-accounts/get-balance) |
| GET | `/financial-accounts/{id}` | [Buscar por ID](/docs/api/reference/financial-accounts/find-by-id) |
| PATCH | `/financial-accounts/{id}` | [Atualizar conta](/docs/api/reference/financial-accounts/update) |
| DELETE | `/financial-accounts/{id}` | [Remover conta](/docs/api/reference/financial-accounts/delete) |
# Categorias financeiras
Uma **categoria financeira** (`/financial-categories`) classifica para onde o dinheiro vai ou de onde ele vem: "Vendas de produtos", "Aluguel", "Folha de pagamento". É o plano de contas do tenant — [contas a receber](/docs/api/guides/receivables) e [contas a pagar](/docs/api/guides/payables) apontam para uma categoria através do campo `financial_category_id`.
Permissão exigida: `financial-categories` (ações `create`, `read`, `update`, `delete`).
Conceitos e campos-chave [#conceitos-e-campos-chave]
| Campo | Tipo | Obrigatório | Observações |
| ---------------- | ------- | ----------- | ----------------------------------------------------------------------- |
| `name` | string | Sim | Nome da categoria. Único entre irmãs (mesmo `parent_id`) |
| `type` | enum | Sim | `revenue` (receita) ou `expense` (despesa) |
| `parent_id` | UUID | Não | Categoria-mãe. Ausente/`null` = categoria raiz |
| `code` | string | Não | Código contábil da sua estrutura (ex.: `3.01.001`) |
| `reference_code` | string | Não | Código do sistema de origem. `null` quando criada direto no bitERP |
| `position` | number | Não | Inteiro de ordenação entre irmãs — é a **ordenação padrão** da listagem |
| `is_locked` | boolean | Não | Trava a categoria para novos documentos (ver abaixo); padrão `false` |
| `description` | string | Não | |
| `metadata` | object | Não | Dados livres da sua integração |
Hierarquia [#hierarquia]
Categorias formam uma árvore, com três regras que a API impõe na criação e na atualização:
1. **Profundidade máxima de 3 níveis.** Uma quarta geração é rejeitada.
2. **O `type` da filha tem que ser igual ao da mãe.** Não existe subcategoria de despesa dentro de uma categoria de receita.
3. **Sem ciclos.** Uma categoria não pode ser mãe de si mesma, nem de uma ancestral sua.
Nome duplicado sob a **mesma** mãe retorna `409 Conflict`. Duas categorias com o mesmo nome em mães diferentes são permitidas.
Quais categorias podem ser usadas em documentos [#quais-categorias-podem-ser-usadas-em-documentos]
Nem toda categoria aceita vínculo com um documento financeiro. Ao informar `financial_category_id` em uma conta a pagar ou a receber, a API exige que a categoria seja:
* **Folha** — categorias que têm filhas servem para agrupar, não para lançar. Vincular uma categoria com filhas é rejeitado.
* **Destravada** — uma categoria com `is_locked: true` continua existindo e mantém o histórico dos documentos que já a usavam, mas é recusada em **novos** documentos. É como aposentar uma categoria sem apagar o passado.
Criar uma categoria [#criar-uma-categoria]
```bash
curl -X POST https://api.biterp.ai/financial-categories \
-H "Authorization: Bearer sk_abc123_secretXYZ" \
-H "Content-Type: application/json" \
-d '{
"name": "Vendas de produtos",
"type": "revenue",
"code": "3.01.001",
"position": 1
}'
```
Para criar uma subcategoria, informe o `parent_id` e repita o `type` da mãe:
```bash
curl -X POST https://api.biterp.ai/financial-categories \
-H "Authorization: Bearer sk_abc123_secretXYZ" \
-H "Content-Type: application/json" \
-d '{
"name": "Vendas no varejo",
"type": "revenue",
"parent_id": "7b3e9d1c-4a2f-4e8b-9c0d-5f1a3b7e2d64",
"position": 1
}'
```
Ler a árvore inteira [#ler-a-árvore-inteira]
`GET /financial-categories/tree` devolve a hierarquia já montada — cada categoria traz suas filhas em `children[]`, recursivamente. É a chamada indicada para popular um seletor na sua interface, porque evita reconstruir a árvore a partir da listagem plana.
```bash
# Só a árvore de despesas
curl "https://api.biterp.ai/financial-categories/tree?type=expense" \
-H "Authorization: Bearer sk_abc123_secretXYZ"
```
O query param `type` é opcional e aceita `revenue` ou `expense`; sem ele, a resposta traz as duas árvores. Diferente de `GET /financial-categories`, este endpoint **não é paginado**.
Listar e manter [#listar-e-manter]
`GET /financial-categories` retorna a lista plana e paginada (ver [filtros e paginação](/docs/api/pagination)).
Campos filtráveis: `type`, `parent_id`, `reference_code`, `name`, `code`, `position`, `is_locked`, `created_at`, `updated_at`. Ordenação por `id`, `type`, `parent_id`, `reference_code`, `name`, `code`, `position`, `is_locked`, `created_at`, `updated_at` — o padrão é `position:asc`.
No contrato de filtros deste recurso, apenas `reference_code` é anulável — então só ele aceita os operadores `is_null` / `is_not_null`. É o caminho para achar o que ainda não foi mapeado a partir do seu sistema:
```bash
GET /financial-categories?type=expense&reference_code[is_null]=true
```
`DELETE /financial-categories/{id}` faz **soft delete** e responde `204 No Content`. **Não há endpoint de restauração** — ver [`DELETE` é de mão única](/docs/api#delete-é-de-mão-única). Para tirar uma categoria de circulação sem perdê-la, prefira `is_locked: true`.
Referência dos endpoints [#referência-dos-endpoints]
| Método | Endpoint | Referência |
| ------ | ---------------------------- | ---------------------------------------------------------------------- |
| POST | `/financial-categories` | [Criar categoria](/docs/api/reference/financial-categories/create) |
| GET | `/financial-categories` | [Listar categorias](/docs/api/reference/financial-categories/find-all) |
| GET | `/financial-categories/tree` | [Listar árvore](/docs/api/reference/financial-categories/find-tree) |
| GET | `/financial-categories/{id}` | [Buscar por ID](/docs/api/reference/financial-categories/find-by-id) |
| PATCH | `/financial-categories/{id}` | [Atualizar categoria](/docs/api/reference/financial-categories/update) |
| DELETE | `/financial-categories/{id}` | [Remover categoria](/docs/api/reference/financial-categories/delete) |
Veja também [#veja-também]
Onde as categorias de receita são aplicadas.
Onde as categorias de despesa são aplicadas.
# Transações e extrato
As **transações financeiras** (`/financial-transactions`) são o **ledger** (livro-razão) do tenant: cada linha é um crédito ou débito lançado em uma [conta financeira](/docs/api/guides/financial-accounts). Na API de Integrações você **não cria nem edita** transações — elas nascem de fluxos de negócio (veja abaixo). O que você faz por aqui é **consultar** (transações, extrato e saldo) e **remover** lançamentos via exclusão lógica (soft delete).
Permissão exigida: `financial-transactions` (ações `read`, `delete`).
Como as transações são criadas [#como-as-transações-são-criadas]
Você não cria transações diretamente pela API de Integrações. Elas são geradas pelo bitERP em fluxos de negócio:
* **Alocação/liquidação** de uma [conta a receber](/docs/api/guides/receivables) ou [a pagar](/docs/api/guides/payables) (o pagamento em si é uma transação).
* **Liquidação automática** ao criar um recebível/pagável com `auto_generate_transaction` em uma conta manual (`cash`/`digital_wallet`).
* **Importação OFX**, **transferências** entre contas e integrações bancárias.
O `source` de cada transação indica sua origem: `manual`, `ofx`, `integration`, `migration` ou `transfer`.
Conceitos e campos-chave [#conceitos-e-campos-chave]
O sinal do `amount` define a natureza do lançamento: **crédito** é positivo, **débito** é negativo.
| Campo | Tipo | Observações |
| --------------- | --------- | ---------------------------------------------------------------------------------------------------------------- |
| `account_id` | UUID | Conta financeira do lançamento |
| `amount` | string | Decimal `numeric(18,6)` **com sinal**: positivo = crédito (`"1500.000000"`), negativo = débito (`"-250.500000"`) |
| `posted_at` | date-time | Data/hora contábil do lançamento |
| `source` | enum | `manual`, `ofx`, `integration`, `migration`, `transfer` |
| `trn_type` | string | Tipo do lançamento (ex.: `CREDIT`, `DEBIT`, `XFER`) |
| `is_reconciled` | boolean | Se a transação já foi conciliada |
| `transfer_id` | UUID | Preenchido quando faz parte de uma transferência |
| `memo`, `name` | string | Descrição / contraparte |
| `metadata` | object | |
Extrato de uma conta [#extrato-de-uma-conta]
`GET /financial-transactions/account/{accountId}` retorna o **extrato** — todas as transações da conta, ordenadas da mais recente para a mais antiga (`posted_at` decrescente). O retorno é um array simples (não paginado).
```bash
curl "https://api.biterp.ai/financial-transactions/account/9c8b7a6d-5e4f-4321-8b0a-1d2c3e4f5a6b" \
-H "Authorization: Bearer sk_abc123_secretXYZ"
```
Saldo [#saldo]
`GET /financial-transactions/balance/{accountId}` retorna o saldo da conta (`initial_balance + SOMA(amount)`), no mesmo formato do endpoint de saldo de [contas financeiras](/docs/api/guides/financial-accounts):
```json
{ "balance": "15234.560000" }
```
Listar e remover [#listar-e-remover]
`GET /financial-transactions` lista transações de forma paginada (ver [filtros e paginação](/docs/api/pagination)); a ordenação padrão é `posted_at` decrescente. `DELETE /financial-transactions/{id}` faz **soft delete** e responde `204 No Content`.
Referência dos endpoints [#referência-dos-endpoints]
| Método | Endpoint | Referência |
| ------ | --------------------------------------------- | ------------------------------------------------------------------------------ |
| GET | `/financial-transactions` | [Listar transações](/docs/api/reference/financial-transactions/find-all) |
| GET | `/financial-transactions/account/{accountId}` | [Extrato da conta](/docs/api/reference/financial-transactions/find-by-account) |
| GET | `/financial-transactions/balance/{accountId}` | [Saldo da conta](/docs/api/reference/financial-transactions/get-balance) |
| GET | `/financial-transactions/{id}` | [Buscar por ID](/docs/api/reference/financial-transactions/find-by-id) |
| DELETE | `/financial-transactions/{id}` | [Remover transação](/docs/api/reference/financial-transactions/delete) |
# Mapeamento de entidades
Os **mapeamentos de entidade** (`/integration-entity-mappings`) resolvem um problema clássico de integração: **relacionar o ID de um registro no seu sistema com o ID correspondente no bitERP**. Com esse vínculo, sua sincronização fica **idempotente** — você sabe se um cliente/produto/nota já foi criado e evita duplicar.
> **Permissão:** o código de permissão deste recurso é **`integration-mappings`** — diferente do caminho da URL (`integration-entity-mappings`). Configure a permissão da API Key com o código `integration-mappings` (ações `read`, `create`, `update`, `delete`).
Como funciona [#como-funciona]
Cada mapeamento liga um `internal_entity_id` (o UUID do registro no bitERP) a um `external_entity_id` (o identificador no seu sistema), dentro de um `entity_type` (ex.: `customer`, `product`, `invoice`) e de uma integração (`tenant_integration_id`).
A unicidade é garantida por duas chaves compostas: `(tenant_integration_id, entity_type, internal_entity_id)` e `(tenant_integration_id, entity_type, external_entity_id)`. Na prática, dentro de uma mesma integração e `entity_type`, um mesmo ID **interno** não pode mapear para dois externos, nem um mesmo ID **externo** para dois internos. Tentar criar um mapeamento que viola qualquer uma dessas chaves retorna `409 Conflict` — é esse comportamento que torna a operação idempotente.
Manual vs. automático [#manual-vs-automático]
O campo `mapping_origin` indica a origem do vínculo:
* **`manual`** — criado por você via `POST /integration-entity-mappings`.
* **`automatic`** — criado pelo próprio bitERP durante fluxos de sincronização. Esse caminho **não** é exposto pela API; ele aparece apenas na leitura.
`mapping_origin`, `entity_type` e `internal_entity_id` são **imutáveis** após a criação.
Campos de criação (POST) [#campos-de-criação-post]
| Campo | Tipo | Obrigatório | Observações |
| ----------------------- | ------ | ----------- | ----------------------------------------------------------------- |
| `tenant_integration_id` | UUID | Sim | A integração à qual o mapeamento pertence |
| `entity_type` | string | Sim | Tipo da entidade (texto livre, ≤ 100; ex.: `customer`, `product`) |
| `internal_entity_id` | UUID | Sim | ID do registro no bitERP |
| `external_entity_id` | string | Sim | ID no sistema externo (≤ 255) |
| `sync_status` | enum | Não | `synced`, `pending` ou `error` (padrão `synced`) |
| `metadata` | object | Não | Dados livres da sua integração |
> `entity_type` é **texto livre** (não um enum fechado). Padronize seus próprios valores (`customer`, `product`, `invoice`…) e mantenha-os consistentes.
Criar um mapeamento [#criar-um-mapeamento]
```bash
curl -X POST https://api.biterp.ai/integration-entity-mappings \
-H "Authorization: Bearer sk_abc123_secretXYZ" \
-H "Content-Type: application/json" \
-d '{
"tenant_integration_id": "0193f000-1111-7222-8333-444455556666",
"entity_type": "customer",
"internal_entity_id": "0194aaaa-bbbb-7ccc-8ddd-eeeeffff0000",
"external_entity_id": "cus_QabC123XyZ",
"sync_status": "synced",
"metadata": { "source_label": "Cliente no Stripe" }
}'
```
A resposta traz `id`, `mapping_origin: "manual"`, `last_synced_at` e os campos de auditoria.
Atualizar [#atualizar]
`PATCH /integration-entity-mappings/{id}` altera apenas `external_entity_id`, `sync_status` e `metadata`. Definir `sync_status: "synced"` atualiza `last_synced_at`.
Listar e remover [#listar-e-remover]
`GET /integration-entity-mappings` lista de forma paginada. Campos filtráveis: `tenant_integration_id`, `entity_type`, `sync_status`, `mapping_origin`, `internal_entity_id`, `external_entity_id`, `created_at`, `last_synced_at` — úteis para localizar um mapeamento pelo ID externo antes de decidir criar ou atualizar um registro. `DELETE /integration-entity-mappings/{id}` faz **soft delete** (`204 No Content`).
Referência dos endpoints [#referência-dos-endpoints]
| Método | Endpoint | Referência |
| ------ | ----------------------------------- | --------------------------------------------------------------------------- |
| POST | `/integration-entity-mappings` | [Criar mapeamento](/docs/api/reference/integration-entity-mappings/create) |
| GET | `/integration-entity-mappings` | [Listar](/docs/api/reference/integration-entity-mappings/find-all) |
| GET | `/integration-entity-mappings/{id}` | [Buscar por ID](/docs/api/reference/integration-entity-mappings/find-by-id) |
| PATCH | `/integration-entity-mappings/{id}` | [Atualizar](/docs/api/reference/integration-entity-mappings/update) |
| DELETE | `/integration-entity-mappings/{id}` | [Remover](/docs/api/reference/integration-entity-mappings/delete) |
# Notas / faturas
Uma **nota / fatura** (`/invoices`) é o documento de faturamento de uma venda. É a terceira etapa do [fluxo de vendas](/docs/api/guides/sales-flow): pode ser ligada a um [pedido](/docs/api/guides/sales-orders), carregar impostos por item e gerar [contas a receber](/docs/api/guides/receivables).
Permissão exigida: `invoices` (ações `create`, `read`, `update`, `delete`).
> Esta é a nota **comercial**. A API de Integrações oferece o CRUD; ela **não** transmite documentos fiscais (NFe/NFSe) nem faz "emissão" — esses fluxos ficam fora deste canal.
Conceitos e campos-chave [#conceitos-e-campos-chave]
| Campo | Tipo | Obrigatório | Observações |
| -------------------------------- | ------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reference_code` | string | Não | Opcional e anulável: texto livre seu de até 100 caracteres. **Não** é autogerado e **não** é o número fiscal. Envie `""` para limpar (vira `null`) |
| `customer_id` | UUID | Sim\* | \*Ou o objeto `customer` inline — exatamente **um** dos dois |
| `product_type` | enum | Sim | `goods`, `service` ou `mixed` |
| `invoice_date` | date | Sim | `YYYY-MM-DD` |
| `items` | array | Sim | 1 a 200 itens |
| `operation_type` | string | Não | Padrão `sale` |
| `sales_order_id` | UUID | Não | Pedido de origem (ver validações abaixo) |
| `customer_tax_type` | enum | Não | `individual` ou `company` |
| `discount_amount` | string | Não | Desconto em valor (≥ 0) — decimal `numeric(18,6)` como string |
| `discount_rate` | string | Não | Percentual do subtotal (0 a 100) — decimal `numeric(9,6)` como string |
| `fiscal_notes`, `internal_notes` | string | Não | |
| `taxes` | array | Não | Impostos manuais por item (ver abaixo) |
| `receivables` | array | Não | Gera contas a receber (ver abaixo) |
| `metadata`, `br_data`, `us_data` | — | Não | |
> **`discount_rate` usa a escala percentual (0 a 100) e trafega como string decimal** — `"15.000000"` significa 15%, igual a [orçamentos](/docs/api/guides/quotes) e [pedidos](/docs/api/guides/sales-orders).
Itens da nota [#itens-da-nota]
Cada item referencia um `product_id` **ou** traz um `product` inline (exatamente um dos dois). Campos: `item_description`, `quantity` (**string** decimal ≥ `"0.000001"`), `unit_price` (**string** decimal ≥ `"0"`), `discount_amount` (**string** decimal). Para rastrear a origem no pedido, informe `sales_order_item_id` no item.
Impostos (`taxes[]`) [#impostos-taxes]
Impostos são informados por item, com `tax_code`, `tax_name`, `base_amount` (string decimal), `rate` (**string** decimal, 0 a 100), `amount` (string decimal), `position` (inteiro) e flags como `is_withheld` (retido) e `is_included`. Códigos aceitos: `ISS`, `PIS`, `COFINS`, `IR`, `CSLL`, `INSS`, `IBS_STATE`, `IBS_CITY`, `CBS`.
Vínculo com o pedido [#vínculo-com-o-pedido]
Ao enviar `sales_order_id`, a API valida que `customer_id`, `operation_type` e `product_type` da nota **casam** com os do pedido. Só pode existir **uma nota ativa por pedido** — uma segunda tentativa retorna erro. Ver o [fluxo de vendas](/docs/api/guides/sales-flow).
Gerando contas a receber [#gerando-contas-a-receber]
Inclua um array `receivables[]` (`amount` — decimal como string —, `due_date` date-time, `financial_account_id`; opcionais `payment_method_id`, `description`). A **soma** de `receivables[].amount` deve ser **exatamente igual** ao total líquido da nota (`net_receivable_total` = total − impostos retidos), senão a criação é rejeitada. Se houver mais de um, viram parcelas.
Os recebíveis criados nascem `pending` e ficam ligados por `origin_type = "invoice"`; eles **não** aparecem no corpo da nota — consulte-os no recurso [`/receivables`](/docs/api/guides/receivables).
Criar uma nota (ligada ao pedido, com recebível) [#criar-uma-nota-ligada-ao-pedido-com-recebível]
```bash
curl -X POST https://api.biterp.ai/invoices \
-H "Authorization: Bearer sk_xxx_yyy" \
-H "Content-Type: application/json" \
-d '{
"reference_code": "NF-2026-0042",
"customer_id": "8f2c1e4a-9b3d-4c7e-a1f2-6d5b8e0c3a71",
"sales_order_id": "7e6d5c4b-3a2f-4109-8e7d-6c5b4a3f2e1d",
"product_type": "goods",
"invoice_date": "2026-07-06",
"operation_type": "sale",
"items": [
{
"product_id": "3a7b9c2d-1e4f-4a6b-8c0d-2f5e7a9b1c3d",
"item_description": "Cadeira ergonômica",
"quantity": "2.000000",
"unit_price": "450.000000",
"sales_order_item_id": "d4c3b2a1-9f8e-4706-a5b4-c3d2e1f0a9b8"
},
{
"product_id": "5d8e1f3a-6b2c-4d7e-9a0f-1c3b5e7d9a2f",
"item_description": "Mesa de escritório",
"quantity": "1.000000",
"unit_price": "1200.000000"
}
],
"receivables": [
{
"amount": "2100.000000",
"due_date": "2026-08-06T00:00:00.000Z",
"financial_account_id": "9c8b7a6d-5e4f-4321-8b0a-1d2c3e4f5a6b"
}
]
}'
```
> Sem impostos retidos, `net_receivable_total` é igual ao total (2 × 450 + 1 × 1200 = 2100), então `receivables[].amount` soma `"2100.000000"`.
Status e atualização [#status-e-atualização]
Pela API de Integrações, toda nota nasce em `status: draft` (os estados comerciais são `draft`, `finalized`, `cancelled`). No `PATCH`, os campos `status` e `operation_type` **não** são editáveis.
O que pode ser alterado, por estado [#o-que-pode-ser-alterado-por-estado]
A edição depende do estado de transmissão fiscal (BR) e do vínculo com um pedido de venda:
| Estado da nota | Campos aceitos no `PATCH` |
| ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| Rascunho (`draft`) | Todos, sujeitos às restrições de vínculo com pedido abaixo |
| Finalizada (`finalized`) e ainda **não** transmitida (`transmission_status` ausente) | Todos, sujeitos às restrições de vínculo com pedido abaixo |
| Finalizada e transmitida com erro (`transmission_error`, fluxo de retransmissão) | Todos, sujeitos às restrições de vínculo com pedido abaixo |
| Na fila de transmissão fiscal (`queued`) | Nenhum — a nota já foi entregue ao provedor fiscal; nem anotações internas |
| Já transmitida e autorizada (`issued`) | Somente `reference_code`, `internal_notes` e `metadata` |
| Finalizada no Brasil (`finalized`), sem linha em `invoices_br_data` | Somente `reference_code`, `internal_notes` e `metadata` (fail secure — mesmo tratamento de `issued`) |
| Cancelamento em andamento, ou nota cancelada | Nenhum |
Fora do Brasil não existe eixo fiscal: `transmission_status` é sempre ausente, então `draft` e
`finalized` aceitam todos os campos.
Se a nota tiver `sales_order_id`, mesmo fora dos casos acima ficam bloqueados `discount_amount`, `discount_rate`, `product_type`, `salesperson_name` e `fiscal_notes` no cabeçalho e, nos itens, adicionar/remover/reordenar linhas ou alterar `product_id`, `quantity`, `unit_price`, `discount_amount` ou `sales_order_item_id` de uma linha existente — esses dados vêm do pedido. `item_description`, `po_number`, dados fiscais de linha e `taxes` continuam editáveis normalmente.
Um `PATCH` com algum campo não permitido para o estado atual retorna `422` nomeando o(s) campo(s) rejeitado(s).
Antes, uma nota já transmitida (`issued`) aceitava alterações em `taxes`,
`discount_amount`, `discount_rate`, `br_data`, `items` e `product_type` — divergindo
do que já havia sido autorizado no provedor fiscal. Agora esses campos são rejeitados
com `422` nesse estado (e também com a nota na fila de transmissão, `queued`, onde
nenhum campo é aceito). No Brasil, uma nota `finalized` sem registro em
`invoices_br_data` recebe o mesmo tratamento restrito (`internal_notes` e `metadata`
apenas) — qualquer outro campo no corpo do `PATCH` retorna `422` nomeando o(s)
campo(s) rejeitado(s). Se a sua integração corrigia dados fiscais depois da
transmissão, mova a correção para **antes** de transmitir, ou trate o `422` e ajuste o
fluxo.
Enviar `items` **substitui** a lista inteira: linhas com `id` são mantidas, omitidas são removidas.
Já `taxes` tem três comportamentos distintos no `PATCH`:
| `taxes` no corpo | Efeito |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Ausente** | Não altera impostos. Enviando `items`, cada linha mantida preserva os impostos que já tinha. |
| **`[]` (array vazio)** | **Remove** todos os impostos que seriam preservados. |
| **Preenchido** | Com `items`: substituição **por item**, endereçada pelo `item_index` — só as linhas citadas trocam de impostos; as não citadas preservam os que já tinham. Sem `items`: substitui o conjunto inteiro de impostos da nota. |
Um item que traga `tax_resolution_id` é sempre re-resolvido pelo motor fiscal, mesmo com `taxes: []` — o array vazio limpa apenas os impostos preservados, não uma re-resolução pedida no mesmo corpo.
Até então, enviar `items` junto de `taxes: []` era tratado como "nenhuma substituição
por item" e **preservava** os impostos existentes. Agora esse par **remove** os
impostos. Se a sua integração enviava `taxes: []` só para preencher o campo, **omita
`taxes`** do corpo para manter o comportamento anterior.
`DELETE /invoices/{id}` remove notas `draft`/`finalized`; recebíveis já pagos bloqueiam a exclusão.
Referência dos endpoints [#referência-dos-endpoints]
| Método | Endpoint | Referência |
| ------ | ---------------- | -------------------------------------------------------- |
| POST | `/invoices` | [Criar nota](/docs/api/reference/invoices/create) |
| GET | `/invoices` | [Listar notas](/docs/api/reference/invoices/find-all) |
| GET | `/invoices/{id}` | [Buscar por ID](/docs/api/reference/invoices/find-by-id) |
| PATCH | `/invoices/{id}` | [Atualizar nota](/docs/api/reference/invoices/update) |
| DELETE | `/invoices/{id}` | [Remover nota](/docs/api/reference/invoices/delete) |
# Contas a pagar
Uma **conta a pagar** (`/payables`) é um valor que o tenant deve a um [fornecedor](/docs/api/guides/suppliers), vinculado a uma [conta financeira](/docs/api/guides/financial-accounts). É o **espelho de [Contas a receber](/docs/api/guides/receivables)**: mesma máquina de estado, alocação, parcelamento, vencidas e resumo — trocando "receber" por "pagar".
Permissão exigida: `payables` (ações `create`, `read`, `update`, `delete`).
Ciclo de vida [#ciclo-de-vida]
Os status são os mesmos das contas a receber: `pending`, `partial`, `paid`, `overdue`, `cancelled`. Um pagável nasce em `pending` e caminha para `paid` conforme você **aloca transações de débito** (paga). A diferença relevante frente aos recebíveis é que o **`overdue` é aplicado automaticamente** ao recalcular: se restar saldo e o `due_date` já passou, o status vai para `overdue` (inclusive ao alterar a data de vencimento via `PATCH`).
> Assim como em recebíveis, `status` é um campo bloqueado no `PATCH` — mude o estado com as ações (`allocate`, `cancel`).
Criar uma conta a pagar [#criar-uma-conta-a-pagar]
| Campo | Tipo | Obrigatório | Observações |
| --------------------------- | ------- | ----------- | ------------------------------------------------------------------------------------------------ |
| `supplier_id` | UUID | Sim | Fornecedor credor |
| `financial_account_id` | UUID | Sim | Conta financeira (deve estar ativa) |
| `amount` | string | Sim | Valor a pagar (> 0) — decimal `numeric(18,6)` como string (ex.: `"1250.000000"`) |
| `issue_date` | date | Sim | `YYYY-MM-DD` |
| `due_date` | date | Sim | `YYYY-MM-DD` |
| `payment_method_id` | UUID | Não | |
| `financial_category_id` | UUID | Não | |
| `reference_code` | string | Não | Auto-gerado (`PAY-…`) se omitido; único por tenant |
| `discount_amount` | string | Não | (≥ 0) — decimal como string |
| `surcharge_amount` | string | Não | (≥ 0) — decimal como string |
| `description` / `notes` | string | Não | |
| `auto_generate_transaction` | boolean | Não | Em conta manual (`cash`/`digital_wallet`), gera a transação de débito, aloca e marca como `paid` |
| `metadata` | object | Não | |
> Diferente de recebíveis, contas a pagar **não** têm os campos `origin_type`, `origin_id`, `sales_order_id` nem `invoice_id`. O vínculo com o fluxo de compras vem por [notas de compra](/docs/api/guides/purchase-invoices) (que geram os pagáveis).
```bash
curl -X POST https://api.biterp.ai/payables \
-H "Authorization: Bearer sk_abc123_secretXYZ" \
-H "Content-Type: application/json" \
-d '{
"supplier_id": "1a2b3c4d-5e6f-4708-9a0b-1c2d3e4f5a6b",
"financial_account_id": "1a2b3c4d-5e6f-4708-9a1b-2c3d4e5f6071",
"amount": "1250.000000",
"issue_date": "2026-07-06",
"due_date": "2026-08-05",
"description": "Compra de matéria-prima"
}'
```
Pagar: alocar uma transação [#pagar-alocar-uma-transação]
Pagar é **alocar uma [transação](/docs/api/guides/financial-transactions) de débito** ao pagável. `POST /payables/{id}/allocate`:
| Campo | Tipo | Obrigatório | Observações |
| -------------------------- | ------ | ----------- | ------------------------------------------ |
| `financial_transaction_id` | UUID | Sim | Transação de **débito** (amount negativo) |
| `allocated_amount` | string | Sim | Valor a alocar (> 0) — decimal como string |
| `notes` | string | Não | |
As regras espelham as de recebíveis: o pagável não pode estar `paid`/`cancelled`; a transação deve ser da **mesma** conta e ser um débito (negativa); `allocated_amount` respeita o saldo restante do pagável e da transação. Use `DELETE /payables/{id}/allocate/{allocationId}` para desfazer.
```bash
curl -X POST https://api.biterp.ai/payables/{id}/allocate \
-H "Authorization: Bearer sk_abc123_secretXYZ" \
-H "Content-Type: application/json" \
-d '{
"financial_transaction_id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
"allocated_amount": "500.000000",
"notes": "Pagamento parcial ao fornecedor"
}'
```
O que pode ser alterado, por estado [#o-que-pode-ser-alterado-por-estado]
O `PATCH` aceita campos diferentes conforme o `status` do pagável. Depois que o dinheiro se moveu — total (`paid`) ou parcialmente (`partial`) — o título descreve um fato consumado, e só continua editável o que **não** altera esse fato:
| Status do pagável | Campos aceitos no `PATCH` |
| -------------------- | --------------------------------------------------------------- |
| `pending`, `overdue` | Todos os do corpo do `PATCH`, sujeitos às validações do recurso |
| `partial` | Somente `notes`, `financial_category_id` e `metadata` |
| `paid` | Somente `notes`, `financial_category_id` e `metadata` |
| `cancelled` | Nenhum — nem mesmo um corpo vazio |
Um `PATCH` com algum campo não permitido para o estado atual retorna `422` **nomeando o(s) campo(s) rejeitado(s)**. E se o status mudar enquanto o `PATCH` estiver em voo — uma baixa ou um cancelamento concorrente —, a atualização é recusada com `409` em vez de ser aplicada com as regras do estado antigo: releia o registro e repita.
Para alterar qualquer outro campo de um título já baixado, **desfaça, estorne ou desvincule o pagamento** (`DELETE /payables/{id}/allocate/{allocationId}`, ou as ações de estorno). A edição total volta quando o status **recalculado** for `pending` ou `overdue` — e quem decide entre os dois é o **vencimento**, não a ausência de valor baixado: com saldo em aberto e vencimento já passado o título fica `overdue`, que mantém a edição total mesmo com baixa parcial; ainda dentro do prazo e com valor baixado remanescente — várias baixas, ou um estorno parcial —, ele fica `partial` e continua restrito. Não há beco sem saída — só a obrigação de passar pela operação que corresponde ao que de fato aconteceu.
`financial_category_id` continua editável depois da baixa de propósito: a categoria contábil existe **apenas** no título (a transação financeira não tem esse campo), então corrigir uma classificação errada não teria outro caminho a não ser desfazer e refazer o pagamento.
Antes, um pagável `partial` aceitava alterações em `amount`, `due_date`, `issue_date`,
`financial_account_id`, `payment_method_id`, descontos e acréscimos mesmo com o
pagamento já registrado — e um pagável `paid` rejeitava o `PATCH` inteiro, inclusive
anotações. Agora `partial` e `paid` seguem a mesma regra: só `notes`,
`financial_category_id` e `metadata`. Se a sua integração ajustava valores ou datas
depois da baixa, mova o ajuste para **antes** de baixar, ou desfaça a baixa, corrija e
refaça.
Cancelar [#cancelar]
`POST /payables/{id}/cancel` cancela o pagável. É bloqueado se ele já estiver `paid` ou `cancelled`.
Parcelar [#parcelar]
`POST /payables/installments` funciona como em recebíveis (`total_amount` — decimal como string —, `installment_count` de 2 a 120, `issue_date`, `first_due_date`, `supplier_id`, `financial_account_id`), gerando N pagáveis com `installment_group_id` comum e vencimentos mensais. A sobra de arredondamento vai na **última** parcela.
Vencidas e resumo [#vencidas-e-resumo]
* `GET /payables/overdue` — pagáveis com status `pending`/`partial` e `due_date` no passado (paginado).
* `GET /payables/summary` — agregado por status (`status`, `total_count`, `total_amount`), mesmo formato do resumo de [recebíveis](/docs/api/guides/receivables).
Filtros [#filtros]
Campos filtráveis em `GET /payables`: `reference_code`, `status`, `amount`, `due_date`, `issue_date`, `supplier_id`, `financial_account_id`, `financial_category_id`, `payment_method_id`, `installment_group_id`, `created_at`, `updated_at`. Ver [paginação e filtros](/docs/api/pagination).
Referência dos endpoints [#referência-dos-endpoints]
| Método | Endpoint | Referência |
| ------ | ---------------------------------------- | --------------------------------------------------------------------- |
| POST | `/payables` | [Criar](/docs/api/reference/payables/create) |
| GET | `/payables` | [Listar](/docs/api/reference/payables/find-all) |
| POST | `/payables/installments` | [Criar parcelas](/docs/api/reference/payables/create-installments) |
| GET | `/payables/overdue` | [Listar vencidas](/docs/api/reference/payables/find-overdue) |
| GET | `/payables/summary` | [Resumo por status](/docs/api/reference/payables/get-summary) |
| GET | `/payables/{id}` | [Buscar por ID](/docs/api/reference/payables/find-by-id) |
| PATCH | `/payables/{id}` | [Atualizar](/docs/api/reference/payables/update) |
| DELETE | `/payables/{id}` | [Remover](/docs/api/reference/payables/delete) |
| POST | `/payables/{id}/cancel` | [Cancelar](/docs/api/reference/payables/cancel) |
| POST | `/payables/{id}/allocate` | [Alocar transação](/docs/api/reference/payables/allocate-transaction) |
| DELETE | `/payables/{id}/allocate/{allocationId}` | [Remover alocação](/docs/api/reference/payables/remove-allocation) |
# Produtos
O recurso **Produtos** (`/products`) mantém o catálogo de bens e serviços do tenant. Cada produto tem campos universais (nome, preço, unidade de medida) e, opcionalmente, uma extensão fiscal por país (`br_data` no Brasil, `us_data` nos Estados Unidos) — o **Extension Table Pattern** do bitERP.
Permissão exigida: `products` (ações `create`, `read`, `update`, `delete`).
Conceitos e campos-chave [#conceitos-e-campos-chave]
Um produto sempre tem um `type` — `goods` (bem) ou `service` (serviço) — definido na criação. O `reference_code` é o código do produto no seu catálogo (SKU): você pode enviá-lo ou deixar a API gerar um automaticamente com o prefixo `PRD-`. Ele é único por tenant e serve para sincronizar com sistemas externos.
| Campo | Tipo | Obrigatório | Observações |
| ------------------------ | --------- | ----------- | ----------------------------------------------------------------------------------------------------------------- |
| `name` | string | Sim | Nome do produto |
| `type` | enum | Sim | `goods` ou `service` |
| `reference_code` | string | Não | SKU único por tenant; auto-gerado (`PRD-…`) se omitido |
| `sale_price` | string | Não | Preço de venda (≥ 0) — decimal `numeric(18,6)` como string (ex.: `"3499.990000"`); assume `"0.000000"` se omitido |
| `unit_of_measurement_id` | UUID | Não | Unidade de medida (ver [Dados de referência](/docs/api/guides/reference-data)) |
| `description` | string | Não | Descrição livre |
| `tags` | string\[] | Não | Etiquetas de organização |
| `image_url` | string | Não | URL da imagem |
| `is_blocked` | boolean | Não | Bloqueia o produto para uso; padrão `false` |
| `metadata` | object | Não | Dados livres da sua integração |
| `br_data` | object | Não | Extensão fiscal Brasil (ver abaixo) |
| `us_data` | object | Não | Extensão fiscal Estados Unidos |
> **`is_active` está depreciado.** É apenas um alias invertido de `is_blocked` (`is_active = !is_blocked`), mantido por compatibilidade. Prefira `is_blocked`. Se você enviar os dois com valores contraditórios, a API retorna erro de validação.
Unidade de medida [#unidade-de-medida]
Use `unit_of_measurement_id` (UUID) no topo do payload — é a forma canônica. O campo legado `br_data.unit_of_measure` ainda é aceito por compatibilidade e resolve tanto um UUID quanto um código de unidade (`UN`, `CX`, `KG`…) pelo país do tenant, mas está **depreciado**. Se você enviar os dois e eles apontarem para unidades diferentes, a API retorna erro de validação; se forem iguais, `unit_of_measurement_id` prevalece.
Na resposta, a unidade vem resolvida como objeto: `"unit_of_measurement": { "id": "…", "code": "UN", "symbol": "un" }`.
Extensão fiscal Brasil (`br_data`) [#extensão-fiscal-brasil-br_data]
Enviada apenas para tenants do Brasil. Campos aceitos incluem `ncm_code` (8 dígitos), `cest_code` (7 dígitos), `origin` (1 dígito), `extipi`, `nbs_code`, `taxation_type`, códigos da Reforma Tributária (`ibs_cbs_*`) e outros. Enviar `br_data` em um tenant de outro país (ou `us_data` num tenant BR) é rejeitado.
Criar um produto [#criar-um-produto]
```bash
curl -X POST https://api.biterp.ai/products \
-H "Authorization: Bearer sk_abc123_secretXYZ" \
-H "Content-Type: application/json" \
-d '{
"reference_code": "EXT-SKU-1001",
"name": "Notebook Pro 14",
"description": "Notebook 14 polegadas, 16GB RAM",
"type": "goods",
"sale_price": "3499.990000",
"unit_of_measurement_id": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
"tags": ["eletronicos", "informatica"],
"is_blocked": false,
"br_data": {
"ncm_code": "84713012",
"cest_code": "0100100",
"origin": "0"
}
}'
```
Buscar produtos [#buscar-produtos]
Há três formas de localizar produtos, além da listagem paginada padrão (`GET /products` com [filtros e paginação](/docs/api/pagination)):
| Endpoint | Uso |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `GET /products/search` | Busca full-text por `name` e `reference_code`. Aceita `q`, `limit` (padrão 50) e `activeOnly` (padrão `false`). |
| `GET /products/reference/{code}` | Localiza um produto pelo `reference_code` exato. O query param `type` (`goods`/`service`) é **obrigatório**. |
| `GET /products/{id}` | Busca por UUID. |
O endpoint por `reference_code` é o ideal para sincronizar por SKU: se o produto existir mas o `type` informado não bater, a API retorna `404` — o par (`reference_code`, `type`) identifica o produto.
```bash
# Buscar por texto (apenas ativos)
curl -G https://api.biterp.ai/products/search \
-H "Authorization: Bearer sk_abc123_secretXYZ" \
--data-urlencode "q=notebook" \
--data-urlencode "activeOnly=true"
# Localizar por SKU externo
curl "https://api.biterp.ai/products/reference/EXT-SKU-1001?type=goods" \
-H "Authorization: Bearer sk_abc123_secretXYZ"
```
Campos filtráveis em `GET /products`: `name`, `reference_code`, `is_blocked`, `type`, `created_at`, `updated_at`. Ordenação por `id`, `name`, `reference_code`, `created_at`, `updated_at`.
Remover [#remover]
`DELETE /products/{id}` faz **soft delete** (o produto pode ser recuperado) e responde `204 No Content`.
Referência dos endpoints [#referência-dos-endpoints]
| Método | Endpoint | Referência |
| ------ | ---------------------------- | --------------------------------------------------------------------------------- |
| POST | `/products` | [Criar produto](/docs/api/reference/products/create) |
| GET | `/products` | [Listar produtos](/docs/api/reference/products/find-all) |
| GET | `/products/search` | [Buscar por texto](/docs/api/reference/products/search) |
| GET | `/products/reference/{code}` | [Buscar por reference\_code](/docs/api/reference/products/find-by-reference-code) |
| GET | `/products/{id}` | [Buscar por ID](/docs/api/reference/products/find-by-id) |
| PATCH | `/products/{id}` | [Atualizar produto](/docs/api/reference/products/update) |
| DELETE | `/products/{id}` | [Remover produto](/docs/api/reference/products/delete) |
# Notas de compra
Uma **nota de compra** (`/purchase-invoices`) registra um documento recebido de um [fornecedor](/docs/api/guides/suppliers). É o espelho, no lado das compras, da [nota de venda](/docs/api/guides/invoices): tem itens, impostos e pode gerar **[contas a pagar](/docs/api/guides/payables)** após a finalização.
Permissão exigida: `purchase-invoices` (ações `create`, `read`, `update`, `delete`).
Conceitos e campos-chave [#conceitos-e-campos-chave]
| Campo | Tipo | Obrigatório | Observações |
| -------------------------------------------------------- | ------ | ----------- | ------------------------------------------------------------------------- |
| `supplier_id` | UUID | Sim | Fornecedor — **precisa existir** (não há cadastro inline) |
| `invoice_number` | string | Sim | Número do documento do fornecedor (≤ 60) |
| `product_type` | enum | Sim | `goods`, `service` ou `mixed` |
| `purchase_date` | date | Sim | |
| `items` | array | Sim | 1+ itens |
| `reference_code` | string | Não | Autogerado (`PINV-…`) se omitido |
| `invoice_series` | string | Não | Série do documento |
| `operation_type` | string | Não | Padrão `purchase` (≤ 50 caracteres) |
| `received_date`, `due_date` | date | Não | |
| `discount_amount` | string | Não | Desconto em valor (≥ 0) — decimal `numeric(18,6)` como string |
| `discount_rate` | string | Não | Desconto em **percentual** (0 a 100) — decimal `numeric(9,6)` como string |
| `taxes` | array | Não | Impostos por item |
| `payables` | array | Não | Planejamento financeiro (ver abaixo) |
| `description`, `notes`, `metadata`, `br_data`, `us_data` | — | Não | |
Itens da nota de compra [#itens-da-nota-de-compra]
Cada item tem `item_description` (obrigatório), `quantity` (**string** decimal ≥ `"0.000001"`) e `unit_price` (**string** decimal ≥ `"0"`); `product_id` é **opcional** (permite lançar itens sem vínculo a um produto do catálogo). Em atualizações, informe `id` no item para reconciliação.
> **Unicidade:** não pode haver nota de compra ativa com o mesmo fornecedor, série e número de documento — a segunda tentativa é rejeitada.
Planejamento e materialização de contas a pagar [#planejamento-e-materialização-de-contas-a-pagar]
O fluxo financeiro segue o mesmo princípio do [pedido de venda](/docs/api/guides/sales-orders): o `create` **não** gera títulos reais.
1. **`POST /purchase-invoices`** — inclua `payables[]` opcional (`amount`, `due_date`, `financial_account_id` obrigatório; opcionais `financial_category_id`, `payment_method_id`, `description`). A soma deve bater com `net_payable_total`. A nota nasce `draft` com `pending_payables` no response (somente leitura).
2. **`POST /purchase-invoices/{id}/finalize`** — materializa os payables (`purchase-invoices:update`) e limpa o snapshot. Só então os títulos aparecem em [`/payables`](/docs/api/guides/payables).
Cada parcela carrega a **própria** `financial_category_id` (opcional). Não envie `financial_account_id` nem `financial_category_id` no cabeçalho da nota.
Criar uma nota de compra (com parcelas planejadas) [#criar-uma-nota-de-compra-com-parcelas-planejadas]
```bash
curl -X POST https://api.biterp.ai/purchase-invoices \
-H "Authorization: Bearer sk_xxx_yyy" \
-H "Content-Type: application/json" \
-d '{
"supplier_id": "1a2b3c4d-5e6f-4708-9a0b-1c2d3e4f5a6b",
"invoice_number": "12345",
"invoice_series": "1",
"product_type": "goods",
"purchase_date": "2026-07-06",
"due_date": "2026-08-05",
"items": [
{
"item_description": "Matéria-prima X",
"quantity": "100.000000",
"unit_price": "12.500000"
}
],
"payables": [
{
"amount": "1250.000000",
"due_date": "2026-08-05",
"financial_account_id": "9c8b7a6d-5e4f-4321-8b0a-1d2c3e4f5a6b"
}
]
}'
```
Depois, finalize:
```bash
curl -X POST https://api.biterp.ai/purchase-invoices/{id}/finalize \
-H "Authorization: Bearer sk_xxx_yyy" \
-H "Content-Type: application/json" \
-d '{}'
```
Status e atualização [#status-e-atualização]
A nota nasce em `status: draft` (estados comerciais: `draft`, `finalized`). `finalized` é **terminal** — não há `cancel` nem `reopen`; para corrigir, exclua e recrie.
No `PATCH` em `draft`, `payables` substitui o snapshot por completo (`[]` limpa). Se o total mudar sem `payables`, o snapshot só é preservado quando a soma continuar compatível; caso contrário a API retorna `422`. Em `finalized`, alterações financeiras são bloqueadas.
`DELETE /purchase-invoices/{id}` remove notas `draft`/`finalized` e, quando finalizada, exclui em cascata os payables vinculados (bloqueado se algum já estiver parcial ou pago).
Referência dos endpoints [#referência-dos-endpoints]
| Método | Endpoint | Referência |
| ------ | ---------------------------------- | ------------------------------------------------------------------------ |
| POST | `/purchase-invoices` | [Criar nota de compra](/docs/api/reference/purchase-invoices/create) |
| GET | `/purchase-invoices` | [Listar notas de compra](/docs/api/reference/purchase-invoices/find-all) |
| GET | `/purchase-invoices/{id}` | [Buscar por ID](/docs/api/reference/purchase-invoices/find-by-id) |
| PATCH | `/purchase-invoices/{id}` | [Atualizar](/docs/api/reference/purchase-invoices/update) |
| DELETE | `/purchase-invoices/{id}` | [Remover](/docs/api/reference/purchase-invoices/delete) |
| POST | `/purchase-invoices/{id}/finalize` | [Finalizar](/docs/api/reference/purchase-invoices/finalize) |
# Orçamentos
Um **orçamento** (`/quotes`) é uma proposta comercial: um [cliente](/docs/api/guides/customers), uma lista de itens e valores, com validade. É a primeira etapa do [fluxo de vendas](/docs/api/guides/sales-flow) — sem compromisso financeiro até virar um [pedido](/docs/api/guides/sales-orders).
Permissão exigida: `quotes` (ações `create`, `read`, `update`, `delete`).
Conceitos e campos-chave [#conceitos-e-campos-chave]
| Campo | Tipo | Obrigatório | Observações |
| ------------------------------------------- | ------ | ----------- | ------------------------------------------------------------------------- |
| `customer_id` | UUID | Sim | Cliente da proposta |
| `product_type` | enum | Sim | `goods`, `service` ou `mixed` |
| `items` | array | Sim | 1 a 200 itens (ver abaixo) |
| `status` | enum | Não | `pending`, `approved`, `rejected`, `expired` |
| `reference_code` | string | Não | Auto-gerado (`QTE-…`) se omitido |
| `quote_date` | date | Não | |
| `valid_until` | date | Não | Validade da proposta |
| `salesperson_name` | string | Não | |
| `discount_amount` | string | Não | Desconto em valor (≥ 0) — decimal `numeric(18,6)` como string |
| `discount_rate` | string | Não | Desconto em **percentual** (0 a 100) — decimal `numeric(9,6)` como string |
| `description`, `tax_notes`, `payment_notes` | string | Não | |
| `metadata` | object | Não | |
Itens do orçamento [#itens-do-orçamento]
> `quantity` e `unit_price` são **strings decimais** (`numeric(18,6)`), como em pedidos e notas — o mesmo payload de itens vale para os três recursos.
| Campo | Tipo | Obrigatório | Observações |
| ------------------ | ------ | ----------- | -------------------------------------------------- |
| `product_id` | UUID | Sim | Produto do catálogo (não há produto inline) |
| `item_description` | string | Sim | Máx. 256 caracteres |
| `quantity` | string | Sim | Decimal positivo (ex.: `"2.000000"`, `"0.500000"`) |
| `unit_price` | string | Sim | Decimal ≥ 0 (ex.: `"450.000000"`) |
| `discount_amount` | string | Não | Decimal ≥ 0 |
Os totais (`subtotal`, `total_discount`, `total`) são calculados pela API e retornados como **strings decimais** na resposta (ex.: `"2100.000000"`).
Cliente embutido nas respostas [#cliente-embutido-nas-respostas]
Em `GET /quotes` e `GET /quotes/{id}`, cada orçamento pode incluir um objeto `customer` com dados resumidos do cliente vinculado (`customer_id`). O shape é o **mesmo** em listagem e detalhe — igual ao de [pedidos de venda](/docs/api/guides/sales-orders) e [notas](/docs/api/guides/invoices):
| Campo | Tipo | Descrição |
| ------------ | ------ | -------------------------------------------------- |
| `id` | UUID | Identificador do cliente |
| `legal_name` | string | Razão social ou nome completo |
| `trade_name` | string | Nome fantasia (pode ser `null`) |
| `tax_id` | string | Documento fiscal (CPF/CNPJ/EIN…) (pode ser `null`) |
```json
"customer": {
"id": "8f2c1e4a-9b3d-4c7e-a1f2-6d5b8e0c3a71",
"legal_name": "ACME Comércio Ltda",
"trade_name": "ACME",
"tax_id": "12345678000195"
}
```
> **Breaking change (2026-08):** versões anteriores devolviam \~17 campos no `customer` embutido (endereço, `tax_id_type`, `metadata`, `type`, `website`, `is_blocked`, timestamps, etc.). Esses campos foram removidos do payload de orçamentos. O cadastro completo continua em [`GET /customers/{id}`](/docs/api/reference/customers/find-by-id). Ver o [changelog em versionamento](/docs/api/versioning#changelog-breaking-changes-em-v1).
Criar um orçamento [#criar-um-orçamento]
```bash
curl -X POST https://api.biterp.ai/quotes \
-H "Authorization: Bearer sk_xxx_yyy" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "8f2c1e4a-9b3d-4c7e-a1f2-6d5b8e0c3a71",
"product_type": "goods",
"quote_date": "2026-07-06",
"valid_until": "2026-07-20",
"salesperson_name": "Ana Souza",
"discount_rate": "5.000000",
"items": [
{
"product_id": "3a7b9c2d-1e4f-4a6b-8c0d-2f5e7a9b1c3d",
"item_description": "Cadeira ergonômica",
"quantity": "2.000000",
"unit_price": "450.000000"
},
{
"product_id": "5d8e1f3a-6b2c-4d7e-9a0f-1c3b5e7d9a2f",
"item_description": "Mesa de escritório",
"quantity": "1.000000",
"unit_price": "1200.000000"
}
]
}'
```
Atualizar [#atualizar]
A operação [Atualizar orçamento](/docs/api/reference/quotes/update) aceita os mesmos campos (todos opcionais). Para editar itens, envie o array `items`: o `id` de cada item é opcional e determina a reconciliação — quando informado, a linha existente é atualizada; quando ausente, uma nova linha é criada. `position` é apenas uma dica opcional de ordenação e **não** define se o item é criado ou atualizado.
Converter em pedido [#converter-em-pedido]
Não há endpoint de conversão. Para transformar um orçamento em venda, crie um [pedido de venda](/docs/api/guides/sales-orders) com a operação [Criar pedido](/docs/api/reference/sales-orders/create), enviando o `quote_id` — a API valida que o `product_type` do pedido casa com o do orçamento. Ver o [fluxo de vendas](/docs/api/guides/sales-flow).
Buscar pelo número do orçamento [#buscar-pelo-número-do-orçamento]
Além do UUID, cada orçamento recebe um `record_number` — um **inteiro sequencial por tenant**, visível para o usuário e distinto do `reference_code`. É também o critério de [ordenação padrão](/docs/api/pagination) da listagem.
```bash
curl "https://api.biterp.ai/quotes/by-record-number/1042" \
-H "Authorization: Bearer sk_xxx_yyy"
```
Retorna `404` se não existir no tenant.
Remover [#remover]
A operação [Remover orçamento](/docs/api/reference/quotes/delete) faz **soft delete** e responde `204 No Content`. **Não há endpoint de restauração** — ver [`DELETE` é de mão única](/docs/api#delete-é-de-mão-única).
Consulte a [referência OpenAPI de orçamentos](/docs/api/reference/quotes/find-all) para todos os endpoints disponíveis.
# Contas a receber
Uma **conta a receber** (`/receivables`) é um valor que um [cliente](/docs/api/guides/customers) deve ao tenant, vinculado a uma [conta financeira](/docs/api/guides/financial-accounts) e com data de vencimento. Este recurso é mais do que um CRUD: ele tem uma **máquina de estado** e o recebimento acontece por **alocação de transações**.
Permissão exigida: `receivables` (ações `create`, `read`, `update`, `delete`).
Ciclo de vida [#ciclo-de-vida]
O campo `status` reflete a situação do recebível:
| Status | Significado |
| ----------- | -------------------------------------------------------------- |
| `pending` | Criado, nada recebido ainda |
| `partial` | Parcialmente recebido (há alocação, mas ainda resta valor) |
| `paid` | Totalmente recebido |
| `overdue` | Vencido e não quitado (marcado pelo sistema após o vencimento) |
| `cancelled` | Cancelado |
Um recebível nasce em `pending`. Conforme você **aloca transações** (recebe), ele recalcula o status automaticamente: se o valor restante chega a zero, vira `paid`; se ainda resta algo, vira `partial`. Remover uma alocação recalcula no sentido inverso (`paid` → `partial` → `pending`). O `cancel` leva a `cancelled`.
```
aloca (quita) aloca (parcial)
pending ──────────────────► paid pending ──────────► partial ──► paid
│ │ │
│ cancel │ remove alocação │ remove alocação
▼ ▼ ▼
cancelled pending partial/pending
```
> O status `overdue` **não** é definido nas suas chamadas — ele é aplicado por uma rotina do sistema quando o vencimento passa e a conta ainda está `pending`/`partial`. Você não pode alterar `status` diretamente (é um campo bloqueado no `PATCH`); mude o estado usando as ações (`allocate`, `cancel`).
Criar uma conta a receber [#criar-uma-conta-a-receber]
| Campo | Tipo | Obrigatório | Observações |
| --------------------------- | ----------- | ----------- | ------------------------------------------------------------------------------------------------- |
| `customer_id` | UUID | Sim | Cliente devedor |
| `financial_account_id` | UUID | Sim | Conta financeira (deve estar ativa) |
| `amount` | string | Sim | Valor a receber (> 0) — decimal `numeric(18,6)` como string (ex.: `"1500.000000"`) |
| `issue_date` | date | Sim | Data de emissão (`YYYY-MM-DD`) |
| `due_date` | date | Sim | Data de vencimento |
| `payment_method_id` | UUID | Não | Forma de pagamento (do país do tenant) |
| `financial_category_id` | UUID | Não | Categoria financeira |
| `reference_code` | string | Não | Auto-gerado (`REC-…`) se omitido; único por tenant |
| `discount_amount` | string | Não | Desconto (≥ 0) — decimal como string |
| `surcharge_amount` | string | Não | Acréscimo/juros (≥ 0) — decimal como string |
| `origin_type` / `origin_id` | enum / UUID | Não | Origem do lançamento (`manual`, `quote`, `sale_order`, `invoice`, `contract`, `recurring`) |
| `sales_order_id` | UUID | Não | Pedido de venda de origem |
| `description` / `notes` | string | Não | |
| `auto_generate_transaction` | boolean | Não | Em conta manual (`cash`/`digital_wallet`), gera a transação de crédito, aloca e marca como `paid` |
| `metadata` | object | Não | |
```bash
curl -X POST https://api.biterp.ai/receivables \
-H "Authorization: Bearer sk_abc123_secretXYZ" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
"financial_account_id": "1a2b3c4d-5e6f-4708-9a1b-2c3d4e5f6071",
"amount": "1500.000000",
"issue_date": "2026-07-01",
"due_date": "2026-08-01",
"description": "Venda de serviços - julho/2026"
}'
```
Receber: alocar uma transação [#receber-alocar-uma-transação]
Receber um valor é **alocar uma [transação](/docs/api/guides/financial-transactions) de crédito** ao recebível. `POST /receivables/{id}/allocate`:
| Campo | Tipo | Obrigatório | Observações |
| -------------------------- | ------ | ----------- | ------------------------------------------ |
| `financial_transaction_id` | UUID | Sim | Transação de **crédito** (amount positivo) |
| `allocated_amount` | string | Sim | Valor a alocar (> 0) — decimal como string |
| `notes` | string | Não | |
Regras de validação:
* O recebível não pode estar `paid` nem `cancelled`.
* A transação deve pertencer à **mesma** `financial_account_id` do recebível e ser um crédito (positiva).
* `allocated_amount` não pode exceder o valor restante do recebível **nem** o saldo restante da transação.
O status é recalculado após a alocação, e a transação é marcada como conciliada.
```bash
curl -X POST https://api.biterp.ai/receivables/{id}/allocate \
-H "Authorization: Bearer sk_abc123_secretXYZ" \
-H "Content-Type: application/json" \
-d '{
"financial_transaction_id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
"allocated_amount": "500.000000",
"notes": "Recebimento parcial via PIX"
}'
```
Para desfazer, use `DELETE /receivables/{id}/allocate/{allocationId}` — a alocação é removida e o status recalculado (`204 No Content`).
O que pode ser alterado, por estado [#o-que-pode-ser-alterado-por-estado]
O `PATCH` aceita campos diferentes conforme o `status` do recebível. Depois que o dinheiro se moveu — total (`paid`) ou parcialmente (`partial`) — o título descreve um fato consumado, e só continua editável o que **não** altera esse fato:
| Status do recebível | Campos aceitos no `PATCH` |
| -------------------- | --------------------------------------------------------------- |
| `pending`, `overdue` | Todos os do corpo do `PATCH`, sujeitos às validações do recurso |
| `partial` | Somente `notes`, `financial_category_id` e `metadata` |
| `paid` | Somente `notes`, `financial_category_id` e `metadata` |
| `cancelled` | Nenhum — nem mesmo um corpo vazio |
Um `PATCH` com algum campo não permitido para o estado atual retorna `422` **nomeando o(s) campo(s) rejeitado(s)**. E se o status mudar enquanto o `PATCH` estiver em voo — uma baixa ou um cancelamento concorrente —, a atualização é recusada com `409` em vez de ser aplicada com as regras do estado antigo: releia o registro e repita.
Para alterar qualquer outro campo de um título já baixado, **desfaça, estorne ou desvincule o recebimento** (`DELETE /receivables/{id}/allocate/{allocationId}`, ou as ações de estorno). A edição total volta quando o status **recalculado** for `pending` ou `overdue` — e quem decide entre os dois é o **vencimento**, não a ausência de valor baixado: com saldo em aberto e vencimento já passado o título fica `overdue`, que mantém a edição total mesmo com baixa parcial; ainda dentro do prazo e com valor baixado remanescente — várias baixas, ou um estorno parcial —, ele fica `partial` e continua restrito. Não há beco sem saída — só a obrigação de passar pela operação que corresponde ao que de fato aconteceu.
`financial_category_id` continua editável depois da baixa de propósito: a categoria contábil existe **apenas** no título (a transação financeira não tem esse campo), então corrigir uma classificação errada não teria outro caminho a não ser desfazer e refazer o recebimento.
Antes, um recebível `partial` aceitava alterações em `amount`, `due_date`,
`issue_date`, `financial_account_id`, `payment_method_id`, descontos e acréscimos
mesmo com o recebimento já registrado — e um recebível `paid` rejeitava o `PATCH`
inteiro, inclusive anotações. Agora `partial` e `paid` seguem a mesma regra: só
`notes`, `financial_category_id` e `metadata`. Se a sua integração ajustava valores ou
datas depois da baixa, mova o ajuste para **antes** de baixar, ou desfaça a baixa,
corrija e refaça.
Cancelar [#cancelar]
`POST /receivables/{id}/cancel` cancela o recebível (e suas alocações, de forma atômica). É bloqueado se a conta já estiver `paid`.
Parcelar [#parcelar]
`POST /receivables/installments` gera várias contas a receber de uma vez, todas com o mesmo `installment_group_id`:
| Campo | Tipo | Obrigatório | Observações |
| ----------------------- | ------ | ----------- | ------------------------------------------------------------------- |
| `customer_id` | UUID | Sim | |
| `financial_account_id` | UUID | Sim | |
| `total_amount` | string | Sim | Valor total (> 0), dividido entre as parcelas — decimal como string |
| `installment_count` | int | Sim | Número de parcelas (2–120) — inteiro, **não** string |
| `issue_date` | date | Sim | |
| `first_due_date` | date | Sim | Vencimento da 1ª parcela; as demais somam 1 mês cada |
| `reference_code_prefix` | string | Não | Gera `PREFIXO/1`, `PREFIXO/2`… (senão, sequência `REC-`) |
| `description` / `notes` | string | Não | |
O total é dividido igualmente (6 casas decimais); as sobras de arredondamento são distribuídas nas primeiras parcelas. O retorno é o array das parcelas criadas.
```bash
curl -X POST https://api.biterp.ai/receivables/installments \
-H "Authorization: Bearer sk_abc123_secretXYZ" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
"financial_account_id": "1a2b3c4d-5e6f-4708-9a1b-2c3d4e5f6071",
"total_amount": "1200.000000",
"installment_count": 3,
"issue_date": "2026-07-01",
"first_due_date": "2026-08-05",
"reference_code_prefix": "REC-2026-PED-15"
}'
```
Vencidas e resumo [#vencidas-e-resumo]
* `GET /receivables/overdue` lista as contas **vencidas** — critério dinâmico: status `pending` ou `partial` **e** `due_date` no passado. Retorno paginado.
* `GET /receivables/summary` retorna um agregado **por status**, cada linha com a contagem e a soma dos valores:
```json
{
"data": [
{ "status": "pending", "total_count": 12, "total_amount": "18400.000000" },
{ "status": "paid", "total_count": 30, "total_amount": "52310.500000" }
]
}
```
Filtros [#filtros]
Campos filtráveis em `GET /receivables`: `reference_code`, `status`, `amount`, `due_date`, `issue_date`, `customer_id`, `financial_account_id`, `financial_category_id`, `payment_method_id`, `installment_group_id`, `origin_type`, `sales_order_id`, `invoice_id`, `created_at`, `updated_at`. Ver [paginação e filtros](/docs/api/pagination).
Referência dos endpoints [#referência-dos-endpoints]
| Método | Endpoint | Referência |
| ------ | ------------------------------------------- | ------------------------------------------------------------------------ |
| POST | `/receivables` | [Criar](/docs/api/reference/receivables/create) |
| GET | `/receivables` | [Listar](/docs/api/reference/receivables/find-all) |
| POST | `/receivables/installments` | [Criar parcelas](/docs/api/reference/receivables/create-installments) |
| GET | `/receivables/overdue` | [Listar vencidas](/docs/api/reference/receivables/find-overdue) |
| GET | `/receivables/summary` | [Resumo por status](/docs/api/reference/receivables/get-summary) |
| GET | `/receivables/{id}` | [Buscar por ID](/docs/api/reference/receivables/find-by-id) |
| PATCH | `/receivables/{id}` | [Atualizar](/docs/api/reference/receivables/update) |
| DELETE | `/receivables/{id}` | [Remover](/docs/api/reference/receivables/delete) |
| POST | `/receivables/{id}/cancel` | [Cancelar](/docs/api/reference/receivables/cancel) |
| POST | `/receivables/{id}/allocate` | [Alocar transação](/docs/api/reference/receivables/allocate-transaction) |
| DELETE | `/receivables/{id}/allocate/{allocationId}` | [Remover alocação](/docs/api/reference/receivables/remove-allocation) |
# Dados de referência
Os **dados de referência** são catálogos **somente leitura** que alimentam os cadastros: o `bank_id` de uma [conta financeira](/docs/api/guides/financial-accounts), a `unit_of_measurement_id` de um [produto](/docs/api/guides/products), o `payment_method_id` de um [recebível](/docs/api/guides/receivables), o `city_id`/`state_id` de um [cliente](/docs/api/guides/customers) ou [fornecedor](/docs/api/guides/suppliers).
São seis recursos, cada um com sua permissão (`banks`, `payment-methods`, `units-of-measurement`, `countries`, `states`, `cities`), todos na ação `read`. Todas as listagens são paginadas e aceitam `search` (ver [paginação e filtros](/docs/api/pagination)).
Escopo por país do tenant [#escopo-por-país-do-tenant]
A maioria dos catálogos é **escopada ao país do tenant** — você só enxerga o que faz sentido para a sua região:
* **Bancos** pertencem a um único país: a lista traz os bancos ativos do país do tenant. (As buscas por `swift/{code}` e por `{id}` **não** são escopadas por país.)
* **Formas de pagamento** e **unidades de medida** usam uma lista de países (`country_codes`): uma lista **vazia significa universal** (disponível em todos os países); caso contrário, o país do tenant precisa estar nela.
* **Países** são um catálogo **global** — não filtrado pelo tenant.
Bancos (`/banks`) [#bancos-banks]
Campos principais da resposta: `id`, `code` (código nacional, ex.: `341`), `name`, `official_name`, `swift_code`, `country_code`, `logo_url`, `aliases`, `is_active`.
* `GET /banks` — lista os bancos do país do tenant.
* `GET /banks/code/{code}` — por código nacional (escopado ao país).
* `GET /banks/swift/{code}` — por SWIFT/BIC.
* `GET /banks/{id}` — por UUID.
Formas de pagamento (`/payment-methods`) [#formas-de-pagamento-payment-methods]
Campos: `id`, `code` (ex.: `pix`, `credit_card`), `country_codes`, `titles` (por idioma), `descriptions`, `is_active`. Uma forma universal tem `country_codes: []`.
* `GET /payment-methods` — disponíveis para o país do tenant.
* `GET /payment-methods/code/{code}` — por código.
* `GET /payment-methods/{id}` — por UUID.
Unidades de medida (`/units-of-measurement`) [#unidades-de-medida-units-of-measurement]
Campos: `id`, `code` (ex.: `UN`, `KG`), `symbol`, `country_codes`, `titles`, `descriptions`, `is_active`. É a `unit_of_measurement_id` usada em [produtos](/docs/api/guides/products).
* `GET /units-of-measurement` — disponíveis para o país do tenant.
* `GET /units-of-measurement/code/{code}` — por código.
* `GET /units-of-measurement/{id}` — por UUID.
Geografia: países, estados e cidades [#geografia-países-estados-e-cidades]
A geografia é hierárquica: **país → estado → cidade**. É assim que você obtém os `city_id`/`state_id` exigidos por [fornecedores](/docs/api/guides/suppliers) (e aceitos por [clientes](/docs/api/guides/customers)).
```
GET /countries → escolha country.id
GET /states?country_id={id} → escolha state.id
GET /cities?state_id={id} → escolha city.id
```
Países (`/countries`) [#países-countries]
Catálogo global. Campos: `id`, `iso2`, `iso3`, `numeric_code`, `name_en`, `name_pt`, `name_es`, `currency`, `region`, entre outros.
* `GET /countries` — lista.
* `GET /countries/iso2/{code}` — por ISO2 (ex.: `BR`).
* `GET /countries/iso3/{code}` — por ISO3 (ex.: `BRA`).
* `GET /countries/{id}` — por ID.
Estados (`/states`) [#estados-states]
Campos: `id`, `country_id`, `state_code`, `country_code`, `name`. A lista e a busca aceitam um seletor de país **opcional**: `country_id`, `country_iso2` ou `country_iso3` (o alias `country_code` está depreciado).
* `GET /states?country_id={id}` — estados do país.
* `GET /states/search?q={texto}` — busca por nome (`q` obrigatório).
* `GET /states/code/{state_code}` — por código.
* `GET /states/{id}` — por ID.
Cidades (`/cities`) [#cidades-cities]
Campos: `id`, `country_id`, `state_id`, `country_code`, `state_code`, `name`. Cidades são navegadas por **`state_id`, que é obrigatório**.
* `GET /cities?state_id={id}` — cidades do estado (`state_id` obrigatório).
* `GET /cities/search?state_id={id}&q={texto}` — busca por nome no estado (`state_id` e `q` obrigatórios).
* `GET /cities/{id}` — por ID.
Referência dos endpoints [#referência-dos-endpoints]
| Recurso | Método + Endpoint | Referência |
| ------------------- | --------------------------------------- | ------------------------------------------------------------------- |
| Bancos | `GET /banks` | [Listar bancos](/docs/api/reference/banks/find-all) |
| Bancos | `GET /banks/code/{code}` | [Por código](/docs/api/reference/banks/find-by-code) |
| Bancos | `GET /banks/swift/{code}` | [Por SWIFT](/docs/api/reference/banks/find-by-swift-code) |
| Bancos | `GET /banks/{id}` | [Por ID](/docs/api/reference/banks/find-by-id) |
| Formas de pagamento | `GET /payment-methods` | [Listar](/docs/api/reference/payment-methods/find-all) |
| Formas de pagamento | `GET /payment-methods/code/{code}` | [Por código](/docs/api/reference/payment-methods/find-by-code) |
| Formas de pagamento | `GET /payment-methods/{id}` | [Por ID](/docs/api/reference/payment-methods/find-by-id) |
| Unidades | `GET /units-of-measurement` | [Listar](/docs/api/reference/units-of-measurement/find-all) |
| Unidades | `GET /units-of-measurement/code/{code}` | [Por código](/docs/api/reference/units-of-measurement/find-by-code) |
| Unidades | `GET /units-of-measurement/{id}` | [Por ID](/docs/api/reference/units-of-measurement/find-by-id) |
| Países | `GET /countries` | [Listar](/docs/api/reference/countries/find-all) |
| Países | `GET /countries/iso2/{code}` | [Por ISO2](/docs/api/reference/countries/find-by-iso2) |
| Países | `GET /countries/iso3/{code}` | [Por ISO3](/docs/api/reference/countries/find-by-iso3) |
| Países | `GET /countries/{id}` | [Por ID](/docs/api/reference/countries/find-by-id) |
| Estados | `GET /states` | [Listar por país](/docs/api/reference/states/find-by-country) |
| Estados | `GET /states/search` | [Buscar](/docs/api/reference/states/search) |
| Estados | `GET /states/code/{state_code}` | [Por código](/docs/api/reference/states/find-by-code) |
| Estados | `GET /states/{id}` | [Por ID](/docs/api/reference/states/find-by-id) |
| Cidades | `GET /cities` | [Listar por estado](/docs/api/reference/cities/find-by-state) |
| Cidades | `GET /cities/search` | [Buscar](/docs/api/reference/cities/search) |
| Cidades | `GET /cities/{id}` | [Por ID](/docs/api/reference/cities/find-by-id) |
# Fluxo de vendas
Esta página é um **mapa** do fluxo comercial na API de Integrações. Ela costura os quatro recursos de venda e mostra como eles se ligam; cada etapa tem seu guia dedicado com os detalhes de campos.
As etapas [#as-etapas]
```
Orçamento ─────► Pedido de venda ─────► Nota / fatura ─────► Contas a receber
(quote) (sales-order) (invoice) (receivables)
```
1. **[Orçamento (quote)](/docs/api/guides/quotes)** — uma proposta comercial com itens e valores, ainda sem compromisso financeiro.
2. **[Pedido de venda (sales-order)](/docs/api/guides/sales-orders)** — a venda confirmada. Pode nascer a partir de um orçamento.
3. **[Nota / fatura (invoice)](/docs/api/guides/invoices)** — o documento de faturamento gerado para o pedido.
4. **[Contas a receber (receivables)](/docs/api/guides/receivables)** — os valores a receber do cliente, com vencimentos.
Nenhuma etapa é obrigatória para a próxima existir — você pode criar um pedido sem orçamento, ou uma nota sem pedido. Os vínculos são **opcionais** e servem para rastreabilidade e validação.
Como os recursos se ligam [#como-os-recursos-se-ligam]
O bitERP **não** tem endpoints de "converter" ou "gerar automaticamente" na API de Integrações. Os vínculos são feitos por **campos de referência** que você envia no `POST` de cada etapa, e a API valida a coerência entre eles.
| De → Para | Como vincular | O que a API valida |
| ----------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| Orçamento → Pedido | `quote_id` no `POST /sales-orders` | O orçamento existe e o `product_type` do pedido casa com o do orçamento |
| Pedido → Nota | `sales_order_id` no `POST /invoices` | `customer_id`, `operation_type` e `product_type` casam com o pedido; **só 1 nota ativa por pedido** |
| Item da nota → item do pedido | `sales_order_item_id` em cada item da nota | Rastreia a linha de origem |
| Nota → Contas a receber | array `receivables[]` no `POST /invoices` | A **soma** dos `amount` deve igualar o total líquido da nota |
> Ao vincular, os itens **não** são copiados automaticamente — você sempre envia os `items[]` da etapa atual. O `quote_id`/`sales_order_id` apenas registra a origem e dispara as validações de coerência.
Gerando contas a receber [#gerando-contas-a-receber]
Contas a receber **não** são criadas sozinhas ao faturar. Elas surgem quando você inclui um array `receivables[]` no `POST` do pedido de venda **ou** da nota. Nesse caso vale uma regra rígida: a **soma** de `receivables[].amount` deve bater exatamente com o total (o total líquido, no caso da nota). Se você enviar mais de um, eles viram um parcelamento (compartilham `installment_group_id`).
A partir daí, o recebimento segue o ciclo de vida de [Contas a receber](/docs/api/guides/receivables): alocar transações, parcial/quitado, cancelar.
Do lado das compras [#do-lado-das-compras]
O espelho deste fluxo, para o que a empresa compra, é a **[nota de compra (purchase-invoice)](/docs/api/guides/purchase-invoices)**, ligada a um [fornecedor](/docs/api/guides/suppliers) e capaz de gerar **[contas a pagar](/docs/api/guides/payables)** pela mesma mecânica (array `payables[]`).
A primeira etapa: propostas com itens.
A venda confirmada, ligada ao orçamento.
O faturamento e a geração de recebíveis.
# Pedidos de venda
Um **pedido de venda** (`/sales-orders`) é a venda confirmada — a etapa central do [fluxo de vendas](/docs/api/guides/sales-flow). Ele pode nascer de um [orçamento](/docs/api/guides/quotes), gerar [contas a receber](/docs/api/guides/receivables) e ser faturado por uma [nota](/docs/api/guides/invoices).
Permissão exigida: `sales-orders` (ações `create`, `read`, `update`, `delete`).
Ciclo de vida [#ciclo-de-vida]
O `status` de um pedido é `draft`, `confirmed` ou `cancelled`. Na criação, só `draft` ou `confirmed` são aceitos — o **padrão é `confirmed`**. As transições permitidas via `PATCH`:
* `draft` → `confirmed` (confirmar)
* `draft` ou `confirmed` → `cancelled` (cancelar)
* `confirmed` → `draft` é **bloqueado**
Um pedido `cancelled` não é editável.
Conceitos e campos-chave [#conceitos-e-campos-chave]
| Campo | Tipo | Obrigatório | Observações |
| --------------------------------------------- | ------ | ----------- | ------------------------------------------------------------------------- |
| `customer_id` | UUID | Sim\* | \*Ou o objeto `customer` inline — envie **um** dos dois |
| `product_type` | enum | Sim | `goods`, `service` ou `mixed` |
| `order_date` | date | Sim | |
| `items` | array | Sim | 1 a 200 itens |
| `status` | enum | Não | `draft` ou `confirmed` (padrão `confirmed`) |
| `reference_code` | string | Não | Auto-gerado (`SOR-…`) se omitido |
| `operation_type` | string | Não | Tipo de operação (padrão `sale`) |
| `quote_id` | UUID | Não | Orçamento de origem (`product_type` deve casar) |
| `delivery_date` | date | Não | |
| `salesperson_name` | string | Não | |
| `discount_amount` | string | Não | Desconto em valor (≥ 0) — decimal `numeric(18,6)` como string |
| `discount_rate` | string | Não | Desconto em **percentual** (0 a 100) — decimal `numeric(9,6)` como string |
| `receivables` | array | Não | Gera contas a receber (ver abaixo) |
| `metadata`, `br_data`, `notes`, `description` | — | Não | |
Itens do pedido [#itens-do-pedido]
> `quantity` e `unit_price` são **strings decimais** (ex.: `"2.000000"`, `"450.000000"`), como em todos os campos decimais da API.
Cada item referencia um `product_id` (com `item_description`, `quantity` e `unit_price`) ou traz um objeto `product` inline, que reaproveita/cria o produto pelo `reference_code`. `quantity` é ≥ `"0.000001"` e `unit_price` ≥ `"0"`.
Gerando contas a receber [#gerando-contas-a-receber]
Inclua um array `receivables[]` para já criar os recebíveis do pedido. Cada item exige `amount` (decimal como string), `due_date` (date-time) e `financial_account_id` (ativo); opcionalmente `payment_method_id`, `financial_category_id` e `description`. A resposta traz `has_active_receivables` e o array `receivables` do pedido.
Criar um pedido (a partir de um orçamento, com recebível) [#criar-um-pedido-a-partir-de-um-orçamento-com-recebível]
```bash
curl -X POST https://api.biterp.ai/sales-orders \
-H "Authorization: Bearer sk_xxx_yyy" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "8f2c1e4a-9b3d-4c7e-a1f2-6d5b8e0c3a71",
"quote_id": "b1d2f3a4-5c6e-4718-9a0b-2c3d4e5f6a7b",
"product_type": "goods",
"order_date": "2026-07-06",
"status": "confirmed",
"salesperson_name": "Ana Souza",
"items": [
{
"product_id": "3a7b9c2d-1e4f-4a6b-8c0d-2f5e7a9b1c3d",
"item_description": "Cadeira ergonômica",
"quantity": "2.000000",
"unit_price": "450.000000"
},
{
"product_id": "5d8e1f3a-6b2c-4d7e-9a0f-1c3b5e7d9a2f",
"item_description": "Mesa de escritório",
"quantity": "1.000000",
"unit_price": "1200.000000"
}
],
"receivables": [
{
"amount": "2100.000000",
"due_date": "2026-08-06T00:00:00.000Z",
"financial_account_id": "9c8b7a6d-5e4f-4321-8b0a-1d2c3e4f5a6b"
}
]
}'
```
Atualizar [#atualizar]
Na operação [Atualizar pedido](/docs/api/reference/sales-orders/update), os campos `customer_id`, `quote_id`, `receivables` e `operation_type` **não** são editáveis. Além disso, você **não pode alterar itens nem descontos quando há recebíveis ativos** vinculados ao pedido — cancele/ajuste os recebíveis primeiro.
Buscar pelo número do pedido [#buscar-pelo-número-do-pedido]
Além do UUID, cada pedido recebe um `record_number` — um **inteiro sequencial por tenant**, visível para o usuário e distinto do `reference_code`. É também o critério de [ordenação padrão](/docs/api/pagination) da listagem.
```bash
curl "https://api.biterp.ai/sales-orders/by-record-number/2087" \
-H "Authorization: Bearer sk_xxx_yyy"
```
Um `record_number` que não seja inteiro positivo retorna `400`; um número inexistente no tenant retorna `404`.
Remover [#remover]
A operação [Remover pedido](/docs/api/reference/sales-orders/delete) faz **soft delete** e responde `204 No Content`. **Não há endpoint de restauração** — ver [`DELETE` é de mão única](/docs/api#delete-é-de-mão-única).
Consulte a [referência OpenAPI de pedidos de venda](/docs/api/reference/sales-orders/find-all) para todos os endpoints disponíveis.
# Fornecedores
O recurso **Fornecedores** (`/suppliers`) mantém o cadastro de fornecedores do tenant. Ele é o **espelho de [Clientes](/docs/api/guides/customers)**: a mesma estrutura de pessoa física/jurídica, `tax_id`, contatos, dados bancários e extensões por país (`br_data`, `us_data`). Fornecedores são a ponta de origem do fluxo de compras — [notas de compra](/docs/api/guides/purchase-invoices) e [contas a pagar](/docs/api/guides/payables).
Permissão exigida: `suppliers` (ações `create`, `read`, `update`, `delete`).
Conceitos e campos-chave [#conceitos-e-campos-chave]
A estrutura é idêntica à de clientes, com **duas diferenças** que valem atenção:
1. **`country_code` é obrigatório e deve ser ISO2** (ex.: `BR`, `US`). Em clientes ele é opcional e aceita vários formatos; aqui é obrigatório e restrito ao código de duas letras.
2. **Sem resolução de cidade/estado por texto.** Fornecedores aceitam apenas `city_id` e `state_id` (IDs do catálogo, obtidos via [Dados de referência](/docs/api/guides/reference-data)) — não há campos `city`/`state` de texto livre.
| Campo | Tipo | Obrigatório | Observações |
| ---------------- | --------- | ----------- | ------------------------------------------------------------------------- |
| `type` | enum | Sim | `individual` ou `company` |
| `tax_id` | string | Sim | Documento fiscal; único por tenant; normalizado (sem validação de dígito) |
| `legal_name` | string | Sim | Razão social / nome completo |
| `country_code` | string | Sim | **ISO2** (`BR`, `US`) |
| `tax_id_type` | enum | Não | `CPF`, `CNPJ`, `EIN` ou `SSN` |
| `trade_name` | string | Não | Nome fantasia |
| `reference_code` | string | Não | Auto-gerado (`SPL-…`) se omitido; único por tenant |
| `emails` | string\[] | Não | |
| `phones` | string\[] | Não | |
| `address_line1` | string | Não | Endereço (linha 1) |
| `city_id` | int | Não | ID da cidade no catálogo |
| `state_id` | int | Não | ID do estado no catálogo |
| `postal_code` | string | Não | |
| `tags` | string\[] | Não | |
| `is_active` | boolean | Não | Padrão `true` |
| `metadata` | object | Não | |
| `br_data` | object | Não | `ie`, `ie_exempt`, `cnae_primary`, `pix_key`… |
| `us_data` | object | Não | `w9_on_file`, `naics_code`, `routing_number`… |
O `tax_id` segue a mesma regra de [Clientes](/docs/api/guides/customers): normalizado (pontuação ignorada) e sem validação de dígitos verificadores. Na resposta, o país vem como `country_code` (string), e `city`/`state` como objetos resolvidos.
Criar um fornecedor [#criar-um-fornecedor]
```bash
curl -X POST https://api.biterp.ai/suppliers \
-H "Authorization: Bearer sk_abc123_secretXYZ" \
-H "Content-Type: application/json" \
-d '{
"type": "company",
"tax_id": "98.765.432/0001-10",
"tax_id_type": "CNPJ",
"legal_name": "Fornecedora Industrial S.A.",
"trade_name": "FornInd",
"country_code": "BR",
"emails": ["compras@fornind.com.br"],
"address_line1": "Rua da Indústria, 500",
"postal_code": "04500-000",
"state_id": 26,
"city_id": 5270,
"br_data": {
"ie": "111222333444",
"cnae_primary": "4671100",
"pix_key": "98765432000110",
"pix_key_type": "cnpj"
}
}'
```
> `state_id` e `city_id` devem ser IDs reais do catálogo bitERP. Obtenha-os pelos endpoints de estados e cidades em [Dados de referência](/docs/api/guides/reference-data).
Buscar fornecedores [#buscar-fornecedores]
Além da listagem paginada (`GET /suppliers`, com [filtros e paginação](/docs/api/pagination)):
| Endpoint | Uso |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `GET /suppliers/search` | Busca full-text por `legal_name`, `trade_name` e `tax_id`. Aceita `q`, `limit` (padrão 50, 1–100) e `activeOnly`. |
| `GET /suppliers/tax-id/{taxId}` | Localiza um fornecedor pelo `tax_id` (normalizado). `404` se não existir. |
| `GET /suppliers/count` | Contagem total: `{ "count": 42 }`. |
| `GET /suppliers/{id}` | Busca por UUID. |
Campos filtráveis em `GET /suppliers`: `legal_name`, `trade_name`, `tax_id`, `type`, `city`, `state`, `country_code`, `is_active`, `reference_code`, `tags`, `created_at`, `updated_at`. Ordenação por `id`, `legal_name`, `trade_name`, `reference_code`, `created_at`, `updated_at`.
Remover [#remover]
`DELETE /suppliers/{id}` faz **soft delete** e responde `204 No Content`.
Referência dos endpoints [#referência-dos-endpoints]
| Método | Endpoint | Referência |
| ------ | --------------------------- | ------------------------------------------------------------------ |
| POST | `/suppliers` | [Criar fornecedor](/docs/api/reference/suppliers/create) |
| GET | `/suppliers` | [Listar fornecedores](/docs/api/reference/suppliers/find-all) |
| GET | `/suppliers/search` | [Buscar por texto](/docs/api/reference/suppliers/search) |
| GET | `/suppliers/tax-id/{taxId}` | [Buscar por tax\_id](/docs/api/reference/suppliers/find-by-tax-id) |
| GET | `/suppliers/count` | [Contar fornecedores](/docs/api/reference/suppliers/count) |
| GET | `/suppliers/{id}` | [Buscar por ID](/docs/api/reference/suppliers/find-by-id) |
| PATCH | `/suppliers/{id}` | [Atualizar fornecedor](/docs/api/reference/suppliers/update) |
| DELETE | `/suppliers/{id}` | [Remover fornecedor](/docs/api/reference/suppliers/delete) |
# Entrega e retentativas
Cada evento assinado vira uma **entrega** (*delivery*): uma linha com histórico próprio, visível no painel em **Configurações > WebHooks**, na aba **Histórico** do endpoint.
Ciclo de vida [#ciclo-de-vida]
| Status | O que significa |
| --------------- | ----------------------------------------------------- |
| **Pendente** | A entrega foi criada e aguarda o primeiro envio |
| **Processando** | O envio está em curso |
| **Entregue** | O seu servidor respondeu `2xx` |
| **Falhou** | A tentativa não deu certo e há uma nova agendada |
| **Esgotado** | As tentativas acabaram. Não haverá reenvio automático |
O detalhe de cada entrega registra as tentativas feitas, o status HTTP e o corpo da sua resposta (primeiros 1.024 caracteres), o tempo de resposta e a mensagem de erro quando houve falha de conexão.
Idempotência é obrigatória [#idempotência-é-obrigatória]
O bitERP garante entrega **pelo menos uma vez**, não exatamente uma vez. Uma resposta `2xx` que se perca na rede, um timeout do seu lado que na verdade processou, um reenvio manual — todos produzem uma segunda chegada do mesmo evento.
Use o **`id` do payload** (idêntico ao header `x-biterp-webhook-id`) como chave de deduplicação. Ele é estável entre todas as tentativas do mesmo evento, inclusive nos reenvios manuais.
```js
// Guarde o id antes de processar; ignore o que já viu
const known = await db.webhookEvents.findUnique({ where: { id: event.id } })
if (known) return res.status(200).send("duplicate")
await db.webhookEvents.create({ data: { id: event.id, type: event.event_type } })
```
A ordem não é garantida [#a-ordem-não-é-garantida]
Entregas são independentes e podem ser processadas em paralelo, com retentativas em momentos diferentes. Um `sales-orders.update` que falhou e voltou duas horas depois chega **depois** de um `update` mais recente do mesmo pedido.
A consequência prática: **não reconstrua estado a partir da sequência de eventos**. Trate cada evento como um convite a buscar o estado atual via [API de Integrações](/docs/api/webhooks/events#do-evento-ao-dado-completo) — o `GET` sempre devolve a versão mais recente, o que torna a ordem de chegada irrelevante. Se você precisa descartar processamento obsoleto, compare o `updated_at` do recurso com o que já gravou, não o `timestamp` do evento.
Retentativas e backoff [#retentativas-e-backoff]
Toda tentativa que não resulte em `2xx` dentro de 10 segundos agenda a próxima, com espera exponencial a partir de 1 minuto e teto de 6 horas:
| Tentativa | Espera até a próxima |
| ------------ | -------------------- |
| 1 | 1 minuto |
| 2 | 2 minutos |
| 3 | 4 minutos |
| 4 | 8 minutos |
| 5 | 16 minutos |
| 6 | 32 minutos |
| 7 | 1 hora e 4 minutos |
| 8 | 2 horas e 8 minutos |
| 9 | 4 horas e 16 minutos |
| 10 em diante | 6 horas (teto) |
São **20 tentativas** no total, o que estende a entrega por cerca de **2 dias e 20 horas**. Depois disso a entrega fica **Esgotada** e só volta a ser enviada por reenvio manual.
Uma indisponibilidade curta do seu servidor é absorvida sem perda. Uma queda mais longa que essa janela exige ação no painel.
Desativação automática (circuit breaker) [#desativação-automática-circuit-breaker]
Falhas consecutivas em um endpoint — de qualquer entrega — incrementam um contador. Qualquer entrega bem-sucedida zera esse contador.
| Falhas consecutivas | O que acontece |
| ------------------- | ---------------------------------------------------------------------------------- |
| 10 | Os administradores da empresa recebem um **aviso** no painel, uma vez por episódio |
| 20 | O endpoint é **desativado automaticamente** e os administradores são notificados |
Enquanto o endpoint está desativado, **nenhuma entrega nova é criada** — os eventos daquele período não ficam em fila esperando. É por isso que o aviso aos 10 existe: ele dá margem para corrigir antes do desligamento.
Ao reativar o endpoint no painel, o bitERP executa um **backfill** da janela em que ele esteve fora e gera as entregas que faltaram. A recuperação alcança até 90 dias, limitada pela retenção da trilha de auditoria, e processa um volume máximo por execução — em janelas longas ou de muito movimento, parte dos eventos pode não ser recuperada. Reative o quanto antes.
Reenviar manualmente [#reenviar-manualmente]
Na aba **Histórico** do endpoint, entregas com status **Falhou** ou **Esgotado** têm a ação **Reenviar**. Ela devolve a entrega ao início do ciclo: o contador de tentativas volta a zero e o envio é refeito imediatamente.
O `id` do evento **não muda** no reenvio — a sua deduplicação continua funcionando, e um evento já processado com sucesso do seu lado será corretamente ignorado.
Retenção do histórico [#retenção-do-histórico]
Entregas são apagadas após **30 dias**, independentemente do status. O histórico serve para diagnóstico recente, não como registro permanente: se a sua integração precisa de trilha de auditoria, guarde os eventos do seu lado ao recebê-los.
Diagnóstico [#diagnóstico]
| Sintoma | Onde olhar | Ação |
| -------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------ |
| Nenhuma entrega aparece no histórico | Status do endpoint | Se estiver desativado, reative — o backfill recupera a janela |
| Nenhuma entrega para um recurso específico | Lista de eventos assinados | O evento pode não estar na assinatura, ou o recurso pode não ser assinável |
| Entregas param de repente em horários de pico | Volume por hora | O teto de 1.000 entregas por hora foi atingido; reduza os eventos assinados |
| Todas as entregas com status `401` | Sua verificação de assinatura | Ver [erros comuns](/docs/api/webhooks/receiving#erros-comuns) |
| Entregas com timeout e tempo de resposta próximo de 10 s | Seu handler | Responda `2xx` antes de processar |
| Entregas com erro de conexão e sem status HTTP | DNS e certificado do endpoint | Confirme que a URL resolve para um IP público e que o certificado TLS é válido |
| Eventos duplicados no seu sistema | Sua deduplicação | Use o `id` do evento, não o `resource.id` |
Quando abrir um chamado de suporte, informe o **`id` do evento** (`x-biterp-webhook-id`): ele identifica a entrega exata no histórico.
# Eventos
Todo evento tem o formato `.` — por exemplo `sales-orders.update` ou `customers.create`. Você assina os pares que interessam à sua integração; cada evento assinado gera uma entrega independente.
As quatro ações [#as-quatro-ações]
| Ação | Quando é disparada |
| --------- | ---------------------------------------------------------------------------------------------------------- |
| `create` | O registro foi criado |
| `update` | Qualquer campo do registro mudou — incluindo mudanças de status, baixas, alocações e autorizações fiscais |
| `delete` | O registro foi removido (exclusão lógica — ver [`DELETE` é de mão única](/docs/api#delete-é-de-mão-única)) |
| `restore` | Um registro removido foi restaurado |
A API de Integrações não expõe restauração em nenhum recurso — desfazer uma exclusão é
uma operação de usuário. Assine `restore` se a sua integração precisa reagir a esse
desfazer feito por alguém dentro do bitERP.
Recursos assináveis [#recursos-assináveis]
Cada recurso abaixo aceita as quatro ações. A coluna **Hidratação** indica se existe `GET //:id` na API de Integrações para buscar o estado completo depois de receber o evento.
Cadastros [#cadastros]
| Recurso | Eventos | Hidratação |
| ----------- | ---------------------------------------------------- | ---------------------------------------------------------------- |
| `products` | `products.create`, `.update`, `.delete`, `.restore` | [`GET /products/:id`](/docs/api/reference/products/find-by-id) |
| `customers` | `customers.create`, `.update`, `.delete`, `.restore` | [`GET /customers/:id`](/docs/api/reference/customers/find-by-id) |
| `suppliers` | `suppliers.create`, `.update`, `.delete`, `.restore` | [`GET /suppliers/:id`](/docs/api/reference/suppliers/find-by-id) |
Financeiro [#financeiro]
| Recurso | Eventos | Hidratação |
| ------------------------ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `financial-accounts` | `financial-accounts.create`, `.update`, `.delete`, `.restore` | [`GET /financial-accounts/:id`](/docs/api/reference/financial-accounts/find-by-id) |
| `financial-categories` | `financial-categories.create`, `.update`, `.delete`, `.restore` | [`GET /financial-categories/:id`](/docs/api/reference/financial-categories/find-by-id) |
| `financial-transactions` | `financial-transactions.create`, `.update`, `.delete`, `.restore` | [`GET /financial-transactions/:id`](/docs/api/reference/financial-transactions/find-by-id) |
| `receivables` | `receivables.create`, `.update`, `.delete`, `.restore` | [`GET /receivables/:id`](/docs/api/reference/receivables/find-by-id) |
| `payables` | `payables.create`, `.update`, `.delete`, `.restore` | [`GET /payables/:id`](/docs/api/reference/payables/find-by-id) |
Vendas e faturamento [#vendas-e-faturamento]
| Recurso | Eventos | Hidratação |
| -------------- | ------------------------------------------------------- | ---------------------------------------------------------------------- |
| `quotes` | `quotes.create`, `.update`, `.delete`, `.restore` | [`GET /quotes/:id`](/docs/api/reference/quotes/find-by-id) |
| `sales-orders` | `sales-orders.create`, `.update`, `.delete`, `.restore` | [`GET /sales-orders/:id`](/docs/api/reference/sales-orders/find-by-id) |
| `invoices` | `invoices.create`, `.update`, `.delete`, `.restore` | [`GET /invoices/:id`](/docs/api/reference/invoices/find-by-id) |
Compras [#compras]
| Recurso | Eventos | Hidratação |
| ------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| `purchase-invoices` | `purchase-invoices.create`, `.update`, `.delete`, `.restore` | [`GET /purchase-invoices/:id`](/docs/api/reference/purchase-invoices/find-by-id) |
Sem endpoint público de hidratação [#sem-endpoint-público-de-hidratação]
Estes recursos geram eventos, mas não têm `GET //:id` na API de Integrações. Você recebe a notificação de que algo mudou, sem uma URL pública para buscar os dados.
| Recurso | Eventos |
| ------------------------- | ------------------------------------------------------------------ |
| `fiscal-rules-br` | `fiscal-rules-br.create`, `.update`, `.delete`, `.restore` |
| `subscription-plans` | `subscription-plans.create`, `.update`, `.delete`, `.restore` |
| `customer-subscriptions` | `customer-subscriptions.create`, `.update`, `.delete`, `.restore` |
| `support-tickets` | `support-tickets.create`, `.update`, `.delete`, `.restore` |
| `email-dispatches` | `email-dispatches.create`, `.update`, `.delete`, `.restore` |
| `tenant-email-identities` | `tenant-email-identities.create`, `.update`, `.delete`, `.restore` |
Mudanças em itens contam como `update` do pai [#mudanças-em-itens-contam-como-update-do-pai]
Itens e dados específicos do país não têm eventos próprios — eles fazem parte do recurso a que pertencem:
* Adicionar, alterar ou remover um item de um pedido de venda gera **`sales-orders.update`**, com o `id` do pedido.
* O mesmo vale para itens de orçamento (`quotes.update`), tributos de regra fiscal (`fiscal-rules-br.update`) e mensagens ou anexos de chamado (`support-tickets.update`).
* Alterar os dados fiscais brasileiros de um produto gera **`products.update`**, com o `id` do produto — não um evento separado.
Na prática: **uma operação de negócio produz um evento por endpoint assinante**, apontando sempre para o registro principal. Você não recebe uma enxurrada de eventos ao salvar um pedido com dez itens.
O que não gera evento [#o-que-não-gera-evento]
* **Recursos administrativos**: usuários e permissões, API Keys, configurações da empresa, instalações OAuth, integrações nativas e [mapeamento de entidades](/docs/api/guides/integration-entity-mappings) não são assináveis.
* **Dados de referência**: bancos, formas de pagamento, unidades de medida, países, estados e cidades são [catálogos somente leitura](/docs/api/guides/reference-data) e não produzem eventos.
* **Leituras**: consultas via API, MCP ou painel nunca disparam webhooks — só escritas.
* **A própria configuração de webhooks**: alterar endpoints ou entregas não gera evento, o que evita laços de realimentação.
Do evento ao dado completo [#do-evento-ao-dado-completo]
O payload identifica o recurso, não o seu conteúdo. Para os recursos com hidratação, o caminho é:
```http
GET /sales-orders/550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer sk_...
```
Para eventos `create`, `update` e `restore`, o `GET` simples basta.
Quando a entrega do evento `delete` chega, o registro já saiu das consultas padrão — um
`GET` comum devolve `404`. Busque com `?with_deleted=true` para receber o registro com o
campo `deleted_at` preenchido:
```http
GET /sales-orders/550e8400-e29b-41d4-a716-446655440000?with_deleted=true
```
Se o recurso não tem endpoint público, guarde o evento e trate a notificação como um sinal (por exemplo, para avisar uma pessoa) — não há URL alternativa para buscar o conteúdo.
# Webhooks
Webhooks invertem o sentido da integração: em vez de a sua aplicação perguntar à API se algo mudou, o **bitERP avisa você** assim que a mudança acontece. Você registra uma URL HTTPS, escolhe os eventos que interessam e passa a receber um `POST` assinado a cada ocorrência.
É a alternativa recomendada ao *polling*. Uma rotina que chama `GET /sales-orders` de minuto em minuto gasta requisições, atrasa a reação e ainda perde alterações que aconteceram e foram desfeitas entre duas leituras. Com webhooks você reage ao fato, não à varredura.
O cadastro de endpoints é feito no **painel do bitERP**, por um administrador da
empresa. A API de Integrações não expõe endpoints de webhook — ela é usada depois,
para buscar o estado completo do recurso que mudou.
Como funciona [#como-funciona]
1. Alguém altera um recurso no bitERP — pelo painel, pela API de Integrações, pelo MCP ou por uma automação interna.
2. O bitERP identifica que a mudança corresponde a um evento assinado pelo seu endpoint.
3. Uma **entrega** (*delivery*) é criada e enviada como `POST` para a sua URL, com assinatura HMAC.
4. O seu servidor responde `2xx`. Se não responder, o bitERP **repete a entrega** com intervalos crescentes.
5. O payload identifica **o que** mudou, não o estado do recurso — você busca os dados atuais via [API de Integrações](/docs/api).
Configurar um endpoint [#configurar-um-endpoint]
Os recursos `webhook-endpoints` e `webhook-deliveries` são restritos a administradores
da empresa. Usuários comuns e API Keys não acessam essa configuração.
1. Acesse o painel do bitERP em [app.biterp.ai](https://app.biterp.ai).
2. Navegue até **Configurações > WebHooks**.
3. Clique em **Novo Webhook de saída**.
4. Informe um **nome** (identificação interna) e a **URL** que vai receber os `POST`.
5. Selecione os **eventos** que deseja assinar — ver [Eventos](/docs/api/webhooks/events).
6. Salve. O **signing secret** é exibido **uma única vez**: copie e guarde em local seguro.
O secret tem o formato `whsec_` seguido de 64 caracteres hexadecimais e é a chave usada para [verificar a assinatura](/docs/api/webhooks/receiving) de cada entrega. Se você perdê-lo, é possível gerar um novo pelo painel, em **Trocar segredo** — o secret anterior deixa de valer imediatamente, então atualize a sua aplicação logo em seguida.
Requisitos da URL [#requisitos-da-url]
| Regra | Detalhe |
| -------------------- | ----------------------------------------------------------------------------------------- |
| Protocolo | `https` obrigatório. `http` só é aceito fora de produção |
| Endereço | Precisa resolver para um IP **público** e roteável |
| Endereços bloqueados | `localhost`, faixas privadas (RFC 1918), link-local, CGNAT, multicast e equivalentes IPv6 |
| Unicidade | A mesma URL não pode ser cadastrada duas vezes na mesma empresa |
| Tamanho | Até 2.048 caracteres |
A validação acontece no cadastro **e de novo antes de cada entrega**. Um domínio que passe a apontar para um endereço interno depois do cadastro tem as entregas recusadas — o que conta como falha e alimenta o [circuit breaker](/docs/api/webhooks/delivery).
Para testar localmente, exponha a sua máquina com um túnel HTTPS público (ngrok,
Cloudflare Tunnel, localtunnel). Apontar o endpoint direto para `localhost` ou para um
IP da sua rede interna é recusado.
Testar a conexão [#testar-a-conexão]
Na tela do endpoint, a ação **Testar conexão** envia imediatamente um `POST` assinado com o evento `webhook-endpoints.test` e mostra o status HTTP, o corpo da resposta e o tempo de resposta. É a forma mais rápida de validar que a URL está acessível e que a sua verificação de assinatura funciona.
O teste é **síncrono e avulso**: não gera uma entrega, não aparece no histórico e não conta para as retentativas nem para o circuit breaker.
Status do endpoint [#status-do-endpoint]
| Status | O que significa |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| **Ativo** | Recebendo entregas normalmente |
| **Desativado (manual)** | Desligado por um administrador; nenhum evento novo é enfileirado |
| **Desativado (circuit breaker)** | Desligado automaticamente após falhas consecutivas — ver [Entrega e retentativas](/docs/api/webhooks/delivery) |
Ao reativar um endpoint que estava desativado, o bitERP faz um **backfill automático** da janela em que ele esteve fora, gerando as entregas que não chegaram a existir. Você recebe os eventos atrasados sem precisar de nenhuma ação.
Limites e garantias [#limites-e-garantias]
| Aspecto | Comportamento |
| ----------------------- | ----------------------------------------------------------------------------- |
| Latência típica | Menos de um minuto após a operação |
| Timeout por tentativa | 10 segundos |
| Tentativas por evento | Até 20, com backoff exponencial |
| Janela total de entrega | Cerca de 2 dias e 20 horas |
| Entregas por empresa | Até 1.000 por hora |
| Histórico de entregas | 30 dias |
| Ordem de entrega | **Não garantida** — ver [Entrega e retentativas](/docs/api/webhooks/delivery) |
| Entrega duplicada | Possível — trate o consumo de forma idempotente |
Ao ultrapassar 1.000 entregas em uma hora, os eventos seguintes da empresa **não são
enfileirados** — eles não chegam com atraso, simplesmente não chegam. Assine apenas os
eventos que a sua integração realmente consome, especialmente em recursos de alto
volume como `financial-transactions`.
Próximos passos [#próximos-passos]
O catálogo completo do que você pode assinar e o que dispara cada evento.
Headers, payload, verificação da assinatura HMAC e como responder.
Idempotência, ordem, backoff, circuit breaker e diagnóstico.
Busque o estado completo do recurso que mudou.
# Receber e verificar
Cada entrega chega como um `POST application/json` assinado. Antes de processar qualquer coisa, **verifique a assinatura** — sem isso, qualquer pessoa que descubra a sua URL pode enviar eventos falsos.
Anatomia da requisição [#anatomia-da-requisição]
| Header | Exemplo | Para que serve |
| ---------------------------- | --------------------- | ------------------------------------------------------------------------------------------ |
| `content-type` | `application/json` | O corpo é sempre JSON |
| `user-agent` | `bitERP-Webhooks/1.0` | Identifica a origem (não use como autenticação) |
| `x-biterp-webhook-id` | `018f...:9c2b...` | Identificador único do evento — a chave da sua [idempotência](/docs/api/webhooks/delivery) |
| `x-biterp-webhook-timestamp` | `1774612345` | Momento da assinatura, em segundos desde a época Unix |
| `x-biterp-signature` | `sha256=9a1f...` | HMAC-SHA256 em hexadecimal, prefixado por `sha256=` |
O corpo identifica o que mudou:
```json
{
"id": "018f3c1e-7b2a-7c31-9d44-2f1a0b8e5c77:9c2b6a10-5e4d-4f38-b0c1-7a9d2e3f4b56",
"event_type": "sales-orders.update",
"timestamp": "2026-05-24T12:34:56.000Z",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"resource": {
"type": "sales-orders",
"id": "7f3d9c22-8a61-4e0b-9f52-3c8d1a7e6b04"
}
}
```
| Campo | Descrição |
| --------------- | ------------------------------------------------------------------------------------------- |
| `id` | Mesmo valor do header `x-biterp-webhook-id`. Estável entre as retentativas do mesmo evento |
| `event_type` | O evento assinado, no formato `.` |
| `timestamp` | Momento em que **esta tentativa** foi montada, em ISO 8601 UTC. Muda a cada retentativa |
| `tenant_id` | A empresa em que a mudança ocorreu — útil se o seu sistema atende várias |
| `resource.type` | O recurso, igual ao prefixo do `event_type` |
| `resource.id` | O `id` a usar na [hidratação via API](/docs/api/webhooks/events#do-evento-ao-dado-completo) |
Ele diz **o que** mudou, não **como** ficou. É uma decisão de projeto: o estado no
momento da entrega pode já estar desatualizado, e um payload magro não vaza dados de
negócio para um endpoint que porventura tenha sido comprometido. Busque o registro
pela API quando precisar dos campos.
O evento de teste disparado pelo painel segue o mesmo formato, com `event_type` igual a `webhook-endpoints.test` e `id` no formato `test_` seguido de um UUID.
Verificar a assinatura [#verificar-a-assinatura]
A assinatura é calculada assim:
```
assinatura = HMAC_SHA256(signing_secret, "{timestamp}.{corpo_bruto}")
header = "sha256=" + hexadecimal(assinatura)
```
Três detalhes que costumam quebrar a verificação:
1. **Use o corpo bruto**, exatamente como chegou. Se o seu framework já transformou o JSON em objeto e você o serializa de novo, a menor diferença de espaço ou de ordem de chaves invalida o cálculo.
2. **A chave é o secret inteiro**, incluindo o prefixo `whsec_`. Não remova nada.
3. **O separador é um ponto** entre o timestamp e o corpo — `1774612345.{"id":...}`.
Compare com uma função de tempo constante (`crypto.timingSafeEqual`, `hmac.compare_digest`) e **rejeite timestamps antigos** — uma tolerância de cinco minutos barra a repetição de uma requisição capturada.
Node.js (Express) [#nodejs-express]
```js
import crypto from "node:crypto"
import express from "express"
const app = express()
const SECRET = process.env.BITERP_WEBHOOK_SECRET // whsec_...
const TOLERANCE_SECONDS = 300
function isValid(rawBody, timestamp, signature) {
if (!timestamp || !signature) return false
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp))
if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false
const digest = crypto
.createHmac("sha256", SECRET)
.update(`${timestamp}.${rawBody}`)
.digest("hex")
const expected = Buffer.from(`sha256=${digest}`)
const received = Buffer.from(signature)
return (
expected.length === received.length && crypto.timingSafeEqual(expected, received)
)
}
// express.raw preserva o corpo original — express.json() o descartaria
app.post("/webhooks/biterp", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8")
if (
!isValid(
rawBody,
req.get("x-biterp-webhook-timestamp"),
req.get("x-biterp-signature")
)
) {
return res.status(401).send("invalid signature")
}
const event = JSON.parse(rawBody)
enqueue(event) // processe fora do ciclo da requisição
res.status(200).send("ok")
})
```
Python (Flask) [#python-flask]
```python
import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["BITERP_WEBHOOK_SECRET"].encode() # whsec_...
TOLERANCE_SECONDS = 300
@app.post("/webhooks/biterp")
def receive():
timestamp = request.headers.get("x-biterp-webhook-timestamp", "")
signature = request.headers.get("x-biterp-signature", "")
raw_body = request.get_data() # bytes, sem reserializar
try:
age = abs(int(time.time()) - int(timestamp))
except ValueError:
abort(401)
if age > TOLERANCE_SECONDS:
abort(401)
digest = hmac.new(SECRET, f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(f"sha256={digest}", signature):
abort(401)
enqueue(request.get_json()) # processe fora do ciclo da requisição
return "ok", 200
```
Como responder [#como-responder]
| Regra | Detalhe |
| ------------------- | ------------------------------------------------------------------------------------------------------- |
| Sucesso | Qualquer status `2xx`. O corpo é irrelevante |
| Falha | Qualquer outro status, timeout ou erro de conexão agenda uma [retentativa](/docs/api/webhooks/delivery) |
| Prazo | **10 segundos** por tentativa. Depois disso a conexão é abortada e a entrega conta como falha |
| Assinatura inválida | Responda `401`. Isso sinaliza um problema real de configuração, e o histórico no painel mostra o status |
**Responda antes de processar.** Grave o evento numa fila ou tabela, devolva `2xx` e faça o trabalho pesado depois. Chamar a API do bitERP, gerar relatórios ou atualizar sistemas de terceiros dentro do handler é o caminho mais curto para estourar os 10 segundos e transformar entregas boas em retentativas.
Os primeiros 1.024 caracteres da sua resposta ficam registrados no histórico de entregas — útil para diagnosticar, então devolva uma mensagem de erro legível quando algo der errado do seu lado.
Erros comuns [#erros-comuns]
| Sintoma | Causa provável |
| --------------------- | ---------------------------------------------------------------------------------------------------- |
| Assinatura nunca bate | O corpo foi reserializado (`express.json()`, `body-parser`) em vez de usar o bruto |
| Assinatura nunca bate | O prefixo `whsec_` foi removido do secret |
| Assinatura nunca bate | O secret foi rotacionado no painel e a aplicação continua com o anterior |
| Funcionava e parou | Proxy ou CDN à frente do endpoint alterando o corpo, ou removendo os headers `x-biterp-*` |
| Entregas com timeout | Processamento síncrono dentro do handler |
| Nenhuma entrega chega | Endpoint desativado pelo circuit breaker — ver [Entrega e retentativas](/docs/api/webhooks/delivery) |
# List banks available for the authenticated tenant's country
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get bank by national code (for tenant country)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get bank by ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get bank by SWIFT/BIC code
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get city by ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List cities by state ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Search cities by name within a state
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List all active countries
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get country by ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get country by ISO2 code
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get country by ISO3 code
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Activate customer subscription
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancel customer subscription
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create customer subscription
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete customer subscription
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List customer subscriptions
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List customer subscription billing cycles
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get customer subscription by ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get customer subscription by record number
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List customer subscription events
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get next suggested customer subscription reference code
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Check customer subscription reference code availability
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Pause customer subscription
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Resume customer subscription
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update customer subscription
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Contar clientes
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Criar cliente
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remover cliente
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Listar clientes
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar cliente por ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar cliente por tax_id
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar clientes por texto
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Atualizar cliente
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Criar conta financeira
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remover conta financeira
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Listar contas financeiras
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar conta financeira por ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Calcular saldo da conta financeira
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Atualizar conta financeira
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Criar categoria financeira
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remover categoria financeira
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Listar categorias financeiras
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar categoria financeira por ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Listar arvore de categorias financeiras
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Atualizar categoria financeira
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remover transacao financeira
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Listar transacoes financeiras
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Listar transacoes de uma conta (extrato)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar transacao financeira por ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Calcular saldo da conta financeira
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Health check da API integrations
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Criar mapeamento manual
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remover mapeamento (soft delete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Listar mapeamentos de entidade da integracao
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar mapeamento por ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Atualizar mapeamento
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create invoice
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete invoice
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List invoices
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get invoice by ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update invoice
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# OAuth token endpoint
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Alocar transacao em conta a pagar
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancelar conta a pagar
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Criar parcelas de contas a pagar
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Criar conta a pagar
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remover conta a pagar
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Listar contas a pagar
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar conta a pagar por ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Listar contas a pagar vencidas
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Resumo agregado por status
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remover alocacao da conta a pagar
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Atualizar conta a pagar
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List payment methods available for tenant country
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get payment method by code
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get payment method by ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Criar produto
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remover produto
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Listar produtos
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar produto por ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar produto por reference_code
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar produtos por texto
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Atualizar produto
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create purchase invoice
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete purchase invoice
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Finalize purchase invoice and materialize payables
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List purchase invoices
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get purchase invoice by ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update purchase invoice
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create quote
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete quote
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List quotes
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get quote by ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get quote by record number
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update quote
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Alocar transacao em conta a receber
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancelar conta a receber
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Criar parcelas de contas a receber
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Criar conta a receber
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remover conta a receber
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Listar contas a receber
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar conta a receber por ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Listar contas a receber vencidas
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Resumo agregado por status
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remover alocacao da conta a receber
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Atualizar conta a receber
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create sales order
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete sales order
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List sales orders
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get sales order by ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get sales order by record number
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update sales order
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get state by code
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List states by country filter
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get state by ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Search states by name and country filter
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create subscription plan
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete subscription plan
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List subscription plans
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get subscription plan by ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get subscription plan by record number
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get next suggested subscription plan reference code
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Check subscription plan reference code availability
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update subscription plan
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Contar fornecedores
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Criar fornecedor
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remover fornecedor
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Listar fornecedores
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar fornecedor por ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar fornecedor por tax_id
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Buscar fornecedores por texto
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Atualizar fornecedor
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Obter tenant atual
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List units of measurement available for tenant country
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get unit of measurement by code
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get unit of measurement by ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}