Unofficial Rust SDK for the Basecamp API.
Note: This is a community-maintained SDK. For official SDKs, see basecamp/basecamp-sdk (TypeScript, Go, Python, Ruby).
- Full coverage of Basecamp API services
- OAuth 2.0 and static token authentication
- Automatic retry with exponential backoff
- Pagination handling with
get_paginated - Structured errors with CLI-friendly exit codes
- HTTPS enforcement for non-localhost URLs
- Observability hooks for logging, metrics, and tracing
Add to your Cargo.toml:
[dependencies]
basecamp-sdk-rs = "0.1"
tokio = { version = "1", features = ["full"] }Requires Rust 1.70+.
use basecamp_sdk_rs::{Client, Config, BearerAuth};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure the client
let config = Config::builder()
.base_url("https://3.basecampapi.com/999")
.build()?;
// Use a static token
let auth = BearerAuth::from_token("your-access-token");
let client = Client::new(config, auth)?;
// List all projects
let projects = client.get_paginated::<serde_json::Value>("/projects.json", None).await?;
for project in projects.items {
println!("{:?}", project);
}
Ok(())
}use basecamp_sdk_rs::Config;
use std::time::Duration;
let config = Config::builder()
.base_url("https://3.basecampapi.com/999")
.timeout(Duration::from_secs(60))
.max_retries(5)
.max_pages(100)
.max_items(1000)
.build()?;| Option | Type | Default | Description |
|---|---|---|---|
base_url |
String |
https://3.basecampapi.com |
API base URL |
timeout |
Duration |
30s | Request timeout |
max_retries |
u32 |
3 | Maximum retry attempts |
base_delay |
Duration |
1s | Base delay for retries |
max_jitter |
Duration |
100ms | Maximum jitter for retries |
max_pages |
u32 |
10000 | Maximum pages for pagination |
max_items |
Option<u32> |
None |
Maximum items to return |
| Variable | Description | Default |
|---|---|---|
BASECAMP_TOKEN |
API token | Required |
BASECAMP_ACCOUNT_ID |
Account ID | Required |
BASECAMP_BASE_URL |
API base URL | https://3.basecampapi.com |
use basecamp_sdk_rs::{BearerAuth, Config, Client};
let auth = BearerAuth::from_token("your-token");
let client = Client::new(config, auth)?;use basecamp_sdk_rs::{OAuthToken, Config, Client};
// Implement TokenProvider for automatic token refresh
let token_provider = MyTokenProvider::new();
let client = Client::new(config, token_provider)?;| Service | Methods |
|---|---|
| Projects | list, get, create, update, trash |
| Templates | list, get, createProject |
| Tools | get, list, update |
| People | list, get, me, listPingable |
| Service | Methods |
|---|---|
| Todos | list, get, create, update, trash, complete, uncomplete, reposition |
| Todolists | list, get, create, update, trash |
| Todosets | get |
| Todolist Groups | list, get, create, reposition |
| Service | Methods |
|---|---|
| Messages | list, get, create, update, pin, unpin |
| Message Boards | get |
| Message Types | list, get, create, update, delete |
| Comments | list, get, create, update |
| Campfires | list, get, listLines, getLine, createLine, deleteLine |
| Service | Methods |
|---|---|
| Card Tables | get, listColumns |
| Cards | list, get, create, update, move |
| Card Columns | get, create, update, move |
| Card Steps | list, get, create, update, complete, uncomplete |
| Service | Methods |
|---|---|
| Schedules | get, listEntries, getEntry, createEntry, updateEntry, trashEntry |
| Lineup | create, update, delete |
| Checkins | get, listQuestions, getQuestion, listAnswers, getAnswer |
| Service | Methods |
|---|---|
| Vaults | list, get, create, update |
| Documents | list, get, create, update, trash |
| Uploads | list, get, create, update, trash |
| Attachments | createUploadUrl, create |
| Service | Methods |
|---|---|
| Webhooks | list, get, create, update, delete |
| Subscriptions | get, subscribe, unsubscribe, update |
| Events | list, listForRecording |
| Recordings | archive, unarchive, trash |
| Service | Methods |
|---|---|
| Search | search |
| Reports | progress, upcoming, assigned, overdue, personProgress |
| Timesheets | forRecording, forProject, report |
| Timeline | get |
Use get_paginated for automatic pagination:
let config = Config::builder()
.base_url("https://3.basecampapi.com/999")
.max_pages(10) // Limit pages
.max_items(500) // Limit total items
.build()?;
let client = Client::new(config, auth)?;
// Fetch all pages automatically
let result = client.get_paginated::<serde_json::Value>("/projects.json", None).await?;
println!("Fetched {} items", result.items.len());
println!("Truncated: {}", result.meta.truncated);
println!("Total count: {:?}", result.meta.total_count);The SDK provides structured errors with codes for programmatic handling:
use basecamp_sdk_rs::{BasecampError, ErrorCode};
match client.get("/projects/99999.json", None).await {
Ok(response) => println!("Success: {:?}", response.status()),
Err(e) => {
match e {
BasecampError::NotFound { message, .. } => {
eprintln!("Not found: {}", message);
}
BasecampError::AuthRequired { message, .. } => {
eprintln!("Auth required: {}", message);
}
BasecampError::RateLimit { retry_after, .. } => {
eprintln!("Rate limited. Retry after: {:?}", retry_after);
}
BasecampError::Validation { message, fields, .. } => {
eprintln!("Validation error: {}", message);
for field in fields {
eprintln!(" {}: {}", field.field, field.message);
}
}
_ => eprintln!("Error: {}", e),
}
// Use exit codes for CLI applications
std::process::exit(e.exit_code());
}
}| Code | HTTP Status | Exit Code | Description |
|---|---|---|---|
auth_required |
401 | 3 | Authentication required |
forbidden |
403 | 4 | Access denied |
not_found |
404 | 2 | Resource not found |
rate_limit |
429 | 5 | Rate limit exceeded (retryable) |
network |
- | 6 | Network error (retryable) |
api_error |
5xx | 7 | Server error |
ambiguous |
- | 8 | Multiple matches found |
validation |
400, 422 | 9 | Invalid request data |
usage |
- | 1 | Configuration or argument error |
The SDK automatically retries requests on transient failures:
- Retryable errors: 429 (rate limit) and 503 (service unavailable)
- Backoff: Exponential with jitter
- Rate limits: Respects
Retry-Afterheader - Max retries: 3 attempts by default
Non-idempotent operations (POST) are not retried by default.
use basecamp_sdk_rs::{Client, Config, BearerAuth, console_hooks};
let config = Config::builder().base_url("https://3.basecampapi.com/999").build()?;
let auth = BearerAuth::from_token("token");
let hooks = console_hooks();
let client = Client::with_hooks(config, auth, hooks)?;Output:
[Basecamp] -> GET https://3.basecampapi.com/999/projects.json
[Basecamp] <- GET https://3.basecampapi.com/999/projects.json 200 (145ms)
Implement BasecampHooks for custom observability:
use basecamp_sdk_rs::{BasecampHooks, OperationInfo, OperationResult, RequestInfo, RequestResult};
struct MetricsHooks;
impl BasecampHooks for MetricsHooks {
fn on_operation_start(&self, info: &OperationInfo) {
println!("Starting: {}.{}", info.service, info.operation);
}
fn on_operation_end(&self, info: &OperationInfo, result: &OperationResult) {
println!("Completed in {}ms", result.duration_ms);
}
fn on_request_end(&self, info: &RequestInfo, result: &RequestResult) {
println!("{} {} -> {} ({}ms)",
info.method, info.url, result.status, result.duration_ms);
}
}The SDK enforces HTTPS for non-localhost URLs:
use basecamp_sdk_rs::Config;
// This will fail
let result = Config::builder()
.base_url("http://api.example.com/999")
.build();
assert!(result.is_err());
// Localhost is allowed
let config = Config::builder()
.base_url("http://localhost:8080/999")
.build()?;// List todos in a todolist
let todos = client.get_paginated::<Todo>(
&format!("/buckets/{}/todolists/{}/todos.json", bucket_id, todolist_id),
None
).await?;
// Create a todo
let todo = client.post(
&format!("/buckets/{}/todolists/{}/todos.json", bucket_id, todolist_id),
Some(&serde_json::json!({
"content": "Review pull request",
"description": "<p>Check the new auth flow</p>",
"due_on": "2026-02-01",
"assignee_ids": [12345, 67890]
})),
None
).await?;
// Complete a todo
client.post(&format!("/buckets/{}/todos/{}/completions.json", bucket_id, todo_id), None, None).await?;// List messages
let messages = client.get_paginated::<Message>(
&format!("/buckets/{}/message_boards/{}/messages.json", bucket_id, board_id),
None
).await?;
// Create a message
let msg = client.post(
&format!("/buckets/{}/message_boards/{}/messages.json", bucket_id, board_id),
Some(&serde_json::json!({
"subject": "Weekly Update",
"content": "<p>Here's what we accomplished...</p>",
})),
None
).await?;// Create a webhook
let webhook = client.post(
&format!("/buckets/{}/webhooks.json", bucket_id),
Some(&serde_json::json!({
"payload_url": "https://example.com/webhook",
"types": ["Todo", "Comment"]
})),
None
).await?;
// List webhooks
let webhooks = client.get_paginated::<Webhook>(
&format!("/buckets/{}/webhooks.json", bucket_id),
None
).await?;
// Delete a webhook
client.delete(&format!("/buckets/{}/webhooks/{}.json", bucket_id, webhook_id), None).await?;# Build
cargo build
# Run tests
cargo test
# Run conformance tests
cargo test --test conformance_test
# Check formatting
cargo fmt --check
# Run linter
cargo clippy -- -D warnings
# Generate coverage
cargo llvm-covMIT