Leonardo Pereira
GA4 & GTM16 min de leitura

Evento de Compra VTEX + GA4 via Order Hook: Implementação Server-Side

Como implementar o evento de compra entre VTEX e GA4 via Order Hook da VTEX com o Measurement Protocol. Guia técnico passo a passo com código completo em Node.js / TypeScript.

Leonardo Pereira

Especialista VTEX · 11 de junho de 2026

O problema: por que o evento de compra client-side não é suficiente

Implementar o evento purchase do GA4 via JavaScript no browser é tentador pela simplicidade, mas tem uma falha estrutural: você não controla o que acontece depois que o usuário clica em "finalizar compra".

O usuário pode:

  • Fechar o browser antes de carregar a página de confirmação
  • Ter um adblocker que bloqueia a tag do GTM
  • Perder a conexão exatamente nesse momento
  • Estar usando um browser com ITP agressivo (Safari) que bloqueia cookies e scripts de terceiros

Resultado: entre 10% e 35% dos pedidos não aparecem no GA4, dependendo do perfil de dispositivos e localização do seu tráfego.

A solução definitiva é enviar o evento de compra do servidor, usando o Order Hook da VTEX + Measurement Protocol do GA4.


O que é o Order Hook da VTEX

O Order Hook é um webhook da VTEX que dispara automaticamente quando o status de um pedido muda — incluindo quando um pedido é confirmado.

Quando um cliente finaliza uma compra no VTEX, o sistema:

  1. Cria o pedido no OMS (Order Management System)
  2. Processa o pagamento
  3. Confirma o pedido (status: payment-approved ou order-accepted)
  4. Dispara o Order Hook → POST para uma URL configurada por você

O payload do webhook contém todos os dados do pedido: ID, itens, valor, cliente, forma de pagamento, frete, etc.


Arquitetura da solução

Cliente finaliza o pedido
           ↓
      VTEX OMS confirma
           ↓
   Order Hook dispara POST
   para seu endpoint
           ↓
  Seu servidor recebe o webhook
  e valida a assinatura
           ↓
  Envia evento "purchase" para GA4
  via Measurement Protocol
           ↓
  GA4 registra o pedido
  com 100% de confiabilidade

Você precisará de:

  • Um endpoint HTTPS para receber o webhook do VTEX
  • Acesso às configurações de Order Hook no VTEX Admin
  • Uma API Secret do GA4 (Measurement Protocol API Secret)
  • O Measurement ID da sua propriedade GA4 (G-XXXXXXXX)

Passo 1: Criar o endpoint para receber o webhook

Crie um endpoint que aceita POST requests. Pode ser uma VTEX IO Service App, uma função no Vercel/Cloudflare Workers, ou qualquer backend Node.js.

Exemplo com VTEX IO Service (TypeScript)

// node/routes/orderHook.ts
import { IOContext, ParamsContext } from "@vtex/api";

interface OrderHookPayload {
  Domain: string;
  OrderId: string;
  State: string;
  LastState: string;
  LastChange: string;
  Origin: {
    Account: string;
    Key: string;
  };
}

export async function orderHook(
  ctx: IOContext & ParamsContext,
  next: () => Promise<unknown>
) {
  const body: OrderHookPayload = ctx.body;

  // Só processa quando o pedido é confirmado
  if (body.State !== "payment-approved" && body.State !== "order-accepted") {
    ctx.status = 200;
    return next();
  }

  try {
    // Busca os detalhes completos do pedido
    const order = await ctx.clients.oms.order(body.OrderId);

    // Envia o evento para o GA4
    await sendPurchaseEventToGA4(order);

    ctx.status = 200;
  } catch (err) {
    // Loga o erro mas retorna 200 para o VTEX não fazer retry infinito
    console.error("Erro ao enviar purchase event:", err);
    ctx.status = 200;
  }

  return next();
}

Exemplo com Vercel Serverless Function (TypeScript)

// api/order-hook.ts
import type { NextApiRequest, NextApiResponse } from "next";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== "POST") {
    return res.status(405).end();
  }

  const { OrderId, State } = req.body;

  // Só processa pedidos confirmados
  if (State !== "payment-approved" && State !== "order-accepted") {
    return res.status(200).json({ ok: true });
  }

  try {
    // Busca dados completos do pedido via API VTEX
    const order = await fetchVTEXOrder(OrderId);
    await sendGA4PurchaseEvent(order);
  } catch (err) {
    console.error(err);
  }

  // Sempre retorna 200 para o VTEX não fazer retry
  return res.status(200).json({ ok: true });
}

Passo 2: Buscar os dados do pedido via API VTEX

O payload do Order Hook contém apenas o orderId. Para enviar um evento de compra completo ao GA4, você precisa buscar os detalhes do pedido na API de pedidos da VTEX.

async function fetchVTEXOrder(orderId: string) {
  const account = process.env.VTEX_ACCOUNT; // ex: "minha-loja"
  const appKey = process.env.VTEX_APP_KEY;
  const appToken = process.env.VTEX_APP_TOKEN;

  const response = await fetch(
    `https://${account}.vtexcommercestable.com.br/api/oms/pvt/orders/${orderId}`,
    {
      headers: {
        "X-VTEX-API-AppKey": appKey!,
        "X-VTEX-API-AppToken": appToken!,
        Accept: "application/json",
      },
    }
  );

  if (!response.ok) {
    throw new Error(`Falha ao buscar pedido ${orderId}: ${response.status}`);
  }

  return response.json();
}

Passo 3: Mapear o pedido VTEX para o evento GA4

A parte mais importante é mapear corretamente os campos do pedido VTEX para os parâmetros do evento purchase do GA4.

interface VTEXOrder {
  orderId: string;
  value: number;               // valor total em centavos
  totals: Array<{
    id: string;
    name: string;
    value: number;             // em centavos
  }>;
  items: Array<{
    id: string;
    name: string;
    price: number;             // em centavos
    sellingPrice: number;      // em centavos
    quantity: number;
    productCategories: { [key: string]: string };
    additionalInfo: {
      brandName: string;
    };
  }>;
  clientProfileData: {
    id: string;
    email: string;
  };
  paymentData: {
    transactions: Array<{
      payments: Array<{
        paymentSystemName: string;
      }>;
    }>;
  };
}

function mapVTEXOrderToGA4Event(order: VTEXOrder) {
  // VTEX armazena valores em centavos — divide por 100
  const toDecimal = (cents: number) => cents / 100;

  // Separa os totais
  const shippingTotal = order.totals.find((t) => t.id === "Shipping")?.value ?? 0;
  const taxTotal = order.totals.find((t) => t.id === "Tax")?.value ?? 0;
  const discountsTotal = Math.abs(
    order.totals.find((t) => t.id === "Discounts")?.value ?? 0
  );

  // Valor dos itens (sem frete e sem taxa)
  const itemsValue = toDecimal(order.value - shippingTotal - taxTotal);

  // Mapeia os itens
  const items = order.items.map((item, index) => ({
    item_id: item.id,
    item_name: item.name,
    item_brand: item.additionalInfo?.brandName ?? "",
    item_category: Object.values(item.productCategories ?? {})[0] ?? "",
    price: toDecimal(item.sellingPrice),
    quantity: item.quantity,
    index,
  }));

  // Forma de pagamento (primeiro pagamento da primeira transação)
  const paymentType =
    order.paymentData?.transactions?.[0]?.payments?.[0]?.paymentSystemName ?? "";

  return {
    transaction_id: order.orderId,
    value: itemsValue,
    currency: "BRL",
    shipping: toDecimal(shippingTotal),
    tax: toDecimal(taxTotal),
    coupon: "",  // adicione se quiser rastrear cupons
    payment_type: paymentType,
    items,
  };
}

Passo 4: Enviar o evento via Measurement Protocol

Com o evento mapeado, envie para o GA4 via Measurement Protocol.

async function sendGA4PurchaseEvent(order: VTEXOrder): Promise<void> {
  const measurementId = process.env.GA4_MEASUREMENT_ID; // G-XXXXXXXX
  const apiSecret = process.env.GA4_API_SECRET;          // obtido no GA4 Admin

  const purchaseEvent = mapVTEXOrderToGA4Event(order);

  const payload = {
    // client_id é obrigatório — use o ID do cliente VTEX ou um UUID fixo por usuário
    // O ideal é recuperar o _ga cookie do browser, mas para server-side use o userId
    client_id: order.clientProfileData.id,
    // user_id opcional mas recomendado para cross-device
    user_id: order.clientProfileData.id,
    events: [
      {
        name: "purchase",
        params: {
          ...purchaseEvent,
          // Envia um session_id se disponível para associar ao session correto
          // session_id: "...",
        },
      },
    ],
  };

  const url = new URL("https://www.google-analytics.com/mp/collect");
  url.searchParams.set("measurement_id", measurementId!);
  url.searchParams.set("api_secret", apiSecret!);

  const response = await fetch(url.toString(), {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });

  // Measurement Protocol retorna 204 No Content em sucesso
  if (response.status !== 204) {
    const body = await response.text();
    throw new Error(`GA4 MP retornou ${response.status}: ${body}`);
  }

  console.log(`✅ Purchase event enviado para GA4: ${order.orderId}`);
}

