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)
}
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.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon