Initial release: qbirr Go SDK v1.0.0

This commit is contained in:
2026-07-17 16:42:32 +02:00
commit c457a190e5
3 changed files with 225 additions and 0 deletions
+59
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
module github.com/qbirr/sdk-go
go 1.21
+163
View File
@@ -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
}