Guia de integração com ERP e outros sistemas

Dimensione uma integração antes de construí-la: o que a API alcança, o que ela não alcança e onde ficam os limites

9 min de leitura
Imprimir esta seção

Este guia é para o desenvolvedor que está dimensionando uma integração entre o FreeMaint e outro sistema: um ERP, um historiador SCADA, um software de almoxarifado ou de contabilidade. Os demais artigos desta seção explicam como chamar a API; este fala de propósito sobre fronteiras, porque o erro caro nunca é um cabeçalho errado: é descobrir na metade do desenvolvimento que um recurso que você considerava garantido não existe. Cada limite listado abaixo está aqui de propósito.

Uma chave, três portas

A chave de API (ela começa com fmk_) é criada em Configurações da empresa > API (/company-settings/api) por um administrador, ou por uma pessoa para si mesma em Perfil > Segurança > Chaves API, e enviada como Authorization: Bearer fmk_... . Ela autentica exatamente três coisas: o endpoint GraphQL em POST /api/v1/graphql, o endpoint de ingestão de energia, e /public-api/whoami e /public-api/ping. Ela NÃO autentica as rotas REST em /api/v1/{resource} — essas são a API interna do aplicativo e exigem uma sessão de usuário (JWT). Construa a sua integração sobre GraphQL: é essa a superfície que uma chave alcança.

O que uma chave de API oferece

A chave em texto claro aparece uma única vez, na criação, e nunca mais pode ser recuperada depois disso: guarda-se apenas um hash SHA-256, junto com os 6 caracteres que vêm depois de fmk_ para que a lista consiga identificar a chave na tela.

Nome

Um rótulo legível: «Ponte ERP Oracle», «Quiosque da linha 2». Emita uma chave por sistema consumidor, assim você revoga uma sem atrapalhar as outras.

Nível de acesso

READ, READ_STOCK ou FULL, uma hierarquia ordenada acima das permissões do proprietário. READ executa somente consultas e é o padrão. READ_STOCK acrescenta a mutation adjustStock. FULL permite todas as operações que a função do proprietário permite, inclusive criar, atualizar e excluir — e o endpoint de ingestão de energia quando o proprietário pode criar leituras de energia.

Expiração

Um expiresAt opcional. Passado esse momento, a chave para de autenticar. Deixe-o vazio para uma integração de serviço permanente; preencha-o para um prestador ou um piloto.

Revogação

isActive é um botão liga/desliga individual da chave, que passa a valer em segundos. O registro é mantido para auditoria, então uma chave revogada continua rastreável em vez de sumir.

Último uso

lastUsedAt registra a chamada autenticada mais recente: o bastante para identificar uma chave que ninguém usa mais.

Proprietário

A pessoa em nome de quem a chave age. Toda chamada carrega as permissões dela e a visibilidade de dados dela, e o nome dela assina o que a chave grava. Para um ERP, crie um usuário dedicado — “Integração ERP” — com uma função que cubra exatamente o que a ponte precisa, e entregue a chave a ele.

O que uma chave de API não oferece

São limites reais da implementação atual, não omissões desta página. Se a sua revisão de segurança depender de algum deles, projete considerando isso agora, e não depois de construir.

  • Sem lista de módulos por chave. Uma chave lê e grava o que a função do seu proprietário permite — para restringi-la, entregue-a a um proprietário com uma função mais restrita. Além disso, só valem os três níveis de acesso: você não pode listar, na própria chave, os módulos que ela pode usar.
  • Sem lista de IPs permitidos. Uma chave é válida de qualquer ponto da internet. Trate-a como um segredo: um cofre ou o gerenciador de segredos da sua plataforma, nunca um repositório, um drive compartilhado ou um chamado de suporte.
  • Sem log de uso por chave. Só o lastUsedAt é mantido. Não existe histórico de chamadas por chave, nem cota por chave, nem registro de qual chave executou qual operação. O contador de cota diária é por EMPRESA e é compartilhado por todas as chaves, então uma integração descontrolada pode esgotar a cota de todas as outras.
  • Sem aprovação em nome de outra pessoa. Uma chave nunca aprova uma etapa do fluxo de trabalho, mesmo quando o proprietário poderia: aprovações continuam sendo um ato humano no aplicativo.
  • Uma empresa por chave. Uma chave fica vinculada à empresa em que foi criada e não alcança os dados de outra empresa: um registro de outra empresa simplesmente não é encontrado.

O que você pode ler

O GraphQL expõe 16 consultas de leitura: whoami, workOrders, workOrder(id), assets, asset(id), assetDowntimes, meters(assetId), parts, part (por id ou código de barras), stockMovements, locations, vendors, customers, purchaseOrders, purchaseOrder(id | reference | erpReference) e floorPlans(locationId). Toda lista pode ser percorrida até o fim: workOrders, assets, assetDowntimes, meters, parts, purchaseOrders, locations, vendors e customers aceitam limit (de 1 a 200) mais offset, e workOrders, assets, parts, purchaseOrders, locations, vendors e customers também aceitam updatedSince para sincronizações incrementais. A página padrão tem 50 linhas, exceto em locations, vendors e customers, que mantêm o padrão anterior de 200. Toda lista é ordenada com um critério de desempate pelo id, então a fronteira entre páginas nunca pula nem repete uma linha. stockMovements é o histórico de movimentações de estoque a partir do qual você faz a conciliação: ele é lido do mais antigo para o mais recente e paginado com afterId — informe o id da última movimentação recebida —, o que continua exato enquanto novas movimentações são gravadas, e since permite iniciar uma carga do histórico a partir de uma data. Ordens de serviço na lixeira nunca são retornadas. purchaseOrders também filtra por status e vendorId; os valores vêm na moeda da empresa, que cada pedido traz no campo currency.

Verificar a chave e ler ordens de serviço em uma única chamada: curl -X POST https://freemaint.com/api/v1/graphql -H "Authorization: Bearer $FMK" -H "Content-Type: application/json" -d '{"query":"{ whoami { companyId tier } workOrders(limit: 5, status: \"OPEN\") { id reference title status priority dueDate } }"}'

O que você pode escrever

Doze mutações: criar, atualizar e excluir ordens de serviço, ativos e peças, além de adjustStock para um movimento de estoque com sinal. Elas passam pelos mesmos serviços que o próprio aplicativo usa, então as referências (WO-…, ASS-…, PART-…), os registros de auditoria e as notificações se comportam exatamente como na interface. Para estoque, prefira adjustStock: ela grava um registro de auditoria StockMovement na mesma transação e se recusa a deixar uma quantity abaixo de zero, enquanto updatePart(quantity) define um valor absoluto e registra a diferença como um ajuste. Em uma peça controlada por local, informe locationId para adjustStock; ali, updatePart(quantity) é recusado. A décima primeira, addMeterReading, registra uma leitura de medidor pelo mesmo serviço da tela de medidores, então os gatilhos e a manutenção preventiva por medidor reagem exatamente como para uma leitura digitada no aplicativo. A décima segunda, setPurchaseOrderErpReference, guarda o número de documento do seu ERP em um pedido de compra, para que os dois registros possam ser casados depois com purchaseOrder(erpReference: …). Ela exige uma chave de acesso FULL e, de propósito, não dispara nenhum webhook: o seu ERP nunca recebe a própria gravação como evento.

O que a API não cobre de jeito nenhum

O schema GraphQL contém ordens de serviço, ativos, paradas de máquina, peças e suas movimentações de estoque, medidores, pedidos de compra, locais, fornecedores, clientes e plantas. Nada além disso. O que segue são partes plenamente funcionais do FreeMaint que hoje NÃO têm superfície de API: não podem ser lidas, escritas nem assinadas a partir de uma integração, e nenhuma combinação das consultas existentes as alcança.

  • As inspeções: nem os modelos, nem as execuções agendadas, nem os resultados registrados, nem os códigos de ação. Não existe consulta de inspeção, mutação de inspeção nem evento de webhook de inspeção.
  • Usuários, funções e equipes: você não pode listar, criar nem desativar um usuário por esta API. O provisionamento de usuários acontece no aplicativo; no plano Enterprise, ele pode ser automatizado com SCIM, que é um mecanismo separado e não faz parte desta API.
  • Projetos: nenhuma consulta e nenhuma mutação.
  • Requisições de compra: nenhuma consulta e nenhuma mutação. Os pedidos de compra podem ser lidos (purchaseOrders, purchaseOrder), o seu ERP pode gravar neles a própria referência (setPurchaseOrderErpReference) e os webhooks purchase_order.* os anunciam, mas um pedido não pode ser criado, editado nem recebido pela API: isso continua no aplicativo.
  • Arquivos, fotos e anexos: você não pode enviar um documento nem uma foto, e também não pode lê-los de volta. As fotos das ordens de serviço, as imagens das peças e as evidências de inspeção estão todas fora da API. Uma URL que apareça em uma resposta, como o imageUrl de uma peça, não é um caminho de upload.
  • Os planos de manutenção preventiva, os apontamentos de horas, as tarefas e os checklists: não estão expostos.

Importante

Dimensione o seu desenvolvimento por esta lista, e não pelo que o produto faz em um navegador. Os próprios aplicativos web e mobile do FreeMaint rodam sobre uma API REST interna que uma chave de API não alcança, então o fato de um recurso aparecer na tela não é prova de que uma integração consiga acioná-lo.

Webhooks — o catálogo de eventos

Webhooks são a metade «push» de uma integração: em vez de você ficar consultando, é o FreeMaint que faz um POST para a sua URL no instante em que algo acontece. Você os configura em Configurações da empresa > Webhooks (/company-settings/webhooks) e assina cada webhook aos eventos de que precisa. O catálogo é uma lista fixa.

  • work_order.created — uma ordem de serviço foi criada
  • work_order.completed — uma ordem de serviço chegou a Concluída
  • work_order.status_changed — uma ordem de serviço mudou para qualquer status; o payload traz new_status. Dispara além de work_order.completed, que permanece inalterado
  • request.created — uma solicitação de manutenção foi enviada
  • request.status_changed — uma solicitação foi aprovada, recusada ou cancelada
  • part.low_stock — uma peça caiu para o mínimo ou abaixo dele
  • part.out_of_stock — uma peça chegou a zero; é um evento separado, não uma variação de low_stock
  • part.low_stock_summary — um resumo diário por empresa com as peças no ponto de ressuprimento ou abaixo, inclusive as que nunca se movimentaram
  • part.out_of_stock_summary — o mesmo resumo diário, para as peças zeradas
  • part.consumed — uma peça foi consumida em uma ordem de serviço, ou devolvida dela; quantity tem sinal
  • compliance.chain_divergence — a cadeia de hash de auditoria foi rompida (Enterprise)
  • asset.downtime_started — uma parada de máquina foi aberta
  • asset.downtime_ended — uma parada de máquina foi encerrada; a duração já pode ser apropriada
  • report.daily_summary — os trabalhos concluídos do dia, uma vez por dia no horário que você escolher
  • purchase_order.created — um pedido de compra foi criado; o payload traz o pedido (reference, status, total_cost, currency, expected_date, erp_reference) e o fornecedor
  • purchase_order.status_changed — um pedido de compra mudou de status (aprovado, pedido, cancelado…); o payload traz previous_status e new_status
  • purchase_order.received — uma entrega de um pedido de compra foi recebida (enviado a cada entrega, inclusive as parciais); o payload lista cada linha com quantity_ordered, quantity_received (esta entrega), quantity_received_total e quantity_remaining, além de receipt_id, fully_received e closed_short; new_status é PARTIALLY_RECEIVED enquanto ainda se esperam mercadorias e RECEIVED quando o pedido está completo ou encerrado

Aviso

Não existe evento de inspeção, nem evento de defeito ou de constatação. Se o seu processo é «uma inspeção reprova, meu ERP reage», hoje nada dispara para isso. Planeje essa lacuna explicitamente em vez de supor que existe um evento parecido o bastante.

O envelope de entrega

Toda entrega GENERIC é um POST cujo corpo é o mesmo envelope de três chaves: event, timestamp e data, em que data é indexado por entidade. Três cabeçalhos viajam junto. X-FreeMaint-Signature tem o formato t=<segundos unix>,v1=<hex>, em que v1 é o HMAC-SHA256 da string exata «<t>.<corpo bruto>» assinada com o segredo daquele webhook; X-FreeMaint-Event repete o nome do evento; X-FreeMaint-Delivery-Id identifica a entrega. Verifique a assinatura sobre o corpo BRUTO, antes de fazer o parsing: reserializar o JSON muda os bytes e a assinatura não vai bater.

{ "event": "work_order.created", "timestamp": "2026-09-05T08:14:02.371Z", "data": { "work_order": { "id": 5521, "title": "Vazamento na bomba nº 3", "status": "OPEN", "priority": "HIGH", "dueDate": "2026-09-08T00:00:00.000Z" } } }

Confiabilidade e monitoramento

Uma entrega é tentada 3 vezes com backoff exponencial sobre uma base de 2 segundos — mais ou menos 2 s, depois 4 s, depois 8 s — e cada tentativa expira após 10 segundos. Um 2xx a marca como entregue. Respostas 5xx e erros de rede são repetidos. Um 4xx é tratado como recusa definitiva e NÃO é repetido de forma alguma: se o seu endpoint responder 400 ou 401 por causa de um bug do seu lado, aquela entrega para ali e só um reenvio manual a recupera.

  • Toda entrega é armazenada: o evento, o payload exato enviado, o status, o código HTTP que você devolveu, os primeiros ~500 caracteres do corpo da sua resposta, a contagem de tentativas e a mensagem de erro.
  • Os status são PENDING, DELIVERED, FAILED, RATE_LIMITED (o teto Starter de 100 entregas por dia foi atingido) e SKIPPED_TIER (o plano não dá mais direito a esse destino).
  • Uma entrega armazenada pode ser reenviada pelo histórico do webhook. Ela reenvia o payload salvo sem alteração, então as chaves de negócio ali dentro continuam idênticas e um receptor que deduplique direito não vai lançar duas vezes.
  • Cada webhook carrega lastTriggeredAt, lastSuccessAt, lastFailureAt e um failureCount, o que já basta para uma verificação de saúde sem abrir o histórico completo.
  • A lista de webhooks e o log de entregas podem ser consultados em todos os planos, então uma integração parada por um downgrade fica visível em vez de morrer em silêncio.
  • Nada avisa você quando as entregas começam a falhar: nem e-mail, nem notificação. Se uma queda silenciosa fosse um problema, consulte o log de entregas a partir do seu próprio monitoramento.

