> ## Documentation Index
> Fetch the complete documentation index at: https://docs.appreval.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks da Reval: receba os eventos de assinatura no seu servidor

> Assine suas URLs aos 18 eventos: os 15 do ciclo de vida das assinaturas e os 3 de checkouts e carrinhos abandonados. Assinatura HMAC, novas tentativas com espera crescente, registro de entregas e envio de teste pelo admin.

Um webhook é a Reval avisando o seu servidor de que algo aconteceu: um ciclo foi cobrado, uma cobrança falhou, um cliente pausou ou cancelou. É o complemento da [API](https://docs.appreval.com/pt/desenvolvedores/api.md): a API você consulta, os webhooks avisam você. Tudo se configura em **Reval → Integrações → Webhooks**.

> [!NOTE]
>
> Os webhooks são *best-effort* em relação ao fluxo principal: um endpoint fora do ar nunca segura uma cobrança nem a criação de um pedido. A Reval enfileira o envio, tenta à parte e tenta de novo se falhar.

***

## Configurar um endpoint

1. Em **Reval → Integrações → Webhooks**, cole a **URL** do seu servidor (precisa ser `https`) e escolha a quais **eventos** ela assina.
2. Salve. A Reval gera um **segredo de assinatura** (`whsec_…`) para esse endpoint: veja-o com «Mostrar» e copie para o seu servidor.
3. Use **«Enviar teste»**: o seu endpoint recebe um evento `test` e o admin mostra o resultado (código HTTP ou erro).

Você pode ter até **10 endpoints** por loja, cada um com a sua lista de eventos e o seu segredo. **Pausar** um endpoint para de enviar eventos sem apagar a configuração nem o histórico; **Excluir** remove os dois.

> [!WARNING]
>
> Os payloads incluem dados pessoais do assinante (email, nome, cidade). Por isso só se aceitam URLs `https`, e a Reval recusa endereços internos ou privados (localhost, IPs de rede local, hosts sem domínio). O nome é verificado de novo a cada envio, depois de resolver o DNS: se passar a apontar para um endereço privado, a entrega é descartada com erro.

***

## Os eventos

São os mesmos 15 eventos de assinatura que a Reval envia ao Klaviyo e ao Shopify Flow, com os mesmos dados, mais os 3 de [checkouts e carrinhos abandonados](https://docs.appreval.com/pt/lojista/checkouts-abandonados.md), que ainda não têm assinatura e saem só para o Klaviyo e para webhooks. O `topic` de cada envio é `subscription.<evento>`, `checkout.<evento>` ou `cart.<evento>`:

| Topic | Quando |
| - | - |
| `subscription.created` | O cliente concluiu o checkout e o Mercado Pago autorizou a assinatura |
| `subscription.charged` | A cobrança de um ciclo foi aprovada (o primeiro ou uma renovação) |
| `subscription.payment_failed` | O Mercado Pago recusou a cobrança de um ciclo |
| `subscription.upcoming_renewal` | N dias antes da próxima cobrança (conforme a sua configuração de lembrete) |
| `subscription.item_out_of_stock` | Um pedido do ciclo foi retido por falta de estoque |
| `subscription.item_back_in_stock` | Um pedido retido por estoque finalmente foi criado |
| `subscription.item_variant_deleted` | Um produto da assinatura foi removido da Shopify |
| `subscription.paused` | Pausa pelo portal, pelo admin ou pelo Mercado Pago |
| `subscription.resumed` | Uma assinatura pausada foi retomada |
| `subscription.cancelled` | Cancelamento pelo portal, pelo admin ou pelo Mercado Pago |
| `subscription.completed` | A assinatura atingiu o máximo de ciclos do plano |
| `subscription.payment_method_updated` | O cliente trocou o cartão pelo portal |
| `subscription.surprise_applied` | Uma recompensa de Surpresas foi entregue |
| `subscription.cycle_skipped` | O cliente pulou a próxima entrega |
| `subscription.auto_merged` | O ciclo saiu em um único pedido junto com outra assinatura do cliente |
| `checkout.started` | Um cliente informou o e-mail no checkout da Reval (checkouts abandonados). Sem `subscription_id`: ainda não há assinatura |
| `cart.started` | Um cliente identificável (logado ou e-mail conhecido) encheu o carrinho de assinaturas. Sem `subscription_id` |
| `cart.emptied` | Esse cliente esvaziou o carrinho. O filtro que interrompe o fluxo de recuperação do carrinho |
| `test` | «Enviar teste» pelo admin |

***

## O envio

Cada entrega é um `POST` para a sua URL com um corpo JSON e estes headers:

| Header | Conteúdo |
| - | - |
| `Content-Type` | `application/json` |
| `X-Reval-Topic` | O topic, por exemplo `subscription.cancelled` |
| `X-Reval-Delivery` | ID da entrega. **Mantém-se em cada nova tentativa e em cada reenvio**: use-o para não processar a mesma coisa duas vezes |
| `X-Reval-Shop` | O domínio `tu-tienda.myshopify.com` |
| `User-Agent` | `Reval-Webhooks/1.0 (+https://docs.appreval.com)`, caso você filtre por agente |
| `X-Reval-Signature` | A assinatura, veja abaixo |

```json theme={null}
{
  "id": "cmg…",
  "topic": "subscription.cancelled",
  "created_at": "2026-10-16T14:03:11.000Z",
  "shop": "tu-tienda.myshopify.com",
  "subscription_id": "cmg…",
  "data": {
    "subscription_id": "cmg…",
    "status": "cancelled",
    "customer_email": "ana@example.com",
    "customer_first_name": "Ana",
    "product_name": "Café especial 500 g",
    "product_names": "Café especial 500 g",
    "amount": 185,
    "amount_formatted": "R$ 185,00",
    "currency": "BRL",
    "frequency": 1,
    "frequency_type": "months",
    "frequency_label": "Mensal",
    "cycle_number": 4,
    "next_payment_date": null,
    "portal_subscription_url": "https://sua-loja.com/apps/subscriptions/portal?sub=cmg…",
    "source": "customer",
    "reason": "Vou viajar"
  }
}
```

`data` traz **as mesmas propriedades que o Klaviyo e o Shopify Flow recebem**: identidade do cliente, produto, valores (brutos e formatados), plano, número do ciclo, datas, endereço de entrega e links para o portal, além das propriedades próprias de cada evento (`reason`, `source`, `detail`, `held_product_names`, `merged_with_subscription_ids` etc.).

## As propriedades de `data`

As propriedades são as mesmas para todos os eventos, mais as próprias de cada um. Vão em `snake_case`; os valores brutos são números e os `_formatted` são strings prontas para um email. Os rótulos (`frequency_label`, `next_payment_date_label`, `reason`, `reason_text`) seguem o **idioma configurado da loja** (espanhol ou português); para condições use as propriedades brutas (`status`, `frequency_type`, `source`, datas ISO) e `reason_key`, que não mudam com o idioma.

| Propriedade | O que é |
| - | - |
| `subscription_id` | ID da assinatura na Reval |
| `status` | Status: `pending`, `active`, `paused`, `cancelled` ou `completed` |
| `customer_email` | Email do assinante (a identidade da assinatura) |
| `customer_first_name / customer_last_name` | Nome e sobrenome, do endereço de entrega |
| `product_variant` | GID da Shopify da variante principal |
| `product_name / product_variant_title / product_image_url` | Nome, título da variante e imagem do produto principal |
| `product_names` | Todos os produtos da assinatura, separados por vírgula |
| `quantity / item_count` | Quantidade do produto principal e número de linhas |
| `amount / amount_formatted` | Valor recorrente atual, bruto e formatado na moeda da loja |
| `next_charge_amount / next_charge_amount_formatted` | O que o próximo ciclo cobra (difere de `amount` enquanto vale um desconto de primeiro pedido) |
| `currency` | Código da moeda: `ARS`, `CLP`, `MXN`, `BRL`, `COP`… |
| `total_spent / total_spent_formatted` | Gasto acumulado nesta assinatura |
| `shipping_amount / shipping_title` | Frete fixado na assinatura: valor e nome do método |
| `frequency / frequency_type / frequency_label` | Cadência: número, unidade (`days`, `weeks`, `months`) e rótulo («Mensual») |
| `plan_name / discount_percent / first_order_discount_percent` | Plano e seus descontos |
| `cycle_number / is_first_cycle` | Ciclos cobrados até agora e se é o primeiro |
| `subscription_created_at / subscription_age_days` | Data de criação e idade em dias |
| `last_payment_date / next_payment_date / next_payment_date_label / days_until_next_charge` | Última cobrança, próxima cobrança (ISO e formatada) e dias restantes |
| `shipping_city / shipping_province / shipping_country` | Destino da entrega |
| `portal_url / portal_subscription_url` | Link para o portal do cliente e para esta assinatura dentro dele |

E, além disso, conforme o evento:

| Evento | Propriedades próprias |
| - | - |
| `subscription.created` | `has_one_time` (se o checkout incluiu uma compra avulsa), `reactivated_from` (id da assinatura anterior, ao reativar uma cancelada), `external_ref` (a referência do link de checkout que a criou — veja `checkoutLinkCreate`; null se não veio de um link) |
| `subscription.charged` | `order_type` (`first` ou `renewal`), `charged_amount` |
| `subscription.payment_failed` | `detail` (código bruto do Mercado Pago), `reason_text` (motivo em palavras), `amount` |
| `subscription.upcoming_renewal` | `days_before` |
| `subscription.item_out_of_stock` | `held_variants`, `held_product_names`, `held_count`, `inventory_policy` |
| `subscription.item_back_in_stock` | `shopify_order_id` |
| `subscription.item_variant_deleted` | `gone_variants`, `gone_count`, `cycle_skipped` (se o ciclo inteiro foi pulado) |
| `subscription.paused / resumed / cancelled` | `source` (`customer`, `merchant`, `mp`, `api`), `reason` (o motivo escolhido, no idioma da loja; se um cancelamento por API trouxer `note`, fica «Motivo: nota»), `reason_key` (o mesmo motivo como está salvo, sempre em espanhol, estável para condições) |
| `subscription.completed` | `max_cycles` |
| `subscription.payment_method_updated` | — |
| `subscription.surprise_applied` | `surprise_type` (`discount` ou `gift`), `surprise_rule_id`; com `discount_amount` ou com `surprise_rule_name` e `gift_count` |
| `subscription.cycle_skipped` | `source`, `skipped_date`, `next_charge_date` |
| `subscription.auto_merged` | `merged_with_subscription_ids` (separados por vírgula), `merged_count`, `shopify_order_id` |
| `checkout.started` | `checkout_id`, `checkout_kind` (`product` / `box` / `cart`), `customer_email`, `customer_first_name`, `customer_last_name`, `title`, `product_image_url`, `product_names`, `items_text`, `items` (`title`, `quantity`, `frequency_label`), `item_count`, `amount`, `amount_formatted`, `currency`, `shipping_city`, `shipping_province`, `shipping_country`, `recovery_url`, `checkout_started_at`. Sem as propriedades de assinatura |
| `cart.started` / `cart.emptied` | `checkout_id`, `checkout_kind` (`product` / `box` / `cart`), `customer_email`, `customer_first_name`, `customer_last_name`, `title`, `product_image_url`, `product_names`, `items_text`, `items` (`title`, `quantity`, `frequency_label`), `item_count`, `amount`, `amount_formatted`, `currency`, `shipping_city`, `shipping_province`, `shipping_country`, `recovery_url`, `checkout_started_at`. Sem as propriedades de assinatura |
| `test` | `test: true` e dados de exemplo reconhecíveis (`sub_test`, `test@example.com`) |

Responda **qualquer 2xx** o quanto antes — de preferência antes de processar — e processe em segundo plano. A Reval espera **10 segundos**; depois desiste e conta a tentativa como falha. Redirecionamentos não são seguidos.

***

## Verificar a assinatura

Cada envio vai assinado com o segredo do endpoint para você confirmar que foi a Reval que enviou e que nada foi alterado:

```
X-Reval-Signature: t=1760623391,v1=5f1c…e9a2
```

* `t` é a hora do envio em segundos Unix.
* `v1` é `HMAC-SHA256(segredo, "<t>.<corpo bruto>")` em hexadecimal.

Assine o **corpo bruto** exatamente como chegou (os bytes, não um JSON re-serializado), compare em tempo constante e rejeite assinaturas com mais de 5 minutos de diferença:

```js theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyReval(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1 ?? "", "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}
```

```python theme={null}
import hmac, hashlib, time

def verify_reval(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
    parts = dict(kv.split("=", 1) for kv in header.split(","))
    t = int(parts.get("t", "0"))
    if not t or abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

> [!TIP]
>
> Se você acha que o segredo vazou, exclua o endpoint e crie outro: ele recebe um segredo novo. Os eventos que já estavam na fila do endpoint excluído são descartados.

***

## Novas tentativas

Se o seu servidor responder algo que não seja 2xx, não responder em 10 segundos ou não puder ser resolvido, a Reval tenta de novo com espera crescente:

| Tentativa | Espera desde a anterior |
| - | - |
| 2 | 1 minuto |
| 3 | 10 minutos |
| 4 | 1 hora |
| 5 | 4 horas |
| 6 | 12 horas |
| 7 | 24 horas |

Depois da sétima tentativa a entrega fica **Falhou**. Ela não se perde: você a vê no registro e pode **reenviar** quando o seu servidor voltar.

Como as novas tentativas mantêm o mesmo `X-Reval-Delivery`, um endpoint que respondeu 2xx tarde (logo depois do timeout, por exemplo) pode receber o mesmo evento duas vezes. Guarde o ID e descarte os repetidos.

***

## Registro de entregas

Em cada endpoint, **«Ver entregas»** mostra as últimas 50: evento, data, quantidade de tentativas, status (Pendente, Em andamento, Entregue, Falhou), código HTTP da última resposta e o erro, se houver. Cada linha tem **«Reenviar»**, que manda exatamente o mesmo payload com o mesmo ID.

***

## Perguntas frequentes

**Eventos anteriores à criação do endpoint são enviados?** Não. Um endpoint recebe os eventos que acontecem enquanto ele existe e está ativo. Para o histórico existe a [API](https://docs.appreval.com/pt/desenvolvedores/api.md).

**Posso receber só cancelamentos?** Sim: ao criar ou editar o endpoint, marque só os eventos que interessam.

**O que acontece se eu pausar o endpoint?** Os eventos que ocorrerem enquanto ele estiver pausado não são enviados nem guardados para ele. Ao retomar, ele recebe os novos.

**Meu endpoint pode estar no Zapier, Make ou n8n?** Sim, qualquer URL `https` pública serve. Essas ferramentas costumam responder 2xx na hora, que é justamente o que a Reval espera.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.