truelayer

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 8, 2026 License: MIT Imports: 0 Imported by: 0

README

truelayer-client-go

Go Reference Go Version License

A production-grade Go SDK for the TrueLayer open banking platform — built from the official API Reference and OpenAPI specs.

Zero external dependencies. Pure stdlib Go 1.25+.


API Coverage

Domain Endpoints
Auth (Authentication Server) Generate auth link, exchange code, client credentials, refresh token, delete credential, debug IDs
Payments v3 Create, get, cancel payment; full authorization flow (provider selection, scheme selection, form, consent); refunds (create/get/list); payment links (create/get/list payments)
Payments Providers Search providers, get provider, submit return parameters
Payouts Create payout, get payout
Merchant Accounts List/get accounts, get transactions, setup/disable/get sweeping, get payment sources
Mandates (VRP) Create, list, get mandate; start auth flow, submit provider selection/consent; revoke, confirm funds, get constraints
Data API v1 Connection metadata, user info; accounts (list/get/balance/transactions/pending/standing orders/direct debits); cards (list/get/balance/transactions/pending); providers; direct auth link, reauth link, extend connection
Verification Verify account holder name; create/get account holder verification
Signup+ Get user data by payment/connected account/mandate; generate auth URI
Client Tracking Get tracked events by flow ID
Webhooks HMAC-SHA256 signature verification, replay-attack protection, typed event dispatch, http.Handler implementation

Installation

go get github.com/iamkanishka/truelayer-client-go

Requires Go 1.25+.


Quick Start

import truelayer "github.com/iamkanishka/truelayer-client-go/truelayer"

client, err := truelayer.New(
    truelayer.WithEnvironment(truelayer.Sandbox),        // or truelayer.Live
    truelayer.WithCredentials("client_id", "secret"),
    truelayer.WithRedirectURI("https://yourapp.com/cb"),
    truelayer.WithSigningKey(privateKeyPEM, "key-id"),   // required for Payments
    truelayer.WithWebhookSigningKey(webhookSecret),
)

Every sub-client is accessible through a typed accessor with no global state:

client.Auth()         // *auth.Client
client.Payments()     // *payments.Client
client.Payouts()      // *payouts.Client
client.Merchant()     // *merchant.Client
client.Mandates()     // *mandates.Client
client.Data()         // *data.Client
client.Verification() // *verification.Client
client.SignupPlus()   // *signupplus.Client
client.Tracking()     // *tracking.Client
client.Webhooks()     // *webhooks.Handler

Authentication

TrueLayer uses OAuth2. The SDK handles all token lifecycle automatically.

link, err := client.Auth().GenerateAuthLink(auth.AuthLinkOptions{
    Scopes: auth.PaymentsScopes,   // or auth.DataScopes
    State:  csrfToken,             // validate on redirect — mandatory
})
// Redirect user to link
Exchange code for token
// After user completes bank login and is redirected back with ?code=...
tok, err := client.Auth().ExchangeCode(ctx, authCode, auth.TokenTypePayments)
Token isolation

Payments and Data tokens occupy separate store slots. A Data token can never be used to authorise a payment and vice versa — enforced at the TokenType level throughout the SDK.

Custom token store (Redis, DynamoDB, etc.)
type RedisStore struct{ client *redis.Client }

func (r *RedisStore) Get(ctx context.Context, tt auth.TokenType) (*auth.Token, error) { ... }
func (r *RedisStore) Set(ctx context.Context, tt auth.TokenType, t *auth.Token) error { ... }
func (r *RedisStore) Delete(ctx context.Context, tt auth.TokenType) error { ... }

client, _ := truelayer.New(
    truelayer.WithTokenStore(&RedisStore{client: rdb}),
    // ...
)

Payments API v3

Create a payment
created, err := client.Payments().CreatePayment(ctx, payments.CreatePaymentRequest{
    AmountInMinor: 1000,      // £10.00 in pence
    Currency:      "GBP",
    PaymentMethod: payments.PaymentMethod{
        Type: "bank_transfer",
        ProviderSelection: payments.ProviderSelection{
            Type: "user_selected",
            Filter: &payments.ProviderFilter{Countries: []string{"GB"}},
        },
        Beneficiary: payments.Beneficiary{
            Type:              "merchant_account",
            MerchantAccountID: merchantAccountID,
            Reference:         "Order #12345",
        },
    },
    User: payments.PaymentUser{Name: "Jane Doe", Email: "jane@example.com"},
    Metadata: payments.Metadata{"order_id": "12345"},
}, "order-12345")  // stable operationID → safe retries

The operationID is hashed into a stable idempotency key stored by the SDK's idempotency.Manager. Retrying the same operationID always sends the same Idempotency-Key header, preventing duplicate payments.

Authorization flow
// Start the flow (returns next action for the UI)
flowResp, err := client.Payments().StartAuthorizationFlow(ctx, paymentID,
    payments.StartAuthorizationFlowRequest{
        Redirect: &payments.RedirectConfig{ReturnURI: "https://yourapp.com/cb"},
        ProviderSelection: &payments.AuthorizationFlowProviderSelection{
            EnableSearchByProvider: true,
        },
    })

// Submit provider selection
resp, err := client.Payments().SubmitProviderSelection(ctx, paymentID,
    payments.SubmitProviderSelectionRequest{ProviderID: "ob-monzo"})

// Submit scheme selection
resp, err := client.Payments().SubmitSchemeSelection(ctx, paymentID,
    payments.SubmitSchemeSelectionRequest{SchemeID: "faster_payments_service"})

// Submit form inputs
resp, err := client.Payments().SubmitForm(ctx, paymentID,
    payments.SubmitFormRequest{Inputs: map[string]string{"username": "user123"}})

// Submit consent
resp, err := client.Payments().SubmitConsent(ctx, paymentID,
    payments.SubmitConsentRequest{Consent: true})
Get payment status
payment, err := client.Payments().GetPayment(ctx, paymentID)
fmt.Println(payment.Status) // "authorization_required" | "authorizing" | "authorized" | "executed" | "settled" | "failed"
Refunds
// Create a partial refund
refund, err := client.Payments().CreateRefund(ctx, paymentID,
    payments.CreateRefundRequest{AmountInMinor: 500, Reference: "Partial refund"},
    "refund-op-001")

// Full refund (AmountInMinor: 0)
refund, err := client.Payments().CreateRefund(ctx, paymentID,
    payments.CreateRefundRequest{}, "fullrefund-op-001")

// List all refunds
refunds, err := client.Payments().ListRefunds(ctx, paymentID)
link, err := client.Payments().CreatePaymentLink(ctx, payments.CreatePaymentLinkRequest{
    AmountInMinor: 2500,
    Currency:      "GBP",
    PaymentMethod: payments.PaymentLinkMethod{
        Type:        "bank_transfer",
        Beneficiary: payments.Beneficiary{Type: "merchant_account", MerchantAccountID: maID},
    },
    ReturnURI: "https://yourapp.com/thank-you",
}, "link-op-001")
fmt.Println(link.Link) // Share this URL with the user

Payouts

payout, err := client.Payouts().CreatePayout(ctx, payouts.CreatePayoutRequest{
    MerchantAccountID: merchantAccountID,
    AmountInMinor:     5000,
    Currency:          "GBP",
    Beneficiary: payments.Beneficiary{
        Type:              "external_account",
        AccountHolderName: "Alice Smith",
        AccountIdentifier: &payments.AccountIdentifier{
            Type: "sort_code_account_number", SortCode: "040004", AccountNumber: "12345678",
        },
        Reference: "Payout ref",
    },
}, "payout-op-001")

Merchant Accounts

// List accounts
accounts, err := client.Merchant().ListMerchantAccounts(ctx)

// Get transactions with filters
txns, err := client.Merchant().GetTransactions(ctx, accountID, merchant.TransactionsQuery{
    From: &from, To: &to, Type: merchant.TransactionTypePayout,
})

// Configure sweeping
cfg, err := client.Merchant().SetupSweeping(ctx, accountID, merchant.SetupSweepingRequest{
    MaxAmountInMinor: 100000,
    Currency:         "GBP",
    Frequency:        "daily",
})

// Disable sweeping
err = client.Merchant().DisableSweeping(ctx, accountID)

