Overview
Paypack is a payment gateway that enables businesses to accept mobile money payments in Rwanda. The integration supports MTN Mobile Money, Airtel Money, and other providers through a unified API.
This integration is optional. Only configure Paypack if you need to process payments in Rwanda.
Prerequisites
- A Paypack merchant account - Sign up at paypack.rw
- API credentials (Client ID and Client Secret)
- Webhook URL configured in your Paypack dashboard (for production)
Configuration
Add the following environment variables to your .env file:
PAYPACK_CLIENT_ID=your_client_id_here
PAYPACK_CLIENT_SECRET=your_client_secret_here
These credentials are accessed via the configs package:
configs.GetPaypackId() // Returns PAYPACK_CLIENT_ID
configs.GetPaypackSecret() // Returns PAYPACK_CLIENT_SECRET
Available Functions
Authentication
func Authenticate() (string, error)
Authenticates with the Paypack API and returns an access token. This function is called automatically by other functions.
Example:
import "backend/integrations"
token, err := integrations.Authenticate()
if err != nil {
log.Printf("Authentication failed: %v", err)
return err
}
// Token is valid for the duration specified in the response
Cash-In (Receive Payment)
func PaypackCashIn(amount int, number string) (CashinResponseParams, error)
Initiates a payment request from a customer’s mobile money account.
Parameters:
amount - Amount in Rwandan Francs (integer, e.g., 1000 for 1000 RWF)
number - Customer’s phone number (will be converted to local format automatically)
Returns:
CashinResponseParams with fields:
Ref - Transaction reference for tracking
Status - Initial transaction status
Amount - Transaction amount
Kind - Transaction type
CreatedAt - Timestamp
Example:
// Request 5000 RWF from a customer
response, err := integrations.PaypackCashIn(5000, "0788123456")
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Payment initiation failed",
})
}
// Store the transaction reference
txRef := response.Ref
log.Printf("Transaction initiated: %s", txRef)
Phone numbers are automatically converted to local format using the validate_rw_phone_numbers package. You can pass numbers in any format (e.g., “0788123456” or “+250788123456”).
Cash-Out (Send Money)
func PaypackCashOut(amount int, number string) (CashoutResponseParams, error)
Sends money from your merchant account to a mobile money account.
Parameters:
amount - Amount in Rwandan Francs
number - Recipient’s phone number
Returns:
CashoutResponseParams with transaction details
Example:
// Send 10000 RWF to a user
response, err := integrations.PaypackCashOut(10000, "0788123456")
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Payout failed",
})
}
log.Printf("Money sent: %s (status: %s)", response.Ref, response.Status)
Transaction Status Polling
func PollTransactionStatus(ref string) (PollResponseParams, error)
Checks the current status of a transaction using its reference.
Parameters:
ref - Transaction reference from cash-in or cash-out response
Returns:
PollResponseParams with fields:
Status - Current transaction status (“pending”, “successful”, “failed”)
Amount - Transaction amount
Fee - Transaction fee
Client - Customer phone number
Merchant - Your merchant ID
Timestamp - Last update time
Example:
// Poll transaction status
status, err := integrations.PollTransactionStatus("tx_abc123xyz")
if err != nil {
log.Printf("Failed to poll status: %v", err)
}
switch status.Status {
case "successful":
// Update order status, deliver product, etc.
log.Printf("Payment successful: %d RWF", status.Amount)
case "failed":
// Handle failed payment
log.Printf("Payment failed")
case "pending":
// Still waiting for customer to confirm
log.Printf("Payment pending")
}
Webhook Handling
Paypack sends webhook notifications when transaction status changes. The webhook payload structure:
type PaypackWebhook struct {
EventID string `json:"event_id"`
EventKind string `json:"event_kind"`
CreatedAt time.Time `json:"created_at"`
Data WebhookData `json:"data"`
}
type WebhookData struct {
Ref string `json:"ref"`
Kind string `json:"kind"`
Fee float64 `json:"fee"`
Merchant string `json:"merchant"`
Client string `json:"client"`
Amount int `json:"amount"`
Provider string `json:"provider"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
ProcessedAt time.Time `json:"processed_at"`
}
Example webhook handler:
func HandlePaypackWebhook(c echo.Context) error {
var webhook integrations.PaypackWebhook
if err := c.Bind(&webhook); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid webhook payload",
})
}
// Process based on event type
switch webhook.EventKind {
case "transaction.processed":
if webhook.Data.Status == "successful" {
// Update order, send confirmation, etc.
log.Printf("Payment confirmed: %s", webhook.Data.Ref)
}
}
return c.NoContent(http.StatusOK)
}
Complete Payment Flow Example
package main
import (
"backend/integrations"
"github.com/labstack/echo/v4"
"log"
"net/http"
)
func InitiatePayment(c echo.Context) error {
// 1. Get payment details from request
amount := 5000
phoneNumber := "0788123456"
// 2. Initiate payment
response, err := integrations.PaypackCashIn(amount, phoneNumber)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to initiate payment",
})
}
// 3. Store transaction reference in your database
txRef := response.Ref
log.Printf("Payment initiated: %s", txRef)
// 4. Return reference to frontend
return c.JSON(http.StatusOK, map[string]interface{
"transaction_ref": txRef,
"status": response.Status,
"message": "Please confirm payment on your phone",
})
}
func CheckPaymentStatus(c echo.Context) error {
// Get transaction reference from request
txRef := c.Param("ref")
// Poll status
status, err := integrations.PollTransactionStatus(txRef)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to check status",
})
}
return c.JSON(http.StatusOK, map[string]interface{
"status": status.Status,
"amount": status.Amount,
"fee": status.Fee,
})
}
Testing
Paypack provides a sandbox environment for testing:
- Use test credentials from your Paypack dashboard
- Use test phone numbers provided by Paypack
- Test transactions won’t deduct real money
Error Handling
All functions return Go errors. Common error scenarios:
- Authentication failures: Invalid credentials
- Network errors: API unreachable
- Invalid phone numbers: Wrong format or unsupported provider
- Insufficient balance: Not enough funds in merchant account (cash-out)
- Transaction limits: Amount exceeds daily/transaction limits
response, err := integrations.PaypackCashIn(amount, number)
if err != nil {
log.Printf("Payment error: %v", err)
// Check error message for specific issues
// Return appropriate response to user
}
Best Practices
- Store transaction references: Always save the
Ref field for tracking
- Use webhooks: Don’t rely solely on polling for status updates
- Handle pending states: Transactions may take time to complete
- Validate amounts: Check minimum/maximum transaction limits
- Log all transactions: Keep audit logs for reconciliation
- Secure your webhook endpoint: Verify webhook signatures in production
Production Checklist
Support
For Paypack-specific issues: