Subscribing to Webhooks for Push Notifications

Introduction to Webhooks

Webhooks are a way for external systems to provide real-time information to your application by sending HTTP POST requests to a designated URL, also known as the webhook endpoint. Webhooks are commonly used for event-driven architectures, such as notifying your application about payment updates, order status changes, or other important events.

Why Webhooks are Important

Webhooks enable your application to receive immediate updates without the need to repeatedly poll an API for changes. This leads to more efficient resource utilization and faster response times.

Security Measures for Webhooks

To ensure the security and authenticity of webhook data, consider implementing the following measures:

  • HTTPS: Use HTTPS for your webhook endpoint to encrypt data during transmission.
  • Signature Verification: Include a signature in the webhook payload and verify it to ensure the data is from 100Pay.
  • Secret Key: Use a shared secret key to generate and verify signatures.
  • IP Whitelisting: Whitelist IP addresses from 100Pay's servers to prevent unauthorized requests.

Webhook Integration Steps

  • Setting Up Webhook Endpoint: In your application, create an endpoint to receive webhook notifications. This endpoint should be accessible via HTTPS.
  • Verifying Webhook Signatures: When you receive a webhook request, extract the provided signature and payload. Next, use your secret key to re-calculate the signature based on the payload and compare the calculated signature with the received signature to verify authenticity.
  • Processing Webhook Data and Updating Database: Parse the JSON payload to extract relevant data, such as payment status, amount, and currency, then compare the received amount and currency with your expected values to ensure consistency.

Note: Update your database with the payment status and other relevant information.

Below are some example codes you can copy and paste. If you should have experience any challenges, you can always reach out to our team via our support channels and they will be more than delighted to help.

Example Code Snippets

Below is an example code snippet showing a JSON response from a webhook:

{
  "retries":0,
  "acknowledged":false,
  "dispatched":false,
  "type":"credit",
  "_id":"66deb5e0a7044f0049e24ca3",
  "chargeId":"66deb58ea7044f0049e24c69",
  "reference":"lEdWUHVGoCnUMYG9",
  "data":{"from":"0x8ad4BFe54f00C6B7A394a21405a12bfdd3f97851",
  "to":"160005",
  "network":"bsc",
  "transaction_id":"internal-transfer-66deb5dfa7044f0049e24c96",
  "status":"CONFIRMED",
  "timestamp":"2024-09-09T08:46:24.057Z",
  "value":{"local":{"amount":"242.95","currency":"NGN"},
  "crypto":{"amount":5,"currency":"PAY"}},
  "block":{"hash":"internal-transfer-66deb5dfa7044f0049e24c96"},
  "charge":{"customer":{"user_id":"1","name":"Express User","email":"[email protected]","phone":"0000000000"},
  "billing":{"currency":"PAY","vat":0,"pricing_type":"fixed_price","amount":"5","description":"test","country":"US"},
  "status":{"context":{"status":"overpaid","value":142.95},
  "value":"overpaid","total_paid":242.95},
  "ref_id":"fb324f73-8733-47e2-b371-8a766235694d",
  "payments":[{"from":"0x8ad4BFe54f00C6B7A394a21405a12bfdd3f97851",
  "to":"160005",
  "network":"bsc",
  "transaction_id":"internal-transfer-66deb5dfa7044f0049e24c96",
  "status":"CONFIRMED",
  "timestamp":"2024-09-09T08:46:24.057Z",
  "value":{"local":{"amount":"242.95","currency":"NGN"},"crypto":{"amount":5,"currency":"PAY"}},
  "block":{"height":null,"hash":"internal-transfer-66deb5dfa7044f0049e24c96"}}],
  "charge_source":"external",
  "createdAt":"2024-09-07T12:05:46.431Z",
  "_id":"66deb58ea7044f0049e24c69",
  "metadata":{"is_approved":"yes","order_id":"OR2","charge_ref":"REF"},
  "call_back_url":"http://localhost:8000/verifyorder/",
  "app_id":"64971290f6dbaf011f729325",
  "userId":"64970fbdf6dbaf011f7292be",
  "chargeId":"66deb58ea7044f0049e24c69","__v":0},
  "appId":"64971290f6dbaf011f729325"},
  "cryptoChargeId":"66deb593a7044f0049e24c73","createdAt":"2024-09-09T08:46:24.477Z","__v":0
}

The above JSON response contains information about a credit transaction, including the sender, recipient, network, transaction ID, status, timestamp, value, and other relevant details. This data can be parsed and also used to send email notifications, app push notifications, or trigger other actions in your application.

Next, we provide an example code snippet in Node.js for verifying webhook signatures:

const express = require("express");
const mongoose = require("mongoose");

const app = express();
const YOUR_SECRET_KEY = "your_secret_key";
const MONGO_URI = "mongodb://localhost:27017/your_database_name";

// Define a mongoose model for payments
const Payment = mongoose.model("Payment", {
  transaction_id: String,
  network: String,
  status: String,
  timestamp: Date,
  value: {
    local: {
      amount: Number,
      currency: String,
    },
    crypto: {
      amount: Number,
      currency: String,
    },
  },
});

mongoose.connect(MONGO_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

async function isPaymentProcessed(transactionId) {
  const existingPayment = await Payment.findOne({
    transaction_id: transactionId,
  });
  return existingPayment !== null;
}

async function updateDatabase(payload) {
  const newPaymentTransactionId = payload.new_payment.transaction_id;

  // Check whether newPaymentTransactionId already exists in the database
  if (!(await isPaymentProcessed(newPaymentTransactionId))) {
    const payment = new Payment(payload.new_payment);
    await payment.save();
    console.log(
      `Payment with transaction ID ${newPaymentTransactionId} processed and logged.`
    );
  } else {
    console.log(
      `Payment with transaction ID ${newPaymentTransactionId} already processed.`
    );
  }

  // Update charge status in the database based on the "value" field
  const chargeStatus = payload.charge.status.value;
  if (chargeStatus === "paid") {
    console.log("Charge status updated: Paid");
    // Update the charge status in the database to "Paid"
  } else if (chargeStatus === "overpaid" || chargeStatus === "underpaid") {
    const overpaidValue = payload.charge.status.context.value;
    console.log(
      `Charge status updated: ${
        chargeStatus.charAt(0).toUpperCase() + chargeStatus.slice(1)
      }. Over/underpaid amount: ${overpaidValue}`
    );
    // Update the charge status in the database to "Overpaid" or "Underpaid"
  }
}

app.use(express.json());

app.post("/webhook", async (req, res) => {
  const receivedToken = req.headers["verification-token"];

  if (receivedToken === YOUR_SECRET_KEY) {
    const payload = req.body;

    // Verify the webhook request
    if (await isPaymentProcessed(payload.new_payment.transaction_id)) {
      await updateDatabase(payload);
      res.send("Webhook received and payment processed");
    } else {
      res.status(400).send("Payment already processed or invalid");
    }
  } else {
    res.status(403).send("Webhook verification failed");
  }
});

app.listen(3000, () => {
  console.log("Webhook server started on port 3000");
});

The code snippet above demonstrates how to set up a webhook endpoint in Node.js using Express and Mongoose. The endpoint listens for incoming webhook requests, verifies the authenticity of the request, and updates the database with payment information. The code also includes functions to check if a payment has already been processed and to update the charge status in the database based on the payment value.

The above sample code is writen for a nodejs environment. You may have to adjust it to meet your project goals.

PHP and mysql Example

This PHP script handles the webhook request, verifies it, checks if the payment is processed, and updates the MySQL database accordingly. It echoes messages to indicate the processing steps.

Feel free to use this code as a reference for handling webhooks and MySQL database operations in PHP, and if you have any further questions or need additional assistance, be sure to contact our support team.

<?php

$YOUR_SECRET_KEY = 'your_secret_key';
$MYSQL_HOST = 'localhost';
$MYSQL_USER = 'your_mysql_user';
$MYSQL_PASSWORD = 'your_mysql_password';
$MYSQL_DATABASE = 'your_database_name';

$connection = mysqli_connect($MYSQL_HOST, $MYSQL_USER, $MYSQL_PASSWORD, $MYSQL_DATABASE);

function isPaymentProcessed($transactionId) {
  global $connection;
  $query = "SELECT * FROM payments WHERE transaction_id = '$transactionId'";
  $result = mysqli_query($connection, $query);
  return mysqli_num_rows($result) > 0;
}

function updateDatabase($payload) {
  global $connection;

  $newPaymentTransactionId = $payload['new_payment']['transaction_id'];

  if (!isPaymentProcessed($newPaymentTransactionId)) {
    $localAmount = $payload['new_payment']['value']['local']['amount'];
    $localCurrency = $payload['new_payment']['value']['local']['currency'];
    $query = "INSERT INTO payments (transaction_id, local_amount, local_currency) VALUES ('$newPaymentTransactionId', $localAmount, '$localCurrency')";
    mysqli_query($connection, $query);
    echo "Payment with transaction ID $newPaymentTransactionId processed and logged.\n";
  }
  else {
    echo "Payment with transaction ID $newPaymentTransactionId already processed.\n";
  }

  $chargeStatus = $payload['charge']['status']['value'];
  if ($chargeStatus === 'paid') {
    echo "Charge status updated: Paid\n";
  }
  elseif ($chargeStatus === 'overpaid' || $chargeStatus === 'underpaid') {
    $overpaidValue = $payload['charge']['status']['context']['value'];
    echo "Charge status updated: " . ucfirst($chargeStatus) . ". Over/underpaid amount: $overpaidValue\n";
  }
}

$payload = json_decode(file_get_contents('php://input'), true);
$receivedToken = $_SERVER['HTTP_VERIFICATION_TOKEN'];

if ($receivedToken === $YOUR_SECRET_KEY) {
  if (isPaymentValid($payload)) {
    updateDatabase($payload);
    echo "Webhook received, verified, and payment processed\n";
  }
  else {
    http_response_code(400);
    echo "Payment already processed or invalid\n";
  }
}
else {
  http_response_code(403);
  echo "Webhook verification failed\n";
}

mysqli_close($connection);

?>

Please replace 'your_secret_key', 'your_mysql_user', 'your_mysql_password', and 'your_database_name' with the appropriate values.

Go and Mongodb Example

Here is an example implementation in Go programming language. please adjust it to meet your project goals.

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/gorilla/mux"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

const (
    yourSecretKey = "your_secret_key"
    mongoURI      = "mongodb://localhost:27017"
    databaseName  = "your_database_name"
    collectionName = "payments"
)

type Payment struct {
    TransactionID string    `bson:"transaction_id"`
    Network       string    `bson:"network"`
    Status        string    `bson:"status"`
    Timestamp     time.Time `bson:"timestamp"`
    Value         struct {
        Local struct {
            Amount   float64 `bson:"amount"`
            Currency string  `bson:"currency"`
        } `bson:"local"`
        Crypto struct {
            Amount   float64 `bson:"amount"`
            Currency string  `bson:"currency"`
        } `bson:"crypto"`
    } `bson:"value"`
}

func isPaymentProcessed(transactionID string, collection *mongo.Collection) bool {
    filter := bson.M{"transaction_id": transactionID}
    count, err := collection.CountDocuments(nil, filter)
    if err != nil {
        log.Println("Error checking payment:", err)
        return false
    }
    return count > 0
}

func updateDatabase(payload map[string]interface{}, collection *mongo.Collection) {
    payment := payload["new_payment"].(map[string]interface{})
    transactionID := payment["transaction_id"].(string)

    if !isPaymentProcessed(transactionID, collection) {
        _, err := collection.InsertOne(nil, payment)
        if err != nil {
            log.Println("Error inserting payment:", err)
            return
        }
        fmt.Printf("Payment with transaction ID %s processed and logged.\n", transactionID)
    } else {
        fmt.Printf("Payment with transaction ID %s already processed.\n", transactionID)
    }

    chargeStatus := payload["charge"].(map[string]interface{})["status"].(map[string]interface{})["value"].(string)
    if chargeStatus == "paid" {
        fmt.Println("Charge status updated: Paid")
    } else if chargeStatus == "overpaid" || chargeStatus == "underpaid" {
        overpaidValue := payload["charge"].(map[string]interface{})["status"].(map[string]interface{})["context"].(map[string]interface{})["value"].(float64)
        fmt.Printf("Charge status updated: %s. Over/underpaid amount: %f\n", chargeStatus, overpaidValue)
    }
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    receivedToken := r.Header.Get("Verification-Token")

    if receivedToken == yourSecretKey {
        var payload map[string]interface{}
        decoder := json.NewDecoder(r.Body)
        if err := decoder.Decode(&payload); err != nil {
            http.Error(w, "Invalid payload", http.StatusBadRequest)
            return
        }

        client, err := mongo.Connect(nil, options.Client().ApplyURI(mongoURI))
        if err != nil {
            log.Fatal(err)
        }
        defer client.Disconnect(nil)

        db := client.Database(databaseName)
        collection := db.Collection(collectionName)

        if isPaymentValid(payload) {
            updateDatabase(payload, collection)
            w.WriteHeader(http.StatusOK)
            fmt.Fprintln(w, "Webhook received, verified, and payment processed")
        } else {
            w.WriteHeader(http.StatusBadRequest)
            fmt.Fprintln(w, "Payment already processed or invalid")
        }
    } else {
        w.WriteHeader(http.StatusForbidden)
        fmt.Fprintln(w, "Webhook verification failed")
    }
}

func isPaymentValid(payload map[string]interface{}) bool {
    // Implement payment validation logic here based on payload
    return true
}

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/webhook", webhookHandler).Methods("POST")

    serverAddr := "localhost:3000"
    fmt.Printf("Webhook server started on %s\n", serverAddr)
    log.Fatal(http.ListenAndServe(serverAddr, router))
}

Please replace "your_secret_key", "your_database_name", and implement the isPaymentValid function for your specific validation logic.

This Go script uses the Gorilla Mux router and the MongoDB Go driver for handling webhooks and interacting with the MongoDB database.

Feel free to use this code as a reference for handling webhooks and MongoDB operations in Go, and if you have any further questions or need additional assistance, please let me know!

This technical documentation provides a comprehensive guide to integrating 100Pay's webhooks into your application. It covers the importance of webhooks, security considerations, and step-by-step instructions along with example code snippets in JavaScript and Node.js. By following these guidelines, you can effectively receive and process webhook notifications, ensuring accurate payment updates and database management.

If you have any further questions or need assistance, feel free to reach out to our support team at [email protected].