Skip to content

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

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