Webhook Handling

GwalletPay sends webhook notifications to your server whenever an invoice event occurs. This allows you to receive real-time updates without polling the API.

What are Webhooks?

Webhooks are HTTP callbacks triggered when an event occurs in the system.

GwalletPay sends a POST request to your configured endpoint whenever:

  • Payment is completed
  • Payment fails
  • Invoice status changes

๐ŸŒ Webhook Endpoint

Provide a public URL to receive events:

Example:

https://yourdomain.com/webhook/gwalletpay

Webhook Request Format

All webhooks are sent as HTTP POST requests:

Headers:

HeaderDescription
x-gwalletpay-signatureHMAC SHA256 signature of the payload (used to verify authenticity)
x-gwalletpay-timestampTimestamp when webhook was generated (helps prevent replay attacks)
{
  "input_amount": "100",
  "input_currency": "USD",
  "invoice_id": "inv_12345",
  "paid_amount": "100",
  "payment_currency": "TRX",
  "status": "Settled",
  "merchant_order_id": "ORD-98765"
}



๐Ÿ”‘ Security (Important)

To ensure requests are coming from GwalletPay:

  • Validate the request signature
  • Use a secret key to verify payload
  • Reject unknown requests
โš ๏ธ

Never trust webhook data without verification.


Steps to Handle a Webhook

  1. Receive the webhook

    Your server should expose a POST endpoint (e.g., /api/webhook/gwalletpay)

  2. Verify the signature

    Use the x-gwalletpay-signature ย header and your WEBHOOK_SECRET to compute your own signature and compare. If they donโ€™t match โ†’ reject the request.

    const crypto = require("crypto");
    
    function verifySignature(payload, signature, secret) {
      const hash = crypto
        .createHmac("sha256", secret)
        .update(JSON.stringify(payload))
        .digest("hex");
      return hash === signature;
    }
    
    โš ๏ธ

    Reject requests with invalid signature.

  3. Validate the payload

    • Ensure input_amount matches your order amount.
    • Ensure input_currency matches what you expected.
    • Check merchant_order_id matches an existing order in your DB.

4. Update your database

    • If status === "Settled" โ†’ mark the order as PAID / COMPLETED.
    • If status === "Failed" or status === "Expired" โ†’ mark as FAILED.
  1. Respond quickly

    Always return 200 OK after processing. gwalletpay will retry if your server is down or takes too long.

๐Ÿ” Retry Policy

If your server does not respond with HTTP 200:

  • Webhook will be retried automatically
  • Multiple retry attempts will be madeย 

โš ๏ธ Ensure your endpoint is always available.


๐Ÿง  Idempotency (Duplicate Handling)

Webhooks may be delivered more than once.

๐Ÿ‘‰ Always:

  • Check payment_id or invoice_id
  • Avoid processing the same event twice

๐Ÿš€ Webhook Example

This example shows how to handle gwalletpay webhooks .

import express from "express";
import crypto from "crypto";

const app = express();
app.use(express.json());

// Your webhook secret from gwalletpay Dashboard
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

/**
 * Verify gwalletpay signature
 */
function verifySignature(payload, signature, secret) {
  const hash = crypto
    .createHmac("sha256", secret)
    .update(JSON.stringify(payload))
    .digest("hex");

  return hash === signature;
}

app.post("/webhook/gwalletpay", async (req, res) => {
  try {
    const signature = req.headers["x-gwalletpay-signature"];
    const timestamp = req.headers["x-gwalletpay-timestamp"];
    const payload = req.body;

    // 1. Ensure secret is configured
    if (!WEBHOOK_SECRET) {
      return res.status(500).json({ message: "WEBHOOK_SECRET not configured" });
    }

    // 2. Verify signature
    if (!signature || !verifySignature(payload, signature, WEBHOOK_SECRET)) {
      return res.status(401).json({ message: "Invalid signature" });
    }

    // 3. Extract relevant fields
    const { input_amount, input_currency, merchant_order_id, status } = payload;

    // 4. Validate order in your database (pseudo-code)
    const order = await db.orders.findOne({ id: merchant_order_id });
    if (!order) {
      return res.status(404).json({ message: "Order not found" });
    }

    if (Number(order.amount) !== Number(input_amount) || input_currency !== "USD") {
      return res.status(400).json({ message: "Invalid order details" });
    }

    // 5. Update order status
    if (status === "Settled") {
      await db.orders.update(
        { id: merchant_order_id },
        { status: "COMPLETED" }
      );
    }

    // 6. Respond quickly (200 OK)
    return res.json({ message: "Webhook processed successfully" });
  } catch (err) {
    console.error("Webhook Error:", err);
    return res.status(500).json({ message: "Internal Server Error" });
  }
});

