Skip to content

Latest commit

 

History

44 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

moneyhub-go

Go Reference

A production-grade, dependency-free Go client for the Moneyhub Open Finance API - Open Banking account aggregation (AIS), payment initiation (PIS), data categorisation/enrichment, affordability, and webhooks.

Built entirely on the Go standard library (net/http, crypto/rsa, encoding/json, context) - no third-party dependencies, no version conflicts, no supply-chain surface beyond the Go toolchain itself.

Features

  • OpenID Connect authentication - Pushed Authorisation Requests (PAR), private_key_jwt client assertions (signed with stdlib crypto/rsa), authorisation code exchange, client_credentials tokens for ongoing per-user access, refresh tokens, and OIDC discovery.
  • Data Aggregation (AIS) - accounts (manual balances, standing orders, sync status), transactions (manual transactions, splits, file attachments), regular transaction (subscription/rent/salary) detection, connection lifecycle (immediate sync, connection-type filtered catalogs), categories and category groups, categorisation-as-a-service, counterparties (per-user and global), beneficiaries, investment holdings with ISIN matching, spending analysis, savings/spending goals, rental records, affordability reports, Standard Financial Statements, notification thresholds, account statements, tax (SA105) data, projects, consent history, bank icons, reseller checks, and both lightweight (users) and SCIM-based (scimusers) user records.
  • Payments (PIS) - payees, single immediate payments, Variable Recurring Payments (VRP) with sweep triggering and funds confirmation, standing orders, bulk pay files, shareable pay links, and refunds.
  • Webhooks - verifies both plain-JSON and signed-JWT webhook deliveries against Moneyhub's published JWKS (RS256 signature verification implemented with stdlib crypto/rsa, no JWT library).
  • Automatic retry with backoff for 429 (honouring Retry-After) and 5xx responses, a structured *transport.Error type instead of bare errors, full context propagation, and a clean DDD package layout - one Go package per bounded context.

Installation

go get github.com/iamkanishka/moneyhub-go

Go 1.21+ required (for the any alias and generics-adjacent stdlib features used internally).

Project layout

moneyhub-go/
├── config/                    # Config type and functional options
├── internal/transport/        # Shared HTTP client, retry/backoff, errors
└── pkg/domain/                # One package per bounded context (DDD)
    ├── auth/                  # OIDC: PAR, token exchange, JWKS, id_token verify
    ├── accounts/
    ├── transactions/
    ├── connections/
    ├── ... (34 domain packages total)
    └── webhooks/

Each domain package exposes a Service type constructed via New(cfg), and has no knowledge of any other domain package (aside from auth and webhooks sharing the JWKS verification primitive).

Configuration

Build a *config.Config once and pass it to every domain package's New constructor. In production, Moneyhub requires private_key_jwt client authentication:

import (
    "github.com/iamkanishka/moneyhub-go/config"
    "github.com/iamkanishka/moneyhub-go/pkg/domain/auth"
)

privateKeyPEM, err := os.ReadFile("/path/to/private_key.pem")
if err != nil {
    log.Fatal(err)
}
privateKey, err := auth.LoadRSAPrivateKeyPEM(privateKeyPEM)
if err != nil {
    log.Fatal(err)
}

cfg, err := config.New(
    os.Getenv("MONEYHUB_CLIENT_ID"),
    config.Production,
    config.WithPrivateKeyJWT(privateKey, os.Getenv("MONEYHUB_KEY_ID")),
    config.WithRedirectURI("https://myapp.example.com/moneyhub/callback"),
)
if err != nil {
    log.Fatal(err)
}

For early sandbox development, client_secret_basic is also supported:

cfg, err := config.New(
    "my-client-id",
    config.Sandbox,
    config.WithClientSecretBasic("my-client-secret"),
    config.WithRedirectURI("https://myapp.example.com/moneyhub/callback"),
)

Quick start: connect a bank account, then read transactions

ctx := context.Background()

authSvc := auth.New(cfg)
accountsSvc := accounts.New(cfg)
transactionsSvc := transactions.New(cfg)