Segurança

As URLs são validadas quando você salva o webhook, e as regras são rígidas de propósito: um webhook é uma requisição de saída feita pelos nossos servidores, então uma regra permissiva seria um vetor de SSRF.

  • As chaves são armazenadas como hashes SHA-256: o valor em texto claro não pode ser recuperado do FreeMaint depois da criação.
  • As URLs de webhook precisam usar https. O http simples e qualquer outro esquema são recusados.
  • Somente as portas 443 e 8443 são aceitas.
  • Hosts de loopback, de link local e de faixas privadas (127.x, 10.x, 192.168.x, 172.16–31.x, ::1 e afins) são recusados: o receptor precisa ser acessível pela internet pública. Um sistema que só existe dentro da sua planta precisa de um relay público ou de um túnel.
  • Credenciais na URL (https://user:pass@host) são recusadas.
  • Redirecionamentos não são seguidos: o endpoint que você cadastra é o endpoint que recebe o POST.
  • Cada webhook tem o próprio segredo de assinatura e existe uma ação de rotação de segredo. A rotação vale imediatamente, então implante o novo segredo do seu lado na mesma janela.
  • Os destinos SLACK e TEAMS recebem uma mensagem formatada para a plataforma e NENHUM cabeçalho HMAC do FreeMaint: para eles, o sigilo da URL é a única autenticação. Use GENERIC para tudo o que você precisar verificar criptograficamente.

Padrões que funcionam bem hoje

Estas são as integrações que a superfície atual suporta com clareza, e o detalhe que torna cada uma delas segura em produção.

Espelhar o consumo em um ERP

Assine part.consumed. quantity tem sinal — negativo quando a peça sai para o serviço, positivo quando volta — e o payload traz um stock_movement_id que se mantém estável entre novas tentativas e reenvios, então use-o como chave de idempotência. A mutação adjustStock deliberadamente NÃO dispara esse evento, e é isso que impede que as suas próprias gravações voltem para você em eco, formando um laço.

Empurrar correções de estoque

Chame adjustStock com um delta com sinal do seu lado. Uma chave READ_STOCK consegue fazer isso sem poder criar, atualizar ou excluir nada, o que a torna uma escolha justa para uma ponte de estoque de mão única.

Criar ordens de serviço a partir de outro sistema

createWorkOrder a partir de um alarme SCADA, de um chamado de helpdesk ou de um aviso de quebra do ERP. A ordem de serviço recebe a referência normal dela e dispara work_order.created como qualquer outra.

Manter um catálogo de peças em dia

parts(updatedSince:) devolve só o que mudou desde o instante que você escolher. createPart e updatePart aceitam customReference — o seu próprio código para a peça — e part(barcode:) passa a casar por ele, então você consegue localizar uma peça pelo identificador que o seu ERP já usa. A unicidade não é imposta; garanti-la é com você.

Fechar o ciclo na conclusão

work_order.completed leva a ordem de serviço e o status anterior dela, o que já basta para encerrar um aviso de manutenção do outro lado.

O que você precisa ter

Duas travas independentes: uma para a API, outra para os webhooks. Uma empresa no plano Starter pode receber webhooks sem ter a API.

API GraphQL e chaves de API

Business e acima. Core e Starter não conseguem criar chave nenhuma.

Cota diária

5.000 chamadas por dia no Business, 100.000 no Enterprise. Contadas por empresa e por operação, zerando às 00:00 UTC — agrupar operações em uma única requisição HTTP economiza idas e voltas, nunca cota.

Webhooks

Starter e acima, com teto de 100 entregas por dia; ilimitadas a partir do Business.

Destinos Slack e Teams

Enterprise. Um webhook genérico está disponível a partir do Starter.

Dica

Está planejando algo que esbarra em uma das lacunas acima? Conte para a gente o que você está construindo em contact@freemaint.com. Qual recurso sai na sequência é decidido por aquilo que está de fato travando os clientes.

Esta página foi útil?