Webhooks allow JamsrPay to notify your application when a payment status changes (e.g., pending β success
).
Every time a payment is updated, JamsrPay will send a POST request to the webhook URL you set in your dashboard.
You can also check our Prebuilt Webhook Examples on GitHub
π¦ Example Webhook Payload
{
"input_amount": "100",
"input_currency": "USD",
"invoice_id": "inv_12345",
"paid_amount": "100",
"payment_currency": "TRX",
"status": "Settled",
"merchant_order_id": "ORD-98765"
}
π Important Headers
Header | Description |
---|---|
x-jamsrpay-signature | HMAC SHA256 signature of the payload (used to verify authenticity). |
x-jamsrpay-timestamp | Timestamp when webhook was generated (helps prevent replay attacks). |
β
Steps to Handle a Webhook
- Receive the webhook
Your server should expose aPOST
endpoint (e.g.,/api/webhook/jamsrpay
). - Verify the signature
Use thex-jamsrpay-signature
header and yourWEBHOOK_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 computed = crypto .createHmac("sha256", secret) .update(JSON.stringify(payload)) .digest("hex"); return computed === signature; }
- 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.
- Ensure
- Update your database
- If
status === "Settled"
β mark the order as PAID / COMPLETED. - If
status === "Failed"
orstatus === "Expired"
β mark as FAILED.
- If
- Respond quickly
Always return200 OK
after processing. JamsrPay will retry if your server is down or takes too long.
π Best Practices
- Store your WEBHOOK_SECRET securely in environment variables.
- Always verify signature before trusting the webhook.
- Respond within success status to avoid retries.
- Keep webhook handling idempotent (safe to run multiple times).
π Webhook Example
This example shows how to handle JamsrPay webhooks .
import express from "express";
import crypto from "crypto";
const app = express();
app.use(express.json());
// Your webhook secret from JamsrPay Dashboard
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
/**
* Verify JamsrPay signature
*/
function verifySignature(payload, signature, secret) {
const computed = crypto
.createHmac("sha256", secret)
.update(JSON.stringify(payload))
.digest("hex");
return computed === signature;
}
app.post("/webhook/jamsrpay", async (req, res) => {
try {
const signature = req.headers["x-jamsrpay-signature"];
const timestamp = req.headers["x-jamsrpay-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 JamsrPay 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 JamsrPay Dashboard
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET as string;
/**
* Type definition for webhook payload
*/
interface JamsrPayWebhookPayload {
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 JamsrPay signature
*/
function verifySignature(payload: object, signature: string, secret: string): boolean {
const computed = crypto
.createHmac("sha256", secret)
.update(JSON.stringify(payload))
.digest("hex");
return computed === signature;
}
app.post("/webhook/jamsrpay", async (req: Request, res: Response) => {
try {
const signature = req.headers["x-jamsrpay-signature"] as string | undefined;
const timestamp = req.headers["x-jamsrpay-timestamp"] as string | undefined;
const payload = req.body as JamsrPayWebhookPayload;
// 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 JamsrPay 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-jamsrpay-signature');
$secret = env('JAMSRPAY_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 jamsrPaySecret = "your_jamsrpay_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(jamsrPaySecret))
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-jamsrpay-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")
}