Skip to content
← Back to Blog

I needed a Go SDK for Wise. Nobody built one.

Published:
Tags:gotype-designapi-clients

In 2020 I built a Discord bot that pays my international employees through Wise. Kotlin, direct REST calls, v1 and v3 endpoints. Quotes, transfers, funding — all through Discord commands. It worked. It was fine. I moved on.

Years later I am building my own accounting software. I operate two German GmbHs across multiple countries, I hate every accounting product I have tried, and I need real-time banking data. So I started bank-sync — a Go CLI that pulls transactions from Wise and Qonto into a local SQLite database I control.

I needed a Go SDK for Wise. Surely, by 2026, someone had built one. There is an API reference page. There are SDKs for other languages. There is even an OpenAPI spec you can download.

There is no official Go SDK. The community SDKs are thin wrappers around net/http that expose Wise’s wire types directly — float64 for money, string for dates, untyped IDs.

So I built wise-go.

float64 money in a financial API

Wise is not even consistent about it. Balance statement values come back as 9.94. Transfer webhooks send amount: 120. The balance statement endpoint returns major units as floats. The webhook sends an integer. Same API, same currency, different representation. You cannot trust a single type from an API that does not trust itself.

In wise-go, money enters as float64 in an unexported internal/raw package and leaves as int64 cents. The conversion happens in one function:

func (a BalanceAmount) Cents() int64 {
    return int64(math.Round(a.Value * centsPerUnit))
}

When Wise changes their wire format — and they will — the blast radius is that function, not every call site in every consumer’s codebase.

What I deliberately did not build

A Money type that does arithmetic. Every Go developer who sees type Money struct { Cents int64; Currency Currency } reaches for func (m Money) Add(other Money) Money. I did not write that. The moment you add Add, you have to decide: what happens when currencies mismatch? Do you convert? At what rate? Whose rate? Now you are a financial library, not an API client.

I also did not build pagination. Wise does not paginate their transaction endpoint. Speculatively building a Page[T] type for an API that does not paginate is how you end up with abstractions that encode assumptions the API does not make.

The CARD_REFUND bug

In v0.3.0 I found a bug in my own transaction type classifier. Wise has a transaction type called CARD_PAYMENT. When the amount is positive — a refund rather than a charge — it should be classified as CARD_REFUND. My code was classifying positive-amount CARD_PAYMENT transactions as card payments. Silent, wrong, and exactly the kind of bug that untyped enums make easy and typed enums make impossible.

The fix was five lines. But it is the kind of bug your type boundary exists to prevent: when the API’s types are unreliable, your boundary is the only thing between a silent misclassification and a correct one.

What I would do differently

I used AI coding agents heavily, and the project moved fast. But I would have pointed the agents at Wise’s documentation more aggressively and insisted on strict types earlier. The float64-to-int64 conversion and the branded IDs should have been there from the first commit, not introduced in later versions. I would also have done more sandbox testing earlier — catching the CARD_REFUND bug in a sandbox instead of in a code review.

Where it stands

wise-go is at v0.5.0. Three resources — profiles, balances, transactions — all read-only. No transfers. No webhooks. 94.8% test coverage. A flat package structure that I know will not scale past six more resources.

The type boundary is the contract. Everything else — the API’s documentation, its wire format, its enum variants — is what you defend against.

Read the code. It is better than Wise’s documentation.