Pular para o conteúdo
arquitetura

Integração de Sistemas Empresariais: Como Conectar ERPs, CRMs e APIs Externas

Estratégias e padrões para integrar sistemas empresariais: ETL, webhooks, filas de mensagens, APIs REST e como evitar os erros mais comuns em projetos de integração.

Douglas M. Pereira5 min de leitura
integração de sistemasapiwebhooksetlfilaserpmiddleware

A realidade da integração em empresas brasileiras

A maioria das empresas não tem um único sistema que faz tudo. Elas têm um ERP (TOTVS, Sankhya), um CRM (Salesforce, HubSpot), uma plataforma de e-commerce (VTEX, Shopify), um sistema de logística próprio e uma ferramenta de BI. Esses sistemas precisam conversar.

Projetos de integração têm reputação de complexos, lentos e frágeis — e muitas vezes são, quando feitos sem uma estratégia clara. Este guia apresenta os padrões que funcionam.

Os 4 padrões de integração

1. API REST síncrona — para dados em tempo real

Quando o sistema A precisa de uma resposta imediata do sistema B:

// Integração com API de CEP para preencher endereço
async function buscarEnderecoPorCep(cep: string): Promise<Address> {
  // Remove formatação
  const cleaned = cep.replace(/\D/g, '')
  
  if (cleaned.length !== 8) {
    throw new Error('CEP inválido')
  }
  
  // Retry automático em caso de falha transitória
  const response = await fetch(`https://viacep.com.br/ws/${cleaned}/json/`, {
    signal: AbortSignal.timeout(5000),  // timeout de 5 segundos
  })
  
  if (!response.ok) {
    throw new Error(`ViaCEP retornou ${response.status}`)
  }
  
  const data = await response.json()
  
  if (data.erro) {
    throw new Error('CEP não encontrado')
  }
  
  return {
    street: data.logradouro,
    neighborhood: data.bairro,
    city: data.localidade,
    state: data.uf,
    zipCode: cleaned,
  }
}

Quando usar: autenticação em tempo real, consulta de estoque, validação de dados, pagamentos.

Problema: se o sistema B estiver fora do ar, o sistema A para.

2. Webhooks — para notificações assíncronas

O sistema B avisa o sistema A quando algo acontece:

// Receber webhook do gateway de pagamento
app.post('/webhooks/payment', async (req, res) => {
  // 1. Responder IMEDIATAMENTE ao gateway (< 5 segundos)
  // Caso contrário, o gateway fará retry
  res.status(200).json({ received: true })
  
  // 2. Verificar assinatura (CRÍTICO para segurança)
  const signature = req.headers['x-webhook-signature'] as string
  const isValid = verifySignature(req.rawBody, signature, process.env.WEBHOOK_SECRET!)
  
  if (!isValid) {
    logger.warn('Webhook com assinatura inválida — possível ataque')
    return  // res já foi enviado
  }
  
  // 3. Processar de forma assíncrona (após resposta)
  const event = req.body
  
  try {
    await processPaymentEvent(event)
  } catch (error) {
    logger.error({ event, error }, 'Erro ao processar webhook de pagamento')
    // Enfileirar para retry posterior
    await queue.add('payment-webhook-retry', event)
  }
})

function verifySignature(payload: Buffer, signature: string, secret: string): boolean {
  const hmac = crypto.createHmac('sha256', secret)
  const expected = `sha256=${hmac.update(payload).digest('hex')}`
  // Comparação em tempo constante previne timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature), 
    Buffer.from(expected)
  )
}

Quando usar: notificações de pagamento, eventos de shipping, mudanças de status em sistemas externos.

3. Filas de mensagens — para processamento confiável

Para processos que não precisam ser síncronos e precisam de garantia de entrega:

import Bull from 'bull'

const integrationQueue = new Bull('erp-sync', { redis: process.env.REDIS_URL })

