> ## 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.

# Verificación de Firmas de Webhook

> Autentica cada entrega de webhook validando su firma HMAC-SHA256 — con implementaciones de referencia en Python, JavaScript, Go, Java y C#.

SpherePay firma cada entrega de webhook para que puedas confirmar que genuinamente provino de SpherePay y que no fue alterada en tránsito. SpherePay nunca envía una entrega sin firmar. **Siempre verifica la firma antes de procesar un payload**, y rechaza cualquier cosa que falle la verificación con una respuesta no `2xx`.

## Encabezados de entrega

Cada intento de entrega lleva estos encabezados:

| Encabezado                | Ejemplo                       | Descripción                                                                                                                                          |
| ------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Sphere-Signature`        | `5c8b4e2f9a...`               | Firma HMAC-SHA256 de esta entrega, codificada en hexadecimal.                                                                                        |
| `Sphere-Timestamp`        | `1786455602`                  | Marca de tiempo Unix (segundos) de cuándo se envió este intento. Firmada como parte de la entrada del HMAC.                                          |
| `Sphere-Delivery-Id`      | `eventDelivery_2351653173...` | La entrega de evento a la que pertenece este intento. Úsalo para buscar la entrega en la [API de Eventos](/es/concepts/webhooks/events-and-replays). |
| `Sphere-Delivery-Attempt` | `1`                           | Contador de intentos para esta entrega, comenzando en `1`.                                                                                           |
| `Sphere-Delivery-Type`    | `original` o `replay`         | Indica si este es el primer intento o un reenvío manual.                                                                                             |
| `Sphere-Replay-Reason`    | `manual`                      | Presente solo en los reenvíos.                                                                                                                       |
| `User-Agent`              | `Sphere-Webhooks/2.0`         | Constante.                                                                                                                                           |
| `Content-Type`            | `application/json`            | Constante.                                                                                                                                           |

Los encabezados describen el *intento de entrega*; el cuerpo describe el *evento*. En un reenvío, `Sphere-Timestamp` y `Sphere-Signature` se generan de nuevo — de modo que la misma ruta de código de verificación maneja originales y reenvíos — mientras que el cuerpo permanece idéntico byte por byte al original.

## Cómo se calcula la firma

La firma es un digest HMAC-SHA256, codificado en hexadecimal:

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

* `secret` — el secreto de firma del endpoint (`whsec_...`), devuelto una vez cuando [creaste el endpoint](/es/concepts/webhooks/managing-endpoints).
* `timestamp` — el valor del encabezado `Sphere-Timestamp`.
* `rawBody` — los bytes crudos del cuerpo de la solicitud HTTP, exactamente como se transmitieron.

Para verificar: calcula el mismo digest tú mismo y compáralo con `Sphere-Signature` usando una comparación de tiempo constante.

<Warning>
  Verifica contra el **cuerpo crudo de la solicitud**, no contra una versión re-serializada. Analizar el JSON y volver a codificarlo puede reordenar las claves o cambiar los espacios en blanco, lo que cambia los bytes y rompe la verificación. La mayoría de los frameworks web requieren configuración explícita para exponer el cuerpo crudo — captúralo antes de que se ejecute cualquier middleware de JSON.
</Warning>

### Protégete contra ataques de repetición

Para evitar que entregas capturadas sean reenviadas por un tercero, rechaza las entregas cuyo `Sphere-Timestamp` tenga más de **5 minutos** de antigüedad. Los reenvíos manuales reciben una marca de tiempo y una firma nuevas, por lo que los reenvíos legítimos siempre pasan esta verificación.

## Implementaciones de referencia

Cada fragmento de código toma los bytes crudos del cuerpo, los dos encabezados y el secreto de tu endpoint, y devuelve si la entrega es 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>

## Cuando la verificación falla

Responde con un código de estado no `2xx` (por ejemplo `401`) y no proceses el payload. La entrega se registra como `failed` del lado de SpherePay, dándote un registro de auditoría en la [API de Eventos](/es/concepts/webhooks/events-and-replays).

<Note>
  Si registraste la misma URL receptora bajo múltiples aplicaciones de SpherePay, el endpoint de cada aplicación tiene su propio secreto. Usa el `applicationId` del payload para seleccionar el secreto correcto — consulta [Payloads de eventos](/es/concepts/webhooks/event-payloads). Si no puedes analizar el cuerpo antes de verificar, prueba cada uno de tus secretos conocidos.
</Note>
