onceo-core — Open-source payment webhook reconciliation for African providers (Paystack, Flutterwave, OPay, M-Pesa). Exactly-once delivery, HMAC verification, idempotent stores.
# onceo-core
> **one clean payment event, every rail, exactly once.**
A thin, auditable reconciliation layer for payment webhooks from African payment providers. It sits on top of rails you already use — it never becomes a payment processor itself.
---
## Supported Providers
| Provider | Body Integrity | Status |
|----------|---------------|--------|
| Paystack | HMAC-SHA512 over body | Shipped |
| Flutterwave | **None** — see warning below | Shipped |
| OPay | HMAC-SHA512 over body | Shipped |
| M-Pesa (Daraja) | Pre-shared callback token (constant-time cmp) | Shipped |
| Svix | HMAC-SHA256 over `svix-id.svix-timestamp.body` | Shipped |
| Bachs | HMAC-SHA256 over `timestamp.body` (hex) | Shipped |
## Installation
```
go get
github.com
```
## Quick Start
```go
package main
import (
"errors"
"io"
"net/http"
"os"
onceo "
github.com"
"
github.com"
)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Bound the body before reading: onceo.Process rejects payloads over
// MaxBodySize (1 MiB), but you must not hand it an unbounded blob first.
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, onceo.MaxBodySize))
if err != nil {
http.Error(w, "request body too large", http.StatusRequestEntityTooLarge)
return
}
provider, _ := paystack.New(os.Getenv("PAYSTACK_SECRET_KEY"))
store := onceo.NewMemoryStore() // or store/redis
event, err := onceo.Process(ctx, provider, store, r.Header, body)
if errors.Is(err, onceo.ErrInvalidSignature) {
http.Error(w, "invalid signature", http.StatusForbidden)
return
}
if errors.Is(err, onceo.ErrDuplicateEvent) {
w.WriteHeader(http.StatusOK) // idempotent: already processed
return
}
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
// event is the canonical, normalized payment event
switch event.Status {
case onceo.StatusSuccess:
fulfillOrder(event.Reference, event.AmountMinor, …