Mandates (Variable Recurring Payments)

validTo := time.Now().AddDate(0, 6, 0)

mandate, err := client.Mandates().CreateMandate(ctx, mandates.CreateMandateRequest{
    MandateType: mandates.MandateTypeVRPSweeping,
    Currency:    "GBP",
    User:        payments.PaymentUser{Name: "Jane", Email: "jane@example.com"},
    Constraints: mandates.Constraints{
        ValidTo:          &validTo,
        MaxSinglePayment: &mandates.SinglePaymentLimit{MaxAmountInMinor: 50000},
        PeriodicLimits: []mandates.PeriodicLimit{{
            MaxAmountInMinor: 200000,
            PeriodType:       mandates.PeriodicMonth,
            PeriodAlignment:  mandates.AlignmentCalendar,
        }},
    },
    Beneficiary:       payments.Beneficiary{Type: "merchant_account", MerchantAccountID: maID},
    ProviderSelection: payments.ProviderSelection{Type: "user_selected"},
}, "mandate-op-001")

// Check funds availability
funds, err := client.Mandates().ConfirmFunds(ctx, mandate.ID, 10000)
fmt.Println(funds.Confirmed) // true/false

// Revoke a mandate
err = client.Mandates().RevokeMandate(ctx, mandate.ID)

Data API v1

// List all accounts
accounts, err := client.Data().ListAccounts(ctx)

// Get balance
balance, err := client.Data().GetAccountBalance(ctx, accountID)

// Paginated transaction iterator — handles all pages automatically
from := time.Now().AddDate(0, -3, 0)
iter := client.Data().TransactionIterator(ctx, accountID, data.TransactionQuery{From: &from})
for iter.Next() {
    for _, txn := range iter.Page() {
        fmt.Printf("%s %+.2f %s\n", txn.Timestamp.Format("2006-01-02"), txn.Amount, txn.Currency)
    }
}
if err := iter.Err(); err != nil {
    log.Fatal(err)
}

// Or collect all at once
all, err := iter.All()

// Cards
cards, err := client.Data().ListCards(ctx)
balance, err := client.Data().GetCardBalance(ctx, cardAccountID)
txns, err := client.Data().GetCardTransactions(ctx, cardAccountID, data.TransactionQuery{})

// Standing orders & direct debits
orders, err := client.Data().GetStandingOrders(ctx, accountID)
debits, err := client.Data().GetDirectDebits(ctx, accountID)

// Identity
info, err := client.Data().GetUserInfo(ctx)
fmt.Println(info.FullName)

// Connection management
err = client.Data().ExtendConnection(ctx, data.ExtendConnectionRequest{})

Verification

// Simple name verification
result, err := client.Verification().VerifyAccountHolderName(ctx,
    verification.VerifyAccountHolderRequest{
        AccountHolderName: "Jane Doe",
        AccountIdentifier: verification.AccountIdentifier{
            Type: "sort_code_account_number",
            SortCode: "040004", AccountNumber: "12345678",
        },
    })
fmt.Println(result.Result) // "match" | "no_match"

// Async account holder verification (with webhook)
ahv, err := client.Verification().CreateAccountHolderVerification(ctx,
    verification.CreateAccountHolderVerificationRequest{
        AccountHolderName: "Jane Doe",
        AccountIdentifier: verification.AccountIdentifier{Type: "iban", IBAN: "GB29NWBK60161331926819"},
    })

// Poll result
ahv, err = client.Verification().GetAccountHolderVerification(ctx, ahv.ID)

Signup+

// Get verified user data after a payment
userData, err := client.SignupPlus().GetUserDataByPayment(ctx, paymentID)
fmt.Printf("Name: %s, Email: %s\n", userData.Name, userData.Email)

// Get user data after a mandate
userData, err = client.SignupPlus().GetUserDataByMandate(ctx, mandateID)

// Generate a Signup+ auth URI
authURI, err := client.SignupPlus().GenerateAuthURI(ctx, signupplus.GenerateAuthURIRequest{
    RedirectURI: "https://yourapp.com/signup/callback",
    Scopes:      []string{"openid", "name", "email", "address"},
    State:       csrfToken,
    PaymentID:   paymentID,
})

Webhooks

Mount the handler on your router — it implements http.Handler:

wh := client.Webhooks()

wh.On(webhooks.EventPaymentExecuted, func(ctx context.Context, e webhooks.Event) error {
    var p webhooks.PaymentPayload
    webhooks.ParsePayload(e, &p)
    log.Printf("payment %s executed", p.PaymentID)
    return nil
})

wh.On(webhooks.EventPaymentFailed, func(ctx context.Context, e webhooks.Event) error {
    var p webhooks.PaymentPayload
    webhooks.ParsePayload(e, &p)
    log.Printf("payment %s failed: %s", p.PaymentID, p.FailureReason)
    return nil
})

wh.On(webhooks.EventRefundExecuted, func(ctx context.Context, e webhooks.Event) error {
    var p webhooks.RefundPayload
    webhooks.ParsePayload(e, &p)
    log.Printf("refund %s executed for payment %s", p.RefundID, p.PaymentID)
    return nil
})

wh.On(webhooks.EventPayoutExecuted, func(ctx context.Context, e webhooks.Event) error {
    var p webhooks.PayoutPayload
    webhooks.ParsePayload(e, &p)
    return nil
})

wh.On(webhooks.EventMandateAuthorized, func(ctx context.Context, e webhooks.Event) error {
    var p webhooks.MandatePayload
    webhooks.ParsePayload(e, &p)
    return nil
})

// Catch-all for event types you haven't explicitly handled
wh.OnFallback(func(ctx context.Context, e webhooks.Event) error {
    log.Printf("unhandled event type: %s", e.EventType)
    return nil
})

// Mount
http.Handle("/webhooks/truelayer", wh)
Full event type list
webhooks.EventPaymentAuthorized
webhooks.EventPaymentExecuted
webhooks.EventPaymentSettled
webhooks.EventPaymentFailed
webhooks.EventRefundExecuted
webhooks.EventRefundFailed
webhooks.EventPayoutExecuted
webhooks.EventPayoutFailed
webhooks.EventMerchantAccountPaymentSettled
webhooks.EventMerchantAccountPaymentFailed
webhooks.EventMandateAuthorized
webhooks.EventMandateRevoked
webhooks.EventMandateFailed
webhooks.EventVRPPaymentExecuted
webhooks.EventVRPPaymentFailed
webhooks.EventPaymentLinkPaymentExecuted
webhooks.EventAHVCompleted
webhooks.EventAHVFailed
webhooks.EventSignupAuthURIExpired

Observability

Structured logging (log/slog)
truelayer.WithLogger(obs.NewSlogLogger(slog.Default()))

Or implement obs.Logger to bridge to zap, zerolog, etc.:

type ZapLogger struct{ l *zap.SugaredLogger }

func (z *ZapLogger) InfoCtx(ctx context.Context, msg string, args ...any)  { z.l.Infow(msg, args...) }
func (z *ZapLogger) ErrorCtx(ctx context.Context, msg string, args ...any) { z.l.Errorw(msg, args...) }
func (z *ZapLogger) DebugCtx(ctx context.Context, msg string, args ...any) { z.l.Debugw(msg, args...) }
func (z *ZapLogger) WarnCtx(ctx context.Context, msg string, args ...any)  { z.l.Warnw(msg, args...) }

client, _ := truelayer.New(truelayer.WithLogger(&ZapLogger{l: sugar}), ...)
Prometheus metrics
type PrometheusHook struct {
    hist *prometheus.HistogramVec
}
func (p *PrometheusHook) RecordRequest(ctx context.Context, method, path string, status int, latency time.Duration) {
    p.hist.WithLabelValues(method, path, strconv.Itoa(status)).Observe(latency.Seconds())
}

client, _ := truelayer.New(truelayer.WithMetrics(&PrometheusHook{hist: requestDuration}), ...)
OpenTelemetry tracing
type OTelTracer struct{ tracer trace.Tracer }

func (o *OTelTracer) StartSpan(ctx context.Context, name string) (context.Context, func()) {
    ctx, span := o.tracer.Start(ctx, name)
    return ctx, span.End
}

