squad

package module
v1.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 11, 2026 License: MIT Imports: 17 Imported by: 0

README

go-squad

CI Go Reference Go Report Card

The comprehensive, idiomatic Go SDK for the Squad by GTCO payment gateway.

What's included:

  • Payment initiation, verification, and refunds
  • Virtual accounts (NUBAN-compliant)
  • Fund transfers and account lookup
  • Sub-merchant management for aggregators and marketplaces
  • Dispute management with evidence upload
  • Value-added services: airtime, data, cable TV, electricity, SMS
  • Webhook signature validation + typed event router
  • Auto-pagination iterator — loop over thousands of records without manual paging
  • Idempotency keys — prevent duplicate charges on retried requests
  • Structured logging interface — works with log/slog, zap, zerolog, and others
  • Request/response hooks — for metrics, tracing, and custom headers
  • squadtest package — mock Squad API server for unit testing your integration
  • Zero external runtime dependencies
  • Context-aware — every method accepts context.Context

Installation

go get github.com/kingztech2019/go-squad

Requires Go 1.21 or later.


Quick Start

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    squad "github.com/kingztech2019/go-squad"
)

func main() {
    // Sandbox is auto-detected from the "sandbox_sk_" key prefix.
    client := squad.New(os.Getenv("SQUAD_SECRET_KEY"))

    resp, err := client.Transactions.InitiatePayment(context.Background(), &squad.InitiatePaymentParams{
        Email:       "customer@example.com",
        Amount:      squad.NGN(5000), // ₦5,000 — no kobo confusion
        Currency:    "NGN",
        CallbackURL: "https://yoursite.com/callback",
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Redirect customer to: %s\n", resp.CheckoutURL)
}

Configuration

// Sandbox auto-detected from key prefix
client := squad.New("sandbox_sk_xxx")

// Explicit environment
client := squad.New(key, squad.WithSandbox())
client := squad.New(key, squad.WithProduction())

// Development logging (stdout)
client := squad.New(key, squad.WithLogger(squad.StdLogger()))

// Production logging (your logger, e.g. slog)
type slogAdapter struct{ l *slog.Logger }
func (a slogAdapter) Info(msg string, kv ...any)  { a.l.Info(msg, kv...) }
func (a slogAdapter) Error(msg string, kv ...any) { a.l.Error(msg, kv...) }
client := squad.New(key, squad.WithLogger(slogAdapter{slog.Default()}))

// Metrics + distributed tracing hooks
client := squad.New(key,
    squad.WithBeforeRequest(func(req *http.Request) {
        req.Header.Set("X-Correlation-ID", getCorrelationID())
    }),
    squad.WithAfterResponse(func(req *http.Request, resp *http.Response, d time.Duration) {
        metrics.Record("squad.request", req.URL.Path, resp.StatusCode, d)
    }),
)

// Auto-generate idempotency keys on all POST requests
client := squad.New(key, squad.WithAutoIdempotency())

// Custom timeout
client := squad.New(key, squad.WithTimeout(60*time.Second))

// Custom HTTP client (retry, custom TLS, etc.)
client := squad.New(key, squad.WithHTTPClient(myHTTPClient))

Money Helpers

The Squad API uses the lowest currency denomination (kobo for NGN, cents for USD). These helpers eliminate the most common payment integration bug:

squad.NGN(5000)       // → 500000 (₦5,000 in kobo)
squad.NGN(1)          // → 100    (₦1.00 in kobo)
squad.USD(50)         // → 5000   ($50.00 in cents)
squad.FromKobo(50000) // → 500.0  (display value)
squad.FromCents(5000) // → 50.0   (display value)

Idempotency Keys

Protect against duplicate charges when a request is retried after a network failure. Store the key before making the request so you can reuse it on retry.

// Tie the key to your business operation
key, err := squad.GenerateIdempotencyKey()
if err != nil { log.Fatal(err) }

// Store key in your DB alongside the order before calling Squad.
ctx = squad.WithIdempotencyKey(ctx, "order-"+orderID+"-"+key)

resp, err := client.Transactions.InitiatePayment(ctx, params)
// If this times out and you retry, use the SAME ctx — same key, no double charge.

Or let the SDK auto-generate keys for every POST:

client := squad.New(key, squad.WithAutoIdempotency())

Transactions

Initiate Payment
resp, err := client.Transactions.InitiatePayment(ctx, &squad.InitiatePaymentParams{
    Email:           "customer@example.com",
    Amount:          squad.NGN(1000),
    Currency:        "NGN",
    TransactionRef:  "order-ref-001",
    CallbackURL:     "https://yoursite.com/callback",
    PaymentChannels: []string{"card", "bank", "ussd", "transfer"},
    CustomerName:    "John Doe",
    Metadata:        map[string]any{"order_id": "123"},
})
// Redirect user to resp.CheckoutURL
Verify Transaction
txn, err := client.Transactions.VerifyTransaction(ctx, "order-ref-001")
if txn.Status == "Success" {
    fmt.Printf("Paid: %s\n", squad.FromKobo(txn.Amount))
}
Refund Transaction
refund, err := client.Transactions.RefundTransaction(ctx, &squad.RefundTransactionParams{
    GatewayTransactionRef: "gw_ref",
    TransactionRef:        "order-ref-001",
    RefundType:            "Partial", // "Full" or "Partial"
    ReasonForRefund:       "Customer request",
    Amount:                squad.NGN(500),
})
Recurring Payments (Tokenisation)
// Squad includes a ChargeToken in the webhook body after a successful payment.
// Use it to charge the card again without the customer re-entering card details:
resp, err := client.Transactions.InitiatePayment(ctx, &squad.InitiatePaymentParams{
    Email:       "customer@example.com",
    Amount:      squad.NGN(1000),
    Currency:    "NGN",
    IsRecurring: true,
    ChargeToken: &squad.ChargeToken{
        Token:       "tok_abc123",
        ExpiryMonth: 12,
        ExpiryYear:  2027,
    },
})
Iterate Over Missed Webhooks
iter := client.Transactions.AllMissedWebhooks(ctx, nil)
for iter.Next() {
    tx := iter.Item()
    fmt.Println(tx.TransactionRef, tx.Status)
}
if err := iter.Err(); err != nil { log.Fatal(err) }

Virtual Accounts

// Create a NUBAN virtual account
account, err := client.VirtualAccounts.Create(ctx, &squad.CreateVirtualAccountParams{
    CustomerIdentifier: "cust-001",
    FirstName:          "Adaeze",
    LastName:           "Okafor",
    MobileNum:          "2348012345678",
    Email:              "adaeze@example.com",
    BVN:                "12345678901",
    DOB:                "01/01/1990",
})
fmt.Println(account.VirtualAccountNumber)

// Iterate over ALL transactions (pages fetched automatically)
iter := client.VirtualAccounts.AllTransactions(ctx, "cust-001", nil)
for iter.Next() {
    tx := iter.Item()
    fmt.Printf("%s credited ₦%.2f from %s\n",
        tx.TransactionRef, squad.FromKobo(tx.Amount), tx.SenderName)
}

// Simulate a credit (sandbox only)
_, err = client.VirtualAccounts.Simulate(ctx, &squad.SimulateVirtualAccountParams{
    VirtualAccountNumber: account.VirtualAccountNumber,
    Amount:               5000,
})

Transfers

// Always verify the account before transferring
lookup, err := client.Transfers.AccountLookup(ctx, "057", "0123456789")
fmt.Println("Sending to:", lookup.AccountName)

// Transfer funds
transfer, err := client.Transfers.FundsTransfer(ctx, &squad.FundsTransferParams{
    TransactionRef: "pay-out-001",
    Amount:         squad.NGN(2000),
    BankCode:       "057",
    AccountNumber:  "0123456789",
    AccountName:    lookup.AccountName,
    Currency:       "NGN",
    Remark:         "Salary payment",
})

// Squad-to-Squad transfer
_, err = client.Transfers.IntraTransfer(ctx, &squad.IntraTransferParams{
    TransactionRef:     "intra-001",
    Amount:             squad.NGN(1000),
    SenderIdentifier:   "merchant-A",
    ReceiverIdentifier: "merchant-B",
})

// Iterate over all transfer history
iter := client.Transfers.All(ctx, &squad.TransferListParams{Status: "Success"})
for iter.Next() {
    fmt.Println(iter.Item().TransactionRef, iter.Item().Status)
}

Sub-Merchant Management

For aggregators and marketplace platforms that manage vendors or sub-accounts.

// Onboard a new sub-merchant
merchant, err := client.SubMerchants.Create(ctx, &squad.CreateSubMerchantParams{
    DisplayName:   "Vendor Store",
    AccountName:   "Emeka Obi",
    AccountNumber: "0123456789",
    BankCode:      "057",
    Email:         "emeka@vendor.ng",
})
fmt.Println("Sub-merchant ID:", merchant.ID)

// Route a payment through a specific sub-merchant
resp, err := client.Transactions.InitiatePayment(ctx, &squad.InitiatePaymentParams{
    Email:               "buyer@example.com",
    Amount:              squad.NGN(5000),
    Currency:            "NGN",
    InitiatorCustomerID: merchant.MerchantID,
    CallbackURL:         "https://yourplatform.com/callback",
})

// Iterate over all sub-merchants
iter := client.SubMerchants.All(ctx, nil)
for iter.Next() {
    m := iter.Item()
    fmt.Printf("%s — %s\n", m.ID, m.DisplayName)
}

// Remove a sub-merchant
_, err = client.SubMerchants.Delete(ctx, merchant.ID)

Disputes

// Iterate over all open disputes
iter := client.Disputes.All(ctx, &squad.DisputeListParams{Status: "open"})
for iter.Next() {
    d := iter.Item()
    fmt.Printf("Ticket: %s | ₦%.2f | %s\n", d.TicketID, squad.FromKobo(d.Amount), d.Reason)
}

// Upload evidence (PDF, PNG, or JPG)
fileData, _ := os.ReadFile("proof-of-delivery.pdf")
_, err = client.Disputes.UploadEvidence(ctx, "ticket-001", fileData, "proof-of-delivery.pdf")

// Reject the dispute (evidence must be uploaded first)
_, err = client.Disputes.RejectDispute(ctx, "ticket-001")

// Or accept it (concede)
_, err = client.Disputes.AcceptDispute(ctx, "ticket-001")

Value-Added Services (VAS)

// Buy airtime
_, err := client.VAS.BuyAirtime(ctx, &squad.BuyAirtimeParams{
    PhoneNumber:    "2348012345678",
    Amount:         squad.NGN(50), // minimum ₦50
    Network:        "MTN",         // "MTN", "AIRTEL", "GLO", "9MOBILE"
    TransactionRef: "air-001",
})

// Buy data bundle
plans, _ := client.VAS.GetDataPlans(ctx, "MTN")
_, err = client.VAS.BuyData(ctx, &squad.BuyDataParams{
    PhoneNumber:    "2348012345678",
    PlanCode:       plans.Plans[0].PlanCode,
    Network:        "MTN",
    TransactionRef: "data-001",
})

// Subscribe to cable TV
packages, _ := client.VAS.GetCablePackages(ctx, "DSTV")
_, err = client.VAS.BuyCable(ctx, &squad.BuyCableParams{
    SmartCardNumber: "1234567890",
    PackageCode:     packages.Packages[0].PackageCode,
    Provider:        "DSTV",
    TransactionRef:  "cable-001",
})

// Buy electricity — token returned in response
billers, _ := client.VAS.GetElectricityBillers(ctx)
elec, err := client.VAS.BuyElectricity(ctx, &squad.BuyElectricityParams{
    MeterNumber:    "04123456789",
    Amount:         squad.NGN(5000),
    BillerCode:     billers.Billers[0].BillerCode,
    MeterType:      "prepaid",
    TransactionRef: "elec-001",
})
fmt.Println("Meter token:", elec.ElectricityToken)

// Send SMS
_, err = client.VAS.SendSMS(ctx, &squad.SendSMSParams{
    To:             []string{"2348012345678"},
    From:           "MyBrand",
    Body:           "Your order has been confirmed.",
    TransactionRef: "sms-001",
})

Webhooks

The WebhookRouter validates signatures and dispatches events to typed handlers. Register it directly as an http.Handler:

router := squad.NewWebhookRouter(os.Getenv("SQUAD_SECRET_KEY")).
    OnTransactionSuccess(func(ctx context.Context, body *squad.WebhookTransactionBody) error {
        return fulfillOrder(body.TransactionRef, body.Amount)
    }).
    OnVirtualAccountCredit(func(ctx context.Context, body *squad.WebhookVirtualAccountBody) error {
        return creditCustomer(body.CustomerIdentifier, body.Amount)
    }).
    OnTransferSuccess(func(ctx context.Context, body *squad.WebhookTransferBody) error {
        return markPayoutComplete(body.TransactionRef)
    }).
    OnDisputeOpened(func(ctx context.Context, body *squad.WebhookDisputeBody) error {
        return notifyTeam(body.TicketID, body.Reason)
    }).
    OnError(func(w http.ResponseWriter, r *http.Request, err error) {
        log.Printf("webhook error: %v", err)
        http.Error(w, "error", http.StatusInternalServerError)
    })

http.Handle("/webhook/squad", router)
Manual Webhook Handling
func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    sig := r.Header.Get("x-squad-signature")

    event, err := squad.ParseWebhook(body, sig, os.Getenv("SQUAD_SECRET_KEY"))
    if errors.Is(err, squad.ErrInvalidSignature) {
        http.Error(w, "forbidden", http.StatusForbidden)
        return
    }

    switch event.Event {
    case squad.EventTransactionSuccess:
        parsed, _ := event.ParseBody()
        txn := parsed.(*squad.WebhookTransactionBody)
        fmt.Println(txn.TransactionRef, squad.FromKobo(txn.Amount))
    }
    w.WriteHeader(http.StatusOK)
}
Supported Event Types
Constant Event String
EventTransactionSuccess charge.success
EventTransactionFailed charge.failed
EventVirtualAccountCredit virtual-account.credit
EventTransferSuccess transfer.success
EventTransferFailed transfer.failed
EventTransferReversed transfer.reversed
EventDisputeOpened dispute.opened
EventDisputeResolved dispute.resolved

Auto-Pagination Iterator

All listing endpoints expose an All* / All iterator that fetches pages transparently. No more manual pagination loops:

// Without iterator — repetitive, error-prone
page := 1
for {
    result, _ := client.Transfers.GetAllTransactions(ctx, &squad.TransferListParams{Page: page, PerPage: 50})
    for _, t := range result.Transfers { process(t) }
    if len(result.Transfers) < 50 { break }
    page++
}

// With iterator — clean, concise, handles edge cases automatically
iter := client.Transfers.All(ctx, &squad.TransferListParams{PerPage: 50})
for iter.Next() {
    process(iter.Item())
}
if err := iter.Err(); err != nil { log.Fatal(err) }

Available iterators:

Service Method
Transactions AllMissedWebhooks(ctx, params)
VirtualAccounts AllTransactions(ctx, customerID, params)
Transfers All(ctx, params)
Disputes All(ctx, params)
SubMerchants All(ctx, params)

Testing Your Integration with squadtest

The squadtest package provides a mock Squad API server so you can test your integration without real API calls or a sandbox account.

go get github.com/kingztech2019/go-squad/squadtest
import (
    "testing"
    squad "github.com/kingztech2019/go-squad"
    "github.com/kingztech2019/go-squad/squadtest"
)

func TestMyCheckoutService(t *testing.T) {
    // Start a mock server — automatically shut down when the test ends.
    srv := squadtest.NewServer(t)

    // Register typed handlers that control the response.
    srv.OnInitiatePayment(func(p *squad.InitiatePaymentParams) (*squad.InitiatePaymentResponse, error) {
        // Assert on the request your code sent.
        if p.Amount != squad.NGN(5000) {
            t.Errorf("unexpected amount: %d", p.Amount)
        }
        return &squad.InitiatePaymentResponse{
            CheckoutURL:    "https://fake-checkout.squadco.com/abc",
            TransactionRef: p.TransactionRef,
        }, nil
    })

    srv.OnVerifyTransaction(func(ref string) (*squad.VerifyTransactionResponse, error) {
        return &squad.VerifyTransactionResponse{
            TransactionRef: ref,
            Status:         "Success",
            Amount:         squad.NGN(5000),
        }, nil
    })

    // Inject srv.Client() into the code under test.
    myService := checkout.NewService(srv.Client())

    url, err := myService.StartCheckout("customer@example.com", squad.NGN(5000))
    if err != nil { t.Fatal(err) }
    if url == "" { t.Error("expected checkout URL") }

    // Assert on what your code actually sent.
    if srv.RequestCount() != 1 {
        t.Errorf("expected 1 request, got %d", srv.RequestCount())
    }
}
squadtest API
Method Description
NewServer(t) Create and start a mock server
srv.Client() Get a pre-configured *squad.Client pointing at the mock
srv.OnInitiatePayment(fn) Handle POST /transaction/initiate
srv.OnVerifyTransaction(fn) Handle GET /transaction/verify/{ref}
srv.OnRefundTransaction(fn) Handle POST /transaction/refund
srv.OnCreateVirtualAccount(fn) Handle POST /virtual-account
srv.OnFundsTransfer(fn) Handle POST /payout/transfer
srv.OnAccountLookup(fn) Handle GET /payout/account/lookup
srv.OnBuyAirtime(fn) Handle POST /vas/airtime
srv.OnBuyElectricity(fn) Handle POST /vas/electricity
srv.OnCreateSubMerchant(fn) Handle POST /merchant/sub-merchant
srv.OnUploadEvidence(fn) Handle POST /dispute/upload-evidence/{id}
srv.Handle(method, path, fn) Register a custom handler for any endpoint
srv.Requests() All recorded requests
srv.LastRequest() Most recent request
srv.RequestCount() Total requests received
srv.Reset() Clear all handlers and recorded requests

Error Handling

txn, err := client.Transactions.VerifyTransaction(ctx, ref)
if err != nil {
    if squad.IsUnauthorized(err) {
        log.Fatal("invalid API key")
    }
    if squad.IsBadRequest(err) {
        log.Printf("validation error: %v", err)
    }
    if squad.IsNotFound(err) {
        log.Printf("transaction not found: %s", ref)
    }

    // Inspect the full Squad error envelope
    var squadErr *squad.Error
    if errors.As(err, &squadErr) {
        log.Printf("squad status=%d http=%d msg=%s",
            squadErr.Status, squadErr.HTTPStatus, squadErr.Message)
    }
}
Retry Logic

The SDK intentionally does not implement retries — inject a retrying transport instead:

import "github.com/hashicorp/go-retryablehttp"

retryClient := retryablehttp.NewClient()
retryClient.RetryMax = 3

// Always use a stored idempotency key when retrying payments
client := squad.New(secretKey, squad.WithHTTPClient(retryClient.StandardClient()))

Testing

make test          # full test suite with race detector
make test-short    # fast run without race detector
make cover-html    # HTML coverage report

Amount Convention

All monetary amounts use the lowest currency denomination:

Currency Unit Example
NGN kobo (1 NGN = 100 kobo) squad.NGN(1000)100000
USD cents (1 USD = 100 cents) squad.USD(10)1000

Contributing

See CONTRIBUTING.md for guidelines.


License

MIT — see LICENSE.


Resources

Documentation

Overview

Package squad provides a Go client for the Squad by GTCO payment gateway API.

Getting Started

client := squad.New("sandbox_sk_your_key_here")

resp, err := client.Transactions.InitiatePayment(ctx, &squad.InitiatePaymentParams{
    Email:    "customer@example.com",
    Amount:   squad.NGN(5000), // ₦5,000
    Currency: "NGN",
    CallbackURL: "https://yoursite.com/callback",
})

Environments

The sandbox environment is automatically selected when your key starts with "sandbox_sk_". For production, use your live key or explicitly call WithProduction().

Idempotency

key, _ := squad.GenerateIdempotencyKey()
ctx = squad.WithIdempotencyKey(ctx, key)
resp, err := client.Transactions.InitiatePayment(ctx, params)

Pagination

iter := client.Transfers.All(ctx, nil)
for iter.Next() {
    fmt.Println(iter.Item().TransactionRef)
}

Webhook Verification

event, err := squad.ParseWebhook(body, r.Header.Get("x-squad-signature"), secretKey)

Webhook Router

router := squad.NewWebhookRouter(secretKey).
    OnTransactionSuccess(handlePayment).
    OnVirtualAccountCredit(handleCredit)
http.Handle("/webhook/squad", router)

Index

Constants

View Source
const (
	// Version is the current SDK version.
	Version = "1.1.0"

	// DefaultTimeout is the default HTTP request timeout.
	DefaultTimeout = 30 * time.Second

	// SandboxBaseURL is the Squad sandbox API base URL.
	SandboxBaseURL = "https://sandbox-api-d.squadco.com"

	// ProductionBaseURL is the Squad production API base URL.
	ProductionBaseURL = "https://api-d.squadco.com"
)

Variables

View Source
var (
	ErrUnauthorized = &Error{HTTPStatus: 401, Status: 401, Message: "unauthorized"}
	ErrForbidden    = &Error{HTTPStatus: 403, Status: 403, Message: "forbidden"}
	ErrBadRequest   = &Error{HTTPStatus: 400, Status: 400, Message: "bad request"}
	ErrNotFound     = &Error{HTTPStatus: 404, Status: 404, Message: "not found"}
)

Sentinel errors for common API failure modes.

View Source
var ErrInvalidSignature = fmt.Errorf("squad: webhook signature validation failed")

ErrInvalidSignature is returned by ParseWebhook when HMAC-SHA512 validation fails.

Functions

func FromCents

func FromCents(cents int64) float64

FromCents converts a cents amount back to dollars for display purposes.

squad.FromCents(5000) // → 50.00 ($50.00)

func FromKobo

func FromKobo(kobo int64) float64

FromKobo converts a kobo amount back to naira for display purposes.

squad.FromKobo(500000) // → 5000.00 (₦5,000)

func GenerateIdempotencyKey

func GenerateIdempotencyKey() (string, error)

GenerateIdempotencyKey generates a cryptographically random 32-character hex key. Use this when you don't have a natural business key (e.g. order ID) to use.

key, err := squad.GenerateIdempotencyKey()
// Store key alongside the order before initiating payment.
ctx = squad.WithIdempotencyKey(ctx, key)

func IsBadRequest

func IsBadRequest(err error) bool

IsBadRequest reports whether err is a 400 validation failure.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err is a 404 not-found failure.

func IsUnauthorized

func IsUnauthorized(err error) bool

IsUnauthorized reports whether err is a 401 authorization failure.

func NGN

func NGN(naira float64) int64

NGN converts a naira amount to kobo (the lowest denomination used by the Squad API).

squad.NGN(5000)  // → 500000 kobo (₦5,000)
squad.NGN(1)     // → 100 kobo (₦1)

func USD

func USD(dollars float64) int64

USD converts a dollar amount to cents (the lowest denomination for USD transactions).

squad.USD(50)   // → 5000 cents ($50.00)
squad.USD(0.50) // → 50 cents ($0.50)

func VerifySignature

func VerifySignature(payload []byte, signature string, secret string) bool

VerifySignature checks whether the HMAC-SHA512 signature matches the payload. signature is the hex-encoded value from the "x-squad-signature" header. secret is the merchant's Squad secret key.

Uses constant-time comparison to prevent timing attacks.

func WithIdempotencyKey

func WithIdempotencyKey(ctx context.Context, key string) context.Context

WithIdempotencyKey attaches an idempotency key to the context. The SDK sends it as X-Idempotency-Key on all POST requests, preventing duplicate charges when a request is retried after a network failure.

The key should be unique per business operation and attempt. Store it before making the request so you can reuse it on retry.

key, _ := squad.GenerateIdempotencyKey()
ctx = squad.WithIdempotencyKey(ctx, "order-"+orderID+"-"+key)
resp, err := client.Transactions.InitiatePayment(ctx, params)
// On network error, retry with the SAME ctx to use the same key.

Types

type AccountLookupResponse

type AccountLookupResponse struct {
	AccountName   string `json:"account_name"`
	AccountNumber string `json:"account_number"`
	BankCode      string `json:"bank_code"`
	BankName      string `json:"bank_name"`
}

AccountLookupResponse is returned by AccountLookup after verifying a bank account.

type BuyAirtimeParams

type BuyAirtimeParams struct {
	PhoneNumber    string `json:"phone_number"`
	Amount         int64  `json:"amount"`
	Network        string `json:"network_provider"` // "MTN", "AIRTEL", "GLO", "9MOBILE"
	TransactionRef string `json:"transaction_ref"`
}

BuyAirtimeParams holds parameters for purchasing airtime. PhoneNumber should be in international format without "+": "2348012345678". Minimum amount is 50 NGN. Charges come from the Squad wallet.

type BuyCableParams

type BuyCableParams struct {
	SmartCardNumber string `json:"smart_card_number"`
	PackageCode     string `json:"package_code"`
	Provider        string `json:"cable_provider"` // "DSTV", "GOTV", "STARTIMES"
	TransactionRef  string `json:"transaction_ref"`
	Amount          int64  `json:"amount,omitempty"`
}

BuyCableParams holds parameters for subscribing to a cable TV package.

type BuyDataParams

type BuyDataParams struct {
	PhoneNumber    string `json:"phone_number"`
	PlanCode       string `json:"plan_code"`
	Network        string `json:"network_provider"`
	TransactionRef string `json:"transaction_ref"`
}

BuyDataParams holds parameters for purchasing a data bundle.

type BuyElectricityParams

type BuyElectricityParams struct {
	MeterNumber    string `json:"meter_number"`
	Amount         int64  `json:"amount"`
	BillerCode     string `json:"biller_code"`
	MeterType      string `json:"meter_type"` // "prepaid" or "postpaid"
	TransactionRef string `json:"transaction_ref"`
	PhoneNumber    string `json:"phone_number,omitempty"`
}

BuyElectricityParams holds parameters for purchasing electricity units.

type CablePackage

type CablePackage struct {
	PackageCode string `json:"package_code"`
	PackageName string `json:"package_name"`
	Amount      int64  `json:"amount"`
	CycleType   string `json:"cycle_type"` // "monthly", "quarterly"
}

CablePackage describes a single cable TV subscription package.

type CablePackagesResponse

type CablePackagesResponse struct {
	Packages []CablePackage `json:"packages"`
}

CablePackagesResponse holds available cable TV packages for a provider.

type ChargeToken

type ChargeToken struct {
	Token       string `json:"token"`
	ExpiryMonth int    `json:"expiry_month"`
	ExpiryYear  int    `json:"expiry_year"`
}

ChargeToken holds card tokenization data used for recurring charges.

type Client

type Client struct {

	// Transactions handles payment initiation, verification, and refunds.
	Transactions *TransactionService

	// VirtualAccounts handles NUBAN virtual account creation and management.
	VirtualAccounts *VirtualAccountService

	// Transfers handles fund transfers to Nigerian bank accounts.
	Transfers *TransferService

	// Disputes handles chargeback disputes and evidence submission.
	Disputes *DisputeService

	// VAS handles value-added services: airtime, data, cable TV, electricity, and SMS.
	VAS *VASService

	// SubMerchants manages sub-merchant accounts for aggregators and platforms.
	SubMerchants *SubMerchantService
	// contains filtered or unexported fields
}

Client is the root SDK object. All API services are accessible as fields. A Client is safe for concurrent use across goroutines.

func New

func New(secretKey string, opts ...Option) *Client

New constructs a ready-to-use Client.

secretKey is your Squad secret key (sandbox_sk_* for sandbox, live secret for production). The sandbox environment is automatically selected when the key starts with "sandbox_sk_". Pass functional options to override defaults.

client := squad.New("sandbox_sk_xxx")
client := squad.New("live_sk_xxx", squad.WithLogger(squad.StdLogger()))

type CreateSubMerchantParams

type CreateSubMerchantParams struct {
	DisplayName   string `json:"display_name"`
	AccountName   string `json:"account_name"`
	AccountNumber string `json:"account_number"`
	BankCode      string `json:"bank_code"`
	Email         string `json:"email"`
	MobileNumber  string `json:"mobile_number,omitempty"`
}

CreateSubMerchantParams holds parameters for onboarding a sub-merchant. Requires an aggregator-level Squad account.

type CreateVirtualAccountParams

type CreateVirtualAccountParams struct {
	CustomerIdentifier string `json:"customer_identifier"`
	FirstName          string `json:"first_name"`
	LastName           string `json:"last_name"`
	MiddleName         string `json:"middle_name,omitempty"`
	MobileNum          string `json:"mobile_num"`
	Email              string `json:"email"`
	BVN                string `json:"bvn"`
	DOB                string `json:"dob"` // "DD/MM/YYYY"
	Address            string `json:"address,omitempty"`
	Gender             string `json:"gender,omitempty"` // "1" = male, "2" = female
	BeneficiaryAccount string `json:"beneficiary_account,omitempty"`
}

CreateVirtualAccountParams holds all parameters to create a NUBAN virtual account. CBN profiling on the Squad dashboard is required before this endpoint is available.

type CustomerInfo

type CustomerInfo struct {
	CustomerName  string `json:"customer_name"`
	CustomerEmail string `json:"customer_email"`
	CustomerPhone string `json:"customer_phone_number"`
	MerchantID    string `json:"merchant_id"`
}

CustomerInfo is embedded in transaction responses.

type DataPlan

type DataPlan struct {
	PlanCode string `json:"plan_code"`
	PlanName string `json:"plan_name"`
	Amount   int64  `json:"amount"`
	Validity string `json:"validity"`
	Network  string `json:"network"`
}

DataPlan describes a single data bundle offering for a network provider.

type DataPlansResponse

type DataPlansResponse struct {
	Plans []DataPlan `json:"plans"`
}

DataPlansResponse holds available data plans for a network provider.

type DeleteSubMerchantResponse

type DeleteSubMerchantResponse struct {
	MerchantID string `json:"merchant_id"`
	Status     string `json:"status"`
	Message    string `json:"message"`
}

DeleteSubMerchantResponse is returned when a sub-merchant is removed.

type Dispute

type Dispute struct {
	TicketID         string `json:"ticket_id"`
	TransactionRef   string `json:"transaction_ref"`
	Amount           int64  `json:"amount"`
	Currency         string `json:"currency"`
	Status           string `json:"dispute_status"`
	Reason           string `json:"dispute_reason"`
	CustomerEmail    string `json:"customer_email"`
	CustomerName     string `json:"customer_name"`
	DueDate          string `json:"due_date"`
	MerchantResponse string `json:"merchant_response"`
	CreatedAt        string `json:"created_at"`
	UpdatedAt        string `json:"updated_at"`
}

Dispute represents a single chargeback or dispute record.

type DisputeActionResponse

type DisputeActionResponse struct {
	TicketID string `json:"ticket_id"`
	Status   string `json:"status"`
	Message  string `json:"message"`
}

DisputeActionResponse is returned by AcceptDispute and RejectDispute.

type DisputeEvidence

type DisputeEvidence struct {
	TicketID    string `json:"ticket_id"`
	EvidenceURL string `json:"evidence_url"`
	FileName    string `json:"file_name"`
	UploadedAt  string `json:"uploaded_at"`
}

DisputeEvidence represents evidence attached to a dispute.

type DisputeListParams

type DisputeListParams struct {
	Page      int    `json:"page,omitempty"`
	PerPage   int    `json:"per_page,omitempty"`
	StartDate string `json:"start_date,omitempty"`
	EndDate   string `json:"end_date,omitempty"`
	Status    string `json:"status,omitempty"` // "open", "closed", "pending"
}

DisputeListParams holds pagination and filter parameters for listing disputes.

type DisputeListResponse

type DisputeListResponse struct {
	Disputes []Dispute `json:"disputes"`
	Total    int       `json:"total"`
	Page     int       `json:"page"`
	PerPage  int       `json:"per_page"`
}

DisputeListResponse holds a paginated list of disputes.

type DisputeService

type DisputeService struct {
	// contains filtered or unexported fields
}

DisputeService handles chargeback disputes and evidence submission.

func (*DisputeService) AcceptDispute

func (s *DisputeService) AcceptDispute(ctx context.Context, ticketID string) (*DisputeActionResponse, error)

AcceptDispute accepts a chargeback (merchant concedes the dispute).

func (*DisputeService) All

All returns a lazy iterator over all disputes, fetching pages on demand. Filters from params (Status, StartDate, EndDate) are preserved across pages.

iter := client.Disputes.All(ctx, &squad.DisputeListParams{Status: "open"})
for iter.Next() {
    d := iter.Item()
    fmt.Println(d.TicketID, d.Reason)
}

func (*DisputeService) GetAllDisputes

func (s *DisputeService) GetAllDisputes(ctx context.Context, params *DisputeListParams) (*DisputeListResponse, error)

GetAllDisputes retrieves a paginated list of all disputes on the merchant account.

func (*DisputeService) GetDisputeDetail

func (s *DisputeService) GetDisputeDetail(ctx context.Context, ticketID string) (*Dispute, error)

GetDisputeDetail retrieves the full details of a single dispute by its ticket ID.

func (*DisputeService) GetDisputeEvidence

func (s *DisputeService) GetDisputeEvidence(ctx context.Context, ticketID string) (*DisputeEvidence, error)

GetDisputeEvidence retrieves any previously uploaded evidence for a dispute.

func (*DisputeService) RejectDispute

func (s *DisputeService) RejectDispute(ctx context.Context, ticketID string) (*DisputeActionResponse, error)

RejectDispute contests a chargeback (merchant disputes the claim). Evidence must be uploaded via UploadEvidence before calling this method.

func (*DisputeService) UploadEvidence

func (s *DisputeService) UploadEvidence(ctx context.Context, ticketID string, fileData []byte, fileName string) (*EvidenceUploadResponse, error)

UploadEvidence uploads a file as evidence for a dispute. fileData is the raw file bytes (PDF, PNG, or JPG). fileName includes the extension. Call this before RejectDispute — evidence must be present to contest a chargeback.

type ElectricityBiller

type ElectricityBiller struct {
	BillerCode string   `json:"biller_code"`
	BillerName string   `json:"biller_name"`
	MeterTypes []string `json:"meter_types"` // ["prepaid", "postpaid"]
}

ElectricityBiller describes a DISCO (electricity distribution company).

type ElectricityBillersResponse

type ElectricityBillersResponse struct {
	Billers []ElectricityBiller `json:"billers"`
}

ElectricityBillersResponse holds the list of available electricity DISCOs.

type ElectricityResponse

type ElectricityResponse struct {
	VASTransactionResponse
	MeterNumber      string `json:"meter_number"`
	Units            string `json:"units"`
	ElectricityToken string `json:"electricity_token"`
}

ElectricityResponse is returned by BuyElectricity, including the token for the meter.

type Error

type Error struct {
	HTTPStatus int
	Status     int
	Message    string
	Code       string
}

Error represents a Squad API error response. It wraps both the HTTP-level status and Squad's application-level status code.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is allows errors.Is comparisons by HTTPStatus and Status fields, not pointer identity.

type EventType

type EventType string

EventType is the string identifier for a Squad webhook event.

const (
	EventTransactionSuccess   EventType = "charge_successful"
	EventTransactionFailed    EventType = "charge_failed"
	EventVirtualAccountCredit EventType = "virtualaccount_credited"
	EventTransferSuccess      EventType = "transfer_successful"
	EventTransferFailed       EventType = "transfer_failed"
	EventTransferReversed     EventType = "transfer_reversed"
	EventDisputeOpened        EventType = "dispute_opened"
	EventDisputeResolved      EventType = "dispute_resolved"
)

Squad webhook event type constants. Squad uses snake_case with underscores (confirmed from live sandbox payloads).

type EvidenceUploadResponse

type EvidenceUploadResponse struct {
	TicketID  string `json:"ticket_id"`
	FileName  string `json:"file_name"`
	UploadURL string `json:"upload_url"`
	Status    string `json:"status"`
}

EvidenceUploadResponse is returned after a successful evidence upload.

type FundsTransferParams

type FundsTransferParams struct {
	TransactionRef string `json:"transaction_ref"`
	Amount         int64  `json:"amount"`
	BankCode       string `json:"bank_code"`
	AccountNumber  string `json:"account_number"`
	AccountName    string `json:"account_name"`
	Currency       string `json:"currency_id"` // "NGN"
	Remark         string `json:"remark,omitempty"`
}

FundsTransferParams holds parameters for transferring funds to a Nigerian bank account. Amount is in kobo (lowest denomination). Settlement is T+1 by default.

type InitiatePaymentParams

type InitiatePaymentParams struct {
	Email               string         `json:"email"`
	Amount              int64          `json:"amount"`
	Currency            string         `json:"currency"`
	InitiatorCustomerID string         `json:"initiator_customer_id,omitempty"`
	TransactionRef      string         `json:"transaction_ref,omitempty"`
	CallbackURL         string         `json:"callback_url,omitempty"`
	PaymentChannels     []string       `json:"payment_channels,omitempty"`
	Metadata            map[string]any `json:"metadata,omitempty"`
	PassCharge          bool           `json:"pass_charge,omitempty"`
	CustomerName        string         `json:"customer_name,omitempty"`
	IsRecurring         bool           `json:"is_recurring,omitempty"`
	PlanCode            string         `json:"plan_code,omitempty"`
	ChargeToken         *ChargeToken   `json:"charge_token,omitempty"`
}

InitiatePaymentParams holds all parameters for creating a new payment transaction. Amount must be in the lowest currency denomination (kobo for NGN: 100000 = ₦1,000).

type InitiatePaymentResponse

type InitiatePaymentResponse struct {
	// CheckoutURL is returned by Squad when initiate_type is "inline".
	// The SDK builds it from TransactionRef as a fallback if Squad omits it.
	CheckoutURL string `json:"checkout_url"`

	TransactionRef     string       `json:"transaction_ref"`
	Amount             int64        `json:"transaction_amount"`
	Currency           string       `json:"currency"`
	CallbackURL        string       `json:"callback_url"`
	IsRecurring        bool         `json:"is_recurring"`
	AuthorizedChannels []string     `json:"authorized_channels"`
	MerchantInfo       MerchantInfo `json:"merchant_info"`
}

InitiatePaymentResponse is the Data payload returned by InitiatePayment. Redirect the end-user to CheckoutURL to complete payment.

type IntraTransferParams

type IntraTransferParams struct {
	TransactionRef     string `json:"transaction_ref"`
	Amount             int64  `json:"amount"`
	SenderIdentifier   string `json:"sender_identifier"`
	ReceiverIdentifier string `json:"receiver_identifier"`
	Narration          string `json:"narration,omitempty"`
}

IntraTransferParams holds parameters for a Squad-to-Squad wallet transfer.

type Iter

type Iter[T any] struct {
	// contains filtered or unexported fields
}

Iter is a generic lazy iterator for paginated Squad API responses. Pages are fetched on demand — only when the current buffer is exhausted.

Usage:

iter := client.Transfers.All(ctx, &squad.TransferListParams{PerPage: 50})
for iter.Next() {
    t := iter.Item()
    fmt.Println(t.TransactionRef, squad.FromKobo(t.Amount))
}
if err := iter.Err(); err != nil {
    log.Fatal(err)
}

func (*Iter[T]) Err

func (i *Iter[T]) Err() error

Err returns the error that stopped iteration, or nil on clean completion.

func (*Iter[T]) Item

func (i *Iter[T]) Item() T

Item returns the current item. Must only be called after Next() returns true.

func (*Iter[T]) Next

func (i *Iter[T]) Next() bool

Next advances the iterator to the next item. Fetches the next page transparently when the current buffer is exhausted. Returns false when all items have been yielded or an error occurs.

type Logger

type Logger interface {
	Info(msg string, fields ...any)
	Error(msg string, fields ...any)
}

Logger is the interface for SDK-level request/response logging. Implement this to integrate with your application's logging library.

Compatible with log/slog, go.uber.org/zap, sirupsen/logrus, rs/zerolog, and others. Fields are passed as key-value pairs: key string, value any, key string, value any, ...

// log/slog example:
type slogAdapter struct{ l *slog.Logger }
func (a slogAdapter) Info(msg string, kv ...any)  { a.l.Info(msg, kv...) }
func (a slogAdapter) Error(msg string, kv ...any) { a.l.Error(msg, kv...) }
client := squad.New(key, squad.WithLogger(slogAdapter{slog.Default()}))

func StdLogger

func StdLogger() Logger

StdLogger returns a Logger that writes to stderr using the standard log package. Suitable for development. For production, use your application's structured logger.

type MerchantInfo added in v1.1.1

type MerchantInfo struct {
	MerchantName string `json:"merchant_name"`
	MerchantID   string `json:"merchant_id"`
}

MerchantInfo contains basic merchant identity returned inside payment responses.

type MissedWebhookParams

type MissedWebhookParams struct {
	Page    int    `json:"page,omitempty"`
	PerPage int    `json:"per_page,omitempty"`
	Action  string `json:"action,omitempty"`
}

MissedWebhookParams holds query parameters for GetMissedWebhookTransactions.

type MissedWebhookResponse

type MissedWebhookResponse struct {
	Transactions []VerifyTransactionResponse `json:"transactions"`
	Total        int                         `json:"total"`
	Page         int                         `json:"page"`
	PerPage      int                         `json:"per_page"`
}

MissedWebhookResponse is the Data payload returned by GetMissedWebhookTransactions.

type Option

type Option func(*config)

Option is a functional option for configuring a Client.

func WithAfterResponse

func WithAfterResponse(fn func(*http.Request, *http.Response, time.Duration)) Option

WithAfterResponse registers a hook called after every HTTP response is received. duration is the total round-trip time. Use this for metrics, tracing, or audit logging.

squad.WithAfterResponse(func(req *http.Request, resp *http.Response, d time.Duration) {
    metrics.RecordLatency("squad", req.URL.Path, resp.StatusCode, d)
})

func WithAutoIdempotency

func WithAutoIdempotency() Option

WithAutoIdempotency automatically generates and attaches an X-Idempotency-Key header to every POST request that does not already have one set via WithIdempotencyKey. This protects against accidental duplicate charges from network-level retries.

func WithBaseURL

func WithBaseURL(url string) Option

WithBaseURL sets a custom base URL. Useful for proxies, local mocks, or testing. Must include scheme and host with no trailing slash: "https://my-proxy.example.com"

func WithBeforeRequest

func WithBeforeRequest(fn func(*http.Request)) Option

WithBeforeRequest registers a hook called before every HTTP request is sent. Use this for custom header injection, request signing, or distributed tracing.

squad.WithBeforeRequest(func(req *http.Request) {
    req.Header.Set("X-Correlation-ID", getCorrelationID())
})

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient replaces the default *http.Client. Use this to inject transports with retry logic, tracing, or custom TLS config.

func WithLogger

func WithLogger(l Logger) Option

WithLogger attaches a Logger to the client for request/response logging. By default all logging is discarded. Use StdLogger() for development output.

client := squad.New(key, squad.WithLogger(squad.StdLogger()))

func WithProduction

func WithProduction() Option

WithProduction explicitly sets the production base URL.

func WithSandbox

func WithSandbox() Option

WithSandbox configures the client to use Squad's sandbox environment. This is also inferred automatically when the secret key starts with "sandbox_sk_".

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the HTTP request timeout, overriding the default 30-second timeout.

func WithUserAgent

func WithUserAgent(ua string) Option

WithUserAgent appends a custom string to the User-Agent header.

type RefundTransactionParams

type RefundTransactionParams struct {
	GatewayTransactionRef string `json:"gateway_transaction_ref"`
	TransactionRef        string `json:"transaction_ref"`
	RefundType            string `json:"refund_type"`
	ReasonForRefund       string `json:"reason_for_refund"`
	Amount                int64  `json:"refund_amount,omitempty"`
	RRN                   string `json:"rrn,omitempty"`
}

RefundTransactionParams holds parameters for initiating a transaction refund. RefundType must be "Full" or "Partial". For partial refunds, Amount is required.

type RefundTransactionResponse

type RefundTransactionResponse struct {
	GatewayRef     string `json:"gateway_ref"`
	TransactionRef string `json:"transaction_ref"`
	RefundStatus   string `json:"refund_status"`
	AmountRefunded int64  `json:"amount_refunded"`
}

RefundTransactionResponse is the Data payload returned by RefundTransaction.

type SMSResponse

type SMSResponse struct {
	TransactionRef string   `json:"transaction_ref"`
	Status         string   `json:"status"`
	Recipients     []string `json:"recipients"`
	MessageID      string   `json:"message_id"`
}

SMSResponse is returned by SendSMS.

type SendSMSParams

type SendSMSParams struct {
	To             []string `json:"to"`
	From           string   `json:"from"`
	Body           string   `json:"body"`
	TransactionRef string   `json:"transaction_ref"`
}

SendSMSParams holds parameters for sending an SMS to one or more recipients.

type SimulateResponse

type SimulateResponse struct {
	TransactionRef string `json:"transaction_ref"`
	Status         string `json:"status"`
}

SimulateResponse is returned by Simulate.

type SimulateVirtualAccountParams

type SimulateVirtualAccountParams struct {
	VirtualAccountNumber string  `json:"virtual_account_number"`
	Amount               float64 `json:"amount"`
}

SimulateVirtualAccountParams holds parameters for a sandbox credit simulation.

type SubMerchant

type SubMerchant struct {
	ID            string `json:"id"`
	MerchantID    string `json:"merchant_id"`
	DisplayName   string `json:"display_name"`
	AccountName   string `json:"account_name"`
	AccountNumber string `json:"account_number"`
	BankCode      string `json:"bank_code"`
	BankName      string `json:"bank_name"`
	Email         string `json:"email"`
	Status        string `json:"status"`
	CreatedAt     string `json:"created_at"`
	UpdatedAt     string `json:"updated_at"`
}

SubMerchant represents a sub-merchant account under an aggregator.

type SubMerchantListParams

type SubMerchantListParams struct {
	Page    int `json:"page,omitempty"`
	PerPage int `json:"per_page,omitempty"`
}

SubMerchantListParams holds pagination parameters for listing sub-merchants.

type SubMerchantListResponse

type SubMerchantListResponse struct {
	Merchants []SubMerchant `json:"merchants"`
	Total     int           `json:"total"`
	Page      int           `json:"page"`
	PerPage   int           `json:"per_page"`
}

SubMerchantListResponse holds a paginated list of sub-merchants.

type SubMerchantService

type SubMerchantService struct {
	// contains filtered or unexported fields
}

SubMerchantService manages sub-merchant accounts for aggregators and marketplace platforms. Requires an aggregator-level Squad account with the appropriate permissions.

func (*SubMerchantService) All

All returns a lazy iterator over all sub-merchants, fetching pages on demand.

iter := client.SubMerchants.All(ctx, nil)
for iter.Next() {
    fmt.Println(iter.Item().DisplayName)
}

func (*SubMerchantService) Create

Create onboards a new sub-merchant under the aggregator account. The sub-merchant receives their own merchant ID and dashboard access.

func (*SubMerchantService) Delete

Delete removes a sub-merchant from the aggregator account.

func (*SubMerchantService) Get

func (s *SubMerchantService) Get(ctx context.Context, merchantID string) (*SubMerchant, error)

Get retrieves the details of a sub-merchant by their merchant ID.

func (*SubMerchantService) List

List returns a paginated list of all sub-merchants under the aggregator account.

type TransactionService

type TransactionService struct {
	// contains filtered or unexported fields
}

TransactionService handles payment initiation, verification, and refunds.

func (*TransactionService) AllMissedWebhooks

AllMissedWebhooks returns a lazy iterator over all missed webhook transactions.

iter := client.Transactions.AllMissedWebhooks(ctx, nil)
for iter.Next() {
    tx := iter.Item()
    fmt.Println(tx.TransactionRef, tx.Status)
}

func (*TransactionService) GetMissedWebhookTransactions

func (s *TransactionService) GetMissedWebhookTransactions(ctx context.Context, params *MissedWebhookParams) (*MissedWebhookResponse, error)

GetMissedWebhookTransactions retrieves transactions whose webhooks were not delivered. Use for reconciliation. Delete processed entries to prevent re-delivery.

func (*TransactionService) GetUSSDbanks

func (s *TransactionService) GetUSSDbanks(ctx context.Context) (*USSDbanksResponse, error)

GetUSSDbanks returns the list of banks that support USSD payment collection.

func (*TransactionService) InitiatePayment

InitiatePayment creates a new payment transaction and returns a checkout URL. Redirect the end-user to Response.CheckoutURL to complete payment via the Squad modal.

func (*TransactionService) RefundTransaction

RefundTransaction initiates a full or partial refund for a completed transaction. Set params.RefundType to "Full" or "Partial". Partial refunds require params.Amount.

func (*TransactionService) VerifyTransaction

func (s *TransactionService) VerifyTransaction(ctx context.Context, transactionRef string) (*VerifyTransactionResponse, error)

VerifyTransaction retrieves the current status of a transaction by its reference. Call this after the user returns from the checkout URL or inside a webhook handler.

type TransferListParams

type TransferListParams struct {
	Page      int    `json:"page,omitempty"`
	PerPage   int    `json:"per_page,omitempty"`
	StartDate string `json:"start_date,omitempty"`
	EndDate   string `json:"end_date,omitempty"`
	Status    string `json:"status,omitempty"` // "Success", "Processing", "Failed"
}

TransferListParams holds pagination and filter parameters for listing transfers.

type TransferListResponse

type TransferListResponse struct {
	Transfers []TransferStatusResponse `json:"transfers"`
	Total     int                      `json:"total"`
	Page      int                      `json:"page"`
	PerPage   int                      `json:"per_page"`
}

TransferListResponse holds a paginated list of transfer records.

type TransferResponse

type TransferResponse struct {
	TransactionRef string  `json:"transaction_reference"`
	Amount         int64   `json:"amount"`
	Fee            float64 `json:"transaction_charge"`
	Status         string  `json:"status"`
	AccountName    string  `json:"account_name"`
	AccountNumber  string  `json:"account_number"`
	BankCode       string  `json:"bank_code"`
	BankName       string  `json:"bank_name"`
	CreatedAt      string  `json:"created_at"`
}

TransferResponse is the Data payload returned by both FundsTransfer and IntraTransfer.

type TransferService

type TransferService struct {
	// contains filtered or unexported fields
}

TransferService handles fund transfers to Nigerian bank accounts and Squad wallets.

func (*TransferService) AccountLookup

func (s *TransferService) AccountLookup(ctx context.Context, bankCode, accountNumber string) (*AccountLookupResponse, error)

AccountLookup verifies a bank account number and returns the account holder's name. Always call this before FundsTransfer to validate the destination account.

func (*TransferService) All

All returns a lazy iterator over all transfer transactions, fetching pages on demand. Filters from params (StartDate, EndDate, Status) are preserved across pages.

iter := client.Transfers.All(ctx, &squad.TransferListParams{Status: "Success"})
for iter.Next() {
    fmt.Println(iter.Item().TransactionRef, squad.FromKobo(iter.Item().Amount))
}

func (*TransferService) FundsTransfer

func (s *TransferService) FundsTransfer(ctx context.Context, params *FundsTransferParams) (*TransferResponse, error)

FundsTransfer transfers funds from the Squad merchant wallet to any Nigerian bank account. Use AccountLookup first to verify the destination account before transferring.

func (*TransferService) GetAllTransactions

func (s *TransferService) GetAllTransactions(ctx context.Context, params *TransferListParams) (*TransferListResponse, error)

GetAllTransactions returns a paginated list of all transfer transactions.

func (*TransferService) GetTransactionStatus

func (s *TransferService) GetTransactionStatus(ctx context.Context, transactionRef string) (*TransferStatusResponse, error)

GetTransactionStatus retrieves the current status of a previously initiated transfer.

func (*TransferService) IntraTransfer

func (s *TransferService) IntraTransfer(ctx context.Context, params *IntraTransferParams) (*TransferResponse, error)

IntraTransfer transfers funds between two Squad wallet holders.

type TransferStatusResponse

type TransferStatusResponse struct {
	TransactionRef  string  `json:"transaction_ref"`
	Status          string  `json:"status"`
	Amount          int64   `json:"amount"`
	Fee             float64 `json:"fee"`
	ResponseCode    string  `json:"response_code"`
	ResponseMessage string  `json:"response_message"`
	UpdatedAt       string  `json:"updated_at"`
}

TransferStatusResponse is returned by GetTransactionStatus.

type USSDBank

type USSDBank struct {
	BankCode string `json:"bank_code"`
	BankName string `json:"bank_name"`
	USSD     string `json:"ussd"`
}

USSDBank describes a single bank supporting USSD payments.

type USSDbanksResponse

type USSDbanksResponse struct {
	Banks []USSDBank `json:"banks"`
}

USSDbanksResponse is the Data payload returned by GetUSSDbanks.

type UpdateVirtualAccountParams

type UpdateVirtualAccountParams struct {
	CustomerIdentifier string `json:"customer_identifier"`
	BVN                string `json:"bvn,omitempty"`
	FirstName          string `json:"first_name,omitempty"`
	LastName           string `json:"last_name,omitempty"`
	MiddleName         string `json:"middle_name,omitempty"`
	MobileNum          string `json:"mobile_num,omitempty"`
}

UpdateVirtualAccountParams holds fields that can be modified on an existing virtual account.

type VASService

type VASService struct {
	// contains filtered or unexported fields
}

VASService handles value-added services: airtime, data, cable TV, electricity, and SMS. All purchases are charged from the Squad merchant wallet.

func (*VASService) BuyAirtime

func (s *VASService) BuyAirtime(ctx context.Context, params *BuyAirtimeParams) (*VASTransactionResponse, error)

BuyAirtime purchases airtime and credits the specified phone number. Minimum purchase amount is 50 NGN. Supported networks: MTN, AIRTEL, GLO, 9MOBILE.

func (*VASService) BuyCable

func (s *VASService) BuyCable(ctx context.Context, params *BuyCableParams) (*VASTransactionResponse, error)

BuyCable subscribes to a cable TV package for a smart card number. Use GetCablePackages to retrieve valid package codes before calling this.

func (*VASService) BuyData

func (s *VASService) BuyData(ctx context.Context, params *BuyDataParams) (*VASTransactionResponse, error)

BuyData purchases a data bundle for a phone number. Use GetDataPlans to retrieve valid plan codes before calling this.

func (*VASService) BuyElectricity

func (s *VASService) BuyElectricity(ctx context.Context, params *BuyElectricityParams) (*ElectricityResponse, error)

BuyElectricity purchases electricity units for a prepaid or postpaid meter. The returned ElectricityToken must be entered into the customer's meter.

func (*VASService) GetCablePackages

func (s *VASService) GetCablePackages(ctx context.Context, cableProvider string) (*CablePackagesResponse, error)

GetCablePackages retrieves available cable TV subscription packages for the given provider. cableProvider is one of: "DSTV", "GOTV", "STARTIMES".

func (*VASService) GetDataPlans

func (s *VASService) GetDataPlans(ctx context.Context, networkProvider string) (*DataPlansResponse, error)

GetDataPlans retrieves available data bundle plans for the given network provider. networkProvider is one of: "MTN", "AIRTEL", "GLO", "9MOBILE".

func (*VASService) GetElectricityBillers

func (s *VASService) GetElectricityBillers(ctx context.Context) (*ElectricityBillersResponse, error)

GetElectricityBillers retrieves the list of available electricity distribution companies (DISCOs).

func (*VASService) SendSMS

func (s *VASService) SendSMS(ctx context.Context, params *SendSMSParams) (*SMSResponse, error)

SendSMS sends a personalised SMS message to one or more recipients.

type VASTransactionResponse

type VASTransactionResponse struct {
	TransactionRef string `json:"transaction_ref"`
	Amount         int64  `json:"amount"`
	Status         string `json:"status"`
	PhoneNumber    string `json:"phone_number,omitempty"`
	Network        string `json:"network,omitempty"`
	CreatedAt      string `json:"created_at"`
}

VASTransactionResponse is the common response for airtime, data, and cable purchases.

type VerifyTransactionResponse

type VerifyTransactionResponse struct {
	TransactionRef  string         `json:"transaction_ref"`
	Amount          int64          `json:"transaction_amount"`
	Fee             float64        `json:"fee"`
	MerchantAmount  int64          `json:"merchant_amount"`
	Status          string         `json:"transaction_status"`
	Currency        string         `json:"transaction_currency_id"`
	Email           string         `json:"email"`
	TransactionType string         `json:"transaction_type"`
	MerchantName    string         `json:"merchant_name"`
	MerchantEmail   string         `json:"merchant_email"`
	Meta            map[string]any `json:"meta,omitempty"`
	IsRecurring     bool           `json:"is_recurring"`
	ChargeToken     *ChargeToken   `json:"charge_token,omitempty"`
	CreatedAt       string         `json:"created_at"`
	UpdatedAt       string         `json:"updated_at"`
}

VerifyTransactionResponse is the Data payload returned by VerifyTransaction.

type VirtualAccount

type VirtualAccount struct {
	VirtualAccountNumber string `json:"virtual_account_number"`
	CustomerIdentifier   string `json:"customer_identifier"`
	FirstName            string `json:"first_name"`
	LastName             string `json:"last_name"`
	MiddleName           string `json:"middle_name"`
	MobileNum            string `json:"mobile_num"`
	Email                string `json:"email"`
	UniqueID             string `json:"unique_id"`
	BeneficiaryAccount   string `json:"beneficiary_account"`
	CreatedAt            string `json:"created_at"`
	UpdatedAt            string `json:"updated_at"`
}

VirtualAccount is the core virtual account object returned by the API.

type VirtualAccountService

type VirtualAccountService struct {
	// contains filtered or unexported fields
}

VirtualAccountService handles NUBAN virtual account creation and management.

func (*VirtualAccountService) AllTransactions

func (s *VirtualAccountService) AllTransactions(ctx context.Context, customerIdentifier string, params *VirtualAccountTxParams) *Iter[VirtualAccountTransaction]

AllTransactions returns a lazy iterator over all transactions for a virtual account. Filters from params (StartDate, EndDate, Action) are preserved across pages.

iter := client.VirtualAccounts.AllTransactions(ctx, "cust-001", nil)
for iter.Next() {
    tx := iter.Item()
    fmt.Println(tx.TransactionRef, tx.SenderName)
}

func (*VirtualAccountService) Create

Create creates a NUBAN-compliant virtual account for a customer. The account is permanently tied to CustomerIdentifier and receives payments on their behalf. CBN profiling must be completed on the Squad dashboard before calling this endpoint.

func (*VirtualAccountService) GetTransactions

func (s *VirtualAccountService) GetTransactions(ctx context.Context, customerIdentifier string, params *VirtualAccountTxParams) (*VirtualAccountTxResponse, error)

GetTransactions retrieves paginated transactions for a virtual account by customer identifier.

func (*VirtualAccountService) Query

func (s *VirtualAccountService) Query(ctx context.Context, virtualAccountNumber string) (*VirtualAccount, error)

Query retrieves the details of a virtual account by its account number.

func (*VirtualAccountService) Simulate

Simulate credits a sandbox virtual account with a test transaction. This method returns an error when called against a production base URL.

func (*VirtualAccountService) Update

Update modifies metadata on an existing virtual account.

type VirtualAccountTransaction

type VirtualAccountTransaction struct {
	TransactionRef      string `json:"transaction_ref"`
	Amount              int64  `json:"amount"`
	Currency            string `json:"currency"`
	SenderName          string `json:"sender_name"`
	SenderBank          string `json:"sender_bank"`
	SenderAccountNumber string `json:"sender_account_number"`
	Status              string `json:"transaction_status"`
	CreatedAt           string `json:"created_at"`
}

VirtualAccountTransaction is a single credit transaction on a virtual account.

type VirtualAccountTxParams

type VirtualAccountTxParams struct {
	Page      int    `json:"page,omitempty"`
	PerPage   int    `json:"per_page,omitempty"`
	StartDate string `json:"start_date,omitempty"` // "YYYY-MM-DD"
	EndDate   string `json:"end_date,omitempty"`
	Action    string `json:"action,omitempty"`
}

VirtualAccountTxParams holds query parameters for fetching virtual account transactions.

type VirtualAccountTxResponse

type VirtualAccountTxResponse struct {
	Transactions []VirtualAccountTransaction `json:"transactions"`
	Total        int                         `json:"total"`
	Page         int                         `json:"page"`
	PerPage      int                         `json:"per_page"`
}

VirtualAccountTxResponse holds paginated transactions for a virtual account.

type WebhookDisputeBody

type WebhookDisputeBody struct {
	TicketID       string `json:"ticket_id"`
	TransactionRef string `json:"transaction_ref"`
	Amount         int64  `json:"amount"`
	Status         string `json:"dispute_status"`
	Reason         string `json:"dispute_reason"`
	CreatedAt      string `json:"created_at"`
}

WebhookDisputeBody holds the body payload for dispute events.

type WebhookEvent

type WebhookEvent struct {
	Event          EventType       `json:"Event"`
	TransactionRef string          `json:"TransactionRef"`
	Body           json.RawMessage `json:"Body"`
}

WebhookEvent is the top-level structure of all Squad webhook payloads. Squad uses PascalCase field names at the top level. Use event.Event to determine the event type, then call ParseBody() to decode the body.

func ParseWebhook

func ParseWebhook(payload []byte, signature string, secret string) (*WebhookEvent, error)

ParseWebhook parses and validates an inbound Squad webhook.

payload is the raw HTTP request body bytes. signature is the value of the "x-squad-encrypted-body" HTTP header. secret is the merchant's Squad secret key (same key used for API calls).

Note: Squad sandbox does not send the signature header. Pass an empty string to skip signature validation during development (production always signs).

Returns ErrInvalidSignature if the HMAC-SHA512 signature does not match. Returns a parse error if the JSON payload is malformed.

func (*WebhookEvent) ParseBody

func (e *WebhookEvent) ParseBody() (any, error)

ParseBody decodes the Body field into the appropriate typed struct based on Event. Returns one of:

  • *WebhookTransactionBody for EventTransactionSuccess / EventTransactionFailed
  • *WebhookVirtualAccountBody for EventVirtualAccountCredit
  • *WebhookTransferBody for EventTransferSuccess / EventTransferFailed / EventTransferReversed
  • *WebhookDisputeBody for EventDisputeOpened / EventDisputeResolved
  • json.RawMessage for unknown or future event types

type WebhookRouter

type WebhookRouter struct {
	// contains filtered or unexported fields
}

WebhookRouter is an http.Handler that validates Squad webhook signatures and dispatches each event to its registered typed handler function.

Register handlers with the fluent On* methods, then mount the router as an HTTP handler:

router := squad.NewWebhookRouter(os.Getenv("SQUAD_SECRET_KEY")).
    OnTransactionSuccess(handlePayment).
    OnVirtualAccountCredit(handleCredit).
    OnDisputeOpened(handleDispute)

http.Handle("/webhook/squad", router)

func NewWebhookRouter

func NewWebhookRouter(secret string) *WebhookRouter

NewWebhookRouter creates a new WebhookRouter that validates signatures using secret. secret is the same Squad secret key used to initialise the Client.

func (*WebhookRouter) OnDisputeOpened

func (r *WebhookRouter) OnDisputeOpened(fn func(context.Context, *WebhookDisputeBody) error) *WebhookRouter

OnDisputeOpened registers a handler for dispute.opened events.

func (*WebhookRouter) OnDisputeResolved

func (r *WebhookRouter) OnDisputeResolved(fn func(context.Context, *WebhookDisputeBody) error) *WebhookRouter

OnDisputeResolved registers a handler for dispute.resolved events.

func (*WebhookRouter) OnError

func (r *WebhookRouter) OnError(fn func(http.ResponseWriter, *http.Request, error)) *WebhookRouter

OnError registers a custom error handler called when signature validation fails, JSON parsing fails, or a dispatched handler returns an error. The default behaviour is to write an HTTP 400 or 403 response.

func (*WebhookRouter) OnTransactionFailed

func (r *WebhookRouter) OnTransactionFailed(fn func(context.Context, *WebhookTransactionBody) error) *WebhookRouter

OnTransactionFailed registers a handler for charge.failed events.

func (*WebhookRouter) OnTransactionSuccess

func (r *WebhookRouter) OnTransactionSuccess(fn func(context.Context, *WebhookTransactionBody) error) *WebhookRouter

OnTransactionSuccess registers a handler for charge.success events.

func (*WebhookRouter) OnTransferFailed

func (r *WebhookRouter) OnTransferFailed(fn func(context.Context, *WebhookTransferBody) error) *WebhookRouter

OnTransferFailed registers a handler for transfer.failed events.

func (*WebhookRouter) OnTransferReversed

func (r *WebhookRouter) OnTransferReversed(fn func(context.Context, *WebhookTransferBody) error) *WebhookRouter

OnTransferReversed registers a handler for transfer.reversed events.

func (*WebhookRouter) OnTransferSuccess

func (r *WebhookRouter) OnTransferSuccess(fn func(context.Context, *WebhookTransferBody) error) *WebhookRouter

OnTransferSuccess registers a handler for transfer.success events.

func (*WebhookRouter) OnUnknown

func (r *WebhookRouter) OnUnknown(fn func(context.Context, *WebhookEvent) error) *WebhookRouter

OnUnknown registers a fallback handler for unrecognised or future event types. The raw *WebhookEvent is passed so the caller can inspect event.Body directly.

func (*WebhookRouter) OnVirtualAccountCredit

func (r *WebhookRouter) OnVirtualAccountCredit(fn func(context.Context, *WebhookVirtualAccountBody) error) *WebhookRouter

OnVirtualAccountCredit registers a handler for virtual-account.credit events.

func (*WebhookRouter) ServeHTTP

func (r *WebhookRouter) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP implements http.Handler. It reads the request body, validates the x-squad-signature header, parses the event, and dispatches to the registered handler. Returns 403 on invalid signature, 400 on parse errors, 500 if a handler returns an error, and 200 on success.

type WebhookTransactionBody

type WebhookTransactionBody struct {
	TransactionRef string         `json:"transaction_ref"`
	GatewayRef     string         `json:"gateway_ref"`
	Amount         int64          `json:"amount"`
	MerchantAmount int64          `json:"merchant_amount"`
	Currency       string         `json:"currency"`
	Status         string         `json:"transaction_status"`
	Channel        string         `json:"transaction_type"` // "Card", "Bank", "Ussd", "MerchantUssd"
	Email          string         `json:"email"`
	CustomerName   string         `json:"customer_name,omitempty"`
	MerchantID     string         `json:"merchant_id"`
	Meta           map[string]any `json:"meta,omitempty"`
	IsRecurring    bool           `json:"is_recurring"`
	ChargeToken    *ChargeToken   `json:"charge_token,omitempty"`
	CreatedAt      string         `json:"created_at"`
}

WebhookTransactionBody holds the body payload for charge_successful and charge_failed events.

type WebhookTransferBody

type WebhookTransferBody struct {
	TransactionRef string `json:"transaction_ref"`
	Amount         int64  `json:"amount"`
	Status         string `json:"status"`
	AccountName    string `json:"account_name"`
	AccountNumber  string `json:"account_number"`
	BankCode       string `json:"bank_code"`
	CreatedAt      string `json:"created_at"`
}

WebhookTransferBody holds the body payload for transfer events.

type WebhookVirtualAccountBody

type WebhookVirtualAccountBody struct {
	VirtualAccountNumber string `json:"virtual_account_number"`
	Amount               int64  `json:"amount"`
	Currency             string `json:"currency"`
	SenderName           string `json:"sender_name"`
	SenderBank           string `json:"sender_bank"`
	TransactionRef       string `json:"transaction_ref"`
	CustomerIdentifier   string `json:"customer_identifier"`
	CreatedAt            string `json:"created_at"`
}

WebhookVirtualAccountBody holds the body payload for virtualaccount_credited events.

Directories

Path Synopsis
examples
payment command
Package main demonstrates Squad payment initiation and verification.
Package main demonstrates Squad payment initiation and verification.
virtualaccount command
Package main demonstrates Squad virtual account creation and management.
Package main demonstrates Squad virtual account creation and management.
webhook command
Package main demonstrates Squad webhook signature validation and event routing.
Package main demonstrates Squad webhook signature validation and event routing.
Package squadtest provides a mock Squad API server for use in tests.
Package squadtest provides a mock Squad API server for use in tests.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL