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

# Verifying Webhook Signatures

> Authenticate every webhook delivery by validating its HMAC-SHA256 signature — with reference implementations in Python, JavaScript, Go, Java, and C#.

SpherePay signs every webhook delivery so you can confirm it genuinely came from SpherePay and was not tampered with in transit. SpherePay never sends an unsigned delivery. **Always verify the signature before processing a payload**, and reject anything that fails verification with a non-`2xx` response.

## Delivery headers

Every delivery attempt carries these headers:

| Header                    | Example                       | Description                                                                                                                            |
| ------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `Sphere-Signature`        | `5c8b4e2f9a...`               | Hex-encoded HMAC-SHA256 signature of this delivery.                                                                                    |
| `Sphere-Timestamp`        | `1786455602`                  | Unix timestamp (seconds) of when this attempt was sent. Signed as part of the HMAC input.                                              |
| `Sphere-Delivery-Id`      | `eventDelivery_2351653173...` | The event delivery this attempt belongs to. Use it to look up the delivery in the [Events API](/concepts/webhooks/events-and-replays). |
| `Sphere-Delivery-Attempt` | `1`                           | Attempt counter for this delivery, starting at `1`.                                                                                    |
| `Sphere-Delivery-Type`    | `original` or `replay`        | Whether this is the first attempt or a manual replay.                                                                                  |
| `Sphere-Replay-Reason`    | `manual`                      | Present on replays only.                                                                                                               |
| `User-Agent`              | `Sphere-Webhooks/2.0`         | Constant.                                                                                                                              |
| `Content-Type`            | `application/json`            | Constant.                                                                                                                              |

Headers describe the *delivery attempt*; the body describes the *event*. On a replay, `Sphere-Timestamp` and `Sphere-Signature` are freshly generated — so the same verification code path handles originals and replays — while the body remains byte-for-byte identical to the original.

## How the signature is computed

The signature is an HMAC-SHA256 digest, hex-encoded:

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

* `secret` — the endpoint's signing secret (`whsec_...`), returned once when you [created the endpoint](/concepts/webhooks/managing-endpoints#register-an-endpoint).
* `timestamp` — the value of the `Sphere-Timestamp` header.
* `rawBody` — the raw HTTP request body bytes, exactly as transmitted.

To verify: compute the same digest yourself and compare it to `Sphere-Signature` using a constant-time comparison.

<Warning>
  Verify against the **raw request body**, not a re-serialized version. Parsing the JSON and re-encoding it can reorder keys or change whitespace, which changes the bytes and breaks verification. Most web frameworks require explicit configuration to expose the raw body — capture it before any JSON middleware runs.
</Warning>

### Guard against replay attacks

To prevent captured deliveries from being re-sent by a third party, reject deliveries whose `Sphere-Timestamp` is more than **5 minutes** old. Manual replays get a fresh timestamp and signature, so legitimate replays always pass this check.

## Reference implementations

Each snippet takes the raw body bytes, the two headers, and your endpoint secret, and returns whether the delivery is authentic.

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

## When verification fails

Respond with a non-`2xx` status code (for example `401`) and do not process the payload. The delivery is recorded as `failed` on SpherePay's side, giving you an audit trail in the [Events API](/concepts/webhooks/events-and-replays).

<Note>
  If you registered the same receiver URL under multiple SpherePay applications, each application's endpoint has its own secret. Use the `applicationId` in the payload to select the correct secret — see [Event payloads](/concepts/webhooks/event-payloads#the-applicationid-field). If you cannot parse the body before verifying, try each of your known secrets.
</Note>