// 1. Build an authorisation URL for a new user (Moneyhub assigns the sub)
claims := auth.NewClaims().PutSub("")

result, err := authSvc.PushAuthorisationRequest(ctx, auth.AuthorisationURLOptions{
    Scope:  auth.AISOfflineScopes(),
    Claims: claims,
})
if err != nil {
    log.Fatal(err)
}

// 2. Redirect the user's browser to result.URL. They authenticate at
//    their bank and are redirected back to your RedirectURI with
//    ?code=...&state=...

// 3. Exchange the code for tokens and verify the id_token
tokens, err := authSvc.ExchangeCode(ctx, code, "")
if err != nil {
    log.Fatal(err)
}
idClaims, err := authSvc.VerifyIDToken(ctx, tokens.IDToken)
if err != nil {
    log.Fatal(err)
}
userID, _ := idClaims.String("sub")

// 4. From now on, fetch fresh data tokens for this user as needed
dataTokens, err := authSvc.TokenForUser(ctx, userID, "")
if err != nil {
    log.Fatal(err)
}

accountsList, err := accountsSvc.List(ctx, dataTokens.AccessToken, accounts.ListOptions{})
if err != nil {
    log.Fatal(err)
}

txs, err := transactionsSvc.List(ctx, dataTokens.AccessToken, transactions.ListOptions{
    AccountID: accountsList[0].ID,
})

Quick start: a single immediate payment

paymentsSvc := payments.New(cfg)

payment := map[string]any{
    "amount": map[string]any{"amount": 10.50, "currency": "GBP"},
    "creditorAccount": map[string]any{
        "identification": map[string]any{
            "sortCode":      "010203",
            "accountNumber": "12345678",
        },
    },
    "reference": "Invoice 123",
}

claims := auth.NewClaims().PutSub("").PutPayment(payment)

result, err := authSvc.PushAuthorisationRequest(ctx, auth.AuthorisationURLOptions{
    Scope:  auth.PaymentScopes(),
    Claims: claims,
})

// redirect the user to result.URL to authorise the payment at their bank, then:

tokens, err := authSvc.ExchangeCode(ctx, code, "")
idClaims, err := authSvc.VerifyIDToken(ctx, tokens.IDToken)
paymentInfo := idClaims["mh:payment"]

Webhooks

webhooksVerifier := webhooks.New(cfg)

func handleWebhook(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    event, err := webhooksVerifier.Parse(r.Context(), body)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    switch event.ID {
    case "newTransactions":
        go processNewTransactions(event.Payload)
    default:
        go handleGenericEvent(event)
    }

    w.WriteHeader(http.StatusOK)
}

Moneyhub's webhook delivery has a 5 second response timeout and at most one retry - acknowledge with 200 immediately and do slow processing in a goroutine afterwards.

Error handling

Every function that can fail returns a *transport.Error (which implements error) with a structured Reason (ReasonConfig/ReasonNetwork/ReasonAPI/ReasonRateLimited/ ReasonDecode/ReasonJWT/ReasonValidation) instead of an opaque error string:

import "errors"

accountsList, err := accountsSvc.List(ctx, token, accounts.ListOptions{})
if err != nil {
    var apiErr *transport.Error
    if errors.As(err, &apiErr) {
        switch apiErr.Reason {
        case transport.ReasonRateLimited:
            time.Sleep(time.Duration(apiErr.RetryAfter) * time.Second)
        case transport.ReasonAPI:
            log.Printf("moneyhub API error %d: %s", apiErr.Status, apiErr.Code)
        }
    }
}

Testing

The whole test suite uses only net/http/httptest - no mocking framework, no third-party assertion library:

go test ./...
go test ./... -race

Documentation

Full package documentation: https://pkg.go.dev/github.com/iamkanishka/moneyhub-go.

License

MIT

About

A production-grade, dependency-free Go client for the [Moneyhub Open Finance API](https://docs.moneyhubenterprise.com/) - Open Banking account aggregation (AIS), payment initiation (PIS), data categorisation/enrichment, affordability, and webhooks.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages