> For the complete documentation index, see [llms.txt](https://bagelpay.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://bagelpay.gitbook.io/docs/documentation/developers-and-api-usage/webhooks/verify-webhook-requests.md).

# Verify Webhook Requests

## How to verify BagelPay signature?

BagelPay signature is sent in the `Bagelpay-Signature` header of the webhook request. The signature is generated using the **HMAC-SHA256** algorithm with the `Signing Secret` as the key, and the `Request Body and Timestamp` as the message.

<details>

<summary>Sample Webhook Header</summary>

```json
{
	"Bagelpay-Signature": "dd7bdd2cf1f6bac6e171c6c508c157b7cd3cc1fd196394277fb59ba0bdd9b87b",
	"timestamp": "1756301826"
}
```

</details>

To verify the signature, you need to generate the signature using the same algorithm and compare it with the signature sent in the header.&#x20;

If the two signatures match, the request is authentic.

{% hint style="info" %}
You can find your webhook secret on the Developer>Webhook page.
{% endhint %}

<figure><img src="/files/qukKZoDK8jEZonYfD9Fv" alt=""><figcaption></figcaption></figure>

To generate the `Bagelpay-Signature`, you can use the following code example:

{% tabs %}
{% tab title="Python" %}

```python
import hmac
import hashlib

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import uvicorn
from pyngrok import ngrok
ngrok.set_auth_token("your_ngrok_key")
WEBHOOK_SECRET = "your_webhook_key"

app = FastAPI()


def verify_webhook_signature(signature_data: bytes, signature: str, secret: str) -> bool:
    """Verify webhook signature for security"""
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        signature_data,
        hashlib.sha256
    ).hexdigest()

    print("expected_signature: ", expected_signature)
    print("signature: ", signature)

    return hmac.compare_digest(expected_signature, signature)


@app.post("/api/webhooks")
async def handle_post(request: Request):
    """Handle BagelPay webhook notifications"""
    payload = await request.body()
    timestamp = request.headers.get('timestamp').encode()
    signature = request.headers.get('Bagelpay-Signature')
    # Combine payload and timestamp
    signature_data = timestamp + ".".encode() + payload
    print("payload: ", payload)
    print("timestamp: ", timestamp)
    print("signature: ", signature)
    print("signature_data: ", signature_data)

    if not verify_webhook_signature(signature_data, signature, WEBHOOK_SECRET):
        return JSONResponse(status_code=401, content={"error": "Invalid signature"})

    print(payload)
    return JSONResponse(status_code=200, content={"message": "Success"})


if __name__ == "__main__":
    listening_port = "8000"
    public_url = ngrok.connect(
        addr=listening_port,
        proto="http",
        hostname="stunning-crane-direct.ngrok-free.app"
    )
    print(f"ngrok Public URL: {public_url}")
    uvicorn.run(app, host="0.0.0.0", port=int(listening_port))

```

{% endtab %}

{% tab title="TypeScripts" %}

```python
import express from 'express';
import crypto from 'crypto';
import ngrok from 'ngrok';

const app = express();
const WEBHOOK_SECRET = 'your_webhook_key';

// Middleware to parse raw body for signature verification
app.use('/api/webhooks', express.raw({ type: 'application/json' }));

function verifyWebhookSignature(signatureData: Buffer, signature: string, secret: string): boolean {
  /**
   * Verify webhook signature for security
   */
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signatureData)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature, 'hex'),
    Buffer.from(signature, 'hex')
  );
}

app.post('/api/webhooks', async (req, res) => {
  /**
   * Handle BagelPay webhook notifications
   */
  const payload = req.body;
  const timestamp = req.headers['timestamp'] as string;
  const signature = req.headers['bagelpay-signature'] as string;
  
  // Combine payload and timestamp
  const signatureData = Buffer.concat([
    Buffer.from(timestamp, 'utf8'),
    Buffer.from('.', 'utf8'),
    payload
  ]);

  if (!verifyWebhookSignature(signatureData, signature, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  try {
    const event = JSON.parse(payload.toString());
    const eventType = event.event_type;
    const data = event.object;

    switch (eventType) {
      case 'checkout.completed':
        // Handle checkout completed events
        console.log('Checkout completed:', event);
        break;
      
      case 'checkout.failed':
        // Handle checkout failed events
        console.log('Checkout failed:', event);
        break;
      
      case 'checkout.cancel':
        // Handle checkout cancelled events
        console.log('Checkout cancelled:', event);
        break;
      
      case 'subscription.trialing':
        // Handle subscription trialing events
        console.log('Subscription trialing:', event);
        break;
      
      case 'subscription.paid':
        // Handle subscription paid events
        console.log('Subscription paid:', event);
        break;
      
      case 'subscription.canceled':
        // Handle subscription cancelled events
        console.log('Subscription cancelled:', event);
        break;
      
      case 'refund.created':
        // Handle refund created events
        console.log('Refund created:', event);
        break;
      
      default:
        console.log(`Unhandled event type: ${eventType}`);
    }

    return res.status(200).json({ message: 'Success' });
  } catch (error) {
    console.error('Webhook processing error:', error);
    return res.status(500).json({ error: 'Processing failed' });
  }
});

// Start server with ngrok tunnel
async function startServer() {
  const listeningPort = 8000;
  
  try {
    // Set your ngrok auth token
    await ngrok.authtoken('your_ngrok_key');
    
    const publicUrl = await ngrok.connect({
      addr: listeningPort,
      proto: 'http',
      subdomain: 'stunning-crane-direct' // Optional: use your reserved subdomain
    });
    
    console.log(`ngrok Public URL: ${publicUrl}`);
    
    app.listen(listeningPort, '0.0.0.0', () => {
      console.log(`Server running on port ${listeningPort}`);
    });
  } catch (error) {
    console.error('Failed to start server:', error);
  }
}

if (require.main === module) {
  startServer();
}
```

{% endtab %}
{% endtabs %}

<figure><img src="/files/U1HGnfJfxz133knyir5Z" alt=""><figcaption></figcaption></figure>

Simply compare the generated Signature with the one received on the header to complete the verification process.
