Skip to content

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 · GA

Go modules

Version
v1.2.1
Runtime
Go 1.22+

Covers

  • Payments
  • Billing
  • Transfers
  • Sync
  • Shield
  • Tax

Install

Add the module
go get github.com/wajubhq/wajub-go

The 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.

wajubclient/client.go
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
}
FieldWhat it does
APIKeyYour sk. or sk_test. key. Falls back to WAJUB_API_KEY
WebhookSecretThe whsec_ secret. Falls back to WAJUB_WEBHOOK_SECRET
HTTPClientYour own *http.Client, for a proxy, tracing or tests
MaxNetworkRetriesHow many times a failed request is retried, as a *int. nil keeps the default, 0 turns retries off
TimeoutA 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.

Create a payment
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.

Reading past the six struct fields
reference, _ := payment.Raw["reference"].(string)
createdAt, _ := payment.Raw["created_at"].(string)

Every resource on the client

ServiceMethods
client.GlobalPing, Channels, Countries, Currencies
client.PaymentsCreate, Retrieve, List, Cancel, Process, ProcessSplit, ListRefunds
client.CustomersCreate, Retrieve, Update, Delete, List, Block, Unblock, Activate, Deactivate, ListTaxIDs, CreateTaxID, DeleteTaxID
client.RefundsCreate, Retrieve, List
client.TransfersCreate, Retrieve, List
client.BeneficiariesCreate, Retrieve, Update, Delete, List
client.LinksCreate, Retrieve, Update, Delete, List
client.InvoicesCreate, Retrieve, Update, Delete, List, Send, MarkPaid, Cancel
client.AccountsCreate, Retrieve, Update, Delete, List, RegenerateToken
client.WebhookEndpointsCreate, Retrieve, Update, Delete, List, RotateSecret
client.BalanceRetrieve
client.EventsList, Retrieve, Resend
client.DisputesList, Retrieve, SubmitEvidence, Accept, Close, SendMessage
client.IdentityResolve, Validate
client.TaxGetSettings, UpdateSettings, Rates, Calculate, Reports, ListCodes, RetrieveCode, ListRegistrations, CreateRegistration, RetrieveRegistration, UpdateRegistration, DeleteRegistration, Jurisdictions, Thresholds, ThresholdAlerts
client.ShieldGetSettings, UpdateSettings, Stats, ListBlocklist, AddToBlocklist, RemoveFromBlocklist
client.WebhooksConstructEvent
  • webhooks signature verification runs locally, no HTTP call. All other resources call the merchant REST API.
  • links, invoices, tax and shield are live only. A sandbox key gets 403 This feature is only available in live mode. on every one of their methods.
  • refunds and transfers are create, retrieve and list only.

Paging through a list

One page, or all of them
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.

An order number is the best key
payment, err := Client.Payments.Create(ctx, params, &wajub.RequestOptions{
	IdempotencyKey: "order-" + orderID,
})

To turn retries off, point MaxNetworkRetries at a zero.

No retry at all
zero := 0
c, err := wajub.New(wajub.Config{MaxNetworkRetries: &zero})

Acting for a connected account

One call, one seller
_, 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.

A net/http handler
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.

Match the specific one first
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)
}
TypeReturned on
*AuthenticationError401
*PermissionError403
*NotFoundError404
*InvalidRequestError400 and 422
*RateLimitError429, with RetryAfter in seconds
*WajubErrorEvery other status, and the struct embedded in all of the above
*APIConnectionErrorNo response at all. errors.Is reaches the cause, a timeout for example
*WebhookSignatureVerificationErrorA webhook that did not verify

Every one of them carries Message, Code, HTTPStatus, Errors and Raw.

Charging without the hosted page

Mobile Money push
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

A transfer to a phone number
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

Two terminals
wajub listen --forward-to localhost:8080/webhooks/wajub
wajub trigger payment.succeeded

More on the CLI.

What did you think of this content?