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.
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.
