TypeScript SDK
Use the @cobalt-money/sdk to call the Cobalt API from Node, Bun, Deno, or the browser.
Overview
@cobalt-money/sdk is a typed TypeScript client generated from the Cobalt OpenAPI spec. It works in Node, Bun, Deno, Cloudflare Workers, and the browser. Every endpoint returns a { data, error } discriminated union — no exceptions on 4xx/5xx.
Install
bun add @cobalt-money/sdknpm install @cobalt-money/sdkpnpm add @cobalt-money/sdkyarn add @cobalt-money/sdkGet an API key
Issue a key from the Cobalt app: profile icon → Account → Developer → API Keys → Create Key. Keys are prefixed ck_live_. Treat as secrets — do not commit to source or ship in client-side bundles.
Initialize
import { Cobalt } from "@cobalt-money/sdk";
const cobalt = new Cobalt({
auth: process.env.COBALT_API_KEY!,
});The SDK defaults baseUrl to https://api.cobaltpf.com/v1. Do not pass baseUrl: "https://api.cobaltpf.com" — it overrides the /v1 suffix and every call 404s.
Response shape
Every method returns { data, error }. data is the typed payload; error is set only when the request fails. The SDK does not throw on HTTP errors — check error explicitly.
const { data, error } = await cobalt.accounts.list();
if (error) {
// network failure, 4xx, or 5xx
console.error(error);
return;
}
for (const account of data) {
console.log(account.id, account.balance);
}Resource shapes (response payloads) are flat — no inner data wrapper:
// GET /v1/accounts → Account[]
// GET /v1/accounts/{id} → Account
// GET /v1/transactions → { items: Transaction[], hasMore, nextCursor }
// GET /v1/categories → { categories: Category[], groups: CategoryGroup[] }
// GET /v1/spending → SpendingItemSign conventions
Two gotchas worth memorizing:
Account.balanceis signed. Liability accounts (type: "credit_card"or"loan") return negative balances. Net worth isaccounts.reduce((sum, a) => sum + (a.balance ?? 0), 0).Transaction.amountis signed but inverted vs Plaid/Mint. Positive = money out (spending/debit). Negative = money in (refund/credit/income).
Reading data
// All accounts
const { data: accounts } = await cobalt.accounts.list();
// One account
const { data: account } = await cobalt.accounts.get({ path: { id: "acc_..." } });
// Transactions with filters
const { data } = await cobalt.transactions.list({
query: { startDate: "2026-01-01", endDate: "2026-05-22", limit: 100 },
});
console.log(data.items.length, "transactions, hasMore:", data.hasMore);
// Brokerage positions
const { data: positions } = await cobalt.positions.list();
// Spending aggregate
const { data: spending } = await cobalt.spending.get({
query: { period: "1m", accountType: "all" },
});Writing data
Manual accounts, manual transactions, tags, and categories all support writes via API key.
// Create a manual credit card with a $750 balance owed
const { data: card } = await cobalt.accounts.create({
body: {
type: "credit_card",
subtype: "credit card",
name: "Apple Card",
currentBalance: -750, // signed; liabilities negative
currency: "USD",
},
});
// Add a manual transaction (positive = spending)
const { data: txn } = await cobalt.transactions.create({
body: {
accountId: card.id,
amount: 24.5,
date: "2026-05-22",
name: "Coffee",
merchantName: "Blue Bottle",
},
});Pagination
/v1/transactions is the only paginated endpoint today. Drive a loop off nextCursor:
async function* allTransactions() {
let cursor: string | undefined;
do {
const { data, error } = await cobalt.transactions.list({
query: { cursor, limit: 200 },
});
if (error) throw error;
yield* data.items;
cursor = data.nextCursor ?? undefined;
} while (cursor);
}
for await (const txn of allTransactions()) {
// ...
}Browser usage
The SDK runs in the browser, but the API key is a server-side secret. Do not embed ck_live_* keys in client bundles, mobile apps, or any client you do not control. Instead, proxy through your own backend:
// app/api/cobalt/[...path]/route.ts
export async function GET(req: Request) {
const url = new URL(req.url);
const upstream = `https://api.cobaltpf.com/v1${url.pathname.replace(/^\/api\/cobalt/, "")}${url.search}`;
return fetch(upstream, {
headers: { Authorization: `Bearer ${process.env.COBALT_API_KEY!}` },
});
}Then point the browser SDK at your proxy:
const cobalt = new Cobalt({ baseUrl: "/api/cobalt" });Recipes
Net worth
const { data: accounts } = await cobalt.accounts.list();
const netWorth = accounts.reduce((sum, a) => sum + (a.balance ?? 0), 0);Net-worth timeline
const { data: balances } = await cobalt.balances.snapshots({
query: { startDate: "2026-01-01", endDate: "2026-05-22" },
});
const { data: portfolio } = await cobalt.portfolio.snapshots({
query: { startDate: "2026-01-01", endDate: "2026-05-22" },
});
// Group by day, sum across accounts. Liability balances are already negative
// in the snapshot stream so the sum is correct.
const byDate = new Map<string, number>();
for (const r of [...balances, ...portfolio]) {
byDate.set(r.date, (byDate.get(r.date) ?? 0) + ("currentBalance" in r ? r.currentBalance : r.value));
}Spending by category over the last 6 months
const { data } = await cobalt.spending.get({ query: { period: "6m" } });
console.log(data.totalSpending, data.averageSpending, data.averageLabel);
for (const bucket of data.buckets) console.log(bucket.date, bucket.amount);Errors
error carries { code, error: message } for documented failures (not_found, validation, etc.) and the raw fetch failure otherwise. Inspect the discriminator before reading data:
const { data, error } = await cobalt.accounts.get({ path: { id: "acc_missing" } });
if (error) {
if ("code" in error && error.code === "account_not_found") {
// handle 404
} else {
throw error;
}
}Reference
The full endpoint list with request and response shapes is in the API Reference. Types ship with the SDK — your editor will surface them inline.