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

# Verificar a assinatura

> Toda entrega é assinada no padrão Standard Webhooks, com HMAC-SHA256 e o segredo whsec_ do webhook. Confira antes de confiar no corpo.

Qualquer um pode mandar um `POST` para o seu endereço. A assinatura prova que a entrega saiu da Westhetics, para este webhook, e não foi alterada no caminho. A Westhetics segue o [Standard Webhooks](https://www.standardwebhooks.com), o mesmo padrão de outros serviços: dá para usar as bibliotecas prontas.

## Os três cabeçalhos

| Cabeçalho | O quê |
| - | - |
| `webhook-id` | o id do evento; o mesmo em toda nova tentativa |
| `webhook-timestamp` | quando esta tentativa saiu, em segundos desde 1970 (UTC) |
| `webhook-signature` | `v1,` seguido da assinatura em base64; durante a troca de segredo, duas, separadas por espaço |

A assinatura é o HMAC-SHA256 de `{webhook-id}.{webhook-timestamp}.{corpo cru}`, com a chave que vem depois do `whsec_` do segredo, decodificada de base64.

> [!WARNING]
>
> Use o corpo **cru**, como chegou, byte a byte. Se o seu framework já interpretou o JSON e você
> serializar de novo, a assinatura não confere.

## Com a biblioteca do padrão

As bibliotecas aceitam o segredo com o prefixo `whsec_`, conferem a assinatura em tempo constante e recusam entrega com mais de 5 minutos de diferença no relógio.

```javascript Node (standardwebhooks) theme={null}
// npm install standardwebhooks
import express from 'express'
import { Webhook } from 'standardwebhooks'

const wh = new Webhook(process.env.WESTHETICS_WEBHOOK_SECRET) // "whsec_…"
const app = express()

app.post('/webhooks/westhetics', express.raw({ type: 'application/json' }), (req, res) => {
  let event
  try {
    event = wh.verify(req.body, req.headers) // devolve o evento já interpretado
  } catch {
    return res.status(400).send('assinatura inválida')
  }
  queue.push(event) // trate fora da requisição
  res.status(204).end()
})
```

```python Python (standardwebhooks) theme={null}
# pip install standardwebhooks
import os
from flask import Flask, request
from standardwebhooks.webhooks import Webhook

wh = Webhook(os.environ["WESTHETICS_WEBHOOK_SECRET"])  # "whsec_…"
app = Flask(__name__)

@app.post("/webhooks/westhetics")
def westhetics():
    try:
        event = wh.verify(request.get_data(), dict(request.headers))
    except Exception:
        return "assinatura inválida", 400
    queue.put(event)  # trate fora da requisição
    return "", 204
```

```php PHP (standard-webhooks) theme={null}
<?php
// composer require standard-webhooks/standard-webhooks
$wh = new \StandardWebhooks\Webhook(getenv('WESTHETICS_WEBHOOK_SECRET'));

$payload = file_get_contents('php://input');
$headers = array_change_key_case(getallheaders(), CASE_LOWER);

try {
    $event = $wh->verify($payload, $headers);
} catch (\Exception $e) {
    http_response_code(400);
    exit;
}
http_response_code(204);
```

```go Go (standard-webhooks) theme={null}
import (
	"io"
	"net/http"
	"os"

	standardwebhooks "github.com/standard-webhooks/standard-webhooks/libraries/go"
)

var wh, _ = standardwebhooks.NewWebhook(os.Getenv("WESTHETICS_WEBHOOK_SECRET"))

func westhetics(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil || wh.Verify(body, r.Header) != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	// enfileire o corpo e responda
	w.WriteHeader(http.StatusNoContent)
}
```

## Sem biblioteca

O mesmo cálculo, à mão. Compare em tempo constante e recuse diferença de relógio acima de 5 minutos.

```javascript Node theme={null}
import crypto from 'node:crypto'

export function verifyWesthetics(secret, headers, rawBody) {
  const id = headers['webhook-id']
  const timestamp = headers['webhook-timestamp']
  const signatures = headers['webhook-signature']
  if (!id || !timestamp || !signatures) return false
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false

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

  return signatures.split(' ').some((part) => {
    const [version, signature] = part.split(',')
    if (version !== 'v1' || !signature) return false
    const a = Buffer.from(signature)
    const b = Buffer.from(expected)
    return a.length === b.length && crypto.timingSafeEqual(a, b)
  })
}
```

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

def verify_westhetics(secret: str, headers: dict, raw_body: bytes) -> bool:
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signatures = headers.get("webhook-signature")
    if not (msg_id and timestamp and signatures):
        return False
    if abs(time.time() - int(timestamp)) > 300:
        return False

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

    for part in signatures.split(" "):
        version, _, signature = part.partition(",")
        if version == "v1" and hmac.compare_digest(signature, expected):
            return True
    return False
```

```php PHP theme={null}
<?php
function verify_westhetics(string $secret, array $headers, string $rawBody): bool
{
    $id = $headers['webhook-id'] ?? null;
    $timestamp = $headers['webhook-timestamp'] ?? null;
    $signatures = $headers['webhook-signature'] ?? null;
    if (!$id || !$timestamp || !$signatures) return false;
    if (abs(time() - (int) $timestamp) > 300) return false;

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

    foreach (explode(' ', $signatures) as $part) {
        [$version, $signature] = array_pad(explode(',', $part, 2), 2, '');
        if ($version === 'v1' && hash_equals($expected, $signature)) return true;
    }
    return false;
}
```

```go Go theme={null}
package westhetics

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"net/http"
	"strconv"
	"strings"
	"time"
)

func Verify(secret string, h http.Header, body []byte) bool {
	id, timestamp, signatures := h.Get("webhook-id"), h.Get("webhook-timestamp"), h.Get("webhook-signature")
	if id == "" || timestamp == "" || signatures == "" {
		return false
	}
	sent, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil {
		return false
	}
	if d := time.Now().Unix() - sent; d > 300 || d < -300 {
		return false
	}
	key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
	if err != nil {
		return false
	}
	mac := hmac.New(sha256.New, key)
	mac.Write([]byte(id + "." + timestamp + "."))
	mac.Write(body)
	expected := base64.StdEncoding.EncodeToString(mac.Sum(nil))
	for _, part := range strings.Split(signatures, " ") {
		version, signature, ok := strings.Cut(part, ",")
		if ok && version == "v1" && hmac.Equal([]byte(signature), []byte(expected)) {
			return true
		}
	}
	return false
}
```

## Depois de conferir

* **Ignore o repetido.** A mesma entrega pode chegar de novo (uma tentativa que você respondeu tarde, um reenvio manual). Guarde o `webhook-id` e descarte o que já tratou.
* **Não confie na ordem.** Ordene por `createdAt` do evento.
* **Responda rápido.** `2xx` em até 10 segundos; o trabalho vai para uma fila.

## O segredo

O segredo `whsec_…` aparece quando o webhook é criado. Depois, a gestora vê em **Revelar segredo**, na tela do app, e o próprio app pede por `GET /v1/webhooks/{id}/secret`. Cada revelação fica na trilha.

**Trocar o segredo** gera outro e mantém o anterior válido por **24 horas**. Nesse tempo, cada entrega leva as duas assinaturas em `webhook-signature`, separadas por espaço: o seu código aceita a entrega se uma das duas conferir. Troque o segredo no seu servidor dentro desse prazo.


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