Handle Paystack Webhooks in Go
To handle Paystack webhooks in Go, create an HTTP handler that reads the raw body with io.ReadAll, computes an HMAC SHA-512 hash using crypto/hmac, compares it to the X-Paystack-Signature header with hmac.Equal, and processes the event. Return a 200 status.
Webhook Handler
import (
"crypto/hmac"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
signature := r.Header.Get("X-Paystack-Signature")
if signature == "" {
http.Error(w, "Missing signature", http.StatusBadRequest)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
mac := hmac.New(sha512.New, []byte(paystackSecret))
mac.Write(body)
expectedMAC := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expectedMAC), []byte(signature)) {
http.Error(w, "Invalid signature", http.StatusBadRequest)
return
}
var event struct {
Event string "json:\"event\""
Data json.RawMessage "json:\"data\""
}
json.Unmarshal(body, &event)
// Process async
go processEvent(event.Event, event.Data)
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func processEvent(eventType string, data json.RawMessage) {
switch eventType {
case "charge.success":
var txn struct {
Reference string "json:\"reference\""
}
json.Unmarshal(data, &txn)
if txn.Reference != "" {
if err := verifyAndFulfill(txn.Reference, db); err != nil {
log.Printf("Fulfillment error: %v", err)
}
}
case "subscription.create":
log.Println("New subscription created")
case "invoice.payment_failed":
log.Println("Invoice payment failed")
default:
log.Printf("Unhandled event: %s", eventType)
}
}
Route Registration
func main() {
http.HandleFunc("/api/pay", initializeHandler)
http.HandleFunc("/payment/callback", callbackHandler)
http.HandleFunc("/webhooks/paystack", webhookHandler)
log.Println("Server running on :8080")
http.ListenAndServe(":8080", nil)
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Testing
func TestWebhookHandler(t *testing.T) {
payload := []byte("{\"event\":\"charge.success\",\"data\":{\"reference\":\"test\"}}")
mac := hmac.New(sha512.New, []byte("sk_test_xxx"))
mac.Write(payload)
sig := hex.EncodeToString(mac.Sum(nil))
req := httptest.NewRequest("POST", "/webhooks/paystack", bytes.NewBuffer(payload))
req.Header.Set("X-Paystack-Signature", sig)
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
webhookHandler(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Expected 200, got %d", rr.Code)
}
}
Key Takeaways
- ✓Use io.ReadAll(r.Body) for the raw bytes needed for HMAC verification.
- ✓Go crypto/hmac and crypto/sha512 handle signature computation.
- ✓Use hmac.Equal for constant-time comparison to prevent timing attacks.
- ✓Launch event processing in a goroutine for async handling after returning 200.
- ✓Make handlers idempotent. Paystack retries failed deliveries.
- ✓Go is well-suited for high-throughput webhook processing.
Frequently Asked Questions
- Is goroutine processing safe for webhooks?
- Yes. Goroutines are lightweight and safe to spawn per request. The idempotent database update handles concurrency. Make sure your database operations are goroutine-safe.
- Should I use a message queue instead of goroutines?
- For simple processing, goroutines work well. For complex workflows or guaranteed delivery, use a message queue like NATS or RabbitMQ.
- How do I test webhooks locally?
- Use ngrok to expose your local server, or compute the HMAC in your test and use httptest for unit tests.
Learn Paystack Integration
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course