Bonaventure OgetoBy Bonaventure Ogeto|

Verify Paystack Payments in Go

To verify a Paystack payment in Go, make a GET request to the verify endpoint, decode the JSON response into a typed struct, check status, amount, and currency, and use an atomic database update to prevent double-granting.

Verification Function

func verifyTransaction(reference string) (*VerifyResponse, error) {
    req, err := http.NewRequest("GET",
        "https://api.paystack.co/transaction/verify/"+reference, nil)
    if err != nil {
        return nil, err
    }
    req.Header.Set("Authorization", "Bearer "+paystackSecret)

    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var result VerifyResponse
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, err
    }

    return &result, nil
}

func verifyAndFulfill(reference string, db *sql.DB) error {
    result, err := verifyTransaction(reference)
    if err != nil {
        return fmt.Errorf("verification failed: %w", err)
    }

    if !result.Status || result.Data.Status != "success" {
        return fmt.Errorf("payment not successful: %s", result.Data.Status)
    }

    // TODO: Check amount and currency against database

    // Atomic update
    res, err := db.Exec(
        "UPDATE orders SET paid = true, paid_at = NOW() WHERE reference = $1 AND paid = false",
        reference,
    )
    if err != nil {
        return err
    }

    rows, _ := res.RowsAffected()
    if rows == 0 {
        // Already fulfilled
        return nil
    }

    // Deliver value
    return nil
}

Retry Logic

func verifyWithRetry(reference string, maxRetries int) (*VerifyResponse, error) {
    for attempt := 0; attempt < maxRetries; attempt++ {
        result, err := verifyTransaction(reference)
        if err == nil {
            return result, nil
        }

        if attempt < maxRetries-1 {
            time.Sleep(time.Duration(1<

Verify Handler

func verifyHandler(w http.ResponseWriter, r *http.Request) {
    reference := r.URL.Query().Get("reference")
    if reference == "" {
        http.Error(w, "Missing reference", http.StatusBadRequest)
        return
    }

    err := verifyAndFulfill(reference, db)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]bool{"success": true})
}

Ship Payments Faster

Key Takeaways

  • Go structs with json tags provide type-safe Paystack response parsing.
  • Always check status, amount, and currency against your database records.
  • Use SQL UPDATE with a WHERE clause on paid=false for atomic idempotent fulfillment.
  • Go error handling makes verification failures explicit and easy to trace.
  • Create a reusable VerifyAndFulfill function for use in callbacks and webhooks.
  • Retry with exponential backoff using time.Sleep for network failures.

Frequently Asked Questions

Can I verify a transaction multiple times?
Yes. The Paystack verify endpoint is idempotent and returns the same result every time.
Should I use context.Context for verification?
Yes. In production, use http.NewRequestWithContext to respect request cancellation and timeouts.
How do I handle verification in goroutines?
The atomic SQL UPDATE with WHERE paid=false ensures only one goroutine fulfills the order, even if multiple run concurrently.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon