commit c457a190e57c8275bc5010c2bea6e5cee3a49e7e Author: admin Date: Fri Jul 17 16:42:32 2026 +0200 Initial release: qbirr Go SDK v1.0.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5996d6 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# qbirr-go + +Go client for the qbirr Ethiopian bank verification API. + +## Install + +```bash +go get github.com/qbirr/sdk-go +``` + +## Quick start + +```go +package main + +import ( + "context" + "fmt" + "os" + + "github.com/qbirr/sdk-go" +) + +func main() { + qb := qbirr.New(os.Getenv("QBIRR_API_KEY")) + + r, err := qb.Verify(context.Background(), qbirr.VerifyRequest{ + Provider: qbirr.ProviderCBE, + Ref: "FT23001234ABC", + Amount: 500, + ReceiverName: "YOUR NAME", + ReceiverAccount: "1000017692643", + }) + if err != nil { + fmt.Println("API error:", err) + return + } + if r.Verified { + fmt.Printf("✓ Paid by %s, amount: %.2f\n", r.Payer, r.Amount) + } else { + fmt.Println("✗", r.Error) + } +} +``` + +## Errors + +Network and HTTP errors return as Go `error`. Verification failures (wrong amount, wrong receiver, etc.) are NOT errors — they're returned as `VerifyResult{Verified: false, Error: "..."}`. + +```go +var apiErr *qbirr.APIError +if errors.As(err, &apiErr) { + fmt.Println("HTTP status:", apiErr.Status) +} +``` + +## License + +MIT diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6de6087 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/qbirr/sdk-go + +go 1.21 diff --git a/qbirr.go b/qbirr.go new file mode 100644 index 0000000..ebb59e0 --- /dev/null +++ b/qbirr.go @@ -0,0 +1,163 @@ +// Package qbirr is a thin Go client for the qbirr payment verification API. +// +// qb := qbirr.New("your_api_key") +// r, err := qb.Verify(context.Background(), qbirr.VerifyRequest{ +// Provider: "cbe", +// Ref: "FT23001234ABC", +// Amount: 500, +// ReceiverName: "YOUR NAME", +// ReceiverAccount: "1000017692643", +// }) +// if err != nil { return err } +// if r.Verified { fmt.Println("Paid by", r.Payer) } +package qbirr + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" +) + +const ( + Version = "1.0.0" + DefaultBaseURL = "https://verify.qbirr.com" + DefaultTimeout = 30 * time.Second +) + +// Provider identifies which bank or wallet the reference belongs to. +type Provider = string + +const ( + ProviderCBE Provider = "cbe" + ProviderTelebirr Provider = "telebirr" + ProviderAbyssinia Provider = "abyssinia" + ProviderDashen Provider = "dashen" + ProviderMpesa Provider = "mpesa" + ProviderAwash Provider = "awash" + ProviderEbirr Provider = "ebirr" + ProviderZamZam Provider = "zamzam" +) + +// VerifyRequest is the payload for /api/v1/verify. +type VerifyRequest struct { + Provider Provider `json:"provider"` + Ref string `json:"ref"` + Amount float64 `json:"amount"` + ReceiverName string `json:"receiver_name"` + ReceiverAccount string `json:"receiver_account,omitempty"` // required for CBE & Abyssinia +} + +// VerifyResult is the API response. Verification failures arrive here (Verified=false + Error set), +// NOT as Go errors. Errors are reserved for network/HTTP problems. +type VerifyResult struct { + Verified bool `json:"verified"` + Payer string `json:"payer"` + Amount float64 `json:"amount"` + Error string `json:"error"` +} + +// UsageStatus is the response from /api/v1/usage. +type UsageStatus struct { + Used int `json:"used"` + Limit int `json:"limit"` + Remaining int `json:"remaining"` + Plan string `json:"plan"` + PeriodStart string `json:"period_start"` + PeriodEnd string `json:"period_end"` +} + +// APIError wraps a non-2xx HTTP response. +type APIError struct { + Status int + Message string +} + +func (e *APIError) Error() string { return fmt.Sprintf("qbirr: HTTP %d: %s", e.Status, e.Message) } + +// Client talks to the qbirr API. +type Client struct { + APIKey string + BaseURL string + HTTPClient *http.Client +} + +// New returns a client with sensible defaults. +func New(apiKey string) *Client { + return &Client{ + APIKey: apiKey, + BaseURL: DefaultBaseURL, + HTTPClient: &http.Client{Timeout: DefaultTimeout}, + } +} + +// Verify performs a verification. Returns (result, nil) for both verified=true and verified=false +// (real verification failures). Returns (nil, error) only for transport/HTTP issues. +func (c *Client) Verify(ctx context.Context, req VerifyRequest) (*VerifyResult, error) { + if c.APIKey == "" { + return nil, errors.New("qbirr: APIKey is empty") + } + var out VerifyResult + if err := c.do(ctx, "POST", "/api/v1/verify", req, &out); err != nil { + return nil, err + } + return &out, nil +} + +// Usage returns the current calendar-month quota status. +func (c *Client) Usage(ctx context.Context) (*UsageStatus, error) { + var out UsageStatus + if err := c.do(ctx, "GET", "/api/v1/usage", nil, &out); err != nil { + return nil, err + } + return &out, nil +} + +func (c *Client) do(ctx context.Context, method, path string, body any, out any) error { + var rdr io.Reader + if body != nil { + buf, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("qbirr: marshal: %w", err) + } + rdr = bytes.NewReader(buf) + } + req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, rdr) + if err != nil { + return err + } + req.Header.Set("X-API-Key", c.APIKey) + req.Header.Set("Accept", "application/json") + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + req.Header.Set("User-Agent", "qbirr-go/"+Version) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return fmt.Errorf("qbirr: network: %w", err) + } + defer resp.Body.Close() + raw, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("qbirr: read body: %w", err) + } + if resp.StatusCode >= 400 { + // Try to extract { error: "..." } + var apiErr struct{ Error string `json:"error"` } + _ = json.Unmarshal(raw, &apiErr) + msg := apiErr.Error + if msg == "" { + msg = string(raw) + } + return &APIError{Status: resp.StatusCode, Message: msg} + } + if err := json.Unmarshal(raw, out); err != nil { + return fmt.Errorf("qbirr: decode (HTTP %d): %w", resp.StatusCode, err) + } + return nil +}