app.listen(3000, () => {
  console.log("โœ… Listening for gwalletpay webhooks on http://localhost:3000");
});
import express, { Request, Response } from "express";
import crypto from "crypto";

const app = express();
app.use(express.json());

// Your webhook secret from gwalletpay Dashboard
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET as string;

/**
 * Type definition for webhook payload
 */
interface gwalletpayWebhookPayload {
  input_amount: string;
  input_currency: string;
  invoice_id: string;
  paid_amount: string;
  payment_currency: string;
  status: "Settled" | "Failed" | "Expired" | string;
  merchant_order_id: string;
}

/**
 * Verify gwalletpay signature
 */
function verifySignature(payload: object, signature: string, secret: string): boolean {
  const hash = crypto
    .createHmac("sha256", secret)
    .update(JSON.stringify(payload))
    .digest("hex");

  return hash === signature;
}

app.post("/webhook/gwalletpay", async (req: Request, res: Response) => {
  try {
    const signature = req.headers["x-gwalletpay-signature"] as string | undefined;
    const timestamp = req.headers["x-gwalletpay-timestamp"] as string | undefined;
    const payload = req.body as gwalletpayWebhookPayload;

    // 1. Ensure secret is configured
    if (!WEBHOOK_SECRET) {
      return res.status(500).json({ message: "WEBHOOK_SECRET not configured" });
    }

    // 2. Verify signature
    if (!signature || !verifySignature(payload, signature, WEBHOOK_SECRET)) {
      return res.status(401).json({ message: "Invalid signature" });
    }

    // 3. Extract relevant fields
    const { input_amount, input_currency, merchant_order_id, status } = payload;

    // 4. Validate order in your database (pseudo-code)
    const order = await db.orders.findOne({ id: merchant_order_id }); // Replace with your DB query
    if (!order) {
      return res.status(404).json({ message: "Order not found" });
    }

    if (Number(order.amount) !== Number(input_amount) || input_currency !== "USD") {
      return res.status(400).json({ message: "Invalid order details" });
    }

    // 5. Update order status
    if (status === "Settled") {
      await db.orders.update(
        { id: merchant_order_id },
        { status: "COMPLETED" }
      );
    }

    // 6. Respond quickly (200 OK)
    return res.json({ message: "Webhook processed successfully" });
  } catch (err) {
    console.error("Webhook Error:", err);
    return res.status(500).json({ message: "Internal Server Error" });
  }
});

app.listen(3000, () => {
  console.log("โœ… Listening for gwalletpay webhooks on http://localhost:3000");
});
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::post('/webhook', function (Request $request) {
    $payload = $request->getContent();
    $signature = $request->header('x-gwalletpay-signature');
    $secret = env('gwalletpay_SECRET');

    // Verify HMAC-SHA256
    $expectedSignature = hash_hmac('sha256', $payload, $secret);

    if (!hash_equals($expectedSignature, $signature)) {
        return response('Invalid signature', 400);
    }

    $event = $request->input('event');
    $data = $request->input('data');

    if ($event === 'payment.finished') {
        \Log::info('โœ… Payment confirmed', $data);
        // Update order status
    }

    return response('Webhook received', 200);
});
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io/ioutil"
	"net/http"

	"github.com/gin-gonic/gin"
)

var gwalletpaySecret = "your_gwalletpay_secret"

type WebhookPayload struct {
	Event     string                 `json:"event"`
	Data      map[string]interface{} `json:"data"`
	Signature string                 `json:"signature"`
}

func verifySignature(payload []byte, signature string) bool {
	h := hmac.New(sha256.New, []byte(gwalletpaySecret))
	h.Write(payload)
	expected := hex.EncodeToString(h.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(signature))
}

func main() {
	r := gin.Default()
	r.POST("/webhook", func(c *gin.Context) {
		body, _ := ioutil.ReadAll(c.Request.Body)

		var payload WebhookPayload
		json.Unmarshal(body, &payload)

		signature := c.GetHeader("x-gwalletpay-signature")

		if !verifySignature(body, signature) {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid signature"})
			return
		}

		if payload.Event == "payment.finished" {
			// Process payment
			c.JSON(http.StatusOK, gin.H{"status": "Payment processed"})
			return
		}

		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})
	r.Run(":3000")
}

๐Ÿ”„ Recommended Flow

User pays invoice
   โ†“
GwalletPay sends webhook
   โ†“
Your server receives event
   โ†“
Update order status