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

# Webhooks

Modem Pay's webhooks notify your server about key events, such as customer creation, payment intent updates, and successful charges. To receive and securely process these notifications, follow the steps outlined here for setting up, validating, and handling webhooks.

<Note>
  You can generate your unique Modem Pay webhook secret in the Modem Pay dashboard under ***Developers > Webhooks***.
</Note>

## Setting Up Webhooks

1. **Define Your Webhook Endpoint**:\
   Create a dedicated `POST` endpoint on your server to receive webhook events from Modem Pay. Ensure this endpoint is accessible over HTTPS to maintain security.

2. **Listening for Events**:\
   Your endpoint will receive a variety of event notifications (e.g., `customer.created`, `payment_intent.created`, `charge.succeeded`). The event type is provided in the `event` field, while the main data is within the `payload`.

### Sample Webhook Event

```javascript json theme={null}
{
  "event": "charge.succeeded",
  "payload": {
    "id": "23419194-7324-4c2b-a74b-d8fba736e692",
    "type": "payment",
    "amount": 2500,
    "currency": "GMD",
    "payment_method": "qmoney",
    "customer": "0f6e0912-2f2f-4d8b-bd68-ac8c97c44ae6",
    "metadata": {},
    "status": "completed",
    "payment_link_id": null,
    "custom_fields_values": {},
    "business_id": "a10a51ae-05c8-406e-968c-7b1d309a77e0",
    "account_id": "7788dd90-3801-47a6-8597-b46eaa0a7d7d",
    "test_mode": true,
    "customer_name": "Zypheron Kade",
    "customer_phone": "7024725",
    "customer_email": "calebchibuike110@gmail.com",
    "createdAt": "2024-12-15T09:34:17.036Z",
    "updatedAt": "2024-12-15T09:34:43.306Z",
    "payment_account": ".... 9944",
    "payment_metadata": {
      "os": "MacOS",
      "browser": "Firefox",
      "ipAddress": "196.223.149.183",
      "timestamp": "2024-12-15T09:34:43.266Z",
      "deviceType": "Desktop",
      "urlIPAddress": "https://db-ip.com/196.223.149.183",
      "screenResolution": "1600x900"
    },
    "payment_intent_id": "273ac751-369d-4198-8340-da480208bded",
    "transaction_reference": "VOAO1CFEN7",
    "payment_method_id": "4dbaaec5-7a16-4908-8fd9-c3d2bd24f739"
  }
}
```

## Webhook Security and Signature Validation

To confirm the integrity of incoming events, Modem Pay includes a unique `x-modem-signature` header in each webhook request. This header allows you to verify that the event originated from Modem Pay.

1. **Retrieve the Signature**:\
   In your webhook endpoint, extract the `x-modem-signature` header from the incoming request.

2. **Use `modempay.webhooks.composeEventDetails` for Validation**:\
   Pass the payload, signature, and your unique Modem Pay secret to the `modempay.webhooks.composeEventDetails` function. This function performs the necessary checks, verifying the signature and parsing the payload.

