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

# Signature Verification

> Verify that webhook events originate authentically from Laiki Pay

# Verifying Webhook Signatures

Each webhook request includes an `X-Laiki-Signature` header. Compute the HMAC-SHA256 signature of the raw request payload using your **Webhook Secret** and compare it using constant-time equality check.

```http Headers theme={null}
X-Laiki-Signature: 8d9a246830cf42e31690184b29bb807a0487...
X-Laiki-Event: card.transaction.completed
Content-Type: application/json
```

***

## Code Implementations

<CodeGroup>
  ```javascript Node.js / Express theme={null}
  const crypto = require('crypto');

  function verifyWebhook(rawPayloadString, signatureHeader, webhookSecret) {
    const hash = crypto
      .createHmac('sha256', webhookSecret)
      .update(rawPayloadString)
      .digest('hex');

    // Use timingSafeEqual to prevent timing attacks
    return crypto.timingSafeEqual(
      Buffer.from(signatureHeader, 'hex'),
      Buffer.from(hash, 'hex')
    );
  }

  // Express Route Example
  app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const sig = req.headers['x-laiki-signature'];
    const isValid = verifyWebhook(req.body.toString('utf8'), sig, process.env.LAIKI_WEBHOOK_SECRET);

    if (!isValid) {
      return res.status(400).send('Invalid signature');
    }

    const event = JSON.parse(req.body);
    console.log('Received event:', event.event);

    res.status(200).json({ received: true });
  });
  ```

  ```python Python / FastAPI theme={null}
  import hmac
  import hashlib
  from fastapi import FastAPI, Request, HTTPException

  app = FastAPI()
  WEBHOOK_SECRET = "whsec_..."

  def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
      computed = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
      return hmac.compare_digest(computed, signature)

  @app.post("/webhook")
  async def handle_webhook(request: Request):
      payload = await request.body()
      signature = request.headers.get("x-laiki-signature")

      if not signature or not verify_signature(payload, signature, WEBHOOK_SECRET):
          raise HTTPException(status_code=400, detail="Invalid signature")

      event = await request.json()
      print("Received event:", event.get("event"))

      return {"received": True}
  ```

  ```php PHP theme={null}
  <?php
  $secret = getenv('LAIKI_WEBHOOK_SECRET');
  $signature = $_SERVER['HTTP_X_LAIKI_SIGNATURE'] ?? '';
  $payload = file_get_contents('php://input');

  $computed = hash_hmac('sha256', $payload, $secret);

  if (!hash_equals($computed, $signature)) {
      http_response_code(400);
      exit('Invalid signature');
  }

  $event = json_decode($payload, true);
  http_response_code(200);
  echo json_encode(['received' => true]);
  ?>
  ```

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

  import (
  	"crypto/hmac"
  	"crypto/sha256"
  	"encoding/hex"
  	"io"
  	"net/http"
  	"os"
  )

  func verifyWebhook(payload []byte, signature, secret string) bool {
  	mac := hmac.New(sha256.New, []byte(secret))
  	mac.Write(payload)
  	expectedMAC := hex.EncodeToString(mac.Sum(nil))
  	return hmac.Equal([]byte(signature), []byte(expectedMAC))
  }

  func webhookHandler(w http.ResponseWriter, r *http.Request) {
  	body, _ := io.ReadAll(r.Body)
  	sig := r.Header.Get("X-Laiki-Signature")
  	secret := os.Getenv("LAIKI_WEBHOOK_SECRET")

  	if !verifyWebhook(body, sig, secret) {
  		http.Error(w, "Invalid signature", http.StatusBadRequest)
  		return
  	}

  	w.WriteHeader(http.StatusOK)
  	w.Write([]byte(`{"received": true}`))
  }
  ```
</CodeGroup>


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