An all-in-one .NET library and CLI companion for Sage Active Public API V2 (powered by Hot Chocolate GraphQL). Built with zero unnecessary dependencies, automated OAuth 2.0 SBC Auth token refreshing, rate limit backoff (3,000 req/min), multi-legislation support (France, Spain, Germany, Portugal), and typed models covering all 468+ API operations in both Sandbox and Production environments.
Authored by Jose Modi / Modi97.
- Highlights
- Package Suite
- Installation
- Configuring Environments: Production vs Sandbox
- Zero-Code CLI Tool (
sage-active) - Quickstart: ASP.NET Core Minimal APIs
- Quickstart: ASP.NET Core MVC
- Quickstart: Standalone C# Script
- Core Architecture & Services
- Order-to-Cash Workflow (Invoicing & Settlement)
- General Ledger & Accounting Entries
- Multipart File Uploads & OCR Processing
- Dynamic Multidimensional Analytics Cubes
- Regional Gateways & Endpoints
- .NET Compatibility Matrix
- Sponsorship
- Contributing & License
- 100% Wire Coverage: Directly supports every single operation from Sage's 468-request Postman library across General Ledger, Accounting, Sales Invoicing, Purchase Invoices, Banking, Third Parties, Products, OCR, and Catalog Analytics.
- Dual Environment Support: Seamless switching between Developer Sandbox (testing & prototyping) and Production (live enterprise operations) via config, environment variables, or CLI.
- Universal .NET Compatibility: Multi-targeted for
netstandard2.0,netcoreapp3.1,net5.0,net6.0,net7.0,net8.0, andnet9.0(compatible with .NET Framework 4.6.1+, .NET 5+, and all modern .NET releases). - Zero-Code Setup CLI: Companion global tool (
dotnet-sage-active/sage-active) with interactive onboarding wizard, connectivity diagnostics, environment switcher, organization manager, and project scaffolder. - Production-Ready Resilience: Automatic OAuth 2.0 SBC Auth token acquisition, token refresh with thread-safe locking, exponential backoff on HTTP 429 (3,000 req/min limit), and GraphQL multipart uploads.
- First-Class ASP.NET Core: Clean dependency injection (
AddSageActive), Minimal API routes (MapSageWebhook), and health check extensions.
| Package | NuGet ID | Target Frameworks | Description |
|---|---|---|---|
| Core SDK | Sage.Active |
netstandard2.0, netcoreapp3.1, net5.0, net6.0, net7.0, net8.0, net9.0 |
Core GraphQL engine, SBC Auth, entities, and 11 domain subclients |
| ASP.NET Core | Sage.Active.AspNetCore |
netstandard2.0, netcoreapp3.1, net5.0, net6.0, net7.0, net8.0, net9.0 |
Dependency Injection (AddSageActive) and Minimal API webhook mapping |
| Global CLI | dotnet-sage-active |
net8.0 (runs on .NET 8, 9, 10+) |
Terminal setup wizard, environment switcher, self-tests, sandbox invoice generator, and query runner |
dotnet add package Sage.Activedotnet add package Sage.Active.AspNetCoredotnet tool install --global dotnet-sage-activeSage Active operates in two environments:
Sandbox: Isolated developer environment for development, schema prototyping, and testing workflows with sandbox organizations without affecting financial records.Production: Live corporate gateway for real accounting ledgers, fiscal filings, real-world customer invoicing, and connected bank reconciliation.
ASP.NET Core automatically applies configuration depending on your runtime environment:
{
"SageActive": {
"Region": "FR",
"Environment": "Sandbox",
"SubscriptionKey": "YOUR_SANDBOX_SUBSCRIPTION_KEY",
"OrganizationId": "YOUR_SANDBOX_ORGANIZATION_ID",
"ClientId": "YOUR_SANDBOX_CLIENT_ID",
"ClientSecret": "YOUR_SANDBOX_CLIENT_SECRET"
}
}{
"SageActive": {
"Region": "FR",
"Environment": "Production",
"SubscriptionKey": "YOUR_PRODUCTION_SUBSCRIPTION_KEY",
"OrganizationId": "YOUR_PRODUCTION_ORGANIZATION_ID",
"ClientId": "YOUR_PRODUCTION_CLIENT_ID",
"ClientSecret": "YOUR_PRODUCTION_CLIENT_SECRET"
}
}Production Deployment Tip: When deploying to Docker, Azure, AWS, or Kubernetes, set environment variables directly without committing secret keys to code:
SageActive__Environment=ProductionSageActive__Region=FR(orES,DE,PT)SageActive__SubscriptionKey=PROD_KEYSageActive__OrganizationId=PROD_ORGANIZATION_UUID
Instantiate clients targeting either environment directly:
using Sage.Active;
using Sage.Active.Models;
// ?? Production Environment Setup
var prodClient = new SageActiveClient(new SageActiveConfig
{
Region = SageRegion.FR,
Environment = SageEnvironment.Production,
SubscriptionKey = "PROD_SUBSCRIPTION_KEY",
OrganizationId = "PROD_COMPANY_ORGANIZATION_UUID"
});
// ?? Sandbox Environment Setup
var sandboxClient = new SageActiveClient(new SageActiveConfig
{
Region = SageRegion.FR,
Environment = SageEnvironment.Sandbox,
SubscriptionKey = "SANDBOX_SUBSCRIPTION_KEY",
OrganizationId = "SANDBOX_ORGANIZATION_UUID"
});
// Or using the quick factory method:
var client = SageActiveClient.Create(
subscriptionKey: "PROD_KEY",
organizationId: "PROD_ORG_ID",
region: SageRegion.FR,
environment: SageEnvironment.Production
);You can inspect or switch the active CLI environment at any time:
# Check current environment
sage-active env
# Switch to Production
sage-active env production
# Switch to Sandbox
sage-active env sandbox
# Or run the interactive setup wizard to configure both:
sage-active initThe sage-active CLI tool allows you to configure, test, and operate Sage Active without writing code:
_____ ___ __ _
/ ___/____ _____ ____ / | _____/ /_(_) _____
\__ \/ __ `/ __ `/ _ \ / /| | / ___/ __/ / | / / _ \
___/ / /_/ / /_/ / __// ___ |/ /__/ /_/ /| |/ / __/
/____/\__,_/\__, /\___//_/ |_|\___/\__/_/ |___/\___/
/____/
| Command | Description | Example |
|---|---|---|
sage-active init |
Interactive setup wizard (prompts for region, sandbox/production, credentials, queries organizations, and saves configuration) | sage-active init |
sage-active env [target] |
View or switch environment between production and sandbox |
sage-active env production |
sage-active test |
Self-test connectivity, user profile, and validates permissions using userAccessPolicyCheck |
sage-active test |
sage-active org |
Lists available organizations and shows which organization is currently active | sage-active org |
sage-active org set <id> |
Switches the active organization ID | sage-active org set 0000-0000-0000 |
sage-active query "<query>" |
Runs an arbitrary GraphQL query or executes a .graphql query file |
sage-active query "{ userProfile { fullName } }" |
sage-active invoice [list] |
Lists recent sales invoices | sage-active invoice |
sage-active invoice create |
Creates and posts a test sales invoice in Sandbox | sage-active invoice create |
sage-active scaffold [minimal|console] |
Generates ready-to-run starter code in the current directory | sage-active scaffold minimal |
Note on Environments: Set
"Environment": "Sandbox"for development/testing against Sage's mock sandbox API (https://sandbox-active.sage.com/api/v2), or"Environment": "Production"for live enterprise operations (https://active.sage.com/api/v2).
{
"SageActive": {
"Region": "FR",
"Environment": "Sandbox", // Set to "Sandbox" for testing, or "Production" for live operations
"SubscriptionKey": "YOUR_SAGE_SUBSCRIPTION_KEY",
"OrganizationId": "YOUR_SAGE_ORGANIZATION_ID",
"ClientId": "YOUR_CLIENT_ID",
"ClientSecret": "YOUR_CLIENT_SECRET"
}
}using Sage.Active;
using Sage.Active.AspNetCore;
using Sage.Active.Models.Entities;
var builder = WebApplication.CreateBuilder(args);
// Register Sage Active SDK via Dependency Injection
builder.Services.AddSageActive(builder.Configuration);
var app = builder.Build();
// Query Chart of Accounts
app.MapGet("/accounting/accounts", async (SageActiveClient client) =>
{
var accounts = await client.Accounting.GetAccountsAsync();
return Results.Ok(accounts.Nodes);
});
// Create and Post Sales Invoice in one operation
app.MapPost("/sales/invoice", async (SageActiveClient client, SalesInvoiceCreateInput input) =>
{
var result = await client.Sales.CreateAndPostInvoiceAsync(input);
return Results.Ok(new
{
invoiceId = result.InvoiceId,
operationalNumber = result.OperationalNumber,
accountingEntry = result.AccountingEntryNumber
});
});
// Receive Webhooks
app.MapSageWebhook("/sage/webhook", async (payload, ctx) =>
{
Console.WriteLine($"Received Sage Event: {payload}");
await Task.CompletedTask;
});
app.Run();using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Sage.Active;
using Sage.Active.Models.Entities;
namespace MyApp.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class AccountingController : ControllerBase
{
private readonly SageActiveClient _sage;
public AccountingController(SageActiveClient sage)
{
_sage = sage;
}
[HttpGet("accounts")]
public async Task<IActionResult> GetAccounts()
{
var accounts = await _sage.Accounting.GetAccountsAsync();
return Ok(accounts.Nodes);
}
[HttpPost("invoices")]
public async Task<IActionResult> CreateInvoice([FromBody] SalesInvoiceCreateInput input)
{
var (invoiceId, number, entryNumber) = await _sage.Sales.CreateAndPostInvoiceAsync(input);
return Ok(new { invoiceId, number, entryNumber });
}
}
}using System;
using System.Threading.Tasks;
using Sage.Active;
using Sage.Active.Models;
// Configure for either Production or Sandbox
var config = new SageActiveConfig
{
Region = SageRegion.FR,
Environment = SageEnvironment.Production, // or SageEnvironment.Sandbox
SubscriptionKey = "YOUR_SUBSCRIPTION_KEY",
OrganizationId = "YOUR_ORGANIZATION_ID",
AccessToken = "YOUR_BEARER_TOKEN"
};
var client = new SageActiveClient(config);
// 1. Verify User Profile
var profile = await client.Users.GetUserProfileAsync();
Console.WriteLine($"Connected as: {profile.FullName} ({profile.AuthenticationEmail})");
// 2. Query Active Customers
var customers = await client.ThirdParties.GetCustomersAsync(onlyActive: true);
foreach (var customer in customers.Nodes)
{
Console.WriteLine($"Customer: {customer.Code} - {customer.SocialName}");
}graph LR
Client[SageActiveClient] --> Org[Organizations & Master Data]
Client --> Users[Users & Access Policies]
Client --> Acc[Accounting & General Ledger]
Client --> TP[Third Parties - CRM]
Client --> Prod[Products & Dynamic Pricing]
Client --> Sales[Sales Documents & Invoicing]
Client --> Purch[Purchase Invoices & OCR]
Client --> Bank[Banking & Treasury]
Client --> Files[Files & Multipart Uploads]
Client --> Cat[Catalog & Analytics Cubes]
Client --> Raw[Custom GraphQL Queries]
-
client.Organizations: Query organizations, onboarding state, legislation codes, countries, zip codes, and currencies. -
client.Users: Retrieve authenticated user profile, users list, and validate action authorizations viauserAccessPolicyCheck. -
client.Accounting: Full chart of accounts, fiscal periods (accountingExercises), journal types, tax treatments, balanced GL entries (createAccountingEntryUsingCodes), trial balance, balance sheet, and profit & loss. -
client.ThirdParties: Manage customers, suppliers, employees, multiple addresses, and contacts. -
client.Products: Product catalog, units of measurement, dynamic price evaluation (productPriceById), and sales tariffs. -
client.Sales: Order-to-cash lifecycle (Quotes$\rightarrow$ Orders$\rightarrow$ Delivery Notes$\rightarrow$ Sales Invoices$\rightarrow$ Credit Notes). Includes automated validation (closeSalesInvoice), GL posting (postSalesInvoice), and open item settlement (salesOpenItemSettlement). -
client.Purchases: Purchase invoices, AP ledger posting, payables settlement, and automated OCR ingestion. -
client.Banks: Connected bank accounts, bank statement movements, matching rules, and bank reconciliation (reconcileBankMovement). -
client.Files: GraphQL multipart file attachments to entities, secure download links, and bulk ZIP export. -
client.Catalog: Multidimensional analytical cubes (aggregationCatalog/aggregationExecute) and standardized query lists (listCatalog/listExecute). -
client.ExecuteQueryAsync<T>: Direct raw GraphQL execution escape hatch for any query, mutation, or schema extension.
Create draft invoices, finalize document numbering, post to general ledger, and record customer payments:
// 1. Create, validate, and post invoice in one line
var (invoiceId, operationalNumber, glEntryNumber) = await client.Sales.CreateAndPostInvoiceAsync(new SalesInvoiceCreateInput
{
CustomerId = "CUSTOMER_UUID",
DocumentDate = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd"),
Lines = new List<SalesInvoiceLineInput>
{
new SalesInvoiceLineInput
{
ProductId = "PRODUCT_UUID",
Quantity = 2,
UnitPrice = 150.00m,
Description = "Consulting Services"
}
}
});
// 2. Query open receivables
var openItems = await client.Sales.GetOpenItemsAsync(invoiceId);
// 3. Settle open item payment
foreach (var item in openItems.Nodes)
{
var settlement = await client.Sales.SettleOpenItemAsync(new SalesOpenItemSettlementInput
{
SalesInvoiceOpenItemId = item.Id,
Amount = item.Amount,
PaymentMethodId = "PAYMENT_METHOD_UUID",
SettlementDate = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd")
});
Console.WriteLine($"Settled in Accounting Entry: {settlement.AccountingEntryNumber}");
}Manage pre-sales documents, sales orders, and compensatory credit notes:
// 1. Create and list commercial quotes (Devis)
var quoteId = await client.Sales.CreateQuoteAsync(new SalesQuoteCreateInput
{
CustomerId = "CUSTOMER_UUID",
DocumentDate = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd"),
Lines = new List<SalesQuoteLineInput>
{
new SalesQuoteLineInput { ProductId = "PRODUCT_UUID", TotalQuantity = 1, UnitPrice = 1200m }
}
});
var quotes = await client.Sales.GetQuotesAsync();
// 2. Create and list sales orders (Commandes)
var orderId = await client.Sales.CreateOrderAsync(new SalesOrderCreateInput
{
CustomerId = "CUSTOMER_UUID",
DocumentDate = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd"),
Lines = new List<SalesOrderLineInput>
{
new SalesOrderLineInput { ProductId = "PRODUCT_UUID", TotalQuantity = 5, UnitPrice = 250m }
}
});
var orders = await client.Sales.GetOrdersAsync();
// 3. Compensate or reverse a posted sales invoice with a credit note (Avoir)
var creditNoteId = await client.Sales.GenerateCreditNoteAsync(invoiceId);// Create a tenant-scoped clone reusing connection pools, auth caches, and throttlers:
var tenantClient = client.WithOrganization("TENANT_BRANCH_UUID");Post multi-line balanced accounting entries directly using account and third-party codes:
var entry = await client.Accounting.CreateEntryUsingCodesAsync(new AccountingEntryCreateUsingCodesInput
{
Description = "Sales Invoice #INV-2026-001",
Date = "2026-09-16",
JournalTypeCode = "VTE", // Sales Journal
Lines = new List<AccountingEntryLineInput>
{
new AccountingEntryLineInput
{
SubAccountCode = "411000", // Customer Account
ThirdCode = "CUST001",
DebitAmount = 120.00m,
CreditAmount = 0
},
new AccountingEntryLineInput
{
SubAccountCode = "706000", // Revenue Account
DebitAmount = 0,
CreditAmount = 100.00m
},
new AccountingEntryLineInput
{
SubAccountCode = "445710", // VAT Output Account
DebitAmount = 0,
CreditAmount = 20.00m
}
}
});
Console.WriteLine($"GL Entry #{entry.Number} created with ID: {entry.Id}");Attach receipts and invoices to entities using standard GraphQL multipart specifications:
using var fileStream = File.OpenRead("receipt.pdf");
var fileId = await client.Files.UploadFileToEntityAsync(
entityType: "CUSTOMER",
entityId: "CUSTOMER_UUID",
fileStream: fileStream,
fileName: "receipt.pdf",
comment: "Proof of purchase"
);
Console.WriteLine($"Uploaded File ID: {fileId}");Query business KPIs and sales cubes grouped by period, customer, or product:
var cube = await client.Catalog.ExecuteAggregationAsync(new AggregationExecuteInput
{
EntityKey = "queryAggregateSalesInvoices",
AggregationType = "SUM",
PeriodType = "Month",
DateMin = "2026-01-01",
DateMax = "2026-12-31",
GroupByName = "Customer",
ValueColumn1 = "Total Net",
ValueColumn2 = "Total Liquid",
Top = 5
});
foreach (var row in cube.Rows)
{
Console.WriteLine($"{row.GroupValue} ({row.Period}): Net={row.Value1:C2}, Total={row.Value2:C2} (Delta: {row.DeltaPercent}%)");
}Preconfigured endpoints for all European legislations:
| Region | Country | Gateway Address | Auth Server |
|---|---|---|---|
| FR | France | https://api.fr.active.sage.com |
https://sbcauth.sage.fr |
| ES | Spain | https://api.es.active.sage.com |
https://sbcauth.sage.fr |
| DE | Germany | https://api.de.active.sage.com |
https://sbcauth.sage.fr |
| PT | Portugal | https://api.es.active.sage.com |
https://sbcauth.sage.fr |
| .NET Version / Platform | Support Mode |
|---|---|
| .NET 5.0 | Native Target (net5.0) + Fallback (netstandard2.0) |
| .NET 6.0, 7.0, 8.0, 9.0, 10.0+ | Native Targets (net6.0, net7.0, net8.0, net9.0) |
| .NET Core (2.0 � 3.1) | Supported via netcoreapp3.1 and netstandard2.0 |
| .NET Framework (4.6.1 � 4.8.1) | Supported via netstandard2.0 |
| Mono / Xamarin / Unity / MAUI | Supported via netstandard2.0 |
If this SDK or CLI suite helps you in your projects or commercial integrations, consider supporting ongoing open-source maintenance and development:
👉 Click here to Support via Pesapal Open Source Sponsorship
Contributions are welcome! Please feel free to open issues or submit pull requests.