Appearance
Quickstart - Kotlin / Java
Connect your first bank account in sandbox mode with the Kotlin/JVM SDK com.budgetbakers:partner-sdk, from Kotlin or Java. You need a partner account in the partner portal - registration provisions sandbox access instantly, no card required.
1. Install
Until the Maven Central package is published, install the SDK from the source zip on the downloads page, unzipped next to your project:
sh
# install the SDK source next to your project
curl -LO https://aisp-docs.bbapi.io/downloads/partner-sdk-kotlin-0.2.1.zip
unzip partner-sdk-kotlin-0.2.1.zipThen include it as a Gradle composite build (Kotlin DSL):
kotlin
// settings.gradle.kts
includeBuild("../partner-sdk-kotlin")
// build.gradle.kts
dependencies {
implementation("com.budgetbakers:partner-sdk:0.2.1")
}Once it is on Maven Central, drop the includeBuild line; the dependency stays the same, and for Maven it is:
xml
<dependency>
<groupId>com.budgetbakers</groupId>
<artifactId>partner-sdk</artifactId>
<version>0.2.1</version>
</dependency>Java 17 or newer; depends on Jackson and the Kotlin standard library; package com.budgetbakers.partner. Every call blocks, so call it from your request threads or your own executor.
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.
kotlin
import com.budgetbakers.partner.BudgetBakers
val bb = BudgetBakers(apiKey = "{{BB_API_KEY}}")
val config = bb.partner.getConfig()
println(config.mode) // "sandbox"From Java, the namespaces are methods and the options have a builder:
java
import com.budgetbakers.partner.BudgetBakers;
BudgetBakers bb = BudgetBakers.builder("{{BB_API_KEY}}").build();
System.out.println(bb.partner().getConfig().getMode()); // "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:
kotlin
val client = bb.clients.create(
ClientCreateRequest(email = "user42@example.com", countryCode = "CZ", externalId = "user-42"),
)
val scope = bb.client(client.id) // all user-scoped calls go through this4. 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 returnUrl 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.
kotlin
val session = scope.connectSessions.create(returnUrl = "https://app.example.com/bb-callback")
redirectUserTo(session.hostedUrl)The redirect back to your returnUrl is a UX signal - the authoritative result is the session state:
kotlin
val done = scope.connectSessions.waitForTerminal(session.sessionId)
if (done.state == "Completed") {
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.
kotlin
import com.budgetbakers.partner.VerifyResult
import com.budgetbakers.partner.WebhookEvent
import com.budgetbakers.partner.WebhookEventType
import com.budgetbakers.partner.Webhooks
fun handleWebhook(signatureHeader: String?, rawBody: ByteArray): Int {
val result = Webhooks.verify(
listOf("{{BB_WEBHOOK_SECRET}}"), // ALL active secrets (two during rotation)
signatureHeader ?: "",
rawBody, // the RAW body bytes
)
if (result != VerifyResult.VALID) return 401
val event = Webhooks.parseEvent(rawBody)
if (event is WebhookEvent && event.type == WebhookEventType.ConnectionCreateSuccess) {
// fetch data, update your records
}
return 200 // always 2xx for verified deliveries - unknown types included
}6. Read accounts and transactions
kotlin
// Every account of the connection, all pages walked. An account the user left
// unchecked in the account picker is listed too, as subscriptionStatus "Disabled".
val accounts = scope.connections.listAccounts(connectionId)
for (tx in scope.accounts.transactions(accounts[0].id)) {
// tx.amount is a java.math.BigDecimal - money is never a float.
// Only id, seq, createdSeq and recordDate are guaranteed; treat every
// other field as nullable.
}Polling for changes? scope.accounts.transactions(accountId, ListTransactionsParams(sinceSeq = lastSeq)) returns every row inserted or modified since the highest seq you have seen. Pages are fetched lazily as you iterate.
Error handling
Branch on error.code only - never parse messages (full code reference):
kotlin
import com.budgetbakers.partner.ErrorCode
import com.budgetbakers.partner.PartnerApiException
try {
scope.connections.refresh(connectionId)
} catch (err: PartnerApiException) {
if (err.code == ErrorCode.REFRESH_COOLDOWN) {
println("retry after ${err.nextRefreshPossibleAt}")
} else {
throw err
}
}The SDK already retries 429/5xx with exponential backoff (honoring Retry-After) and auto-generates Idempotency-Key on creates. A network failure surfaces as PartnerApiUnreachable.
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.