Go
The wajub-go module, its resources, and the context every call takes.
wajub-go is the server side of a Wajub integration. It holds your secret key, creates payments,
reads their real status and verifies webhook signatures.
github.com/wajubhq/wajub-go
Stable · GAGo modules
- Version
- v1.2.1
- Runtime
- Go 1.22+
Covers
- Payments
- Billing
- Transfers
- Sync
- Shield
- Tax
Install
go get github.com/wajubhq/wajub-goThe import path is github.com/wajubhq/wajub-go and the package is named wajub, so the import
needs no alias.
Create the client
New returns an error, and it falls back to the environment, so the common case is a zero
Config.
package wajubclient
import (
"log"
"github.com/wajubhq/wajub-go"
)
// Client is built once at start-up and shared. It is safe for concurrent use.
var Client *wajub.Client
func init() {
// Reads WAJUB_API_KEY and WAJUB_WEBHOOK_SECRET when Config leaves them empty.
c, err := wajub.New(wajub.Config{})
if err != nil {
log.Fatalf("wajub: %v", err)
}
Client = c
}| Field | What it does |
|---|---|
APIKey | Your sk. or sk_test. key. Falls back to WAJUB_API_KEY |
WebhookSecret | The whsec_ secret. Falls back to WAJUB_WEBHOOK_SECRET |
HTTPClient | Your own *http.Client, for a proxy, tracing or tests |
MaxNetworkRetries | How many times a failed request is retried, as a *int. nil keeps the default, 0 turns retries off |
Timeout | A time.Duration. Ignored when HTTPClient is set: put the timeout on your own client |
New returns wajub: api key is required when neither the field nor the variable holds one, so
a bad configuration fails at start-up.
The first call
Every method takes a context.Context first. Pass the request's context and a slow API cancels
with the request instead of holding the goroutine.
payment, err := Client.Payments.Create(ctx, map[string]any{
"amount": 25000,
"currency": "XAF",
"email": "amina@example.com",
"description": "Order 4172",
"reference": "order-4172",
"callback": "https://shop.example.com/complete",
}, nil)
if err != nil {
return fmt.Errorf("create payment: %w", err)
}
http.Redirect(w, r, payment.AuthorizationURL, http.StatusSeeOther)Parameters go in as map[string]any with the API's exact field names. The last argument is
*RequestOptions, and nil is the ordinary value.
Payment is a narrow struct, Raw holds the rest
Payment decodes six fields: ID, Status, Amount, Currency, AuthorizationURL and
AuthorizationToken. Everything else the API returned, reference and created_at included,
lives in payment.Raw as a map[string]any. Reach for Raw rather than assuming a field.
reference, _ := payment.Raw["reference"].(string)
createdAt, _ := payment.Raw["created_at"].(string)Amount is a whole number
Amount is an int64, which suits XAF, XOF, NGN and every currency without decimals. For a
currency with decimals, such as GHS 12.50, read payment.Raw["amount"].(float64).
Every resource on the client
| Service | Methods |
|---|---|
| client.Global | Ping, Channels, Countries, Currencies |
| client.Payments | Create, Retrieve, List, Cancel, Process, ProcessSplit, ListRefunds |
| client.Customers | Create, Retrieve, Update, Delete, List, Block, Unblock, Activate, Deactivate, ListTaxIDs, CreateTaxID, DeleteTaxID |
| client.Refunds | Create, Retrieve, List |
| client.Transfers | Create, Retrieve, List |
| client.Beneficiaries | Create, Retrieve, Update, Delete, List |
| client.Links | Create, Retrieve, Update, Delete, List |
| client.Invoices | Create, Retrieve, Update, Delete, List, Send, MarkPaid, Cancel |
| client.Accounts | Create, Retrieve, Update, Delete, List, RegenerateToken |
| client.WebhookEndpoints | Create, Retrieve, Update, Delete, List, RotateSecret |
| client.Balance | Retrieve |
| client.Events | List, Retrieve, Resend |
| client.Disputes | List, Retrieve, SubmitEvidence, Accept, Close, SendMessage |
| client.Identity | Resolve, Validate |
| client.Tax | GetSettings, UpdateSettings, Rates, Calculate, Reports, ListCodes, RetrieveCode, ListRegistrations, CreateRegistration, RetrieveRegistration, UpdateRegistration, DeleteRegistration, Jurisdictions, Thresholds, ThresholdAlerts |
| client.Shield | GetSettings, UpdateSettings, Stats, ListBlocklist, AddToBlocklist, RemoveFromBlocklist |
| client.Webhooks | ConstructEvent |
webhookssignature verification runs locally, no HTTP call. All other resources call the merchant REST API.links,invoices,taxandshieldare live only. A sandbox key gets403 This feature is only available in live mode.on every one of their methods.refundsandtransfersare create, retrieve and list only.
Paging through a list
page, err := Client.Payments.List(ctx, map[string]any{
"status": "succeeded",
"per_page": 50,
})
if err != nil {
return err
}
for _, payment := range page.Data {
log.Println(payment["id"], payment["amount"])
}
if page.HasMore {
page, err = page.GetNextPage(ctx)
}
// Or collect every page at once.
all, err := page.AutoPagingIter(ctx)page.Data is []map[string]any, so rows are read by key. AutoPagingIter buys convenience with
memory, since it holds every row before returning.
Idempotency and retries
Failed requests are retried automatically, and a retry never creates anything twice. When the same order may be submitted again, pass your own key.
payment, err := Client.Payments.Create(ctx, params, &wajub.RequestOptions{
IdempotencyKey: "order-" + orderID,
})To turn retries off, point MaxNetworkRetries at a zero.
zero := 0
c, err := wajub.New(wajub.Config{MaxNetworkRetries: &zero})Acting for a connected account
_, err := Client.Payments.Create(ctx, params, &wajub.RequestOptions{
Sync: seller.WajubAccountID,
})Sync takes the connected account's id. Setup is on Sync.
Webhooks
ConstructEvent verifies the signature and returns the parsed event. It takes the body as bytes,
exactly as delivered.
func WajubWebhook(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
event, err := Client.Webhooks.ConstructEvent(
body,
r.Header.Get("X-Wajub-Signature"),
r.Header.Get("X-Wajub-Timestamp"),
0, // 0 means the default 300 second window
)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if event["event"] == "payment.succeeded" {
go fulfil(event["data"])
}
w.WriteHeader(http.StatusOK)
}The event name is in event, not in type, and the result is map[string]any, so read it by
key. Details on Signature verification.
Errors
Errors are pointer types wrapping a shared WajubError, so errors.As is how you inspect them.
payment, err := Client.Payments.Create(ctx, params, nil)
if err != nil {
var invalid *wajub.InvalidRequestError
if errors.As(err, &invalid) {
// invalid.Errors is map[string]string keyed by field name
return c.JSON(http.StatusUnprocessableEntity, invalid.Errors)
}
var limited *wajub.RateLimitError
if errors.As(err, &limited) {
w.Header().Set("Retry-After", strconv.Itoa(limited.RetryAfter))
w.WriteHeader(http.StatusServiceUnavailable)
return nil
}
var conn *wajub.APIConnectionError
if errors.As(err, &conn) && errors.Is(conn, context.DeadlineExceeded) {
return ErrPaymentTimedOut
}
return fmt.Errorf("wajub: %w", err)
}| Type | Returned on |
|---|---|
*AuthenticationError | 401 |
*PermissionError | 403 |
*NotFoundError | 404 |
*InvalidRequestError | 400 and 422 |
*RateLimitError | 429, with RetryAfter in seconds |
*WajubError | Every other status, and the struct embedded in all of the above |
*APIConnectionError | No response at all. errors.Is reaches the cause, a timeout for example |
*WebhookSignatureVerificationError | A webhook that did not verify |
Every one of them carries Message, Code, HTTPStatus, Errors and Raw.
Charging without the hosted page
payment, err := Client.Payments.Create(ctx, map[string]any{
"amount": 25000,
"currency": "XAF",
"phone": "+237670000000",
}, nil)
if err != nil {
return err
}
_, err = Client.Payments.Process(ctx, payment.ID, map[string]any{
"channel": "cm.mtn",
"phone": "+237670000000",
}, nil)A channel is country.operator, so cm.mtn is MTN Mobile Money in Cameroon. The full list is on
Mobile Money. The payer still approves on their handset, so the
outcome arrives on the webhook.
Paying out
transfer, err := Client.Transfers.Create(ctx, map[string]any{
"amount": 100000,
"currency": "XAF",
"beneficiary": map[string]any{
"name": "Amina Diallo",
"channel": "cm.mtn",
"phone": "+237670000000",
},
"reference": "payout-892",
}, &wajub.RequestOptions{IdempotencyKey: "payout-892"})beneficiary also takes the ben_… id of a saved beneficiary, which is the better shape once you
pay the same person twice.
Local development
wajub listen --forward-to localhost:8080/webhooks/wajub
wajub trigger payment.succeededMore on the CLI.
Related pages
- SDK QuickstartThe same first call, in five languages side by side.
- WebhooksEvery event, and the delivery guarantees behind them.
- Naming conventionsWhy Go exports CamelCase over a snake_case wire.
- IdempotencyWhat a key protects and for how long.
- Error handlingWhich failures to retry and which to surface.
- API referenceThe endpoints behind every method above.