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

# Validating events

> Protect your webhook-endpoint by validating incoming events.

<CardGroup>
  <Card title="Signature" icon="key">
    Validate signature in order to ensure authenticity.
  </Card>

  <Card title="Timestamp" icon="clock">
    Validate the timestamp in order to protect against replay attacks.
  </Card>

  <Card title="Deduplication" icon="copy">
    Check for duplicates.
  </Card>
</CardGroup>

<Note>You should refuse any event that does not have both the `Lucca-Signature` and
the `Lucca-Timestamp` HTTP headers and return a `401 Unauthorized`.</Note>

## Lucca-Signature

Each event delivery is signed by the server in order for webhook endpoints to be
able to check their authenticity.

The signature is a SHA256 hash calculated from the concatenation of both the delivery
timestamp (i.e. the moment the event was sent from our server) and the request payload,
separated by a `.` (dot) character. Both properties can be found in HTTP headers:

* the signature is in the `Lucca-Signature` HTTP header ;
* the timestamp is in the `Lucca-Timestamp` HTTP header.

If the signature is missing (i.e. no `Lucca-Signature` header was sent) or does not match,
then return a `401 Unauthorized`.

<CodeGroup>
  ```text Hash Content theme={null}
  <content_to_hash> = <timestamp> '.' <body>
  ```
</CodeGroup>

## Lucca-Timestamp

The timestamp sent in the `Lucca-Timestamp` HTTP header matches the exact time the request
was sent from Lucca's servers. It is an UTC date-time in ISO 8601 format, e.g.: "2025-01-01T08:34:23Z".

You should check that the timestamp is within +/- 5 minutes of the moment you receive the event, in order
to protect yourself against replay attacks.

If the timestamp is too old / too far in the future, or is simply missing, then return a
`401 Unauthorized`.

## Handling Duplicates

You may want to **check for duplicate events** (and potentially discard them), as you may
receive an event more than once through our retry feature.

<RequestExample>
  ```php PHP theme={null}
  <?php
  define('WEBHOOK_SECRET', 'my_webhook_secret');
  define('WEBHOOK_TIMESTAMP_FRESHNESS_MINUTES', 5);

  function validate_event($data, $sig, $timestamp)
  {
      # Calculate Signature
      $calc_sig = base64_encode(hash_hmac('sha256', $timestamp . '.' . $data, WEBHOOK_SECRET, true));
      # Compare calculated and given signatures
      $sigValid = hash_equals($sig, $calc_sig);
      # Compare timestamp with now
      $diff = abs(floor((strtotime($timestamp) - time()) / 60));

      return $sigValid && $diff < WEBHOOK_TIMESTAMP_FRESHNESS_MINUTES;
  }

  # Extract the server signature from HTTP header
  $sig = $_SERVER['Lucca-Signature'] ?? null;

  # Extract the server timestamp from HTTP header
  $timestamp = $_SERVER['Lucca-Timestamp'] ?? null;

  # Extract the raw body
  $data = file_get_contents('php://input');

  # Validate
  $valid = validate_event($data, $sig, $timestamp);

  error_log('Webhook validation: '.var_export($valid, true));

  if ($valid) {
      # Check for duplicates (you may have already received this event)
      # Do something with the event

      http_response_code(202);
  } else {
      http_response_code(401);
  }
  ?>
  ```

  ```csharp C# theme={null}
  using Microsoft.AspNetCore.Mvc;
  using Microsoft.Extensions.Logging;
  using System.IO;
  using System.Threading.Tasks;
  using System.Security.Cryptography;
  using System.Text;
  using System;

  namespace YourNamespace.Controllers {
      [ApiController]
      [Route("webhooks/[controller]")]
      public class YourController : ControllerBase
      {
          private readonly ILogger<YourController> _logger;

          private static readonly string WebhookSecret = "your_webhook_secret";
          private static readonly int MaxFreshnessInMinutes = 5;

          private bool ValidateEvent(string data, string signature, string timestamp) {
              using var hasher = new HMACSHA256(Encoding.UTF8.GetBytes(YourController.WebhookSecret));

              var calcSig = Convert.ToBase64String(hasher.ComputeHash(Encoding.UTF8.GetBytes(timestamp + "." + data)));
              TimeSpan diff = DateTime.UtcNow - DateTime.Parse(timestamp);
              return calcSig == signature && Math.Abs(Math.Floor(diff.TotalMinutes)) < YourController.MaxFreshnessInMinutes;
          }

          public YourController(ILogger<YourController> logger)
          {
              _logger = logger;
          }

          [HttpPost]
          public async Task<IActionResult> Post()
          {
              // Extract the server signature from HTTP header
              string signature = Request.Headers["Lucca-Signature"];
              // Extract the server timestamp from HTTP header
              string timestamp = Request.Headers["Lucca-Timestamp"];

              // Read the request body
              using (StreamReader reader = new StreamReader(Request.Body))
              {
                  string data = await reader.ReadToEndAsync();

                  bool valid = ValidateEvent(data, signature, timestamp);

                  if (!valid) {
                      _logger.LogInformation("Invalid event:", data);
                      return Unauthorized();
                  }
              }

              // Check for duplicates (you may have already received this event)
              // Do something with the event
              
              return Ok();
          }
      }
  }
  ```

  ```ruby Ruby theme={null}
  require 'rubygems'
  require 'base64'
  require 'openssl'
  require 'active_support/security_utils'
  require 'time'

  WEBHOOK_SECRET = 'my_webhook_secret'
  WEBHOOK_TIMESTAMP_FRESHNESS_MINUTES = 5

  helpers do
      def validate_event(data, sig, timestamp)
          # Calculate Signature
          calc_sig = Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', WEBHOOK_SECRET, timestamp + '.' + data))
          sig_valid = ActiveSupport::SecurityUtils.secure_compare(calc_sig, sig)
          # Calculate timestamp diff
          diff = ((Time.parse(timestamp) - Time.now.utc) / 60).floor().abs()
          # OK if signatures match and timestamp is +/- 5 minutes from now
          return sig_valid && diff < WEBHOOK_TIMESTAMP_FRESHNESS_MINUTES
      end
  end

  post '/' do
      request.body.rewind

      # Extract raw body
      data = request.body.read

      # Validate
      valid = validate_event(data, env["Lucca-Signature"], env["Lucca-Timestamp"])
      halt 401 unless valid

      # Check for duplicates (you may have already received this event)
      # Do something with the event

      halt 202
  end
  ```

  ```typescript TypeScript theme={null}
  import { createHmac } from 'crypto';

  function validateSignature(
  	luccaSecret: string | null,
  	luccaSignature: string | null,
  	luccaTimestamp: string | null,
  	rawBody: string,
  ): boolean {
  	if (!luccaSecret || !luccaSignature || !luccaTimestamp) {
  		return false;
  	}
  	const luccaDate = new Date(luccaTimestamp);
  	const currentDate = new Date();
  	const timeDiff = Math.abs(currentDate.getTime() - luccaDate.getTime());
  	const timeDiffMinutes = Math.floor(timeDiff / (1000 * 60));
  	if (timeDiffMinutes > 5) {
  		return false;
  	}
  	const expectedSignature = createHmac('sha256', luccaSecret)
  		.update(`${luccaTimestamp}.${rawBody}`)
  		.digest('base64');
  	return expectedSignature === luccaSignature;
  }
  // Example usage in an Express.js route handler
  validateSignature(
      process.env.LUCCA_WEBHOOK_SECRET || null,
      req.headers['lucca-signature'] as string | null,
      req.headers['lucca-timestamp'] as string | null,
      rawBody,
  );
  ```

  ```javascript javascript theme={null}
  const crypto = require('crypto');
  function validateSignature(luccaSecret, luccaSignature, luccaTimestamp, rawBody) {
      if (!luccaSecret || !luccaSignature || !luccaTimestamp) {
          return false;
      }
      const luccaDate = new Date(luccaTimestamp);
      const currentDate = new Date();
      const timeDiff = Math.abs(currentDate.getTime() - luccaDate.getTime());
      const timeDiffMinutes = Math.floor(timeDiff / (1000 * 60));
      if (timeDiffMinutes > 5) {
          return false;
      }
      const expectedSignature = crypto
          .createHmac('sha256', luccaSecret)
          .update(`${luccaTimestamp}.${rawBody}`)
          .digest('base64');
      return expectedSignature === luccaSignature;
  }
  // Example usage in an Express.js route handler
  validateSignature(
      process.env.LUCCA_WEBHOOK_SECRET,
      req.headers['lucca-signature'],
      req.headers['lucca-timestamp'],
      rawBody
  );
  ```
</RequestExample>
