Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

223 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

YNAB for Go

Go Reference CI License

A complete Go client for the YNAB API v1: all 44 operations behind a domain-first surface, exact money arithmetic, first-class delta sync, and zero runtime dependencies. Not an OAuth flow, not a persistence layer, not a budgeting engine — a wire-faithful client.

YNAB's API says budget; this library says plan, following YNAB's own product vocabulary. client.Plan(id) maps to /budgets/{budget_id} on the wire — the mapping is one-to-one.

Install

go get pkg.venceslau.dev/ynab

Requires Go 1.25+ (the minimum in go.mod). The public API follows SemVer: the v1 surface is frozen, and every change is recorded in the CHANGELOG.

Create a Personal Access Token at app.ynab.com → Settings → Developer Settings and export it as YNAB_TOKEN.

Quick start

A complete program — no IDs to look up, PlanIDLastUsed resolves server-side:

package main

import (
	"context"
	"fmt"
	"log/slog"
	"os"

	"pkg.venceslau.dev/ynab"
)

func main() {
	client := ynab.New(os.Getenv("YNAB_TOKEN"))
	plan := client.Plan(ynab.PlanIDLastUsed)

	accounts, _, err := plan.Accounts.List(context.Background())
	if err != nil {
		slog.Error("listing accounts", "err", err)
		os.Exit(1)
	}
	for _, a := range accounts {
		fmt.Printf("%s  %s\n", a.Name, a.BalanceFormatted)
	}
	// Checking  $1,282.23   (your accounts will differ)
}

Tip

Two flavors of examples: the examples/ directory holds runnable programs that hit the real API (quickstart, delta-sync, splits, mock testing), and the package examples on pkg.go.dev cover every service offline — 26 runnable, httptest-backed.

The plan handle

Everything plan-scoped hangs off the handle client.Plan(id) returns — built without I/O, bound to its id for good:

plan := client.Plan(ynab.PlanIDLastUsed)

groups, knowledge, err := plan.Categories.List(ctx)
category, knowledge, err := plan.Categories.Assign(ctx, ynab.CurrentMonth(), categoryID, ynab.UnitsToMilliunits(150))
detail, knowledge, err := plan.Export(ctx) // the whole plan, one request

Money is Milliunits (exact int64 thousandths — never floats), months are a dedicated Month type that accepts the server-resolved ynab.CurrentMonth(), and write payloads use the Optional tri-state: omitted, SetNull (clear), or Set — zero values included, so Approved: ynab.Set(false) reaches the wire.

Delta sync

YNAB's quota is about 200 requests per hour — delta sync is how a polling integration lives inside it. Delta-capable reads return a ServerKnowledge cursor; hand it back to receive only what changed, tombstones included:

st := &ynab.SyncState{} // JSON-persistable; save it between runs

detail, err := plan.Delta(ctx, st) // full read first, increments after
store = ynab.MergeByID(store, detail.Transactions)

See the package examples for the full delta loop and the flatten-before-merge categories caveat.

Errors

API failures match a sentinel taxonomy through errors.Is, class- and sub-code-wise at once:

_, _, err := plan.Transactions.Create(ctx, spec)
switch {
case errors.Is(err, ynab.ErrConflict):    // duplicate import_id
case errors.Is(err, ynab.ErrRateLimited): // wait — a *ynab.Error carries RetryAfter
case errors.Is(err, ynab.ErrForbidden):   // any 403.*, including 403.1 subscription lapsed
}

Client-side pre-flight failures (a zero Month, a 501-character payee name, split legs that do not sum) are *ArgumentError — the request is never sent. Retries for 429/500/503 are built in, write-safe by default, and IsRetryable exposes the same classification for custom loops.

Testing your integration

Two dependency-free seams, both with runnable examples: point WithBaseURL at an httptest.Server, or install a fake http.RoundTripper via WithHTTPClient. You need nothing from this library's internals.

Reliability

Correctness is enforced by machinery, not vigilance: the operation table is diffed both ways against the vendored OpenAPI spec on every CI run, write bodies are asserted byte-exact including their emitted key sets, every decode runs twice with all optional response headers stripped, every nullable field has a null-variant fixture, and a weekly job diffs the live spec for drift. Observed-vs-documented API divergences live in API_NOTES.md.

Upgrading from the archived v1.x line

This module replaces the archived v1.x releases. It is a clean break — budget → plan, one package, no source compatibility. The predecessor's versions stay installable from the module proxy under their original paths (go.bmvs.io/ynab up to v1.3.0, then github.com/brunomvsouza/ynab.go); the archive/* tags here browse that history. Under pkg.venceslau.dev/ynab those old versions — and this module's own pre-rewrite v0.1.0 — are retracted, so go get resolves to the current release; see the CHANGELOG.

Support

Issues are acknowledged within 14 days and resolved — an accept/decline decision, and the fix when accepted — within 30 days of acknowledgement. See CONTRIBUTING.md for the gate workflow before opening a PR.

License

BSD 2-Clause. Not affiliated with YNAB — use at your own risk.

About

Complete, dependency-free Go client for the YNAB API — 44 operations, exact integer money, first-class delta sync, contract-tested. Unofficial.

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Used by

Contributors

Languages