# Webhooks (https://docs.rivoo.gg/webhooks)

Um webhook é uma URL sua que a Rivoo chama (`POST`, JSON) sempre que algo acontece na loja: pedido pago, reembolso, disputa, renovação de assinatura. É o jeito certo de liberar uma compra no seu sistema, em vez de ficar consultando a API.

## Cadastrar [#cadastrar]

<Steps>
<Step>

No painel, abra **Integrações › Webhooks** e clique em **Adicionar**. Ou pela API: [Criar webhook](/api/referencia/webhooks/criar-webhook).

</Step>
<Step>

Informe a URL (HTTPS, pública) e os eventos que ela recebe. Sem eventos marcados, ela recebe todos.

</Step>
<Step>

Guarde o **segredo de assinatura** (`whsec_...`). Ele aparece só nesta hora e serve para conferir que o pedido veio da Rivoo.

</Step>
</Steps>

## O que chega [#o-que-chega]

```http
POST /webhooks/rivoo HTTP/1.1
Content-Type: application/json
User-Agent: Rivoo-Webhooks/1
webhook-id: evt_mhexample0000event00001
webhook-timestamp: 1793567091
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

{
  "id": "evt_mhexample0000event00001",
  "type": "order.paid",
  "createdAt": "2026-10-31T21:04:51.000Z",
  "data": { "object": { "id": "ord_mhexample0000vip0000001", "source": "STOREFRONT", "totalCents": 2990, "paymentId": "pay_mhexample0000vip0000001" } }
}
```

`data.object` muda conforme o evento: veja a [lista de eventos](/webhooks/eventos/order.paid). Valores em dinheiro vêm em centavos (`2990` = R$ 29,90).

## Verificar a assinatura [#verificar-a-assinatura]

O formato segue o padrão aberto [Standard Webhooks](https://www.standardwebhooks.com). A assinatura é um HMAC-SHA256 de `webhook-id.webhook-timestamp.corpo`, com a chave sendo o segredo sem o prefixo `whsec_`, decodificado de base64. Compare com cada assinatura do header (separadas por espaço, cada uma com o prefixo `v1,`): durante a troca do segredo chegam duas.

<Callout type="warn">Use o corpo **cru**, exatamente como chegou, antes de qualquer parse de JSON. E recuse timestamps com mais de 5 minutos de diferença, para ninguém reenviar um pedido antigo.</Callout>

<Tabs items={['JavaScript', 'Python', 'PHP']}>
<Tab value="JavaScript">

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody: o corpo como string, antes do JSON.parse
export function verify(rawBody, headers, secret = process.env.RIVOO_WEBHOOK_SECRET) {
  const id = headers['webhook-id'];
  const ts = Number(headers['webhook-timestamp']);
  if (Math.abs(Date.now() / 1000 - ts) > 300) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest();

  return headers['webhook-signature'].split(' ').some((sig) => {
    const got = Buffer.from(sig.replace(/^v1,/, ''), 'base64');
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}
```

</Tab>
<Tab value="Python">

```python
import base64, hashlib, hmac, os, time

def verify(raw_body: bytes, headers, secret=os.environ["RIVOO_WEBHOOK_SECRET"]) -> bool:
    msg_id = headers["webhook-id"]
    ts = int(headers["webhook-timestamp"])
    if abs(time.time() - ts) > 300:
        return False

    key = base64.b64decode(secret.removeprefix("whsec_"))
    content = f"{msg_id}.{ts}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, content, hashlib.sha256).digest()).decode()

    return any(
        hmac.compare_digest(sig.removeprefix("v1,"), expected)
        for sig in headers["webhook-signature"].split(" ")
    )
```

</Tab>
<Tab value="PHP">

```php
<?php
function verify(string $rawBody, array $headers, ?string $secret = null): bool {
    $secret ??= getenv('RIVOO_WEBHOOK_SECRET');
    $id = $headers['webhook-id'];
    $ts = (int) $headers['webhook-timestamp'];
    if (abs(time() - $ts) > 300) return false;

    $key = base64_decode(preg_replace('/^whsec_/', '', $secret));
    $expected = base64_encode(hash_hmac('sha256', "$id.$ts.$rawBody", $key, true));

    foreach (explode(' ', $headers['webhook-signature']) as $sig) {
        if (hash_equals($expected, preg_replace('/^v1,/', '', $sig))) return true;
    }
    return false;
}
```

</Tab>
</Tabs>

## Responder [#responder]

Responda **2xx em até 10 segundos**. Faça o trabalho pesado depois (fila), não antes de responder. Redirecionamentos não são seguidos.

## Novas tentativas [#novas-tentativas]

Se a resposta não for 2xx (ou demorar mais de 10 s), a Rivoo tenta de novo: 8 tentativas no total, depois de 1 min, 5 min, 30 min, 2 h, 6 h, 12 h e 24 h. O `webhook-id` é o mesmo em todas: guarde os ids já processados e ignore repetidos.

Uma URL que falha seguido por mais de 3 dias é desativada, e você recebe um aviso no painel. Reative em **Integrações › Webhooks** depois de corrigir.

## Testar [#testar]

Use **Enviar teste** no webhook (ou [Testar webhook](/api/referencia/webhooks/testar-webhook)) para receber um evento de exemplo. Em [Envios do webhook](/api/referencia/webhooks/envios-do-webhook) você vê cada tentativa com o código de resposta.

## Trocar o segredo [#trocar-o-segredo]

[Trocar segredo do webhook](/api/referencia/webhooks/trocar-segredo-do-webhook) gera um segredo novo. Por 24 horas as mensagens chegam assinadas com os dois (duas assinaturas no header), para você atualizar sem perder eventos.
