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

# Verificação de Assinaturas de Webhook

> Autentique cada entrega de webhook validando sua assinatura HMAC-SHA256 — com implementações de referência em Python, JavaScript, Go, Java e C#.

O SpherePay assina cada entrega de webhook para que você possa confirmar que ela realmente veio do SpherePay e não foi adulterada em trânsito. O SpherePay nunca envia uma entrega sem assinatura. **Sempre verifique a assinatura antes de processar um payload** e rejeite qualquer coisa que falhe na verificação com uma resposta não-`2xx`.

## Cabeçalhos de entrega

Toda tentativa de entrega carrega estes cabeçalhos:

| Cabeçalho                 | Exemplo                       | Descrição                                                                                                                                            |
| ------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Sphere-Signature`        | `5c8b4e2f9a...`               | Assinatura HMAC-SHA256 desta entrega, codificada em hexadecimal.                                                                                     |
| `Sphere-Timestamp`        | `1786455602`                  | Timestamp Unix (segundos) de quando esta tentativa foi enviada. Assinado como parte da entrada do HMAC.                                              |
| `Sphere-Delivery-Id`      | `eventDelivery_2351653173...` | A entrega de evento à qual esta tentativa pertence. Use-o para localizar a entrega na [API de Eventos](/pt-BR/concepts/webhooks/events-and-replays). |
| `Sphere-Delivery-Attempt` | `1`                           | Contador de tentativas desta entrega, começando em `1`.                                                                                              |
| `Sphere-Delivery-Type`    | `original` ou `replay`        | Indica se esta é a primeira tentativa ou um replay manual.                                                                                           |
| `Sphere-Replay-Reason`    | `manual`                      | Presente apenas em replays.                                                                                                                          |
| `User-Agent`              | `Sphere-Webhooks/2.0`         | Constante.                                                                                                                                           |
| `Content-Type`            | `application/json`            | Constante.                                                                                                                                           |

Os cabeçalhos descrevem a *tentativa de entrega*; o corpo descreve o *evento*. Em um replay, `Sphere-Timestamp` e `Sphere-Signature` são gerados novamente — de forma que o mesmo caminho de código de verificação trata originais e replays — enquanto o corpo permanece idêntico byte a byte ao original.

## Como a assinatura é calculada

A assinatura é um digest HMAC-SHA256, codificado em hexadecimal:

```
signature = hex( HMAC-SHA256( secret, "{timestamp}.{rawBody}" ) )
```

* `secret` — o segredo de assinatura do endpoint (`whsec_...`), retornado uma única vez quando você [criou o endpoint](/pt-BR/concepts/webhooks/managing-endpoints).
* `timestamp` — o valor do cabeçalho `Sphere-Timestamp`.
* `rawBody` — os bytes brutos do corpo da requisição HTTP, exatamente como transmitidos.

Para verificar: calcule o mesmo digest você mesmo e compare-o com `Sphere-Signature` usando uma comparação de tempo constante.

<Warning>
  Verifique contra o **corpo bruto da requisição**, não contra uma versão reserializada. Fazer o parse do JSON e recodificá-lo pode reordenar chaves ou alterar espaços em branco, o que muda os bytes e quebra a verificação. A maioria dos frameworks web exige configuração explícita para expor o corpo bruto — capture-o antes de qualquer middleware de JSON ser executado.
</Warning>

### Proteja-se contra ataques de replay

Para impedir que entregas capturadas sejam reenviadas por terceiros, rejeite entregas cujo `Sphere-Timestamp` tenha mais de **5 minutos**. Replays manuais recebem um timestamp e uma assinatura novos, então replays legítimos sempre passam nesta verificação.

## Implementações de referência

Cada snippet recebe os bytes do corpo bruto, os dois cabeçalhos e o segredo do seu endpoint, e retorna se a entrega é autêntica.

<CodeGroup>
  ```python Python theme={"dark"}
  import hashlib
  import hmac
  import time

  TOLERANCE_SECONDS = 300  # 5 minutes


  def verify_sphere_signature(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
      # Reject stale deliveries (possible replay attack)
      if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
          return False

      signed_content = f"{timestamp}.".encode() + raw_body
      expected = hmac.new(secret.encode(), signed_content, hashlib.sha256).hexdigest()

      # Constant-time comparison
      return hmac.compare_digest(expected, signature)


  # Example (Flask): request.get_data() returns the raw body bytes
  # is_valid = verify_sphere_signature(
  #     raw_body=request.get_data(),
  #     timestamp=request.headers["Sphere-Timestamp"],
  #     signature=request.headers["Sphere-Signature"],
  #     secret=os.environ["SPHERE_WEBHOOK_SECRET"],
  # )
  ```

  ```javascript JavaScript theme={"dark"}
  const crypto = require("crypto");

  const TOLERANCE_SECONDS = 300; // 5 minutes

  function verifySphereSignature(rawBody, timestamp, signature, secret) {
    // Reject stale deliveries (possible replay attack)
    const age = Math.abs(Date.now() / 1000 - Number(timestamp));
    if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) {
      return false;
    }

    const signedContent = `${timestamp}.${rawBody}`;
    const expected = crypto
      .createHmac("sha256", secret)
      .update(signedContent)
      .digest("hex");

    // Constant-time comparison
    const expectedBuf = Buffer.from(expected, "hex");
    const receivedBuf = Buffer.from(signature, "hex");
    if (expectedBuf.length !== receivedBuf.length) {
      return false;
    }
    return crypto.timingSafeEqual(expectedBuf, receivedBuf);
  }

  // Example (Express): capture the raw body before JSON parsing
  // app.use(express.json({ verify: (req, res, buf) => (req.rawBody = buf) }));
  //
  // const isValid = verifySphereSignature(
  //   req.rawBody,
  //   req.header("Sphere-Timestamp"),
  //   req.header("Sphere-Signature"),
  //   process.env.SPHERE_WEBHOOK_SECRET,
  // );
  ```

  ```go Go theme={"dark"}
  package main

  import (
  	"crypto/hmac"
  	"crypto/sha256"
  	"encoding/hex"
  	"fmt"
  	"math"
  	"strconv"
  	"time"
  )

  const toleranceSeconds = 300 // 5 minutes

  func verifySphereSignature(rawBody []byte, timestamp, signature, secret string) bool {
  	// Reject stale deliveries (possible replay attack)
  	ts, err := strconv.ParseInt(timestamp, 10, 64)
  	if err != nil || math.Abs(float64(time.Now().Unix()-ts)) > toleranceSeconds {
  		return false
  	}

  	mac := hmac.New(sha256.New, []byte(secret))
  	fmt.Fprintf(mac, "%s.", timestamp)
  	mac.Write(rawBody)
  	expected := hex.EncodeToString(mac.Sum(nil))

  	// Constant-time comparison
  	return hmac.Equal([]byte(expected), []byte(signature))
  }

  // Example (net/http): io.ReadAll(r.Body) returns the raw body bytes
  // isValid := verifySphereSignature(
  // 	rawBody,
  // 	r.Header.Get("Sphere-Timestamp"),
  // 	r.Header.Get("Sphere-Signature"),
  // 	os.Getenv("SPHERE_WEBHOOK_SECRET"),
  // )
  ```

  ```java Java theme={"dark"}
  import java.nio.charset.StandardCharsets;
  import java.security.MessageDigest;
  import java.util.HexFormat;
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;

  public final class SphereWebhookVerifier {

      private static final long TOLERANCE_SECONDS = 300; // 5 minutes

      public static boolean verifySphereSignature(
              byte[] rawBody, String timestamp, String signature, String secret)
              throws Exception {
          // Reject stale deliveries (possible replay attack)
          long ts = Long.parseLong(timestamp);
          if (Math.abs(System.currentTimeMillis() / 1000 - ts) > TOLERANCE_SECONDS) {
              return false;
          }

          Mac mac = Mac.getInstance("HmacSHA256");
          mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
          mac.update((timestamp + ".").getBytes(StandardCharsets.UTF_8));
          mac.update(rawBody);
          String expected = HexFormat.of().formatHex(mac.doFinal());

          // Constant-time comparison
          return MessageDigest.isEqual(
                  expected.getBytes(StandardCharsets.UTF_8),
                  signature.toLowerCase().getBytes(StandardCharsets.UTF_8));
      }

      // Example (Spring): bind the raw bytes before any JSON deserialization
      // @PostMapping("/sphere-webhook")
      // public ResponseEntity<Void> handle(@RequestBody byte[] rawBody,
      //         @RequestHeader("Sphere-Timestamp") String timestamp,
      //         @RequestHeader("Sphere-Signature") String signature) { ... }
  }
  ```

  ```csharp C# theme={"dark"}
  using System.Security.Cryptography;
  using System.Text;

  public static class SphereWebhookVerifier
  {
      private const int ToleranceSeconds = 300; // 5 minutes

      public static bool VerifySphereSignature(
          byte[] rawBody, string timestamp, string signature, string secret)
      {
          // Reject stale deliveries (possible replay attack)
          if (!long.TryParse(timestamp, out var ts) ||
              Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > ToleranceSeconds)
          {
              return false;
          }

          using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
          var prefix = Encoding.UTF8.GetBytes($"{timestamp}.");
          var signedContent = new byte[prefix.Length + rawBody.Length];
          prefix.CopyTo(signedContent, 0);
          rawBody.CopyTo(signedContent, prefix.Length);

          var expected = Convert.ToHexString(hmac.ComputeHash(signedContent)).ToLowerInvariant();

          // Constant-time comparison
          return CryptographicOperations.FixedTimeEquals(
              Encoding.UTF8.GetBytes(expected),
              Encoding.UTF8.GetBytes(signature.ToLowerInvariant()));
      }

      // Example (ASP.NET Core): read the raw body before model binding runs
      // app.MapPost("/sphere-webhook", async (HttpRequest req) =>
      // {
      //     using var ms = new MemoryStream();
      //     await req.Body.CopyToAsync(ms);
      //     var isValid = SphereWebhookVerifier.VerifySphereSignature(
      //         ms.ToArray(),
      //         req.Headers["Sphere-Timestamp"]!,
      //         req.Headers["Sphere-Signature"]!,
      //         Environment.GetEnvironmentVariable("SPHERE_WEBHOOK_SECRET")!);
      //     return isValid ? Results.Ok() : Results.Unauthorized();
      // });
  }
  ```
</CodeGroup>

## Quando a verificação falha

Responda com um código de status não-`2xx` (por exemplo `401`) e não processe o payload. A entrega é registrada como `failed` do lado do SpherePay, dando a você uma trilha de auditoria na [API de Eventos](/pt-BR/concepts/webhooks/events-and-replays).

<Note>
  Se você registrou a mesma URL receptora em múltiplas aplicações do SpherePay, o endpoint de cada aplicação tem seu próprio segredo. Use o `applicationId` do payload para selecionar o segredo correto — consulte [Payloads de eventos](/pt-BR/concepts/webhooks/event-payloads). Se não puder fazer o parse do corpo antes de verificar, tente cada um dos seus segredos conhecidos.
</Note>