// Producer: quando um pedido é criado, enfileirar sincronização
app.post('/orders', async (req, res) => {
  const order = await db.orders.create({ data: req.body })
  
  // Enfileirar sincronização com ERP
  await integrationQueue.add('sync-order-to-erp', {
    orderId: order.id,
    timestamp: new Date().toISOString(),
  }, {
    attempts: 5,              // tentar 5 vezes
    backoff: {
      type: 'exponential',
      delay: 2000,            // 2s, 4s, 8s, 16s, 32s
    },
    removeOnComplete: true,
  })
  
  return res.status(201).json(order)
})

// Consumer: processar sincronização
integrationQueue.process('sync-order-to-erp', async (job) => {
  const { orderId } = job.data
  
  const order = await db.orders.findUnique({
    where: { id: orderId },
    include: { items: true, user: true }
  })
  
  if (!order) throw new Error(`Pedido ${orderId} não encontrado`)
  
  // Transformar para o formato do ERP
  const erpPayload = transformOrderToERP(order)
  
  // Enviar para o ERP
  await erpClient.createOrder(erpPayload)
  
  // Registrar ID do ERP no nosso banco
  await db.orders.update({
    where: { id: orderId },
    data: { erpId: erpPayload.externalId }
  })
})

Quando usar: sincronização com ERPs, envio de e-mails, geração de relatórios, integrações com sistemas lentos ou instáveis.

4. ETL para sincronização em batch

Para integrar sistemas que não têm API, exportam arquivos ou onde a latência não é crítica:

// Sincronizar clientes do ERP (arquivo CSV diário) para o CRM
async function syncCustomersFromERP() {
  logger.info('Iniciando sync de clientes ERP → CRM')
  
  // 1. Extract: buscar dados do ERP
  const { data: erpCustomers } = await erpClient.exportCustomers({
    updatedAfter: await getLastSyncTimestamp(),
  })
  
  logger.info(`${erpCustomers.length} clientes para sincronizar`)
  
  let successCount = 0
  let errorCount = 0
  
  // 2. Transform + Load: processar em batches
  for (const batch of chunk(erpCustomers, 100)) {
    await Promise.allSettled(batch.map(async (erpCustomer) => {
      try {
        // Transform
        const crmPayload = {
          external_id: erpCustomer.codigo,
          name: erpCustomer.razao_social || erpCustomer.nome,
          email: erpCustomer.email?.toLowerCase(),
          document: normalizeCNPJCPF(erpCustomer.cnpj_cpf),
          phone: formatPhone(erpCustomer.telefone),
          address: {
            city: erpCustomer.cidade,
            state: erpCustomer.uf,
          }
        }
        
        // Load: upsert no CRM
        await crmClient.upsertContact(crmPayload)
        successCount++
      } catch (error) {
        logger.error({ customerId: erpCustomer.codigo, error }, 'Erro ao sincronizar cliente')
        errorCount++
      }
    }))
  }
  
  await saveLastSyncTimestamp()
  logger.info(`Sync concluído: ${successCount} sucesso, ${errorCount} erros`)
}

Idempotência: o princípio mais importante

Integrações falham. Retries acontecem. Toda operação de integração deve ser idempotente — executar a mesma operação duas vezes deve ter o mesmo resultado que executá-la uma vez:

// Idempotência via idempotency key
async function createPayment(amount: number, idempotencyKey: string) {
  // Verificar se já foi processado
  const existing = await db.payments.findUnique({
    where: { idempotencyKey }
  })
  
  if (existing) {
    logger.info('Pagamento já processado, retornando resultado anterior')
    return existing
  }
  
  // Processar e salvar com idempotency key
  const payment = await paymentGateway.charge(amount)
  
  return db.payments.create({
    data: { ...payment, idempotencyKey }
  })
}

Monitoramento de integrações

| Métrica | Como medir | |---|---| | Taxa de sucesso por integração | integration_sync_total{status="success"} | | Latência de processamento | integration_processing_duration_seconds | | Tamanho da fila de mensagens | bull.queue.waiting | | Erros por tipo e sistema | integration_errors_total{system="erp", type="timeout"} | | Tempo desde último sync bem-sucedido | alerta se > threshold |

Sem monitoramento específico, uma integração pode falhar silenciosamente por dias.