client, _ := truelayer.New(truelayer.WithTracer(&OTelTracer{tracer: otel.Tracer("truelayer")}), ...)
Request / response hooks
client, _ := truelayer.New(
    truelayer.WithRequestHook(func(ctx context.Context, req *http.Request) {
        req.Header.Set("X-Correlation-Id", getCorrelationID(ctx))
    }),
    truelayer.WithResponseHook(func(ctx context.Context, req *http.Request, resp *http.Response, elapsed time.Duration) {
        log.Printf("← %s %s %d %dms", req.Method, req.URL.Path, resp.StatusCode, elapsed.Milliseconds())
    }),
)

Error Handling

All errors are wrapped with context. Use errors.As to inspect structured fields:

payment, err := client.Payments().GetPayment(ctx, id)
if err != nil {
    var ae *truelayer.APIError
    if errors.As(err, &ae) {
        fmt.Printf("status=%d trace=%s title=%s\n", ae.StatusCode, ae.TraceID, ae.Title)
        if ae.IsRetryable() {
            // Tl-Should-Retry: true was set — the SDK already retried, this is final
        }
    }
    return
}

Convenience helpers:

truelayer.IsNotFound(err)      // 404
truelayer.IsUnauthorized(err)  // 401
truelayer.IsRateLimited(err)   // 429
truelayer.IsConflict(err)      // 409
truelayer.IsServerError(err)   // 5xx

Retry Policy

Default policy: 3 attempts, 300 ms base delay, 2× multiplier, 10 s cap, full jitter.

truelayer.WithRetryPolicy(retry.Policy{
    MaxAttempts: 5,
    BaseDelay:   500 * time.Millisecond,
    MaxDelay:    30 * time.Second,
    Multiplier:  2.0,
})

Only idempotent requests (GET, HEAD, PUT, DELETE) are retried automatically. POST requests are retried only when TrueLayer explicitly signals it via Tl-Should-Retry: true.


Architecture

github.com/iamkanishka/truelayer-client-go
├── truelayer.go               Package entry point, Sandbox/Live constants
├── client.go                  Root Client, New(), 10 sub-client accessors
├── options.go                 14 functional options
├── errors.go                  IsNotFound/IsRateLimited/IsServerError/IsConflict helpers
│
├── auth/                      OAuth2 tokens, debug IDs, auth links
├── payments/                  Pay-ins, auth flow, refunds, payment links, provider search
├── payouts/                   Merchant → external-account payouts
├── merchant/                  Merchant accounts, sweeping, payment sources
├── mandates/                  VRP mandates (sweeping + commercial)
├── data/                      Accounts, balances, transactions, cards, identity
│   └── iterator.go            Generic Iterator[T] for cursor-based pagination
├── verification/              Account holder name verification
├── signupplus/                Embedded user data collection
├── tracking/                  Authorization flow event tracking
├── webhooks/                  Signature verification, replay protection, dispatch
│
└── internal/
    ├── httpx/                 Instrumented HTTP client; RFC 7807 errors; Tl-* headers
    ├── signing/               ES512 JWS Sign + Verify (P-521)
    ├── retry/                 Exponential backoff with full jitter (math/rand/v2)
    ├── ratelimit/             Token bucket rate limiter
    ├── idempotency/           Stable idempotency key manager (crypto/rand)
    ├── circuitbreaker/        Three-state circuit breaker
    └── obs/                   log/slog bridge; Nop implementations
Key design decisions
Concern Approach
Token isolation TokenType discriminant forces payment vs data tokens into separate store slots
Request signing Mandatory for Payments/Payouts/Mandates — requireSigner() fails fast before any HTTP call
Idempotency Stable key per operationID — same key survives retries; Release() after confirmed success
Retry safety Only GET/HEAD/PUT/DELETE retry unconditionally; POST only retries on Tl-Should-Retry: true
Concurrency Token refresh uses per-type mutex with double-checked locking — no thundering herd
Webhooks hmac.Equal (constant-time) prevents timing attacks; replay window enforced before dispatch
Zero deps Pure stdlib — no external packages in the dependency graph

Running Tests

go test ./...

For integration tests against the Sandbox:

