Webhook Configuration
Webhooks enable your application to receive real-time notifications about payment events. DVPay sends HTTP POST requests to your configured webhook endpoint whenever specific events occur.
Setup Your Webhook
Step 1: Create an Endpoint
Create a publicly accessible HTTPS endpoint in your application to receive webhook events.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
type WebhookPayload struct {
AccountName string `json:"accountName"`
AccountNumber string `json:"accountNumber"`
Amount float64 `json:"amount"`
AppID int64 `json:"appId"`
CreateTimeMilli int64 `json:"createTimeMilli"`
Currency string `json:"currency"`
OrderID int64 `json:"orderId"`
Status string `json:"status"`
TransactionID int64 `json:"transactionId"`
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
// Read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
// Parse webhook payload
var payload WebhookPayload
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Process the webhook event
switch payload.Status {
case "SUCCESS":
fmt.Printf("Payment successful - Order ID: %d, Transaction ID: %d\n", payload.OrderID, payload.TransactionID)
// Update your database, send confirmation email, etc.
case "FAILED":
fmt.Printf("Payment failed - Order ID: %d\n", payload.OrderID)
// Handle failed payment
case "REFUNDED":
fmt.Printf("Refund completed - Order ID: %d\n", payload.OrderID)
// Update order status
}
// Respond with 200 OK
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"received"}`))
}
func main() {
http.HandleFunc("/webhooks/dvpay", webhookHandler)
fmt.Println("Webhook server listening on :8080")
http.ListenAndServe(":8080", nil)
}
const express = require('express');
const app = express();
// Middleware to parse JSON
app.use(express.json());
app.post('/webhooks/dvpay', (req, res) => {
const payload = req.body;
console.log('Received webhook - Status:', payload.status);
// Process webhook events based on status
switch (payload.status) {
case 'SUCCESS':
console.log(`Payment successful - Order ID: ${payload.orderId}, Transaction ID: ${payload.transactionId}`);
console.log(`Account: ${payload.accountName} (${payload.accountNumber})`);
console.log(`Amount: ${payload.amount} ${payload.currency}`);
// Update your database, send confirmation email, etc.
break;
case 'FAILED':
console.log(`Payment failed - Order ID: ${payload.orderId}`);
// Handle failed payment
break;
case 'REFUNDED':
console.log(`Refund completed - Order ID: ${payload.orderId}`);
// Update order status
break;
default:
console.log(`Unknown status: ${payload.status}`);
}
// Always respond with 200 OK
res.status(200).json({ status: 'received' });
});
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Webhook server listening on port ${PORT}`);
});
Step 2: Configure in DVPay App
After creating your endpoint, register it in the DVPay merchant dashboard:
Open DVPay Mobile App
Navigate to your merchant dashboard in the DVPay mobile application.
Go to API Settings
Tap on Settings → API Configuration → Webhook Settings
Enter Your Webhook URL
Input your publicly accessible HTTPS endpoint (e.g., https://yourdomain.com/webhooks/dvpay)
Select Event Types
Choose which events you want to receive:
payment.success- Payment completed successfullypayment.failed- Payment attempt failedpayment.pending- Payment is awaiting confirmationrefund.completed- Refund processed successfullyorder.cancelled- Order was cancelled
Save Configuration
Click Save to activate your webhook endpoint.
Webhook Status Values
DVPay webhooks include a status field with the following possible values:
Webhook Events
payment.success, payment.failed, refund.completed) will be available in a future release. Currently, all webhooks use the status field to indicate transaction state.Payload Structure
All webhook events follow this JSON structure:
{
"accountName": "testPg0",
"accountNumber": "838825694",
"amount": 0.05,
"appId": 9328657108474192,
"createTimeMilli": 1772453630058,
"currency": "USD",
"orderId": 779539349308101,
"status": "SUCCESS",
"transactionId": 779539365712584
}
USD or KHR)SUCCESS, FAILED, PENDING, REFUNDED, CANCELLED)Security Best Practices
Important: Always verify the X-Signature header to ensure webhooks are sent by DVPay and haven't been tampered with.
Signature Formula:
message = rawPayload + timestamp (in seconds)
signature = HMAC-SHA256(message, api_secret)
Golang Example:
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
)
func verifyWebhookSignature(rawPayload, timestamp, signature, apiSecret string) bool {
// Create message: rawPayload + timestamp
message := rawPayload + timestamp
// Generate expected signature
h := hmac.New(sha256.New, []byte(apiSecret))
h.Write([]byte(message))
expectedSignature := hex.EncodeToString(h.Sum(nil))
// Compare signatures (constant-time comparison)
return hmac.Equal([]byte(signature), []byte(expectedSignature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
// Read raw body
body, _ := io.ReadAll(r.Body)
rawPayload := string(body)
// Parse payload
var payload WebhookPayload
json.Unmarshal(body, &payload)
// Get signature header
receivedSignature := r.Header.Get("X-Signature")
if receivedSignature == "" {
http.Error(w, "Missing X-Signature header", http.StatusUnauthorized)
return
}
// Convert timestamp to seconds
timestamp := strconv.FormatInt(payload.CreateTimeMilli/1000, 10)
// Verify signature
apiSecret := os.Getenv("DVPAY_API_SECRET")
if !verifyWebhookSignature(rawPayload, timestamp, receivedSignature, apiSecret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Signature verified - process webhook
// ...
}
Node.js Example:
const crypto = require('crypto');
function verifyWebhookSignature(rawPayload, timestamp, signature, apiSecret) {
// Create message: rawPayload + timestamp
const message = rawPayload + timestamp.toString();
// Generate expected signature
const hmac = crypto.createHmac('sha256', apiSecret);
hmac.update(message);
const expectedSignature = hmac.digest('hex');
// Compare signatures (constant-time comparison)
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Use raw body parser
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));
app.post('/webhooks/dvpay', (req, res) => {
const rawPayload = req.rawBody;
const payload = req.body;
// Get signature header
const receivedSignature = req.headers['x-signature'];
if (!receivedSignature) {
return res.status(401).json({ error: 'Missing X-Signature header' });
}
// Convert timestamp to seconds
const timestamp = Math.floor(payload.createTimeMilli / 1000);
// Verify signature
const apiSecret = process.env.DVPAY_API_SECRET;
if (!verifyWebhookSignature(rawPayload, timestamp, receivedSignature, apiSecret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Signature verified - process webhook
// ...
});
Store processed webhook event IDs to prevent duplicate processing:
const processedEvents = new Set();
if (processedEvents.has(payload.orderId)) {
return res.status(200).json({ status: 'already_processed' });
}
processedEvents.add(payload.orderId);
// Process the event...
Respond with 200 OK within 5 seconds. Process heavy tasks asynchronously:
// Immediately respond
res.status(200).json({ status: 'received' });
// Process asynchronously
processWebhookAsync(payload);
Testing Webhooks
Local Development with ngrok
Expose your local server for webhook testing:
# Install ngrok
npm install -g ngrok
# Start your local server
node server.js
# Expose port 8080
ngrok http 8080
Use the generated ngrok URL (e.g., https://abc123.ngrok.io/webhooks/dvpay) in your webhook configuration.
Manual Testing
Test your webhook endpoint manually:
curl -X POST https://yourdomain.com/webhooks/dvpay \
-H "Content-Type: application/json" \
-d '{
"accountName": "testPg0",
"accountNumber": "838825694",
"amount": 0.05,
"appId": 9328657108474192,
"createTimeMilli": 1772453630058,
"currency": "USD",
"orderId": 779539349308101,
"status": "SUCCESS",
"transactionId": 779539365712584
}'
Retry Logic
If your webhook endpoint returns an error or doesn't respond within 5 seconds, DVPay will retry:
- 1st retry: After 1 minute
- 2nd retry: After 10 minutes
- 3rd retry: After 1 hour
- Final retry: After 6 hours
Troubleshooting
Common causes:
- Endpoint is not publicly accessible
- Using HTTP instead of HTTPS
- Firewall blocking DVPay IP addresses
- Server returning non-200 response
Solution: Test your endpoint with curl and check server logs
Cause: Network issues or slow response times trigger retries
Solution: Implement idempotency using event IDs or order IDs
Cause: Processing takes longer than 5 seconds
Solution: Respond immediately with 200 OK, then process asynchronously