Skip to main content

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

  1. A Paypack merchant account - Sign up at paypack.rw
  2. API credentials (Client ID and Client Secret)
  3. 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:
  1. Use test credentials from your Paypack dashboard
  2. Use test phone numbers provided by Paypack
  3. 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

  1. Store transaction references: Always save the Ref field for tracking
  2. Use webhooks: Don’t rely solely on polling for status updates
  3. Handle pending states: Transactions may take time to complete
  4. Validate amounts: Check minimum/maximum transaction limits
  5. Log all transactions: Keep audit logs for reconciliation
  6. Secure your webhook endpoint: Verify webhook signatures in production

Production Checklist

  • Replace test credentials with production credentials
  • Configure webhook URL in Paypack dashboard
  • Implement webhook signature verification
  • Set up transaction logging
  • Test with real small amounts first
  • Monitor transaction success rates
  • Implement retry logic for failed transactions

Support

For Paypack-specific issues:

Build docs developers (and LLMs) love