A Rust client for the YNAB API. Supports full access to all published YNAB API endpoints. Requires a YNAB account and a Personal Access Token.
[dependencies]
rust-ynab = "0.4.2"All API access requires a Personal Access Token. Pass it to Client::new:
let client = Client::new(&std::env::var("YNAB_TOKEN")?)?;use rust_ynab::{Client, PlanId};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(&std::env::var("YNAB_TOKEN")?)?;
let plans = client.get_plans().include_accounts().send().await?;
for plan in plans {
println!("{}", plan.name);
for acct in &plan.accounts {
println!(" {}", acct.name);
}
}
Ok(())
}Methods that support optional parameters use a builder. Call the factory method on the client, chain any options, then call .send():
// fetch only changes since the last sync
let (transactions, server_knowledge) = client
.get_transactions(PlanId::LastUsed)
.with_server_knowledge(last_known)
.since_date(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap())
.send()
.await?;
// include sub-resources inline
let plans = client.get_plans().include_accounts().send().await?;The YNAB API allows 200 requests per hour. with_rate_limiter enables a token bucket limiter that automatically spaces requests to stay within that limit:
let client = Client::new(&std::env::var("YNAB_TOKEN")?)?.with_rate_limiter(200, Some(10))?;The first argument is the request budget per hour; the second is the optional burst size — the number of requests that can be made immediately before throttling begins. To keep total consumption within YNAB's limit, the sustained rate is reduced by the burst size: with_rate_limiter(200, Some(10)) allows 10 immediate requests, then throttles to 190 per hour. Calls block until a token is available rather than returning an error, so no retry logic is needed on the caller's side.
Rate limiting is opt-in. Omit with_rate_limiter for scripts or one-off tools where request volume is not a concern.
The default request timeout is determined by reqwest. Use with_timeout to override it:
use std::time::Duration;
let client = Client::new(&std::env::var("YNAB_TOKEN")?)?.with_timeout(Duration::from_secs(30))?;Both with_timeout and with_rate_limiter return the client, so they can be chained:
let client = Client::new(&std::env::var("YNAB_TOKEN")?)?
.with_rate_limiter(200, Some(10))?
.with_timeout(Duration::from_secs(30))?;Errors from the API are returned as variants of Error and can be matched directly:
match client.get_plan(PlanId::LastUsed).send().await {
Err(Error::NotFound(e)) => eprintln!("plan not found: {}", e),
Err(Error::Unauthorized(e)) => eprintln!("check your token: {}", e),
Err(e) => return Err(Box::new(e)),
Ok((plan, _)) => { /* ... */ }
}Available error variants: BadRequest, Unauthorized, Forbidden, NotFound, Conflict, RateLimited, InternalServerError, ServiceUnavailable.
- List plans
- Get plan month
- Get category balance
- List transactions
- Create transaction
- Create multiple transactions
- Update transaction
- Update multiple transactions
- Update category budget
- Delete transaction
- Split transaction
- Delta request
| Method | Endpoint |
|---|---|
get_plans |
GET /plans |
get_plan † |
GET /plans/{plan_id} |
get_plan with PlanId::LastUsed |
GET /plans/last-used |
get_plan_settings |
GET /plans/{plan_id}/settings |
| Method | Endpoint |
|---|---|
get_accounts † |
GET /plans/{plan_id}/accounts |
get_account |
GET /plans/{plan_id}/accounts/{account_id} |
create_account |
POST /plans/{plan_id}/accounts |
| Method | Endpoint |
|---|---|
get_categories † |
GET /plans/{plan_id}/categories |
get_category |
GET /plans/{plan_id}/categories/{category_id} |
get_category_for_month |
GET /plans/{plan_id}/months/{month}/categories/{category_id} |
create_category † |
POST /plans/{plan_id}/categories |
create_category_group † |
POST /plans/{plan_id}/category_groups |
update_category † |
PATCH /plans/{plan_id}/categories/{category_id} |
update_category_for_month † |
PATCH /plans/{plan_id}/months/{month}/categories/{category_id} |
update_category_group † |
PATCH /plans/{plan_id}/category_groups/{category_group_id} |
| Method | Endpoint |
|---|---|
get_months † |
GET /plans/{plan_id}/months |
get_month |
GET /plans/{plan_id}/months/{month} |
| Method | Endpoint |
|---|---|
get_payees † |
GET /plans/{plan_id}/payees |
get_payee |
GET /plans/{plan_id}/payees/{payee_id} |
get_payee_locations |
GET /plans/{plan_id}/payee_locations |
get_payee_location |
GET /plans/{plan_id}/payee_locations/{payee_location_id} |
get_payee_locations_by_payee |
GET /plans/{plan_id}/payees/{payee_id}/payee_locations |
create_payee † |
POST /plans/{plan_id}/payees |
update_payee † |
PATCH /plans/{plan_id}/payees/{payee_id} |
| Method | Endpoint |
|---|---|
get_transactions † |
GET /plans/{plan_id}/transactions |
get_transaction † |
GET /plans/{plan_id}/transactions/{transaction_id} |
get_transactions_by_account † |
GET /plans/{plan_id}/accounts/{account_id}/transactions |
get_transactions_by_category † |
GET /plans/{plan_id}/categories/{category_id}/transactions |
get_transactions_by_payee † |
GET /plans/{plan_id}/payees/{payee_id}/transactions |
get_transactions_by_month † |
GET /plans/{plan_id}/months/{month}/transactions |
create_transaction |
POST /plans/{plan_id}/transactions |
create_transactions |
POST /plans/{plan_id}/transactions |
update_transaction |
PUT /plans/{plan_id}/transactions/{transaction_id} |
update_transactions |
PATCH /plans/{plan_id}/transactions |
delete_transaction † |
DELETE /plans/{plan_id}/transactions/{transaction_id} |
import_transactions |
POST /plans/{plan_id}/transactions/import |
| Method | Endpoint |
|---|---|
get_scheduled_transactions † |
GET /plans/{plan_id}/scheduled_transactions |
get_scheduled_transaction |
GET /plans/{plan_id}/scheduled_transactions/{scheduled_transaction_id} |
create_scheduled_transaction |
POST /plans/{plan_id}/scheduled_transactions |
update_scheduled_transaction |
PUT /plans/{plan_id}/scheduled_transactions/{scheduled_transaction_id} |
delete_scheduled_transaction |
DELETE /plans/{plan_id}/scheduled_transactions/{scheduled_transaction_id} |
| Method | Endpoint |
|---|---|
get_money_movements † |
GET /plans/{plan_id}/money_movements |
get_money_movements_by_month † |
GET /plans/{plan_id}/months/{month}/money_movements |
get_money_movement_groups † |
GET /plans/{plan_id}/money_movement_groups |
get_money_movement_groups_by_month † |
GET /plans/{plan_id}/months/{month}/money_movement_groups |
| Method | Endpoint |
|---|---|
get_user |
GET /user |
† Returns server knowledge as a second return value for use with delta requests.
Unit tests use wiremock to cover all endpoints (GET, POST, PATCH, PUT, DELETE), client configuration, error type dispatch, and auth header injection. Write operation tests verify the HTTP method and request body serialization.
cargo test
Integration tests exercise the live API against a real plan and require YNAB_TOKEN and YNAB_TEST_PLAN_ID environment variables. They are opt-in via a feature flag:
YNAB_TOKEN=... YNAB_TEST_PLAN_ID=... cargo test --features integration
I am not affiliated, associated, or in any way officially connected with YNAB or any of its subsidiaries or affiliates. The official YNAB website can be found at https://www.ynab.com. The names YNAB and You Need A Budget, as well as related names, tradenames, marks, trademarks, emblems, and images are registered trademarks of YNAB. YNAB API Terms of Service.