Last Updated: February 11, 2026
Base URL: https://api.trupocket.app/v1
Authentication: Personal Access Tokens (PATs) via Bearer header
Response Format: JSON
Currency: Integers in cents (1234 = $12.34)
Timestamps: Unix timestamps (seconds since epoch)
Documentation: Full API Reference | Privacy Policy | Terms of Service
Welcome developers! This guide will help you get started with the Trupocket API.
Authorization header for all API requestsPATs provide long-lived API access for all use cases. Ideal for scripts, CI/CD, server-to-server integrations, and general API usage.
tp_ prefix followed by 64 hexadecimal characterstp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2Create a PAT from the Trupocket web app settings, or via the API with an existing unscoped PAT (a scoped token cannot create tokens):
POST /v1/users/access-tokens
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"name": "My Integration",
"expiresAt": 1767225600,
"scopes": ["trupocket-api/read", "trupocket-api/write"]
}
# expiresAt is optional (Unix timestamp). Omit for tokens that never expire.
# scopes is optional. Omit it for an unscoped token with full access.
# Response (201):
{
"tokenID": "2BKHGx7z0pFFRyqPqQqRbCPp9xI",
"name": "My Integration",
"token": "tp_a1b2c3d4e5f6...",
"tokenPrefix": "tp_a1b2c3d4",
"expiresAt": 1767225600,
"createdAt": 1704067200
}
# IMPORTANT: The "token" field is only returned at creation!
# Copy and store it securely - you cannot retrieve it later.
scopes is optional. A token created without it is unscoped and has full access to everything the plan permits.scopes reaches only what its grant covers: trupocket-api/read for GET, trupocket-api/write for POST, PUT and PATCH, trupocket-api/delete for DELETE. A call outside the grant returns 403 insufficient-scope.trupocket-api/read, and naming trupocket-api/delete adds trupocket-api/write.403 unscoped-credential-required; use the web app or an unscoped token.curl -X GET "https://api.trupocket.app/v1/households" \
-H "Authorization: Bearer tp_your_personal_access_token_here"
# List all PATs
GET /v1/users/access-tokens
# Response:
{
"accessTokens": [
{
"tokenID": "2BKHGx7z0pFFRyqPqQqRbCPp9xI",
"name": "My Integration",
"tokenPrefix": "tp_a1b2c3d4",
"isActive": true,
"expiresAt": 1767225600,
"lastUsedAt": 1704153600,
"createdAt": 1704067200
}
],
"total": 1,
"limit": 3
}
# Update a PAT (rename or deactivate). Expiration is not editable —
# use the regenerate endpoint below to change it.
PATCH /v1/users/access-tokens/{tokenID}
{
"name": "Renamed Integration",
"isActive": false
}
# Regenerate a PAT (rotate the secret value; tokenID is preserved)
# The old token is invalidated immediately. The new full token value
# is returned only once in the response — store it before you reload.
# Omit "expiresAt" for no expiration; provide it to set a new one.
POST /v1/users/access-tokens/{tokenID}/regenerate
{
"expiresAt": 1767225600
}
# Delete a PAT
DELETE /v1/users/access-tokens/{tokenID}
GET /v1/users/access-tokens
Trupocket runs a Model Context Protocol (MCP) server so AI
assistants (Claude, ChatGPT, Copilot, Gemini CLI, and any MCP-compatible client) can
securely read a household's finances — and, on a write-capable plan,
record to them. One URL, https://api.trupocket.app/v1/mcp, serves every
household the user belongs to: the assistant calls list_households first,
and every other tool takes the household_id it returns. There are two ways
to connect:
https://api.trupocket.app/v1/mcp
and authenticate with a PAT as a bearer token. See the
MCP (AI Access) specification (also listed in the
interactive docs) for the available tools and responses.
MCP access requires a Premium, Pro,
Developer, or Enterprise plan. The read tools are
available on every one of those plans. The create, update, and delete tools require a
Pro, Developer, or Enterprise plan
and a scope grant. The scopes are cumulative:
trupocket-api/read covers the read tools,
trupocket-api/write the create and update tools, and
trupocket-api/delete the destructive delete_* tools.
trupocket-api/write and trupocket-api/delete must each be
asked for by name, and naming delete grants write along with it;
trupocket-api/read is always granted on the OAuth flow whether or not you
list it, and is required in a Personal Access Token's scopes array. Write
does not imply delete — an assistant granted write can record a
transaction but cannot erase an account. The plan is checked per household: a user with
a Pro household and a Premium household sees the create and update tools, and a call
that targets the Premium household is refused with a message naming the plan it needs.
An OAuth client asks for what it needs by naming the scopes in its authorization
request (scope=openid email profile trupocket-api/read trupocket-api/write);
leave them out and the token is read-only. An MCP client that discovers Trupocket
through the 401 challenge does not leave them out — the challenge
names trupocket-api/read trupocket-api/write, so a one-click connection
requests write by default and the consent screen says so. The consent screen also
offers a Connect read-only option, which restarts the authorization
asking for read alone. trupocket-api/delete is never named in the
challenge and must always be requested deliberately. A Personal Access Token created
without a scopes grant is unscoped and keeps full access to
everything the plan permits — the behavior of every token created before scopes
existed. A PAT created with a grant is gated on the REST and MCP surfaces
exactly like an OAuth token.
Scopes gate ordinary REST calls, not just MCP tools. Over REST the
required tier follows the HTTP method — trupocket-api/read for
GET, trupocket-api/write for POST,
PUT and PATCH, trupocket-api/delete for
DELETE — with a handful of operations classified by what they actually do
rather than by their method. A call outside the grant is refused with
403 insufficient-scope. Over MCP the same grant instead decides which
tools are registered, so a client that fails that gate simply does not see those tools
in tools/list. Web sessions and unscoped PATs are subject to neither
gate. A handful of account operations — changing the email, billing sessions,
household invitations, and token management — accept no scoped token at all and
return 403 unscoped-credential-required.
Every write an assistant makes is recorded in a per-household audit trail — what tool
ran, what changed, and whether it succeeded — readable at
GET /v1/households/{householdID}/detailed-tracking/mcp/activity. See the
Assistant Activity specification.
Base URL: https://api.trupocket.app/v1
Response Format: JSON
Date/Time Format: Unix timestamps (seconds since epoch)
Currency Format: Integers in smallest denomination (cents for USD)
| Plan | API Calls/Day | Transactions/Month | Households | Data History | Webhooks |
|---|---|---|---|---|---|
| Free | 20 | 60 | 1 | 90 days | No |
| Premium | 1,000 | Unlimited | Unlimited | 2 years | No |
| Pro | 5,000 | Unlimited | Unlimited | Unlimited | No |
| Developer | 10,000 | Unlimited | Unlimited | Unlimited | Yes |
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1699564800
User
└── Household(s)
├── Accounts (checking, savings, credit cards)
├── Categories (for budgeting)
├── Payees (merchants, employers)
├── Hashtags (custom tags)
├── Transactions
│ ├── Transaction Categories (split transactions)
│ └── Transaction Hashtags
└── Scheduled Transactions (recurring transactions)
dt (Detailed Tracking) - Full transaction register with budgeting. The default when style is omittedhome - Personal finances. The default when purpose is omittedoffice - Business financeshome_office - Mixed (sole proprietor, freelancer)chkg - Checking accountsvgs - Savings accountcc - Credit cardexpense - Money outincome - Money intransfer - Between your accountsHashtags are a powerful dynamic categorization tool that goes beyond traditional categories and budgets.
What are Hashtags?
Hashtags allow you to tag transactions with custom labels by including #hashtag in transaction memos. Unlike categories (which are budget-focused), hashtags provide flexible, cross-category grouping for analysis and reporting.
How Hashtags Work:
"Dinner with clients #business #taxdeductible"Categories vs Hashtags:
| Feature | Categories | Hashtags |
|---|---|---|
| Purpose | Budget tracking | Flexible grouping & analysis |
| Setup | Must create before use | Auto-created on first use |
| Per Transaction | 1-5 categories (required) | Unlimited hashtags (optional) |
| Budget Impact | Affects budget calculations | No budget impact |
| Use Cases | Monthly budgets, spending limits | Projects, tax tracking, trips, clients |
Common Use Cases:
#taxdeductible, #business, #charitable#kitchen-remodel, #wedding, #vacation2024#client-acme, #client-techcorp#birthday, #conference, #holiday#roommate, #reimbursable#savingforhouse, #debtpayoffExample Transaction with Hashtags:
{
"payee": "Home Depot",
"categories": [
{
"name": "Home Improvement",
"amount": 25000,
"memo": "Kitchen backsplash materials #kitchen-remodel #taxdeductible",
"dontImpactBudget": false,
"isCleared": true
}
]
}
# This transaction:
# - Budgeted under "Home Improvement" category
# - Tagged for project tracking (#kitchen-remodel)
# - Tagged for tax purposes (#taxdeductible)
# - Can be filtered/reported on by either hashtag
Filtering by Hashtags:
# Get all transactions for kitchen remodel project
GET /v1/households/{householdID}/detailed-tracking/transactions?hashtags=kitchen-remodel
# Get all tax-deductible transactions
GET /v1/households/{householdID}/detailed-tracking/transactions?hashtags=taxdeductible
# Get transactions for multiple projects
GET /v1/households/{householdID}/detailed-tracking/transactions?hashtags=kitchen-remodel,vacation2024
Pro Tips:
#vacation-hawaii-2024 instead of #trip#needreceipt, #verify# 1. Create an account at https://trupocket.app
# 2. Go to Settings > Access Tokens
# 3. Create a new Personal Access Token (PAT)
# 4. Copy and store the token securely - it's only shown once!
# Use your PAT in all API requests:
curl -X GET "https://api.trupocket.app/v1/users/who-am-i" \
-H "Authorization: Bearer tp_your_personal_access_token_here"
# Response:
{
"userID": "2BxFGhvDwSoabBEqf2V5ZgC3ifo",
"firstName": "John",
"email": "john@example.com",
"planID": "free"
}
POST /v1/households
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"name": "Personal Finances",
"style": "dt",
"purpose": "home"
}
# Response:
{
"householdID": "2BKHJyVqwTkR1234567890ABCDE"
}
POST /v1/households/{householdID}/detailed-tracking/accounts
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"type": "chkg",
"name": "Chase Checking",
"balance": 500000
}
# balance: 500000 = $5,000.00
# Response:
{
"accountID": "2BKHLmNpqRsTuvWxYzAbCdEfGhI"
}
POST /v1/households/{householdID}/detailed-tracking/categories
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"name": "Groceries",
"budgetAmount": 40000
}
# budgetAmount: 40000 = $400.00/month
# Response:
{
"categoryID": "2BKHMnOpQrStUvWxYzAbCdEfGhI"
}
POST /v1/households/{householdID}/detailed-tracking/payees
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"name": "Whole Foods"
}
# Response:
{
"payeeID": "2BKHOpQrStUvWxYzAbCdEfGhIjK"
}
POST /v1/households/{householdID}/detailed-tracking/transactions
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"cashflow": "expense",
"type": "debit_credit",
"fromAccountID": "YOUR_ACCOUNT_ID",
"payee": "Whole Foods",
"date": 1699564800,
"categories": [
{
"name": "Groceries",
"amount": 8547,
"memo": "Weekly groceries #organic",
"dontImpactBudget": false,
"isCleared": true
}
]
}
# date: Unix timestamp for transaction date
# amount: 8547 = $85.47
# payee: Payee name (string, not ID)
# categories[].name: Category name (string, not ID)
# dontImpactBudget: false means this WILL impact the budget
# isCleared: true means this transaction has cleared the bank
# Transaction reduces account balance by $85.47
# Transaction reduces budget balance by $85.47
# Response:
{
"transactionID": "2BKHPqRsTuVwXyZaBcDeFgHiJkL",
"totalAmount": {
"currency": "USD",
"amount": 8547
}
}
# When you receive 401 Unauthorized:
# - If using a PAT: check that it hasn't been deactivated or expired
# - Generate a new PAT from the Trupocket web app if needed
# - PATs without an expiration date never expire unless manually deactivated
GET /v1/households/{householdID}
Authorization: Bearer YOUR_ACCESS_TOKEN
# Returns household with accounts, budgets, etc.
GET /v1/households/{householdID}/detailed-tracking/transactions
Authorization: Bearer YOUR_ACCESS_TOKEN
# Optional filters:
# ?accounts={accountID}
# ?categories={categoryID}
# ?fromDate={YYYY-MM-DD}
# ?toDate={YYYY-MM-DD}
GET /v1/households/{householdID}/detailed-tracking/accounts/{accountID}
Authorization: Bearer YOUR_ACCESS_TOKEN
# Returns account with current balance
GET /v1/households/{householdID}/detailed-tracking/categories
Authorization: Bearer YOUR_ACCESS_TOKEN
# Returns all categories with budget details:
# - budget.amount (allocated)
# - budget.balance (remaining)
POST /v1/households/{householdID}/detailed-tracking/scheduled-transactions
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"cashflow": "expense",
"type": "ach",
"fromAccountID": "YOUR_ACCOUNT_ID",
"payee": "Rent Company",
"frequency": "monthly",
"frequencyDay": 1,
"startDate": 1699564800,
"shouldAutoGenerate": true,
"categories": [
{
"name": "Rent",
"amount": 150000,
"memo": "Monthly rent",
"impactBudget": true
}
]
}
# payee: Payee name (string, not ID)
# frequency: string enum, not a number (one_time, weekly, bi_weekly, twice_monthly,
# monthly, quarterly, bi_annually, annually, every_n_days)
# frequencyDay: 1 = 1st of month
# startDate: Unix timestamp for when schedule begins
# shouldAutoGenerate: true = automatically create transactions (omit and it defaults to false)
# categories[].name: Category name (string, not ID)
# amount: 150000 = $1,500.00
# impactBudget: true = this WILL impact the budget (scheduled uses impactBudget, not dontImpactBudget)
200 - Success201 - Created202 - Accepted (the request was accepted but completes outside this call, e.g. a plan upgrade returns a checkout URL to finish)204 - No Content (success with an empty body, e.g. some deletes)400 - Bad Request (validation error)401 - Unauthorized (missing, invalid, or expired token)402 - Payment Required (a plan limit or plan-gated feature; upgrade to continue)403 - Forbidden (insufficient permissions or token scope)404 - Not Found409 - Conflict (duplicate name or transaction, or the resource is in a state that blocks the change)410 - Gone (a household invitation has expired)413 - Payload Too Large (a transaction import file is too large)422 - Unprocessable Entity (the request is well-formed but does not apply to this resource, e.g. an interest estimate for an account that is not a credit card)429 - Too Many Requests (rate limit exceeded)500 - Internal Server Error501 - Not Implemented (the feature or plan is not available at this time)All API errors return a flat JSON structure with the following fields:
errorCode - Machine-readable error code (lowercase-kebab-case)errorMessage - Human-readable error descriptionerrorField - Field that caused the error (omitted when the error is not tied to a field)For example, POST /v1/households without a name returns 400 with:
{
"errorCode": "no-name-provided",
"errorMessage": "Please specify the name of your household",
"errorField": "name"
}
| Error Code | Description | Solution |
|---|---|---|
no-authorization-header |
401 - No Authorization header was sent |
Send Authorization: Bearer YOUR_ACCESS_TOKEN with every authenticated request |
invalid-token |
401 - The OAuth token is invalid or expired, or the header is not a Bearer credential |
Sign in again for a fresh token, and check the header reads Bearer <token> |
invalid-access-token, access-token-inactive, access-token-expired |
401 - The personal access token is not recognized, has been deactivated, or has expired | Check the PAT is active and not expired, or generate a new one |
rate-limit-exceeded |
429 - Too many API calls | On authenticated endpoints, wait for the rate limit reset or upgrade your plan. On the unauthenticated OAuth endpoints (/oauth/register, /oauth/authorize) this is the 20/day per-IP cap - wait for midnight UTC or authenticate; upgrading your plan does not raise it |
invalid-request-body |
400 - The request body is empty or invalid: it is not valid JSON, or a value has the wrong type or is not one of the allowed values | Check the body against the operation's schema in the API reference. Field-level validation failures return their own codes, with errorField naming the field |
household-not-found |
404 - The household does not exist, or you are not a member of it. Other resources return their own codes, such as account-not-found |
Verify the IDs are correct; GET /v1/households lists the households you can use |
plan-limit-exceeded |
402 - Exceeded plan limits | Upgrade to higher plan |
insufficient-scope |
403 - A scoped token lacks the scope this operation needs | Re-authorize, or create a new token, with the required scope |
unscoped-credential-required |
403 - The operation accepts no scoped token (account email, billing, invitations, token management) | Use the web app or a PAT created without scopes |
The simplest way to use the API is with Personal Access Tokens. No token refresh logic needed.
const axios = require('axios');
const API_BASE = 'https://api.trupocket.app/v1';
const PAT = process.env.TRUPOCKET_PAT; // Store in environment variable
const client = axios.create({
baseURL: API_BASE,
headers: {
'Authorization': `Bearer ${PAT}`,
'Content-Type': 'application/json'
}
});
// List households
const { data } = await client.get('/households');
console.log('Households:', data.households);
// Create a transaction
const transaction = await client.post(
`/households/${householdId}/detailed-tracking/transactions`,
{
cashflow: 'expense',
type: 'debit_credit',
fromAccountID: accountId,
payee: 'Coffee Shop',
date: Math.floor(Date.now() / 1000),
categories: [{ name: 'Food', amount: 500, memo: 'Morning coffee' }]
}
);
console.log('Created:', transaction.data.transactionID);
import os
import requests
import time
API_BASE = 'https://api.trupocket.app/v1'
PAT = os.environ['TRUPOCKET_PAT'] # Store in environment variable
headers = {
'Authorization': f'Bearer {PAT}',
'Content-Type': 'application/json'
}
# List households
response = requests.get(f'{API_BASE}/households', headers=headers)
print('Households:', response.json()['households'])
# Create a transaction
transaction = requests.post(
f'{API_BASE}/households/{household_id}/detailed-tracking/transactions',
headers=headers,
json={
'cashflow': 'expense',
'type': 'debit_credit',
'fromAccountID': account_id,
'payee': 'Coffee Shop',
'date': int(time.time()),
'categories': [{'name': 'Food', 'amount': 500, 'memo': 'Morning coffee'}]
}
)
print('Created:', transaction.json()['transactionID'])
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
const APIBase = "https://api.trupocket.app/v1"
type TrupocketClient struct {
PAT string
}
type TransactionRequest struct {
Cashflow string `json:"cashflow"`
Type string `json:"type"`
FromAccountID string `json:"fromAccountID"`
Payee string `json:"payee"`
Date int64 `json:"date"`
Categories []TransactionCategory `json:"categories"`
}
type TransactionCategory struct {
Name string `json:"name"`
Amount int64 `json:"amount"`
Memo string `json:"memo"`
DontImpactBudget bool `json:"dontImpactBudget"`
IsCleared bool `json:"isCleared"`
}
func NewTrupocketClient(pat string) *TrupocketClient {
return &TrupocketClient{PAT: pat}
}
func (c *TrupocketClient) makeRequest(method, endpoint string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest(method, APIBase+endpoint, body)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.PAT)
req.Header.Set("Content-Type", "application/json")
return (&http.Client{}).Do(req)
}
func (c *TrupocketClient) CreateTransaction(householdID, accountID, categoryName, payeeName string) error {
txn := TransactionRequest{
Cashflow: "expense",
Type: "debit_credit",
FromAccountID: accountID,
Payee: payeeName,
Date: time.Now().Unix(),
Categories: []TransactionCategory{
{
Name: categoryName,
Amount: 5000,
Memo: "Lunch",
DontImpactBudget: false,
IsCleared: true,
},
},
}
body, _ := json.Marshal(txn)
resp, err := c.makeRequest("POST", fmt.Sprintf("/households/%s/detailed-tracking/transactions", householdID), bytes.NewBuffer(body))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
return fmt.Errorf("rate limit exceeded")
} else if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
fmt.Println("Transaction created successfully")
return nil
}
func main() {
client := NewTrupocketClient(os.Getenv("TRUPOCKET_PAT"))
// List households
resp, err := client.makeRequest("GET", "/households", nil)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println("Status:", resp.StatusCode)
}
Coming Soon: Webhooks will allow you to receive real-time notifications when events occur in Trupocket. The following shows the planned webhook format.
transaction.createdtransaction.updatedtransaction.deletedaccount.updatedbudget.exceededscheduled_transaction.generatedWhen implemented, webhook payloads will follow this structure:
{
"event": "transaction.created",
"timestamp": 1699564800,
"data": {
"householdID": "2BxFGhvDwSoabBEqf2V5ZgC3ifo",
"transactionID": "2BxFGi9K3xyMp6QwVN8ZjR4LaPn",
"amount": 5000,
"cashflow": "expense"
}
}
Math.floor(dollars * 100)cents / 100isCleared field for tracking cleared/pending transactionsX-RateLimit-Remaining header before bulk operationsplan-limit-exceeded), upgrade requiredplan-limit-exceeded), upgrade required
Responses support gzip and zstd compression, which
materially shrinks large payloads (e.g. reports). Send an
Accept-Encoding: gzip or Accept-Encoding: zstd header to
opt in; the server negotiates automatically and only compresses responses large
enough to benefit. Most HTTP clients (browsers, fetch,
requests) advertise this and decode it transparently — no extra work.
If you set Accept-Encoding by hand on a raw client, you must decode the
response body yourself; with curl, prefer curl --compressed.
Recommended Next Steps: