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
| Campo | Tipo | Descrição |
|---|---|---|
id | string (UUID) | ID único do evento. Use para deduplicar. |
installation_id | string (UUID) | ID da instalação que gerou este evento |
integration_driver_slug | string | Slug do driver (ex: digital_manager_guru) |
name | string | Tipo do evento (ex: order.paid) |
created_at | string | Data ISO 8601 de criação |
payload | object | Dados específicos do evento. Estrutura varia por tipo. |
superseded | boolean | Se true, este evento chegou fora da ordem esperada |
superseded_by | object | null | Informações do evento posterior que já foi processado |
webhook_dispatched_at | integer | Timestamp Unix do momento do envio |
Headers da Requisição
A requisição HTTP POST contém os seguintes headers:
| Header | Valor | Descrição |
|---|---|---|
Content-Type | application/json | Tipo do conteúdo |
Content-Encoding | gzip | Payload comprimido |
User-Agent | IntegracoesInteligentes/1.0 | Identificaçã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", "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
checkout.abandoned→ Veja Eventos/Checkoutsubscription.*→ Veja Eventos/Subscriptionshipping.*→ Veja Eventos/Shippingfiscal_invoice.*→ Veja Eventos/Fiscal Invoicereverse_logistic.*→ Veja Eventos/Reverse Logistic
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 gzipapp.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:
| Tentativa | Delay |
|---|---|
| 1ª | Imediata |
| 2ª | 5 segundos |
| 3ª | 15 segundos |
| 4ª | 30 segundos |
| 5ª | 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 campoidpara idempotência, não oattempt_number.
Troubleshooting
Não recebo eventos
- Verifique se a
webhook_urlestá configurada:GET /me/tenant - Verifique se o evento está habilitado na installation:
GET /installations/{id} - Verifique se não há firewall bloqueando
- 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
- Estrutura dos Eventos - Conheça todos os tipos
- Reenvio e Replay - Reenviar eventos
- API de Processing - Consultar events
- Troubleshooting - Problemas comuns