Skip to content

Quickstart - Go ​

Connect your first bank account in sandbox mode with github.com/budgetbakers/partner-sdk-go. You need a partner account in the partner portal - registration provisions sandbox access instantly, no card required.

1. Install ​

sh
go get github.com/budgetbakers/partner-sdk-go@v0.2.1

Go 1.24 or newer; standard library only; package name partner. Until the module is published, download partner-sdk-go-0.2.1.zip from the downloads page instead, unpack it next to your module and run go mod edit -replace github.com/budgetbakers/partner-sdk-go=../partner-sdk-go followed by go mod tidy.

2. Create the client ​

Your API key selects the mode: bb_test_… keys run sandbox (fictional *_xf banks), bb_live_… keys run live. Keys are server-side only.

go
import partner "github.com/budgetbakers/partner-sdk-go"

bb := partner.New("{{BB_API_KEY}}")

config, err := bb.Partner.GetConfig(ctx)
if err != nil {
	return err
}
fmt.Println(config.Mode) // "sandbox"

3. Create an end-user client ​

A client is one of your end users - bank connections, accounts and transactions all belong to a client, and the X-Client-Id scoping keeps each user's data isolated. Pass your own user id as ExternalID and the call becomes an upsert - safe to call on every login, no duplicates, no need to store a BudgetBakers-id mapping on your side:

go
client, err := bb.Clients.Create(ctx, partner.ClientCreateRequest{
	ExternalID:  "user-42",
	Email:       "user42@example.com",
	CountryCode: "CZ",
})
if err != nil {
	return err
}
scope := bb.Client(client.ID) // all user-scoped calls go through this

4. Start the hosted connect flow ​

Create a connect session and send the user to HostedURL (a web redirect, or from your app in the system browser; see Mobile apps).

The return URL works like an OAuth redirect URI: the portal (Apps → return URLs) holds your allowlist of patterns - one per platform (web URL, iOS universal link, Android app link, custom scheme) - and each session passes the concrete URL for where this user should land. It must match a registered pattern, otherwise the create fails with validation_error.

go
session, err := scope.ConnectSessions.Create(ctx, partner.ConnectSessionCreateParams{
	ReturnURL: "https://app.example.com/bb-callback",
})
if err != nil {
	return err
}
redirectUserTo(session.HostedURL)

The redirect back to your return URL is a UX signal - the authoritative result is the session state:

go
done, err := scope.ConnectSessions.WaitForTerminal(ctx, session.SessionID)
if err != nil {
	return err
}
if done.State == partner.SessionCompleted {
	fmt.Println("connected:", *done.ConnectionID)
}

5. Receive webhooks ​

Configure your webhook URL in the portal (per mode); every delivery is signed. See the webhook verification guide for the full contract.

go
http.HandleFunc("/webhooks/bb", func(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body) // the RAW body bytes
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	result := partner.Verify(
		[]string{"{{BB_WEBHOOK_SECRET}}"}, // ALL active secrets (two during rotation)
		r.Header.Get(partner.SignatureHeader),
		body,
	)
	if result != partner.VerifyValid {
		w.WriteHeader(http.StatusUnauthorized)
		return
	}

	event, err := partner.ParseEvent(body)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	if ev, ok := event.(*partner.WebhookEvent); ok && ev.Type == partner.EventConnectionCreateSuccess {
		// fetch data, update your records
	}
	w.WriteHeader(http.StatusOK) // always 2xx for verified deliveries - unknown types included
})

6. Read accounts and transactions ​

go
// Every account of the connection, all pages walked. An account the user left
// unchecked in the account picker is listed too, as SubscriptionStatus "Disabled".
accounts, err := scope.Connections.ListAccounts(ctx, connectionID)
if err != nil {
	return err
}

for tx, err := range scope.Accounts.Transactions(ctx, accounts[0].ID, partner.ListTransactionsParams{}) {
	if err != nil {
		return err
	}
	// tx.Amount is a *partner.Decimal - money is never a float.
	// Only ID, Seq, CreatedSeq and RecordDate are guaranteed; every other
	// field is a pointer that may be nil.
	_ = tx
}

Polling for changes? Set SinceSeq: partner.Ptr(lastSeq) in partner.ListTransactionsParams to get every row inserted or modified since the highest Seq you have seen.

Error handling ​

Branch on error.code only - never parse messages (full code reference):

go
_, err := scope.Connections.Refresh(ctx, connectionID)
var apiErr *partner.APIError
if errors.As(err, &apiErr) && apiErr.Code == partner.CodeRefreshCooldown {
	fmt.Println("retry after", apiErr.NextRefreshPossibleAt)
} else if err != nil {
	return err
}

The SDK already retries 429/5xx with exponential backoff (honoring Retry-After) and auto-generates Idempotency-Key on creates.

Going live ​

Complete the go-live checklist in the portal (card verification, business check, terms) - production unlocks automatically and you can mint a bb_live_… key. The code above works unchanged; only the key differs.