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

# Verification

> Verify the authenticity of webhook events and prevent duplicate processing

## Verifying Events

Webhooks can be vulnerable to security risks, such as malicious actors sending fake events or replaying intercepted requests. To safeguard against these threats, you should **prevent replay attacks**, **verify the origin and authenticity of events**, and **prevent duplicate processing of events**.

A **replay attack** can be prevented by blocking intercepted requests from being reused. Every webhook includes the `webhook-timestamp` request header, which provides the [Unix timestamp](https://en.wikipedia.org/wiki/Unix_time) of when the event was signed. Use this timestamp to prevent [replay attacks](https://en.wikipedia.org/wiki/Replay_attack):

* Ensure the `webhook-timestamp` is within a valid time window (we recommend up to 3 minutes).
* Since retries generate a new timestamp for each attempt, you can confidently validate each request.
* Reject requests with timestamps outside the threshold to block tampered or replayed events.

**Verify the origin and authenticity of events** by using the **secret signing key** unique to your account, and the request header `webhook-signature` included as part of the webhook. We sign all webhooks originating from us using the **secret signing key**. Follow these steps to ensure the event was sent by us:

* **Generate the signed content** by concatenating the values of the `webhook-id` request header, `webhook-timestamp` request header, and a string representation of the request body into a single string, separated by periods.
* **Calculate the expected signature**, using the **secret key** and the **signed content** above to generate a `HMAC` `SHA256` signature. Then, encode this value using a `base64` encoding.
* **Verify the signature** by comparing the **expected signature** with the signature sent in the `webhook-signature` header. If the generated signature matches the provided signature, you can proceed with processing the event.

<Warning>
  - Make sure to use the string representation of the request body i.e. the raw request body as any minor modification to the body will generate a completely different signature.
  - Before calculating the expected signature, ensure that you exclude the whsec\_ prefix from the secret signing key.
  - Before verifying the signature, make sure to remove the version prefix and delimiter (e.g. v1,).
  - When comparing the signatures, it is recommended to use a constant-time string comparison method to prevent timing attacks.
</Warning>

***

## Handling Duplicate Events

Duplicate processing of events can be prevented at the request-level by processing events in an idempotent-safe manner. Each webhook request includes both a `webhook-id` in the request header and an `event-id` in the message body. The `webhook-id` uniquely identifies a single delivery attempt, while the `event-id` uniquely identifies the event itself across all retries or replays of the same event.

To prevent duplicates, store the `webhook-id` in a persistent data store and ensure that a request with the same identifier has not been processed before. Using both identifiers ensures that duplicate HTTP deliveries and duplicate logical events are handled safely.

Beyond request-level handling, integrations should always implement operation-level idempotency, as request-level deduplication is often only applied over a limited time window. Operation-level idempotency ensures that repeated or delayed business events do not cause inconsistent state changes after that window has expired.

Before applying an update to a resource such as a `payment_session` or a `payment`, inspect the payload and the current state of the object:

* If the `payment_session` (as identified by the payment session identifier) is already in a final state, for example, `completed`, `expired`, or `canceled`; or a `payment` (as identified by the payment identifier) is already in a final state, for example, `succeeded`, ignore the event by responding with a 2xx status code to prevent redelivery — since no further state change is needed.
* Only apply updates that move the object forward, for example:
  * `payment_session`: `active` → `completed`
  * `payment`: `draft` → `succeeded`

Combining request-level and operation-level idempotency ensures that duplicate or delayed webhook events are non-destructive, keeping states consistent even when events are retried or replayed beyond the initial request-handling window.

***

## Additional Security Precautions

In addition to the security measures used to verify the origin and authenticity of webhook events described above, all webhook events are sent from a specific set of source static IP addresses.

If your webhook endpoint is hosted behind a firewall or a NAT (Network Address Translation) gateway, you'll need to ensure it can receive incoming requests. Whitelist the following source static IP addresses in your network configuration so that webhook traffic is not blocked or dropped by your security settings.

```
52.215.16.239
54.216.8.72
63.33.109.123
2a05:d028:17:8000::/56
```

***

## Sample Code

<Accordion title="Code Snippet - Receive the webhook event">
  <CodeGroup>
    ```javascript webhooks.js theme={"system"}
    // Using NodeJs + Express
    app.post("/my/webhook/url", function(req, res) {
        const requestBody = req.rawBody;

        // TODO: Verify and process the received data

        res.send(200);
    });
    ```

    ```python webhooks.py theme={"system"}
    # Using Python + Flask
    @app.route('/my/webhook/url', methods=['POST'])
    def webhook():
        request_body = request.get_data(as_text=True)

        # TODO: Verify and process the received data

        return "", 200
    ```
  </CodeGroup>
</Accordion>

<Accordion title="Code Snippet - Verify the origin and authenticity of the event">
  **<u>Generate the signed content:</u>**

  <CodeGroup>
    ```javascript generate.js theme={"system"}
    // Using NodeJs + Express
    const headers = req.headers;
    const requestBody = req.rawBody;

    const id = headers['webhook-id'];
    const timestamp = headers['webhook-timestamp'];

    const signedContent = `${id}.${timestamp}.${requestBody}`;
    ```

    ```python generate.py theme={"system"}
    # Using Python + Flask
    headers = request.headers
    request_body = request.get_data(as_text=True)

    id = headers.get('webhook-id')
    timestamp = headers.get('webhook-timestamp')

    signed_content = str(id) + '.' + str(timestamp) + '.' + str(request_body)
    ```
  </CodeGroup>

  **<u>Calculate the expected signature:</u>**

  <CodeGroup>
    ```javascript calculate.js theme={"system"}
    const crypto = require('crypto');

    const secret = 'whsec_MDUyYmQ2NThjZDY3NDU5NzkyYTViNjViOTlhYzI2MzE=';
    const secretBytes = Buffer.from(secret.split("_")[1], "base64");

    const expectedSignature = crypto
        .createHmac('sha256', secretBytes)
        .update(signedContent)
        .digest('base64');
    ```

    ```python calculate.py theme={"system"}
    secret = 'whsec_MDUyYmQ2NThjZDY3NDU5NzkyYTViNjViOTlhYzI2MzE='
    secret_bytes = base64.b64decode(secret.split('_')[1])

    hmac_signature = hmac.new(secret_bytes, signed_content.encode(), hashlib.sha256).digest()
    expected_signature = base64.b64encode(hmac_signature).decode()
    ```
  </CodeGroup>

  **<u>Verify the signature:</u>**

  <CodeGroup>
    ```javascript verify.js theme={"system"}
    // Compare just the first signature
    const signature = headers['webhook-signature'].split(' ')[0].split(',')[1]
    if (crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(signature))) {
        // process webhook event
        return res.send(200);
    }
    // do not process webhook event
    return res.send(403);
    ```

    ```python verify.py theme={"system"}
    # Compare just the first signature
    signature = headers.get('webhook-signature').split(' ')[0].split(',')[1]
    if hmac.compare_digest(signature, expected_signature):
        # process webhook event
        return "", 200

    # do not process webhook event
    return "", 403
    ```
  </CodeGroup>
</Accordion>

<Accordion title="Full Sample Code">
  <CodeGroup>
    ```javascript webhooks.js theme={"system"}
    const express = require('express');
    const crypto = require('crypto');
    const getRawBody = require('raw-body');

    const app = express();

    app.use((req, res, next) => {
        getRawBody(req, {
            length: req.headers['content-length'],
            encoding: 'utf-8'
        }, (err, rawBody) => {
            if (err) return next(err);
            req.rawBody = rawBody;
            next();
        });
    });

    app.post("/my/webhook/url", function(req, res) {
        const headers = req.headers;
        const requestBody = req.rawBody;

        const id = headers['webhook-id'];
        const timestamp = headers['webhook-timestamp'];

        const signedContent = `${id}.${timestamp}.${requestBody}`;

        const secret = 'whsec_M0U0MDI3QjYzMEQ0NTK5NDNCIjVFMENCMDEzNzc1QkE=';
        const secretBytes = new Buffer(secret.split('_')[1], "base64");

        const expectedSignature = crypto
            .createHmac('sha256', secretBytes)
            .update(signedContent)
            .digest('base64');

        const signature = headers['webhook-signature'].split(' ')[0].split(',')[1]
        if (crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(signature))) {
            return res.send(200);
        }
        return res.send(403);
    });

    app.listen(3000, () => {
        console.log('Server is running on port 3000');
    });
    ```

    ```python webhooks.py theme={"system"}
    from flask import Flask, request
    import hashlib
    import base64
    import hmac

    app = Flask(__name__)

    @app.route('/my/webhook/url', methods=['POST'])
    def webhook():
        headers = request.headers
        request_body = request.get_data(as_text=True)

        id = headers.get('webhook-id')
        timestamp = headers.get('webhook-timestamp')

        signed_content = str(id) + '.' + str(timestamp) + '.' + str(request_body)

        secret = 'whsec_M0U0MDI3QjYzMEQ0NTK5NDNCIjVFMENCMDEzNzc1QkE='
        secret_bytes = base64.b64decode(secret.split('_')[1])

        hmac_signature = hmac.new(secret_bytes, signed_content.encode(), hashlib.sha256).digest()
        expected_signature = base64.b64encode(hmac_signature).decode()

        signature = headers.get('webhook-signature').split(' ')[0].split(',')[1]
        if hmac.compare_digest(signature, expected_signature):
            return "", 200

        return "", 403


    if __name__ == '__main__':
        app.run()
    ```
  </CodeGroup>
</Accordion>