Passo 5: Configurar o Order Hook no VTEX Admin

Com o endpoint pronto, configure o Order Hook no painel VTEX.

Via VTEX Admin (UI)

  1. Acesse VTEX Admin → Configurações da Loja → Gerenciamento de Pedidos
  2. Role até Configurações de Integração → Order Hook
  3. Adicione a URL do seu endpoint
  4. Selecione os eventos que devem disparar o hook (recomendado: payment-approved)

Via API REST

curl -X POST \
  "https://ACCOUNT.vtexcommercestable.com.br/api/orders/hook/config" \
  -H "X-VTEX-API-AppKey: SEU_APP_KEY" \
  -H "X-VTEX-API-AppToken: SEU_APP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": {
      "status": ["payment-approved"]
    },
    "hook": {
      "headers": {
        "key": "Authorization",
        "value": "Bearer SEU_TOKEN_SECRETO"
      },
      "url": "https://seu-endpoint.com/api/order-hook"
    }
  }'

Segurança: configure um token secreto no header da request e valide-o no seu endpoint para garantir que apenas o VTEX pode acionar o webhook.


Passo 6: Obter o GA4 API Secret

Para usar o Measurement Protocol, você precisa de uma API Secret gerada no GA4.

  1. Acesse GA4 Admin → Streams de dados → seu stream
  2. Role até Segredos de API para o Measurement Protocol
  3. Clique em Criar e copie o valor gerado
  4. Adicione como variável de ambiente GA4_API_SECRET

Prevenindo eventos duplicados

Um risco real: se o Order Hook for disparado mais de uma vez para o mesmo pedido (timeout, retry), você terá o purchase duplicado no GA4.

Solução 1: GA4 desduplicação nativa

O GA4 desduplicará automaticamente eventos purchase com o mesmo transaction_id dentro de uma janela de 24 horas. Isso é garantido pela spec do GA4.

Solução 2: Cache de transaction_ids processados

Para garantia extra, mantenha um cache dos IDs já processados:

import { createClient } from "redis";

const redis = createClient({ url: process.env.REDIS_URL });

async function shouldProcessOrder(orderId: string): Promise<boolean> {
  const key = `ga4_purchase_sent:${orderId}`;
  const exists = await redis.exists(key);

  if (exists) {
    console.log(`Pedido ${orderId} já foi enviado ao GA4, ignorando.`);
    return false;
  }

  // Marca como processado com TTL de 7 dias
  await redis.setEx(key, 604800, "1");
  return true;
}

Lidando com o client_id

O maior desafio do Measurement Protocol é o client_id — o identificador do usuário que o GA4 usa para associar o evento à sessão correta.

Em implementações client-side, o client_id vem do cookie _ga. No server-side, você precisa recuperá-lo.

Estratégia 1: Passar o _ga cookie via checkout

No checkout customizado, capture o valor do cookie _ga e salve no custom data do pedido VTEX:

// No seu script de checkout
const gaClientId = document.cookie
  .split("; ")
  .find((c) => c.startsWith("_ga="))
  ?.split("=")[1]
  ?.split(".").slice(2).join(".");  // formato: GA1.1.XXXXXXXXXX.XXXXXXXXXX → XXXXXXXXXX.XXXXXXXXXX

if (gaClientId) {
  // Salva no customData do pedido via orderForm API
  vtexjs.checkout.sendAttachment("customData", {
    customApps: [{
      id: "ga-tracking",
      fields: { clientId: gaClientId }
    }]
  });
}

No Order Hook, recupere o client_id do customData:

const gaClientId =
  order.customData?.customApps?.find((a: { id: string }) => a.id === "ga-tracking")
    ?.fields?.clientId ?? order.clientProfileData.id;

Estratégia 2: Usar o email hasheado como user_id

Se não conseguir o client_id, use o email do cliente como user_id. Isso não vai associar ao session mas garante cross-device:

import { createHash } from "crypto";

const hashedEmail = createHash("sha256")
  .update(order.clientProfileData.email.toLowerCase())
  .digest("hex");

// No payload do Measurement Protocol
{
  client_id: order.clientProfileData.id,  // fallback: ID do cliente VTEX
  user_id: hashedEmail,
  events: [...]
}

Evitando conflito com o rastreamento client-side

Se você tem tanto o rastreamento client-side (via GTM) quanto o server-side (via Order Hook), precisa evitar que o evento purchase seja enviado em duplicidade.

Opção A: Desative o evento purchase no GTM

A mais simples: na tag GA4 Event do GTM que dispara o purchase, adicione uma condição de exceção que nunca seja verdadeira — efetivamente desativando-a — e confie 100% no server-side.

Opção B: Envie apenas do server-side, use client-side para os demais eventos

Esta é a arquitetura mais robusta:

EventoClient-side (GTM)Server-side (Order Hook)
view_item
add_to_cart
begin_checkout
add_payment_info
purchase

Validação e troubleshooting

Testando o Measurement Protocol

Use o endpoint de validação do GA4 antes de enviar para produção:

curl -X POST \
  "https://www.google-analytics.com/debug/mp/collect?measurement_id=G-XXXXXXXX&api_secret=SEU_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "123456.7890",
    "events": [{
      "name": "purchase",
      "params": {
        "transaction_id": "PEDIDO-TESTE-001",
        "value": 199.90,
        "currency": "BRL",
        "items": [{
          "item_id": "SKU-001",
          "item_name": "Produto Teste",
          "price": 199.90,
          "quantity": 1
        }]
      }
    }]
  }'

O endpoint /debug/ retorna erros de validação sem enviar dados reais ao GA4.

Problemas comuns e soluções

ProblemaCausa provávelSolução
GA4 não mostra o eventoapi_secret inválidoGere um novo secret no GA4 Admin
Receita errada no GA4Valor em centavos não dividido por 100Confirme toDecimal()
Eventos duplicadosOrder Hook disparando mais de uma vezImplemente cache Redis
client_id inválidoFormato incorretoUse string não vazia
204 mas evento não apareceDelay do GA4 (~1-2 min)Aguarde e verifique DebugView
Order Hook não disparaURL incorreta ou timeoutVerifique logs no VTEX Admin

Estrutura completa de produção

Aqui está a função completa pronta para produção:

// lib/ga4-order-hook.ts

const GA4_ENDPOINT = "https://www.google-analytics.com/mp/collect";
const GA4_DEBUG_ENDPOINT = "https://www.google-analytics.com/debug/mp/collect";

export async function processOrderHook(orderId: string): Promise<void> {
  const [order] = await Promise.all([
    fetchVTEXOrder(orderId),
  ]);

  // Extrai o client_id do customData do pedido
  const gaClientId =
    order.customData?.customApps?.find(
      (a: { id: string }) => a.id === "ga-tracking"
    )?.fields?.clientId ?? order.clientProfileData.id;

  const purchaseParams = mapVTEXOrderToGA4Event(order);

  const payload = {
    client_id: gaClientId,
    user_id: order.clientProfileData.id,
    timestamp_micros: Date.now() * 1000,  // precisão em microssegundos
    non_personalized_ads: false,
    events: [
      {
        name: "purchase",
        params: {
          ...purchaseParams,
          engagement_time_msec: 100,
        },
      },
    ],
  };

  const url = new URL(
    process.env.GA4_DEBUG === "true" ? GA4_DEBUG_ENDPOINT : GA4_ENDPOINT
  );
  url.searchParams.set("measurement_id", process.env.GA4_MEASUREMENT_ID!);
  url.searchParams.set("api_secret", process.env.GA4_API_SECRET!);

  const response = await fetch(url.toString(), {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });

  if (process.env.GA4_DEBUG === "true") {
    const debug = await response.json();
    console.log("GA4 Debug:", JSON.stringify(debug, null, 2));
  }

  if (response.status !== 204 && response.status !== 200) {
    throw new Error(`GA4 MP error: ${response.status}`);
  }
}

Próximos passos

Com o evento de compra server-side implementado, você tem uma base sólida para:

  • Comparar receita GA4 vs. VTEX Admin diariamente para monitorar a qualidade dos dados
  • Configurar alertas automáticos quando a divergência ultrapassar 5%
  • Usar os dados de conversão para treinar campanhas de Smart Bidding no Google Ads com dados 100% confiáveis
  • Enviar o mesmo evento para o Meta Conversions API usando o mesmo Order Hook
  • Exportar para BigQuery e cruzar com dados de CRM para análise de LTV

Para ajuda com a implementação completa, fale com um especialista GA4 ou veja o guia completo de rastreamento de e-commerce.