A complete Go SDK for the CinetPay payment API. Supports payments, transfers, balance inquiries, and webhook verification across multiple African countries.
- Zero external dependencies — only Go standard library (
net/http,encoding/json,crypto/subtle,sync, etc.) - Multi-country credentials — API key + password per country (CI, SN, BF, ML, etc.)
- Auto-detect environment — sandbox (
sk_test_) vs production (sk_live_) from API key prefix - JWT token caching — thread-safe with TTL and singleflight stampede protection
- Automatic token refresh — transparent retry on expired tokens (code 1003)
- Input validation — validates all fields before sending to the API
- Webhook verification — timing-safe token comparison via
crypto/subtle - SSRF protection — only allows known CinetPay domains
- HTTPS enforcement — rejects non-HTTPS base URLs (except localhost)
- Structured logging — via
log/slog(injectable logger interface) - Type-safe constants — currencies, channels, payment methods, statuses
- Context support — all API methods accept
context.Context
- Go 1.21+
go get github.com/cinetpay/cinetpay-gopackage main
import (
"context"
"fmt"
"log"
cinetpay "github.com/cinetpay/cinetpay-go"
)
func main() {
client, err := cinetpay.NewClient(cinetpay.Config{
Credentials: map[string]cinetpay.CountryCredentials{
"CI": {
APIKey: "sk_test_your_key_here",
APIPassword: "your_password_here",
},
},
Debug: true, // Enable debug logging
})
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// Initialize a payment
payment, err := client.Payment.Initialize(ctx, &cinetpay.PaymentRequest{
Currency: cinetpay.CurrencyXOF,
MerchantTransactionID: "ORDER-001",
Amount: 1000,
Lang: "fr",
Designation: "Online purchase",
ClientEmail: "customer@example.com",
ClientFirstName: "Jean",
ClientLastName: "Dupont",
SuccessURL: "https://yoursite.com/success",
FailedURL: "https://yoursite.com/failed",
NotifyURL: "https://yoursite.com/webhook",
Channel: cinetpay.ChannelPush,
}, "CI")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Payment URL: %s\n", payment.PaymentURL)
fmt.Printf("Payment Token: %s\n", payment.PaymentToken)
// Check payment status
status, err := client.Payment.GetStatus(ctx, payment.PaymentToken, "CI")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Status: %s\n", status.Status)
}// Create a client
client, err := cinetpay.NewClient(cinetpay.Config{
Credentials: map[string]cinetpay.CountryCredentials{
"CI": {APIKey: "sk_test_...", APIPassword: "..."},
"SN": {APIKey: "sk_test_...", APIPassword: "..."},
},
BaseURL: "", // Auto-detected from key prefix
TokenTTL: 23 * time.Hour, // Default: 82800s
Timeout: 30 * time.Second, // Default: 30s
Debug: true, // Enable debug logging
})
// List configured countries
countries := client.Countries() // ["CI", "SN"]
// Revoke cached tokens
client.RevokeToken("CI")
client.RevokeAllTokens()// Initialize a payment
resp, err := client.Payment.Initialize(ctx, &cinetpay.PaymentRequest{
Currency: cinetpay.CurrencyXOF,
MerchantTransactionID: "ORDER-001",
Amount: 1000,
Lang: "fr",
Designation: "Online purchase",
ClientEmail: "customer@example.com",
ClientFirstName: "Jean",
ClientLastName: "Dupont",
SuccessURL: "https://yoursite.com/success",
FailedURL: "https://yoursite.com/failed",
NotifyURL: "https://yoursite.com/webhook",
Channel: cinetpay.ChannelPush,
PaymentMethod: cinetpay.PaymentMethodOMCI, // Optional
DirectPay: false, // Optional
}, "CI")
// Check payment status
status, err := client.Payment.GetStatus(ctx, "ORDER-001", "CI")
if cinetpay.IsFinalStatus(status.Status) {
fmt.Println("Transaction complete:", status.Status)
}// Create a transfer
resp, err := client.Transfer.Create(ctx, &cinetpay.TransferRequest{
Currency: cinetpay.CurrencyXOF,
MerchantTransactionID: "TRANSFER-001",
PhoneNumber: "+2250707000001",
Amount: 5000,
PaymentMethod: cinetpay.PaymentMethodOMCI,
Reason: "Customer refund",
NotifyURL: "https://yoursite.com/webhook",
}, "CI")
// Check transfer status
status, err := client.Transfer.GetStatus(ctx, resp.TransactionID, "CI")balance, err := client.Balance.Get(ctx, "CI")
fmt.Printf("Available: %s %s\n", balance.AvailableBalance, balance.Currency)// In your HTTP handler
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
// Parse the notification
notification, err := cinetpay.ParseNotification(body)
if err != nil {
http.Error(w, "Invalid payload", 400)
return
}
// Verify authenticity (timing-safe comparison)
if !cinetpay.VerifyNotification(storedNotifyToken, notification.NotifyToken) {
http.Error(w, "Invalid token", 401)
return
}
// Process the notification
fmt.Printf("Transaction %s: %s\n", notification.TransactionID, notification.MerchantTransactionID)
}All errors returned by the SDK are typed:
import "errors"
resp, err := client.Payment.Initialize(ctx, req, "CI")
if err != nil {
var apiErr *cinetpay.ApiError
if errors.As(err, &apiErr) {
// API returned an error
fmt.Printf("API Error [%d] %s: %s\n", apiErr.ApiCode, apiErr.ApiStatus, apiErr.Description)
}
var authErr *cinetpay.AuthenticationError
if errors.As(err, &authErr) {
// Authentication failed
}
var netErr *cinetpay.NetworkError
if errors.As(err, &netErr) {
// Network failure (DNS, connection, timeout)
}
var valErr *cinetpay.ValidationError
if errors.As(err, &valErr) {
// Request validation failed
fmt.Printf("Field %s: %s\n", valErr.Field, valErr.Error())
}
}Implement the TokenStore interface for Redis, memcached, etc.:
type RedisTokenStore struct {
client *redis.Client
}
func (s *RedisTokenStore) Get(key string) (string, error) {
val, err := s.client.Get(ctx, key).Result()
if err == redis.Nil {
return "", nil
}
return val, err
}
func (s *RedisTokenStore) Set(key, value string, ttl time.Duration) error {
return s.client.Set(ctx, key, value, ttl).Err()
}
func (s *RedisTokenStore) Delete(key string) error {
return s.client.Del(ctx, key).Err()
}
client, _ := cinetpay.NewClient(cinetpay.Config{
Credentials: creds,
TokenStore: &RedisTokenStore{client: redisClient},
})Implement the Logger interface:
type ZapLogger struct {
logger *zap.Logger
}
func (l *ZapLogger) Debug(msg string, args ...any) { l.logger.Sugar().Debugw(msg, args...) }
func (l *ZapLogger) Warn(msg string, args ...any) { l.logger.Sugar().Warnw(msg, args...) }
func (l *ZapLogger) Error(msg string, args ...any) { l.logger.Sugar().Errorw(msg, args...) }
client, _ := cinetpay.NewClient(cinetpay.Config{
Credentials: creds,
Logger: &ZapLogger{logger: zapLogger},
})CurrencyXOF, CurrencyXAF, CurrencyGNF, CurrencyCDF, CurrencyUSD
ChannelPush, ChannelOTP, ChannelQRCode
PaymentMethodOMCI, PaymentMethodMOOVCI, PaymentMethodMTNCI, PaymentMethodWAVECI,
PaymentMethodOMBF, PaymentMethodMOOVBF, PaymentMethodWAVEBF,
PaymentMethodOMML, PaymentMethodMOOVML,
PaymentMethodOMSN, PaymentMethodFREESN, PaymentMethodEXPRSN, PaymentMethodWAVESN,
PaymentMethodMOOVTG, PaymentMethodTMONTG,
PaymentMethodOMGN, PaymentMethodMTNGN,
PaymentMethodOMCM, PaymentMethodMTNCM,
PaymentMethodMOOVBJ, PaymentMethodMTNBJ,
PaymentMethodOMCD, PaymentMethodAIRTELCD, PaymentMethodMPESACD, PaymentMethodAFRICELCD,
PaymentMethodAIRTELNE, PaymentMethodMOOVNE, PaymentMethodZAMANINE
CountryCI, CountryBF, CountryML, CountrySN, CountryTG, CountryGN, CountryCM, CountryBJ, CountryCD, CountryNE
IsFinalStatus(status)— checks if a transaction status is terminalAPICodes— maps numeric API codes toTransactionStatusPaymentMethodsByCountry— maps country codes to available payment methods
MIT