Unofficial .NET 10 API client library for the CashCtrl REST API v1. Provides a fully typed C# client with models, JSON serialization, and ASP.NET Core DI integration.
CashCtrl is a Swiss cloud ERP for accounting and business management.
- 100% CashCtrl REST API v1 coverage (375 endpoints, 58 services, 10 domain groups)
- Immutable record models with three-tier hierarchy (Create, Update, Full)
- ASP.NET Core DI integration with typed
HttpClientand optional HTTP resilience - Standalone usage without DI
- Auto-pagination helper (
PaginationHelper.ListAllAsync) - Filter, sort, and pagination support on all list endpoints
Every release is covered by unit tests (NSubstitute) and WireMock-backed integration tests that run without credentials on every CI build. Separately, fixtures under tests/CashCtrlApiNet.E2eTests/ exercise every service against a live CashCtrl account.
Every domain group has been verified against the live API except Salary and Meta. Fixtures in those two groups are skipped with [Ignore] at the class level until a future verification pass. The library code for those services is fully implemented and usable, but if you hit a model or parameter mismatch there, follow the diagnostic playbook in doc/analysis/2026-03-29-e2e-test-verification.md — the same patterns that surfaced issues in Groups 1-6 (catalogued in doc/analysis/2026-03-29-api-doc-discrepancies.md §§1-33) are likely to surface in Salary and Meta too.
Two Group 1 fixtures in these folders are verified and not ignored: Meta/OrganizationE2eTests and Salary/SalaryFieldE2eTests.
| Package | Description |
|---|---|
| CashCtrlApiNet | API client, connection handler, and all service endpoints |
| CashCtrlApiNet.Abstractions | Models, enums, converters, and serialization helpers |
| CashCtrlApiNet.AspNetCore | ASP.NET Core dependency injection registration |
# ASP.NET Core (includes all packages)
dotnet add package CashCtrlApiNet.AspNetCore
# Standalone (no DI)
dotnet add package CashCtrlApiNetCashCtrl uses API key authentication. Generate an API key in your CashCtrl account under Settings > API. The base URL follows the pattern https://{yourorg}.cashctrl.com/.
Register the client in Program.cs:
builder.Services.AddCashCtrl(options =>
{
options.BaseUri = "https://myorg.cashctrl.com/";
options.ApiKey = "your-api-key";
options.Language = Language.De; // optional, defaults to German
options.EnableResilience = true; // optional, defaults to true (retries, circuit breaker)
});Then inject ICashCtrlApiClient wherever you need it:
public class MyAccountService(ICashCtrlApiClient cashCtrl)
{
public async Task<IEnumerable<AccountListed>> GetAllAccounts()
{
var result = await cashCtrl.Account.Account.GetList();
return result.ResponseData!.Data;
}
}using var connectionHandler = new CashCtrlConnectionHandler(
new CashCtrlConfiguration
{
BaseUri = "https://myorg.cashctrl.com/",
ApiKey = "your-api-key",
DefaultLanguage = "de"
});
// Use services directly via the connection handler
var accountService = new AccountService(connectionHandler);
var result = await accountService.GetList();
foreach (var account in result.ResponseData!.Data)
Console.WriteLine($"{account.Number} - {account.Name}");// List all active articles matching a search query
var result = await cashCtrl.Inventory.Article.GetList(
new ListParams { Query = "widget", OnlyActive = true });
foreach (var article in result.ResponseData!.Data)
Console.WriteLine(article.Name);// Create a new person
var result = await cashCtrl.Person.Person.Create(
new PersonCreate { FirstName = "Jane", LastName = "Doe" });
var newPersonId = result.ResponseData!.InsertId;// Update an existing person
await cashCtrl.Person.Person.Update(
new PersonUpdate { Id = 42, FirstName = "Jane", LastName = "Smith" });// Iterate all journal entries across all pages
await foreach (var entry in PaginationHelper.ListAllAsync(
cashCtrl.Journal.Journal.GetList,
new ListParams { Sort = "dateAdded", Dir = "DESC" },
pageSize: 50))
{
Console.WriteLine($"{entry.DateAdded} - {entry.Amount}");
}The library uses result-based error handling — no exceptions are thrown for HTTP errors, validation failures, or rate limiting. All API calls return ApiResult<T>, which wraps every possible outcome.
| Property | Type | Description |
|---|---|---|
IsHttpSuccess |
bool |
Whether the HTTP request returned a 2xx status code |
HttpStatusCode |
HttpStatusCode |
The HTTP status code from the API |
CashCtrlHttpStatusCodeDescription |
string? |
Human-readable CashCtrl description of the status code |
RequestsLeft |
int? |
Number of API requests remaining (rate limit) |
RawResponseContent |
string? |
Raw response body when JSON deserialization fails (e.g. rate limit messages) |
ResponseData |
T? |
Deserialized API response data |
Write operations (create, update, delete) return ApiResult<NoContentResponse>. Always check both the HTTP status and the business logic result:
var result = await cashCtrl.Person.Person.Create(
new PersonCreate { FirstName = "Jane", LastName = "Doe" });
if (!result.IsHttpSuccess)
{
Console.WriteLine($"HTTP error {result.HttpStatusCode}: {result.CashCtrlHttpStatusCodeDescription}");
return;
}
if (!result.ResponseData!.Success)
{
foreach (var error in result.ResponseData.Errors ?? [])
Console.WriteLine($"Validation error on '{error.Field}': {error.Message}");
return;
}
Console.WriteLine($"Created person with ID {result.ResponseData.InsertId}");Read operations return ApiResult<SingleResponse<T>> or ApiResult<ListResponse<T>>:
var result = await cashCtrl.Account.Account.GetList();
if (!result.IsHttpSuccess)
{
Console.WriteLine($"HTTP error {result.HttpStatusCode}: {result.CashCtrlHttpStatusCodeDescription}");
return;
}
foreach (var account in result.ResponseData!.Data)
Console.WriteLine($"{account.Number} - {account.Name}");Some error responses (e.g. rate limiting) return plain text instead of JSON. When this happens, ResponseData is null and RawResponseContent contains the raw body:
var result = await cashCtrl.Inventory.Article.Get(new Entry { Id = 1 });
if (!result.IsHttpSuccess && result.ResponseData is null)
{
// Non-JSON response (e.g. rate limit or HTML error page)
Console.WriteLine($"HTTP {result.HttpStatusCode}: {result.RawResponseContent}");
Console.WriteLine($"Requests left: {result.RequestsLeft}");
}The client is organized into 10 domain groups, accessible via ICashCtrlApiClient:
| Property | Domain | Examples |
|---|---|---|
Account |
Chart of accounts | Accounts, Categories, Cost Centers, Banks |
Common |
Shared resources | Currencies, Tax Rates, Custom Fields, Rounding |
File |
File management | Files, File Categories |
Inventory |
Products & assets | Articles, Fixed Assets, Units, Imports |
Journal |
Bookkeeping | Journal Entries, Imports |
Meta |
Organization | Settings, Fiscal Periods, Locations |
Order |
Sales & purchases | Orders, Book Entries, Documents, Payments |
Person |
Contacts | Persons, Categories, Titles, Imports |
Report |
Financial reports | Report Sets, Reports, Report Elements |
Salary |
Payroll | Statements, Templates, Certificates, Types |
Special thanks to CashCtrl AG for the excellent cloud ERP platform.
Built and maintained by AMANDA Technology GmbH.