NabooPay Webhook Integration Guide
This guide explains how to integrate NabooPay webhooks to receive real-time notifications about payment events.
Overview
Webhooks allow your application to receive automatic notifications when events occur in your NabooPay account. Instead of polling our API for updates, webhooks push data to your server in real-time.
Available Events
| Event | Description |
|---|---|
payment_status | Triggered when a payment status changes (e.g., payment completed) |
Setting Up Webhooks
1. Create a Webhook Endpoint
First, create an endpoint on your server to receive webhook notifications. Your endpoint must:
- Accept
POSTrequests - Accept
application/jsoncontent type - Return a
2xxstatus code to acknowledge receipt
2. Register Your Webhook
To create a webhook, go to https://platform.naboopay.com/parametre/ and then go to integration where you can create your webhook.
Webhook Payload
V2 Payload (Default for new webhooks)
When a payment event occurs, NabooPay sends a POST request to your webhook URL with the following payload:
{
"order_id": "order_123456",
"method_of_payment": ["wave", "orange_money"],
"selected_payment_method": "wave",
"amount": 10000,
"fees": 250,
"currency": "XOF",
"customer": {
"first_name": "John",
"last_name": "Doe",
"phone": "+221771234567",
"created_at": "2024-01-15T10:30:00Z"
},
"transaction_status": "completed",
"products": [
{
"name": "Product Name",
"price": 10000,
"quantity": 1,
"description": "Product description"
}
],
"is_escrow": false,
"is_merchant": false,
"fees_customer_side": true,
"success_url": "https://your-site.com/success",
"error_url": "https://your-site.com/error",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:35:00Z",
"paid_at": "2024-01-15T10:35:00Z"
}
V1 Payload (Legacy)
Older webhooks use a simplified payload:
{
"order_id": "order_123456",
"amount": 10000,
"currency": "XOF",
"created_at": "2024-01-15T10:30:00Z",
"transaction_status": "paid",
"payment_method": "wave"
}
Verifying Webhook Signatures
All webhook requests include an X-Signature header containing an HMAC SHA256 signature. Always verify this signature to ensure the request is authentic.
How Signature is Generated
- The JSON payload is serialized (compact format, no extra whitespace)
- An HMAC SHA256 hash is computed using your secret key
- The hash is hex-encoded
Verification Examples
Python
import hmac
import hashlib
import json
def verify_signature(payload: dict, signature: str, secret_key: str) -> bool:
"""Verify NabooPay webhook signature."""
# Serialize payload to compact JSON (no extra whitespace)
payload_bytes = json.dumps(payload, separators=(',', ':')).encode('utf-8')
# Compute expected signature
expected_signature = hmac.new(
secret_key.encode('utf-8'),
payload_bytes,
hashlib.sha256
).hexdigest()
# Compare signatures (timing-safe comparison)
return hmac.compare_digest(signature, expected_signature)
# Usage in Flask
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET_KEY = "your-secret-key-min-16-chars"
@app.route('/webhooks/naboopay', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Signature')
payload = request.get_json()
if not verify_signature(payload, signature, SECRET_KEY):
return jsonify({"error": "Invalid signature"}), 401
# Process the webhook
order_id = payload.get('order_id')
status = payload.get('transaction_status')
print(f"Payment {order_id} status: {status}")
return jsonify({"status": "received"}), 200
Node.js
const crypto = require('crypto');
function verifySignature(payload, signature, secretKey) {
// Serialize payload to compact JSON
const payloadString = JSON.stringify(payload);
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', secretKey)
.update(payloadString)
.digest('hex');
// Compare signatures (timing-safe comparison)
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Usage in Express
const express = require('express');
const app = express();
const SECRET_KEY = 'your-secret-key-min-16-chars';
app.post('/webhooks/naboopay', express.json(), (req, res) => {
const signature = req.headers['x-signature'];
const payload = req.body;
if (!verifySignature(payload, signature, SECRET_KEY)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the webhook
const { order_id, transaction_status } = payload;
console.log(`Payment ${order_id} status: ${transaction_status}`);
res.status(200).json({ status: 'received' });
});
PHP
<?php
function verifySignature($payload, $signature, $secretKey) {
// Serialize payload to compact JSON
$payloadString = json_encode($payload, JSON_UNESCAPED_SLASHES);
// Compute expected signature
$expectedSignature = hash_hmac('sha256', $payloadString, $secretKey);
// Compare signatures (timing-safe comparison)
return hash_equals($expectedSignature, $signature);
}
// Usage
$secretKey = 'your-secret-key-min-16-chars';
$payload = json_decode(file_get_contents('php://input'), true);
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
if (!verifySignature($payload, $signature, $secretKey)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Process the webhook
$orderId = $payload['order_id'];
$status = $payload['transaction_status'];
error_log("Payment $orderId status: $status");
http_response_code(200);
echo json_encode(['status' => 'received']);
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
)
func verifySignature(payload []byte, signature, secretKey string) bool {
h := hmac.New(sha256.New, []byte(secretKey))
h.Write(payload)
expectedSignature := hex.EncodeToString(h.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expectedSignature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
secretKey := "your-secret-key-min-16-chars"
signature := r.Header.Get("X-Signature")
body, _ := io.ReadAll(r.Body)
if !verifySignature(body, signature, secretKey) {
http.Error(w, `{"error":"Invalid signature"}`, http.StatusUnauthorized)
return
}
var payload map[string]interface{}
json.Unmarshal(body, &payload)
// Process the webhook
orderId := payload["order_id"].(string)
status := payload["transaction_status"].(string)
log.Printf("Payment %s status: %s", orderId, status)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"received"}`))
}
Retry Behavior
If your webhook endpoint fails to respond with a 2xx status code, NabooPay will retry the delivery:
- Retry attempts: Up to 3 retries
- Retry interval: 5 minutes between each retry
- Timeout: 5 seconds per request
After all retry attempts are exhausted, the webhook delivery is considered failed.
Best Practices
-
Always verify signatures - Never process webhooks without verifying the signature first.
-
Respond quickly - Return a
2xxresponse as fast as possible. Process the webhook asynchronously if needed. -
Handle duplicates - Your endpoint should be idempotent. The same webhook might be delivered multiple times.
-
Use HTTPS - Always use HTTPS for your webhook endpoint to ensure data is encrypted in transit.
-
Store your secret securely - Keep your webhook secret key in environment variables or a secrets manager.
-
Log webhook events - Keep logs of received webhooks for debugging and auditing purposes.
Troubleshooting
Webhook not received
- Verify your endpoint is publicly accessible
- Check that your URL is correct and uses HTTPS
- Ensure your server returns a
2xxstatus code - Check your firewall settings
Invalid signature
- Ensure you're using the correct secret key
- Verify you're serializing the payload as compact JSON (no extra whitespace)
- Check that you're reading the raw request body, not a parsed version
Test webhook with the API
Use the test button in https://platform.naboopay.com/parametre/