Pular para o conteúdo

Receber Eventos

Este guia explica como receber e processar os Eventos entregues via webhook para seu endpoint configurado.

Estrutura do Evento

Cada evento é entregue em um envelope JSON com os seguintes campos:

{
"id": "0192abcd-1234-5678-9abc-def012345679",
"installation_id": "0192abcd-1234-5678-9abc-def012345678",
"integration_driver_slug": "digital_manager_guru",
"name": "order.paid",
"created_at": "2024-01-15T10:45:00.000000Z",
"payload": {
/* ... */
},
"superseded": false,
"superseded_by": null,
"webhook_dispatched_at": 1705319105
}

Para detalhes completos da estrutura, consulte GET /processing/events/{id}.

Campos do Evento

CampoTipoDescrição
idstring (UUID)ID único do evento. Use para deduplicar.
installation_idstring (UUID)ID da instalação que gerou este evento
integration_driver_slugstringSlug do driver (ex: digital_manager_guru)
namestringTipo do evento (ex: order.paid)
created_atstringData ISO 8601 de criação
payloadobjectDados específicos do evento. Estrutura varia por tipo.
supersededbooleanSe true, este evento chegou fora da ordem esperada
superseded_byobject | nullInformações do evento posterior que já foi processado
webhook_dispatched_atintegerTimestamp Unix do momento do envio

Headers da Requisição

A requisição HTTP POST contém os seguintes headers:

HeaderValorDescrição
Content-Typeapplication/jsonTipo do conteúdo
Content-EncodinggzipPayload comprimido
User-AgentIntegracoesInteligentes/1.0Identificação

Importante:

O body é sempre enviado comprimido com gzip. Seu servidor deve descomprimir antes de processar.

Processamento Recomendado

1. Responda Rapidamente (10 segundos)

app.post("/webhooks/integracoes", (req, res) => {
// 1. Responda imediatamente
res.status(200).send("OK");
// 2. Processe assincronamente
processEventAsync(req.body);
});

2. Descomprima Gzip

const zlib = require("zlib");
app.use((req, res, next) => {
if (req.headers["content-encoding"] === "gzip") {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
const buffer = Buffer.concat(chunks);
zlib.gunzip(buffer, (err, result) => {
if (err) return res.status(400).send("Erro gzip");
req.body = JSON.parse(result.toString());
next();
});
});
} else {
next();
}
});

3. Implemente Idempotência

Eventos podem ser entregues múltiplas vezes. Use o id para deduplicar:

const processedEvents = new Set(); // Use Redis/DB em produção
function processEvent(event) {
if (processedEvents.has(event.id)) {
console.log(`Evento ${event.id} já processado. Ignorando.`);
return;
}
processedEvents.add(event.id);
// Processar evento...
handleEvent(event);
}

4. Verifique Sequência de Eventos

O campo superseded indica que um evento chegou fora da ordem esperada. Isso ocorre quando recebemos um evento que deveria ser processado antes de outro evento já recebido para o mesmo pedido.

Exemplo: Se recebermos order.paid antes de order.waiting_payment, o sistema marca o order.waiting_payment como superseded porque o fluxo chegou fora de ordem.

function processEvent(event) {
if (event.superseded) {
console.log(
`Evento ${event.id} chegou fora de ordem (já recebemos ${event.superseded_by.event_name})`,
);
// Esteja ciente de que o estado já foi alterado por um evento posterior
processWithCaution(event);
return;
}
// Processar evento normalmente...
}

Tipos de Payload

A estrutura do payload varia conforme o tipo de evento:

Eventos de Order (order.*)

{
"customer": {
"id": "cust_123",
"name": "João Silva",
"email": "[email protected]",
"document": "12345678900",
"phone": "+5511999999999",
"address": {
"street": "Rua Principal",
"number": "123",
"complement": "Apto 45",
"neighborhood": "Centro",
"city": "São Paulo",
"state": "SP",
"country": "BR",
"postal_code": "01000-000"
}
},
"order": {
"id": "order_456",
"status": "paid",
"raw_status": "paid",
"created_at": 1705319000,
"paid_at": 1705319100,
"updated_at": 1705319100,
"warranty_until": null,
"canceled_at": null,
"refunded_at": null
},
"checkout": {
"id": null,
"url": "https://checkout.example.com/..."
},
"payment": {
"currency": "BRL",
"total": 29990,
"discount_value": 0,
"shipping_value": 1500,
"total_products_value": 28490,
"payment_method": {
"type": "credit_card",
"brand": "visa",
"last_digits": "1234",
"expiration_month": "12",
"expiration_year": "2027"
},
"coupons": []
},
"shipping": {
"carrier": "Correios",
"total_value": 1500,
"tracking_url": null,
"tracking_code": null,
"method": "PAC",
"delivery_address": {
/* ... */
},
"estimated_delivery_date": 1705923900,
"estimated_delivery_time_in_days": 7,
"status": null,
"raw_status": null
},
"products": [
{
"id": "prod_789",
"name": "Curso de Programação",
"type": "product",
"quantity": 1,
"unit_value": 28490,
"total_value": 28490,
"image_url": "https://cdn.example.com/curso.jpg",
"offer_type": "main"
}
],
"lead_tracking": {
"src": "facebook",
"sck": null,
"utm_source": "facebook",
"utm_campaign": "black_friday_2024",
"utm_medium": "paid_social",
"utm_content": null,
"utm_term": null,
"utm_id": null,
"meta_fbp": "fb.1.1234567890",
"google_ga_id": null,
"google_gclid": null,
"google_gclsrc": null,
"google_dclid": null,
"google_gbraid": null,
"google_wbraid": null,
"tiktok_ttlid": null,
"ip": "189.123.45.67"
}
}

Outros Tipos

Exemplo Completo (Node.js)

const express = require("express");
const zlib = require("zlib");
const Redis = require("ioredis");
const app = express();
const redis = new Redis();
// Middleware para descomprimir gzip
app.use((req, res, next) => {
if (req.headers["content-encoding"] === "gzip") {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
const buffer = Buffer.concat(chunks);
zlib.gunzip(buffer, (err, result) => {
if (err) return res.status(400).send("Erro gzip");
try {
req.body = JSON.parse(result.toString());
next();
} catch (e) {
res.status(400).send("JSON inválido");
}
});
});
} else {
next();
}
});
app.post("/webhooks/integracoes", async (req, res) => {
const event = req.body;
// 1. Responder imediatamente
res.status(200).json({ received: true });
// 2. Verificar idempotência
const exists = await redis.get(`event:${event.id}`);
if (exists) {
console.log(`Evento ${event.id} duplicado. Ignorando.`);
return;
}
// 3. Marcar como processado (TTL 24h)
await redis.setex(`event:${event.id}`, 86400, "1");
// 4. Verificar sequência de eventos
if (event.superseded) {
console.log(
`Evento ${event.id} chegou fora de ordem (já recebemos ${event.superseded_by.event_name})`,
);
}
// 5. Processar baseado no tipo
try {
switch (event.name) {
case "order.paid":
await handleOrderPaid(event);
break;
case "order.canceled":
await handleOrderCanceled(event);
break;
case "checkout.abandoned":
await handleCheckoutAbandoned(event);
break;
default:
console.log(`Evento não tratado: ${event.name}`);
}
} catch (error) {
console.error(`Erro ao processar evento ${event.id}:`, error);
// Re-tentativas são automáticas pela plataforma
}
});
async function handleOrderPaid(event) {
const { payload } = event;
console.log(`Pedido pago: ${payload.order.id}`);
console.log(`Cliente: ${payload.customer.name}`);
console.log(
`Valor: ${payload.payment.total / 100} ${payload.payment.currency}`,
);
// Sua lógica aqui...
// Ex: Criar usuário, liberar acesso, enviar email, etc.
}
async function handleOrderCanceled(event) {
const { payload } = event;
console.log(`Pedido cancelado: ${payload.order.id}`);
// Sua lógica aqui...
// Ex: Revogar acesso, processar estorno, etc.
}
async function handleCheckoutAbandoned(event) {
const { payload } = event;
console.log(`Checkout abandonado`);
console.log(`Email: ${payload.customer.email}`);
// Sua lógica aqui...
// Ex: Enviar email de recuperação em 1 hora
}
app.listen(3000, () => {
console.log("Webhook server rodando na porta 3000");
});

Re-tentativas Automáticas

Se seu servidor retornar erro ou não responder em 10 segundos, a plataforma tentará novamente:

TentativaDelay
Imediata
5 segundos
15 segundos
30 segundos
60 segundos

Após 5 tentativas, o evento é marcado como falho e pode ser reenviado manualmente.

Nota:

Cada tentativa incrementa o attempt_number. Use o campo id para idempotência, não o attempt_number.

Troubleshooting

Não recebo eventos

  1. Verifique se a webhook_url está configurada: GET /me/tenant
  2. Verifique se o evento está habilitado na installation: GET /installations/{id}
  3. Verifique se não há firewall bloqueando
  4. Verifique os logs de tentativas: GET /processing/dispatches

Eventos duplicados

Implemente idempotência usando o campo id:

const processedEvents = new Set();
function processEvent(event) {
if (processedEvents.has(event.id)) return;
processedEvents.add(event.id);
// ...
}

Timeout

Responda em menos de 10 segundos. Use processamento assíncrono:

app.post("/webhook", (req, res) => {
res.status(200).send("OK"); // Responda imediatamente
// Processamento assíncrono
setImmediate(() => {
processEvent(req.body);
});
});

Próximos Passos