<CodeGroup>
  ```javascript js-sdk theme={null}
  // Example usage
  const event = modempay.webhooks.composeEventDetails(
      payload,
      xSignatureHeader,
      modemSecret
  );
  ```

  ```python python-sdk theme={null}
  # Example usage
  event = modem_pay.webhooks.compose_event_details(payload, xSignatureHeader, modemSecret)
  ```

  ```php php-sdk theme={null}
  $event = $modemPay->webhooks()->composeEventDetails(
      $payload,
      $xSignatureHeader,
      $modemSecret
  );
  ```

  ```javascript javascript theme={null}
  app.post("/modem-webhook", (req, res) => {
    const payload = JSON.stringify(req.body);
    const signature = req.headers["x-modem-signature"];

    // Ensure the signature is provided
    if (!signature) {
      return res.status(400).json({ message: "Signature missing" });
    }

    const secretHash = process.env.MODEM_PAY_SECRET_HASH;

    // Generate the HMAC-SHA512 hash for signature comparison
    const computedSignature = crypto
      .createHmac("sha512", secretHash)
      .update(payload)
      .digest("hex");

    // Ensure the signature length matches to avoid timing attacks
    if (computedSignature.length !== signature.length) {
      return res.status(400).json({ message: "Invalid signature length" });
    }

    // Use timing-safe comparison to protect against timing attacks
    if (
      !crypto.timingSafeEqual(
        Buffer.from(computedSignature),
        Buffer.from(signature)
      )
    ) {
      return res.status(400).json({ message: "Invalid signature!" });
    }

    // Parse the payload to handle the event
    try {
      const eventPayload = JSON.parse(payload);
      // You can add logic here to process the eventPayload
      res.sendStatus(200); // Acknowledge successful receipt
    } catch (error) {
      res.status(400).json({ message: "Invalid event data" });
    }
  });
  ```

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

  import (
  	"crypto/hmac"
  	"crypto/sha512"
  	"crypto/subtle"
  	"encoding/hex"
  	"encoding/json"
  	"io"
  	"net/http"
  	"os"
  )

  func modemWebhookHandler(w http.ResponseWriter, r *http.Request) {
  	// Read request body
  	body, err := io.ReadAll(r.Body)
  	if err != nil {
  		http.Error(w, `{"message": "Failed to read request body"}`, http.StatusBadRequest)
  		return
  	}

  	// Get signature from header
  	signature := r.Header.Get("x-modem-signature")
  	if signature == "" {
  		http.Error(w, `{"message": "Signature missing"}`, http.StatusBadRequest)
  		return
  	}

  	// Get secret hash from environment
  	secretHash := os.Getenv("MODEM_PAY_SECRET_HASH")

  	// Generate HMAC-SHA512 hash
  	mac := hmac.New(sha512.New, []byte(secretHash))
  	mac.Write(body)
  	computedSignature := hex.EncodeToString(mac.Sum(nil))

  	// Check signature length
  	if len(computedSignature) != len(signature) {
  		http.Error(w, `{"message": "Invalid signature length"}`, http.StatusBadRequest)
  		return
  	}

  	// Timing-safe comparison
  	if subtle.ConstantTimeCompare([]byte(computedSignature), []byte(signature)) != 1 {
  		http.Error(w, `{"message": "Invalid signature!"}`, http.StatusBadRequest)
  		return
  	}

  	// Parse payload
  	var eventPayload interface{}
  	if err := json.Unmarshal(body, &eventPayload); err != nil {
  		http.Error(w, `{"message": "Invalid event data"}`, http.StatusBadRequest)
  		return
  	}

  	// Process eventPayload here if needed
  	// ...

  	// Send success response
  	w.WriteHeader(http.StatusOK)
  }

  func main() {
  	http.HandleFunc("/modem-webhook", modemWebhookHandler)
  	http.ListenAndServe(":8080", nil)
  }
  ```

  ```python python theme={null}
  app = Flask(__name__)

  @app.route("/modem-webhook", methods=["POST"])
  def modem_webhook():
      payload = json.dumps(request.json)
      signature = request.headers.get("x-modem-signature")

      # Ensure the signature is provided
      if not signature:
          return jsonify({"message": "Signature missing"}), 400

      secret_hash = os.environ.get("MODEM_PAY_SECRET_HASH")

      # Generate the HMAC-SHA512 hash for signature comparison
      computed_signature = hmac.new(
          secret_hash.encode(), payload.encode(), hashlib.sha512
      ).hexdigest()

      # Ensure the signature length matches to avoid timing attacks
      if len(computed_signature) != len(signature):
          return jsonify({"message": "Invalid signature length"}), 400

      # Use timing-safe comparison to protect against timing attacks
      if not hmac.compare_digest(computed_signature, signature):
          return jsonify({"message": "Invalid signature!"}), 400

      # Parse the payload to handle the event
      try:
          event_payload = json.loads(payload)
          # Logic to process the eventPayload can go here
          return "", 200  # Acknowledge successful receipt
      except json.JSONDecodeError:
          return jsonify({"message": "Invalid event data"}), 400

  if __name__ == "__main__":
      app.run(debug=True)
  ```
</CodeGroup>

<Tip>
  Always respond to webhook events with an HTTP `200 OK` status to confirm receipt. This ensures Modem Pay doesn’t retry the webhook unnecessarily.
</Tip>

### Event Structure

Upon successful validation, `modempay.webhooks.composeEventDetails` returns an `Event` object containing the event type and payload data. The event types you may receive include:

* **Customer Events**: `"customer.created"`, `"customer.updated"`, `"customer.deleted"`
* **Payment Intent Events**: `"payment_intent.created"`, `"payment_intent.cancelled"`, `"payment_intent.expired"`
* **Charge Events**: `"charge.succeeded"`, `"charge.cancelled"`, `"charge.created"`, `"charge.updated"`, `"charge.expired"`
* **Payout Events**: `"transfer.succeeded"`, `"transfer.cancelled"`, `"transfer.reversed"`, `"transfer.failed"`, `"transfer.flagged"`

### Example Usage

Here’s an example of using `modempay.webhooks.composeEventDetails` to handle incoming webhooks in a Node.js application:

<CodeGroup>
  ```javascript nodejs theme={null}
  const express = require("express");
  const bodyParser = require("body-parser");
  const ModemPay = require("modem-pay");

  const modempay = new ModemPay();
  const app = express();
  app.use(bodyParser.json());

  app.post("/webhook", (req, res) => {
    const payload = req.body;
    const signature = req.headers["x-modem-signature"];
    const secret = process.env.MODEM_WEBHOOK_SECRET;

    try {
      const event = modempay.webhooks.composeEventDetails(payload, signature, secret);
      // Process the event
      switch (event.event) {
        case "customer.created":
          // Handle new customer logic
          break;
        case "charge.succeeded":
          // Process successful payment
          break;
        // Add other cases as needed
      }

      res.status(200).send("Webhook processed!");
    } catch (error) {
      res.status(400).send("Invalid signature!");
    }
  });

  app.listen(3000, () => console.log("Listening for webhooks on port 3000"));
  ```

  ```python flask theme={null}
  import os
  from modempay import ModemPay
  from flask import Flask, request, jsonify

  modem_pay = ModemPay(api_key="YOUR_MODEM_PAY_API_KEY", config={"timeout": 20})

  app = Flask(__name__)

  @app.route("/webhook", methods=["POST"])
  def webhook():
      payload = request.get_json()
      signature = request.headers.get("x-modem-signature")
      secret = os.environ.get("MODEM_WEBHOOK_SECRET")

      try:
          event = modem_pay.webhooks.compose_event_details(payload, signature, secret)
          # Process the event
          if event["event"] == "customer.created":
              # Handle new customer logic
              pass
          elif event["event"] == "charge.succeeded":
              # Process successful payment
              pass
          # Add other cases as needed

          return "Webhook processed!", 200
      except Exception:
          return "Invalid signature!", 400


  if __name__ == "__main__":
      app.run(port=3000, debug=True)

  ```

  ```php laravel theme={null}
  public function handle(Request $request)
  {
      try {
          $event = ModemPay::webhooks()->composeEventDetails(
              $request->getContent(),
              $request->header('x-modem-signature'),
              config('modempay.webhook_secret')
          );

          // Log event
          Log::info('ModemPay webhook received', [
              'event' => $event->event->value,
              'payload' => $event->payload
          ]);

          // Process based on event type
          switch ($event->event) {
              case EventType::CHARGE_SUCCEEDED:
                  Order::where('payment_id', $event->payload['id'])
                       ->update(['status' => 'paid']);
                  break;

              case EventType::CHARGE_FAILED:
                  Order::where('payment_id', $event->payload['id'])
                       ->update(['status' => 'failed']);
                  break;
          }

          return response()->json(['status' => 'success']);

      } catch (\Exception $e) {
          Log::error('Webhook processing failed', ['error' => $e->getMessage()]);
          return response()->json(['error' => 'Processing failed'], 400);
      }
  }
  ```
</CodeGroup>

### Recommended Response

For each webhook event, respond to Modem Pay with an HTTP `200 OK` status as confirmation. This ensures Modem Pay won’t re-send the event unless there's an error response.

<Warning>
  Ensure your webhook endpoint quickly responds with an HTTP `200 OK` status before performing any tasks that might cause delays or timeouts, such as updating records or sending invoices. This prevents webhook retries and ensures smooth processing.
</Warning>

### Webhook Retries

Modem Pay ensures reliable delivery of webhook events through an automatic retry mechanism.

If your server fails to acknowledge a webhook event with a **`200 OK`** HTTP response, Modem Pay will retry the delivery up to **3 times**, with each retry spaced **10 minutes apart**. A failure could be due to reasons such as server timeouts, network errors, or non-2xx response codes.

To avoid unnecessary retries and ensure smooth webhook processing, **always return a `200 OK` response immediately**, even if you need to carry out time-consuming tasks (like writing to a database or triggering external API calls). It’s recommended to offload such operations to a background worker or job queue.

By following this best practice, you help prevent duplicate processing and reduce the risk of delays or rate-limiting due to repeated webhook attempts.

### Summary

By following these steps, you can securely receive and process webhook events from Modem Pay, automating actions based on customer interactions and payment flows. Make sure to store and securely handle the event data, as each event may carry critical transaction or customer information relevant to your application.