export TRUELAYER_CLIENT_ID=your_client_id
export TRUELAYER_CLIENT_SECRET=your_secret
export TRUELAYER_KEY_ID=your_key_id
export TRUELAYER_AUTH_CODE=code_from_redirect

go test ./... -tags integration -v

License

MIT — see LICENSE.


References

Documentation

Overview

Package truelayer is the root of the TrueLayer open banking Go SDK.

The full SDK implementation lives in the truelayer sub-package:

import "github.com/iamkanishka/truelayer-client-go/truelayer"

Quick start

c, err := truelayer.New(
    truelayer.WithEnvironment(truelayer.Sandbox),
    truelayer.WithCredentials("client_id", "client_secret"),
    truelayer.WithSigningKey(pemBytes, "key-id"),
)

The truelayer.Client exposes every TrueLayer API domain:

See the examples/ directory for end-to-end runnable demos.

Directories

Path Synopsis
Package auth implements the TrueLayer Authentication Server.
Package auth implements the TrueLayer Authentication Server.
Package data provides the TrueLayer Data API v1 client.
Package data provides the TrueLayer Data API v1 client.
examples
dataflow command
Command dataflow demonstrates fetching financial data, creating a VRP mandate, and verifying an account holder's name using the TrueLayer Go SDK.
Command dataflow demonstrates fetching financial data, creating a VRP mandate, and verifying an account holder's name using the TrueLayer Go SDK.
payment command
Command payment demonstrates the complete TrueLayer pay-by-bank flow using github.com/iamkanishka/truelayer-client-go.
Command payment demonstrates the complete TrueLayer pay-by-bank flow using github.com/iamkanishka/truelayer-client-go.
internal
circuitbreaker
Package circuitbreaker implements a thread-safe three-state circuit breaker (Closed → Open → HalfOpen → Closed) to prevent cascading failures.
Package circuitbreaker implements a thread-safe three-state circuit breaker (Closed → Open → HalfOpen → Closed) to prevent cascading failures.
httpx
Package httpx provides the shared instrumented HTTP client used by all TrueLayer API domain clients.
Package httpx provides the shared instrumented HTTP client used by all TrueLayer API domain clients.
idempotency
Package idempotency provides a thread-safe idempotency key manager.
Package idempotency provides a thread-safe idempotency key manager.
obs
Package obs defines thin observability interfaces used throughout the SDK.
Package obs defines thin observability interfaces used throughout the SDK.
ratelimit
Package ratelimit provides a client-side token-bucket rate limiter.
Package ratelimit provides a client-side token-bucket rate limiter.
retry
Package retry implements exponential back-off with full jitter.
Package retry implements exponential back-off with full jitter.
signing
Package signing implements JWS ES512 request signing required by the TrueLayer Payments API.
Package signing implements JWS ES512 request signing required by the TrueLayer Payments API.
Package mandates provides the TrueLayer Mandates API client for Variable Recurring Payments (VRP) and commercial sweeping.
Package mandates provides the TrueLayer Mandates API client for Variable Recurring Payments (VRP) and commercial sweeping.
Package merchant provides the TrueLayer Merchant Accounts API client.
Package merchant provides the TrueLayer Merchant Accounts API client.
Package payments provides typed clients for the TrueLayer Payments API v3.
Package payments provides typed clients for the TrueLayer Payments API v3.
Package payouts provides the TrueLayer Payouts API client.
Package payouts provides the TrueLayer Payouts API client.
Package signupplus implements the TrueLayer Signup+ API which lets you collect verified user data embedded within a payment, mandate, or data flow.
Package signupplus implements the TrueLayer Signup+ API which lets you collect verified user data embedded within a payment, mandate, or data flow.
Package tracking provides the TrueLayer Client Tracking API client.
Package tracking provides the TrueLayer Client Tracking API client.
Package truelayer is the TrueLayer open banking SDK.
Package truelayer is the TrueLayer open banking SDK.
Package verification provides the TrueLayer Verification API client.
Package verification provides the TrueLayer Verification API client.
Package webhooks provides TrueLayer webhook signature verification, replay-attack protection, typed event parsing, and a multi-handler dispatcher.
Package webhooks provides TrueLayer webhook signature verification, replay-attack protection, typed event parsing, and a multi-handler dispatcher.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL