Skip to content

cinetpay/cinetpay-go

Repository files navigation

CinetPay Go SDK

A complete Go SDK for the CinetPay payment API. Supports payments, transfers, balance inquiries, and webhook verification across multiple African countries.

Features

  • 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

Requirements

  • Go 1.21+

Installation

go get github.com/cinetpay/cinetpay-go

Quick Start

package 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)
}

API Reference

Client

// 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()

Payments

// 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)
}

Transfers

// 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

balance, err := client.Balance.Get(ctx, "CI")
fmt.Printf("Available: %s %s\n", balance.AvailableBalance, balance.Currency)

Webhooks

// 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)
}

Error Handling

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())
    }
}

Custom Token Store

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},
})

Custom Logger

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},
})

Constants

Currencies

CurrencyXOF, CurrencyXAF, CurrencyGNF, CurrencyCDF, CurrencyUSD

Channels

ChannelPush, ChannelOTP, ChannelQRCode

Payment Methods

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

Countries

CountryCI, CountryBF, CountryML, CountrySN, CountryTG, CountryGN, CountryCM, CountryBJ, CountryCD, CountryNE

Helper Functions

  • IsFinalStatus(status) — checks if a transaction status is terminal
  • APICodes — maps numeric API codes to TransactionStatus
  • PaymentMethodsByCountry — maps country codes to available payment methods

License

MIT

About

SDK Go pour l'API CinetPay v1 — paiements et transferts mobile money en Afrique. Zero dépendance, compatible net/http.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages