Spiris Bokföring och Fakturering API Client for Rust
A comprehensive Rust client library for the Spiris Bokföring och Fakturering API (formerly Visma eAccounting).
Features
- OAuth2 Authentication: Complete OAuth2 flow support with PKCE and token refresh
- Type-safe API: Strongly typed request/response models
- Async/Await: Built on tokio and reqwest for async operations
- Automatic Retries: Exponential backoff for transient failures
- Rate Limiting: Automatic handling of API rate limits (600 req/min)
- Configurable: Builder patterns for client and retry configuration
- Observability: Optional tracing support for request/response logging
- Comprehensive Coverage: Support for customers, invoices, articles, and more
- Error Handling: Rich error types with detailed information
- Production Ready: CI/CD, comprehensive tests, and battle-tested
- Well-documented: Extensive documentation and examples
Installation
Add this to your Cargo.toml:
[]
= "0.1.0"
= { = "1.0", = ["full"] }
Quick Start
use ;
async
Authentication
The Spiris API uses OAuth2 for authentication. Here's how to authenticate:
1. Register Your Application
First, register your application in the Visma Developer Portal to obtain:
- Client ID
- Client Secret
- Redirect URI
2. Implement OAuth2 Flow
use ;
async
Usage Examples
List Customers with Pagination
use ;
let token = new;
let client = new;
let params = new.page.pagesize;
let customers = client.customers.list.await?;
for customer in customers.data
Create a Customer
use ;
let new_customer = Customer ;
let created = client.customers.create.await?;
println!;
Create an Invoice
use ;
use Utc;
let invoice = Invoice ;
let created_invoice = client.invoices.create.await?;
println!;
Search with Filters
use QueryParams;
let query = new
.filter
.select;
let active_customers = client.customers.search.await?;
Manage Articles/Products
use Article;
let article = Article ;
let created_article = client.articles.create.await?;
API Feature Matrix
Endpoints Implemented
| Endpoint | API Path | List | Get | Create | Update | Delete | Search | Extra |
|---|---|---|---|---|---|---|---|---|
| Customers | ||||||||
| Customers | /customers |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Customer Invoice Drafts | /customerinvoicedrafts |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | convert |
| Customer Invoices | /customerinvoices |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | payments, pdf, einvoice |
| Customer Ledger Items | /customerledgeritems |
✓ | ✓ | ✓ | ✓ | |||
| Customer Labels | /customerlabels |
✓ | ✓ | ✓ | ✓ | ✓ | ||
| Suppliers | ||||||||
| Suppliers | /suppliers |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Supplier Invoices | /supplierinvoices |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | payments |
| Supplier Invoice Drafts | /supplierinvoicedrafts |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | convert |
| Supplier Ledger Items | /supplierledgeritems |
✓ | ✓ | ✓ | ✓ | |||
| Supplier Labels | /supplierlabels |
✓ | ✓ | ✓ | ✓ | ✓ | ||
| Articles | ||||||||
| Articles | /articles |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Article Labels | /articlelabels |
✓ | ✓ | ✓ | ✓ | ✓ | ||
| Article Account Codings | /articleaccountcodings |
✓ | ✓ | ✓ | ✓ | ✓ | ||
| Units | /units |
✓ | ✓ | ✓ | ✓ | ✓ | ||
| Accounting | ||||||||
| Accounts | /accounts |
✓ | ✓ | ✓ | ✓ | balances, types, standard | ||
| Fiscal Years | /fiscalyears |
✓ | ✓ | ✓ | opening balances | |||
| VAT Codes | /vatcodes |
✓ | ✓ | |||||
| Vouchers | /vouchers |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Banking | ||||||||
| Bank Accounts | /bankaccounts |
✓ | ✓ | ✓ | ✓ | ✓ | ||
| Banks | /banks |
✓ | foreign payment codes | |||||
| Projects & Cost Centers | ||||||||
| Projects | /projects |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Cost Centers | /costcenters |
✓ | ✓ | ✓ | ✓ | ✓ | items | |
| Allocation Periods | /allocationperiods |
✓ | ✓ | |||||
| Orders & Quotations | ||||||||
| Orders | /orders |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Quotations | /quotations |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Delivery & Payment | ||||||||
| Delivery Methods | /deliverymethods |
✓ | ✓ | ✓ | ✓ | ✓ | ||
| Delivery Terms | /deliveryterms |
✓ | ✓ | ✓ | ✓ | ✓ | ||
| Terms of Payment | /termsofpayment |
✓ | ✓ | ✓ | ✓ | ✓ | ||
| Documents | ||||||||
| Attachments | /attachments |
✓ | ✓ | ✓ | ✓ | upload binary | ||
| Documents | /documents |
✓ | ✓ | |||||
| Settings & Reference | ||||||||
| Company Settings | /companysettings |
✓ | ✓ | |||||
| Countries | /countries |
✓ | ||||||
| Currencies | /currencies |
✓ | ||||||
| Users | /users |
✓ | ✓ | |||||
| Messaging & Approvals | ||||||||
| Message Threads | /messagethreads |
✓ | ✓ | add message | ||||
| Approvals | /approval/* |
VAT report, supplier invoice |
Total: 35+ endpoints with full CRUD operations where applicable
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page |
u32 |
Page number (0-indexed) |
pagesize |
u32 |
Items per page (default: 50, max: 500) |
filter |
String |
OData filter expression (e.g., "IsActive eq true") |
select |
String |
Fields to return (e.g., "Id,Name,Email") |
Data Types
Core Entities:
| Type | Fields |
|---|---|
Customer |
id, customer_number, corporate_identity_number, name, email, phone, mobile_phone, website, invoice_address, delivery_address, payment_terms_in_days, is_active, is_private_person, created_utc, modified_utc |
Supplier |
id, supplier_number, corporate_identity_number, name, email, phone, mobile_phone, website, address, bank_account_number, bank_giro_number, plus_giro_number, is_active, created_utc, modified_utc |
Article |
id, article_number, name, unit, sales_price, purchase_price, is_active, vat_rate_id, created_utc, modified_utc |
Invoices:
| Type | Fields |
|---|---|
Invoice |
id, invoice_number, customer_id, invoice_date, due_date, delivery_date, currency_code, rows, total_amount, total_vat_amount, total_amount_including_vat, is_sent, remarks, created_utc, modified_utc |
CustomerInvoiceDraft |
id, customer_id, invoice_date, due_date, delivery_date, currency_code, rows, total_amount, total_vat_amount, total_amount_including_vat, remarks, your_reference, our_reference, created_utc, modified_utc |
SupplierInvoice |
id, supplier_id, invoice_number, invoice_date, due_date, currency_code, currency_rate, rows, total_amount, total_vat_amount, total_amount_including_vat, is_paid, payment_date, ocr_number, created_utc, modified_utc |
InvoiceRow |
id, article_id, text, unit_price, quantity, discount_percentage, vat_rate_id, total_amount |
Accounting:
| Type | Fields |
|---|---|
Account |
account_number, name, account_type, vat_code_id, fiscal_year_id, is_active, opening_balance |
Voucher |
id, voucher_number, voucher_date, voucher_type, voucher_text, rows, created_utc, modified_utc |
VoucherRow |
account_number, debit_amount, credit_amount, transaction_text, cost_center_item_id, project_id |
FiscalYear |
id, start_date, end_date, is_locked, bookkeeping_method |
VatCode |
id, code, description, vat_rate |
Banking & Payments:
| Type | Fields |
|---|---|
BankAccount |
id, name, account_number, iban, bic, ledger_account_number, currency_code, is_default, is_active |
InvoicePayment |
amount, payment_date, bank_account_id, payment_reference_number, currency_rate |
CustomerLedgerItem |
id, customer_id, customer_invoice_id, currency_amount, currency_code, amount, payment_date, payment_reference_number, voucher_id, voucher_number, created_utc |
Projects & Cost Centers:
| Type | Fields |
|---|---|
Project |
id, number, name, start_date, end_date, customer_id, notes, is_completed, is_active, created_utc, modified_utc |
CostCenter |
id, number, name, is_active |
CostCenterItem |
id, cost_center_id, name, short_name, is_active |
AllocationPeriod |
id, start_date, end_date, name, is_locked |
Orders & Quotations:
| Type | Fields |
|---|---|
Order |
id, order_number, customer_id, order_date, delivery_date, currency_code, rows, total_amount, is_invoiced, created_utc, modified_utc |
Quotation |
id, quotation_number, customer_id, quotation_date, valid_until_date, currency_code, rows, total_amount, is_accepted, created_utc, modified_utc |
Delivery & Payment:
| Type | Fields |
|---|---|
DeliveryMethod |
id, name, code, is_active |
DeliveryTerm |
id, name, code, is_active |
TermsOfPayment |
id, name, code, number_of_days, is_active |
Documents:
| Type | Fields |
|---|---|
Attachment |
id, name, content_type, size, temporary_url, created_utc |
AttachmentLink |
entity_type, entity_id, attachment_id |
Document |
id, document_number, document_type, document_date, voucher_id, attachment_id |
Settings & Reference:
| Type | Fields |
|---|---|
CompanySettings |
name, corporate_identity_number, address, phone, email, website, currency_code, country_code, fiscal_year_start_month |
Country |
code, name |
Currency |
code, name |
User |
id, email, first_name, last_name, is_active, role |
Bank |
id, name, bic, country_code |
ForeignPaymentCode |
id, code, name |
Messaging & Approvals:
| Type | Fields |
|---|---|
MessageThread |
id, subject, entity_type, entity_id, is_read, messages, created_utc, modified_utc |
Message |
id, body, is_from_user, created_utc |
ApprovalAction |
is_approved, comment |
Other:
| Type | Fields |
|---|---|
Address |
address1, address2, postal_code, city, country_code |
CustomerLabel |
id, name, description |
SupplierLabel |
id, name, description |
ArticleLabel |
id, name, description |
ArticleAccountCoding |
id, article_id, account_coding_type, sales_account_number, purchase_account_number |
Unit |
id, code, name, is_active |
Authentication Features
| Feature | Supported |
|---|---|
| OAuth2 Authorization Code + PKCE | ✓ |
| Token refresh | ✓ |
| Token expiration check (5-min buffer) | ✓ |
Scopes: ea:api, ea:sales, offline_access |
✓ |
Client Features
| Feature | Supported | Configuration |
|---|---|---|
| Automatic retry with exponential backoff | ✓ | RetryConfig |
| Rate limit handling (429) | ✓ | Auto-retry |
| Server error retry (5xx) | ✓ | Auto-retry |
| Configurable timeout | ✓ | ClientConfig.timeout_seconds |
| Custom base URL | ✓ | ClientConfig.base_url |
| Tracing/logging | ✓ | ClientConfig.enable_tracing |
| Thread-safe token updates | ✓ | Arc<RwLock<AccessToken>> |
Error Types
| Error | Description |
|---|---|
TokenExpired |
Access token expired (not retried) |
RateLimitExceeded |
429 response (retried) |
NotFound |
404 response |
InvalidRequest |
400 response |
AuthError |
401/403 response |
ApiError |
Other HTTP errors |
OAuth2Error |
OAuth2 flow failures |
Http |
Network/connection errors (retried) |
RetryConfig Options
| Option | Default | Description |
|---|---|---|
max_retries |
3 | Maximum retry attempts |
initial_interval |
500ms | Initial backoff duration |
max_interval |
30s | Maximum backoff duration |
multiplier |
2.0 | Exponential backoff multiplier |
max_elapsed_time |
120s | Total time before giving up |
Error Handling
The library provides comprehensive error handling:
use Error;
match client.customers.get.await
Rate Limiting
The Spiris API has a rate limit of 600 requests per minute per client per endpoint. The library automatically handles rate limit errors and returns appropriate error types.
Token Expiration and Refresh
Access tokens expire after 1 hour. The library checks token expiration before making requests and provides built-in token refresh:
use ;
// Check if token is expired
if client.is_token_expired
Advanced Configuration
The client supports extensive configuration for production use:
use ;
use Duration;
let token = new;
// Configure retry behavior
let retry_config = new
.max_retries
.initial_interval
.max_interval;
// Create client with custom configuration
let config = new
.base_url
.timeout_seconds
.retry_config
.enable_tracing;
let client = with_config;
Retry Logic
The client automatically retries failed requests with exponential backoff:
- Network errors: Automatically retried
- Rate limits (429): Automatically retried with backoff
- Server errors (5xx): Automatically retried
- Client errors (4xx): Not retried (permanent errors)
Configure retry behavior:
let retry_config = new
.max_retries // Max retry attempts
.initial_interval // Initial backoff
.max_interval // Max backoff
.multiplier; // Backoff multiplier
Examples
The examples/ directory contains complete working examples:
oauth_flow.rs: OAuth2 authentication flowlist_customers.rs: List customers with paginationcreate_customer.rs: Create a new customercreate_invoice.rs: Create an invoice
Run an example with:
Testing
Run the test suite:
Run specific tests:
# Run only library tests
# Run only integration tests
# Run with output
Performance Tips
Connection Pooling
The client uses reqwest's built-in connection pooling. Reuse the same Client instance for multiple requests:
// Good: Reuse client
let client = new;
for customer_id in customer_ids
// Bad: Creating new client for each request
for customer_id in customer_ids
Batch Operations
When possible, use pagination to fetch multiple records in one request:
// Fetch 100 customers at once instead of 100 individual requests
let params = new.pagesize;
let customers = client.customers.list.await?;
Timeout Configuration
Adjust timeouts based on your network conditions:
let config = new
.timeout_seconds // Increase for slower networks
.retry_config;
Security Best Practices
Never Hardcode Credentials
Always use environment variables or secure configuration management:
// Good
let token = var?;
// Bad - Never do this!
// let token = "hardcoded_token_12345";
Token Storage
Store refresh tokens securely:
use fs;
use PermissionsExt;
// Write token to file with restricted permissions
let token_json = to_string?;
write?;
set_permissions?;
HTTPS Only
The client uses HTTPS by default. Never modify the base URL to use HTTP:
// The default is already HTTPS - don't change it
const DEFAULT_BASE_URL: &str = "https://eaccountingapi.vismaonline.com/v2/";
Troubleshooting
Token Expired Errors
If you're getting TokenExpired errors:
// Check token expiration before making requests
if client.is_token_expired
Rate Limiting
If you're hitting rate limits (600 requests/minute):
// Configure more aggressive retry backoff
let retry_config = new
.max_retries
.initial_interval
.max_interval;
let config = new.retry_config;
let client = with_config;
Network Timeouts
For unreliable networks:
let config = new
.timeout_seconds // 2 minutes
.retry_config;
Debugging API Requests
Enable tracing to see detailed request/response information:
// Add to Cargo.toml
// tracing-subscriber = "0.3"
use tracing_subscriber;
async
FAQ
Q: Do I need to manually refresh tokens?
A: The client checks token expiration before each request and returns a TokenExpired error if the token is expired. You can either:
- Manually refresh using
OAuth2Handler::refresh_token() - Implement automatic refresh logic in your application
Q: What's the rate limit?
A: The API has a rate limit of 600 requests per minute per client per endpoint. The client automatically retries rate-limited requests with exponential backoff.
Q: Can I use this with multiple accounts?
A: Yes! Create separate Client instances for each account with different access tokens:
let client1 = new;
let client2 = new;
Q: How do I handle pagination for large datasets?
A: Use a loop to fetch all pages:
let mut all_customers = Vecnew;
let mut page = 0;
let pagesize = 100;
loop
Q: What happens if my API call fails?
A: The client automatically retries transient failures (network errors, rate limits, 5xx errors) with exponential backoff. Permanent errors (4xx) are returned immediately.
Q: Can I customize the retry behavior?
A: Yes! See the "Retry Logic" section for configuration options.
Q: Is this thread-safe?
A: Yes! The Client can be safely cloned and shared across threads:
let client = new;
// Clone for use in different threads
let client1 = client.clone;
let client2 = client.clone;
spawn;
spawn;
Q: What's the difference between Spiris and Visma eAccounting?
A: Spiris Bokföring och Fakturering is the new name for Visma eAccounting. All API endpoints and functionality remain exactly the same - only the branding has changed.
Migration Guide
From visma_eaccounting to spiris
If you were using an earlier version with the visma_eaccounting package name:
- Update
Cargo.toml:
[]
# Old
# visma_eaccounting = "0.1.0"
# New
= "0.1.0"
- Update imports:
// Old
use ;
// New
use ;
- Update environment variables:
# Old
# New
All API functionality remains identical - no code changes needed beyond the import statements.
Documentation
Generate and view the documentation:
Browse available modules:
spiris::auth- OAuth2 authenticationspiris::client- HTTP clientspiris::endpoints- API endpointsspiris::error- Error typesspiris::types- Data modelsspiris::retry- Retry configuration
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Development Setup
# Clone the repository
# Run tests
# Run examples (requires API credentials)
# Check formatting
# Run clippy
# Build documentation
Reporting Issues
When reporting issues, please include:
- Rust version (
rustc --version) - Package version
- Minimal code example
- Error messages
- Expected vs actual behavior
License
This project is licensed under the MIT license. See LICENSE-MIT for details.
Resources
- Spiris Bokföring och Fakturering API Documentation
- Visma Developer Portal
- API Authentication Guide
- Visma Community Forum
Note
Spiris Bokföring och Fakturering was formerly known as Visma eAccounting. All API endpoints and technical details remain the same.
Disclaimer
This is an unofficial client library and is not affiliated with or endorsed by Visma or Spiris. Use at your own risk.